Newer
Older
gnexus-ui-kit / src / vue / components / GnFileUpload.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 21 hours ago 4 KB Harden Vue adapter accessibility behavior
import { defineComponent, h, onBeforeUnmount, ref, watch } from "vue";
import { iconNode } from "../utils.js";
import GnButton from "./GnButton.js";
import GnBadge from "./GnBadge.js";

function fileType(file) {
	const ext = file.name.split(".").pop();
	return ext ? ext.slice(0, 6).toUpperCase() : "FILE";
}

function fileSize(file) {
	if(!file.size) {
		return "0 B";
	}

	const units = ["B", "KB", "MB", "GB"];
	const index = Math.min(Math.floor(Math.log(file.size) / Math.log(1024)), units.length - 1);
	const value = file.size / Math.pow(1024, index);
	return `${value.toFixed(value >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
}

export default defineComponent({
	name: "GnFileUpload",
	props: {
		modelValue: { type: Array, default: () => [] },
		title: { type: String, default: "Upload files" },
		description: { type: String, default: "Attach documents, archives or images." },
		primary: { type: String, default: "Choose files" },
		secondary: { type: String, default: "Images get thumbnails, other files show their type" },
		badge: { type: String, default: "" },
		multiple: { type: Boolean, default: true },
		accept: { type: String, default: "" }
	},
	emits: ["update:modelValue", "change"],
	setup(props, { emit, slots }) {
		const urls = ref(new Map());
		const revokeFile = file => {
			const url = urls.value.get(file);

			if(url) {
				URL.revokeObjectURL(url);
				urls.value.delete(file);
			}
		};
		const revokeAll = () => {
			urls.value.forEach(url => URL.revokeObjectURL(url));
			urls.value.clear();
		};
		const setFiles = fileList => {
			const files = Array.from(fileList || []);
			emit("update:modelValue", files);
			emit("change", files);
		};
		const remove = index => {
			revokeFile(props.modelValue[index]);
			const files = props.modelValue.filter((_, itemIndex) => itemIndex !== index);
			emit("update:modelValue", files);
			emit("change", files);
		};
		const previewUrl = file => {
			if(!file.type?.startsWith("image/")) {
				return "";
			}

			if(!urls.value.has(file)) {
				urls.value.set(file, URL.createObjectURL(file));
			}

			return urls.value.get(file);
		};

		watch(() => props.modelValue, files => {
			const active = new Set(files);
			[...urls.value.keys()].forEach(file => {
				if(!active.has(file)) {
					revokeFile(file);
				}
			});
		});

		onBeforeUnmount(revokeAll);

		return () => h("div", { class: "file-upload-panel" }, [
			h("div", { class: "file-upload-form" }, [
				h("div", { class: "file-upload-header" }, [
					h("div", { class: "file-upload-heading" }, [
						h("h3", { class: "file-upload-title" }, slots.title?.() || props.title),
						h("p", { class: "file-upload-description" }, slots.description?.() || props.description)
					]),
					props.badge && h(GnBadge, { variant: "info" }, () => props.badge)
				]),
				h("label", { class: "file-upload-dropzone" }, [
					h("span", { class: "file-upload-icon", "aria-hidden": "true" }, [iconNode("ph-cloud-arrow-up")]),
					h("span", { class: "file-upload-body" }, [
						h("span", { class: "file-upload-primary" }, props.primary),
						h("span", { class: "file-upload-secondary" }, props.secondary)
					]),
					h("input", {
						type: "file",
						multiple: props.multiple,
						accept: props.accept || undefined,
						onChange: event => setFiles(event.target.files)
					})
				]),
				h("div", { class: "file-upload-preview", hidden: !props.modelValue.length }, props.modelValue.map((file, index) => h("figure", {
					class: "file-upload-preview-item"
				}, [
					h("button", {
						class: "file-upload-preview-remove",
						type: "button",
						"aria-label": `Remove ${file.name}`,
						onClick: () => remove(index)
					}, [iconNode("ph-x")]),
					h("div", { class: "file-upload-preview-visual" }, previewUrl(file)
						? h("img", { src: previewUrl(file), alt: "" })
						: h("span", { class: "file-upload-preview-type" }, fileType(file))
					),
					h("figcaption", {}, [
						h("span", { class: "file-upload-preview-name" }, file.name),
						h("span", { class: "file-upload-preview-meta" }, `${fileType(file)} / ${fileSize(file)}`)
					])
				]))),
				slots.actions && h("div", { class: "file-upload-actions" }, slots.actions()),
				!slots.actions && props.modelValue.length > 0 && h("div", { class: "file-upload-actions" }, [
					h(GnButton, {
						variant: "secondary",
						size: "sm",
						onClick: () => {
							revokeAll();
							setFiles([]);
						}
					}, () => "Reset")
				])
			])
		]);
	}
});