summaryrefslogtreecommitdiff
path: root/packages/web-util/src/forms/InputFile.tsx
blob: cd0a96d1cb7024edca083f4c92064d167fc4d3bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import { Fragment, VNode, h } from "preact";
import { UIFormProps } from "./FormProvider.js";
import { noHandlerPropsAndNoContextForField } from "./InputArray.js";
import { LabelWithTooltipMaybeRequired } from "./InputLine.js";
import { useField } from "./useField.js";

export function InputFile<T extends object, K extends keyof T>(
  props: { maxBites: number; accept?: string } & UIFormProps<T, K>,
): VNode {
  const {
    label,
    tooltip,
    required,
    help: propsHelp,
    maxBites,
    accept,
  } = props;
  //FIXME: remove deprecated
  const fieldCtx = useField<T, K>(props.name);
  const { value, onChange, state } =
    props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);

  const help = propsHelp ?? state.help;
  if (state.hidden) {
    return <div />;
  }

  const valueStr = !value ? "" : value.toString();
  const firstColon = valueStr.indexOf(";");

  const { fileName, dataUri } = valueStr.startsWith("file:")
    ? {
        fileName: valueStr.substring(5, firstColon),
        dataUri: valueStr.substring(firstColon + 1),
      }
    : {
        fileName: "",
        dataUri: valueStr,
      };

  return (
    <div class="col-span-full">
      <LabelWithTooltipMaybeRequired
        label={label}
        tooltip={tooltip}
        required={required}
      />
      {!dataUri ? (
        <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1">
          <div class="text-center">
            <svg
              class="mx-auto h-12 w-12 text-gray-300"
              viewBox="0 0 24 24"
              fill="currentColor"
              aria-hidden="true"
            >
              <path
                fill-rule="evenodd"
                d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z"
                clip-rule="evenodd"
              />
            </svg>
            {!state.disabled && (
              <div class="my-2 flex text-sm leading-6 text-gray-600">
                <label
                  for={String(props.name)}
                  class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500"
                >
                  <span>Upload a file</span>
                  <input
                    id={String(props.name)}
                    type="file"
                    class="sr-only"
                    accept={accept}
                    onChange={(e) => {
                      const f: FileList | null = e.currentTarget.files;
                      if (!f || f.length != 1) {
                        return onChange(undefined!);
                      }
                      if (f[0].size > maxBites) {
                        return onChange(undefined!);
                      }
                      const fileName = f[0].name;
                      return f[0].arrayBuffer().then((b) => {
                        const b64 = window.btoa(
                          new Uint8Array(b).reduce(
                            (data, byte) => data + String.fromCharCode(byte),
                            "",
                          ),
                        );
                        if (fileName) {
                          return onChange(
                            `file:${fileName};data:${f[0].type};base64,${b64}` as any,
                          );
                        } else {
                          return onChange(
                            `data:${f[0].type};base64,${b64}` as any,
                          );
                        }
                      });
                    }}
                  />
                </label>
                {/* <p class="pl-1">or drag and drop</p> */}
              </div>
            )}
          </div>
        </div>
      ) : (
        <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative">
          {(dataUri as string).startsWith("data:image/") ? (
            <img src={dataUri} class=" h-24 w-full object-cover relative" />
          ) : (
            <div />
          )}
          {fileName ? (
            <div class="absolute rounded-lg border flex justify-center text-xl items-center text-white ">
              {fileName}
            </div>
          ) : (
            <Fragment />
          )}

          {!state.disabled && (
            <div
              class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer "
              onClick={() => {
                onChange(undefined!);
              }}
            >
              Clear
            </div>
          )}
        </div>
      )}
      {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>}
    </div>
  );
}