/**
* Specify {@link UwcInputElement#type | `[type=file]`} to use an input element that can handle file uploads
*
* @example
*
* ```html
*
* ```
*
* Compared with the native input it doesn't look all that different _but_ you can actually
* drag and drop files on it out of the box.
*
* Once you've selected a few files you'll also notice that each file is added to a visible list
* from where it can also be deleted again.
*
* To upload multiple files you just specify the {@link UwcInputElement.multiple | `[multiple]`} attribute.
*
* ## Min/Max Constraints
*
* It is also possible to make such an input participate in the Constraints API by adding the {@link UwcInputElement.min | `[min]`}
* and {@link UwcInputElement.max | `[max]`} attributes.
*
* @example
*
* ```html
*
*
* ```
*
* ## Pattern constraint
*
* You can make sure the uploaded files have names that conform to a pattern by using the {@link UwcInputElement.pattern | `[pattern]`}
* attribute. Keep in mind that this has nothing to do with a files contents!
*
* @example
*
* ```html
*
* ```
*
* ## Customizing the uploaded files list
*
* Typically `` gives you the option to customize how selected values appear by adding a ``
* element to the ``.
*
* Since there is no sane way for a file to be represented in the DOM Tree with `` you'll have to hide the
* selected files list with css and control the {@link UwcInputElement.files | `.files`} property.
*
* @module
*/
import {
create,
createEventRegistrar,
dispatch,
text,
} from "@hesxenon/prelude/Dom.js";
import { concat, fromArray, remove } from "@hesxenon/prelude/FileList.js";
import { Events } from "../events";
import * as Root from "../parts/root";
import { Connect } from "../uwc-input";
import { iife } from "@hesxenon/prelude/Function.js";
const getDefaults = (
input: UwcInputElement,
config: (typeof UwcInputElement)["config"],
) => {
return Object.entries(config.file.defaults).find(([key]) =>
key.startsWith(input.locale),
)?.[1];
};
/**
* save some state from the given input and restore it on disconnet
*/
const initializeInput: Connect = (input, { disconnect, config }) => {
const originalPlaceholder = input.placeholder;
const originalFilelist = input.files;
input.placeholder ||=
getDefaults(input, config)?.placeholder ?? "No file chosen";
input.files = input.files ?? fromArray([]);
disconnect.signal.addEventListener("abort", () => {
input.placeholder = originalPlaceholder;
input.files = originalFilelist;
});
};
const syncFormValues = (input: UwcInputElement) => {
dispatch(input, {
type: Events.formValuesChanged,
detail: Array.from(input.files ?? []),
});
};
const validate = (input: UwcInputElement) => {
dispatch(input, {
type: Events.validityStateChanged,
detail: {
valueMissing: input.required && !input.files?.length,
rangeUnderflow:
typeof input.min === "number" && (input.files?.length ?? 0) < input.min,
rangeOverflow:
typeof input.max === "number" && (input.files?.length ?? 0) > input.max,
patternMismatch: iife(() => {
if (!input.pattern || input.files == null) {
return false;
}
const regex = new RegExp(input.pattern);
return Array.from(input.files).some((file) => !regex.test(file.name));
}),
},
});
};
/**
* @internal
*/
export const connectFile: Connect = (input, opts) => {
const initial = input.files;
//#region create elements
const button = create("input", {
type: "button",
value: getDefaults(input, opts.config)?.buttonText || "Choose file",
});
const placeholder = create("span", { part: "placeholder" });
const control = create("div", { part: "control" }, [button, placeholder]);
const list = create("ul", { part: "files" });
const nativeInput = create("input", {
type: "file",
style: { display: "none" },
});
//#region attach event listeners
const on = createEventRegistrar(input, opts.disconnect);
on("click", () => {
if (input.readonly) {
return;
}
nativeInput.click();
});
on(Events.filesChanged, () => {
list.innerHTML = "";
list.append(
...Array.from(input.files ?? []).map((file) =>
create("li", {}, [
text(file.name),
create(
"button",
{
onclick: (e) => {
e.stopPropagation();
input.files =
input.files == null
? null
: remove(
input.files,
Array.from(input.files).indexOf(file),
);
dispatch(input, { type: "change" });
},
},
[text("x")],
),
]),
),
);
});
on([Events.filesChanged, Events.nameChanged, Events.formAssociated], () =>
syncFormValues(input),
);
on(Events.multipleChanged, () => {
nativeInput.multiple = input.multiple;
});
on(
[
Events.formValuesChanged,
Events.minChanged,
Events.maxChanged,
Events.patternChanged,
Events.requiredChanged,
],
() => validate(input),
);
on(Events.formReset, () => {
input.files = initial;
});
on("dragover", (e) => {
e.preventDefault();
});
on("drop", (e) => {
e.preventDefault();
input.files =
input.files == null
? (e.dataTransfer?.files ?? null)
: e.dataTransfer?.files != null
? concat(input.files, e.dataTransfer.files)
: input.files;
dispatch(input, { type: "change" });
});
on(Events.placeholderChanged, () => {
placeholder.textContent =
input.placeholder ??
getDefaults(input, opts.config)?.placeholder ??
"No files chosen";
});
on(Events.clear, () => {
input.files = fromArray([]);
});
nativeInput.addEventListener(
"change",
(e) => {
e.stopPropagation();
input.files =
input.files == null
? nativeInput.files == null
? null
: nativeInput.files
: nativeInput.files == null
? input.files
: concat(input.files, nativeInput.files);
},
opts.disconnect,
);
initializeInput(input, opts);
Root.replaceChildren(input, [nativeInput, list, control]);
syncFormValues(input);
};