Newer
Older
gnexus-ui-kit / src / vue / components / GnFileUpload.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 13 hours ago 3 KB Add Vue form adapter components
import { defineComponent, h, ref } 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 setFiles = fileList => {
			const files = Array.from(fileList || []);
			emit("update:modelValue", files);
			emit("change", files);
		};
		const remove = 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);
		};

		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: () => setFiles([])
					}, () => "Reset")
				])
			])
		]);
	}
});