UNPKG

maz-ui

Version:

A standalone components library for Vue.Js 3 & Nuxt.Js 3

505 lines (504 loc) 22.1 kB
import { defineComponent, mergeModels, defineAsyncComponent, useModel, onBeforeMount, ref, computed, createElementBlock, openBlock, normalizeStyle, normalizeClass, unref, renderSlot, createCommentVNode, createElementVNode, createVNode, TransitionGroup, withCtx, Fragment, renderList, withModifiers, Transition, createBlock, mergeProps, toDisplayString, createTextVNode } from "vue"; import { MazCheckCircle, MazXCircle, MazTrash, MazArrowUpOnSquare } from "@maz-ui/icons"; import { useTranslations } from "@maz-ui/translations"; import { n } from "../chunks/sleep.Ci7GE4BQ.js"; import { useInstanceUniqId } from "../composables/useInstanceUniqId.js"; import { useDropzone } from "../composables/useDropzone.js"; import MazBtn from "./MazBtn.js"; import MazIcon from "./MazIcon.js"; import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper.B--vMWp3.js"; import '../assets/MazDropzone._2HwHowu.css';const _hoisted_1 = ["for"], _hoisted_2 = { class: "m-dropzone__icon-container" }, _hoisted_3 = { class: "m-dropzone__description" }, _hoisted_4 = { class: "m-dropzone__file-info" }, _hoisted_5 = { class: "m-dropzone__file-name" }, _hoisted_6 = { class: "m-dropzone__file-size" }, _hoisted_7 = { class: "m-dropzone__content" }, _hoisted_8 = { class: "m-dropzone__upload-text" }, _hoisted_9 = { class: "m-dropzone__divider" }, _hoisted_10 = { key: 0, class: "m-dropzone__info-text" }, _hoisted_11 = { key: 0 }, _hoisted_12 = { key: 0 }, _hoisted_13 = ["id", "multiple", "accept", "disabled"], _sfc_main = /* @__PURE__ */ defineComponent({ __name: "MazDropzone", props: /* @__PURE__ */ mergeModels({ id: {}, multiple: { type: Boolean, default: !1 }, dataTypes: {}, preventDefaultForUnhandled: { type: Boolean, default: !0 }, maxFileSize: {}, maxFiles: {}, disabled: { type: Boolean, default: !1 }, preview: { type: Boolean, default: !0 }, minFileSize: {}, allowDuplicates: { type: Boolean, default: !1 }, translations: {}, color: { default: "primary" }, selectFileBtnProps: { default: () => ({}) }, removeFileBtnProps: { default: () => ({}) }, spinnerProps: { default: () => ({}) }, autoUpload: { type: [String, Boolean], default: !1 }, url: {}, requestOptions: {}, transformBody: { type: Function } }, { modelValue: { default: () => [] }, modelModifiers: {} }), emits: /* @__PURE__ */ mergeModels(["drop", "enter", "leave", "over", "add", "remove", "error", "upload-error", "upload-error-multiple", "upload-success", "upload-success-multiple"], ["update:modelValue"]), setup(__props, { expose: __expose, emit: __emit }) { const emits = __emit, MazSpinner = defineAsyncComponent(() => import("./MazSpinner.js")), filesData = useModel(__props, "modelValue"); onBeforeMount(async () => { filesData.value.length && (filesData.value = await Promise.all(filesData.value.map((fileData) => getFileData(fileData.file)))); }); const dropZoneRef = ref(), isUploading = ref(!1), hasMultiple = computed(() => __props.multiple || (__props.maxFiles ? __props.maxFiles > 1 : !1) || __props.autoUpload === "multiple"), instanceId = useInstanceUniqId({ componentName: "MazDropzone", providedId: __props.id }); function uploadFile(formData) { if (!__props.url) throw emits("error", { files: null, event: null, code: "NO_URL" }), new Error("NO_URL"); try { const body = __props.transformBody?.(formData) ?? formData; return fetch(__props.url, { method: "POST", body, ...__props.requestOptions }); } catch { emits("error", { files: null, event: null, code: "FILE_UPLOAD_ERROR" }); } } function getFormData(file) { const formData = new FormData(); return formData.append("file", file), formData; } function getFormDataMultiple() { const formData = new FormData(); return filesData.value.forEach((fileData) => formData.append("files", fileData.file)), formData; } async function uploadFilesMultiple() { if (!filesData.value.length) { emits("error", { files: filesData.value.map((f) => f.file), event: null, code: "NO_FILES_TO_UPLOAD" }); return; } try { isUploading.value = !0, filesData.value = filesData.value.map((f) => ({ ...f, uploading: !0, success: !1, error: !1 })); const formData = getFormDataMultiple(), response = await uploadFile(formData); filesData.value = filesData.value.map((f) => ({ ...f, uploading: !1, success: !0, error: !1 })), emits("upload-success-multiple", { files: filesData.value.map((f) => f.file), response }); } catch (error) { filesData.value = filesData.value.map((f) => ({ ...f, uploading: !1, success: !1, error: !0 })), emits("upload-error-multiple", { files: filesData.value.map((f) => f.file), code: "FILE_UPLOAD_ERROR_MULTIPLE", error }); } finally { isUploading.value = !1, await n(1e3), filesData.value = filesData.value.filter((f) => f.error); } } async function uploadFiles() { if (!filesData.value.length) throw emits("error", { files: filesData.value.map((f) => f.file), event: null, code: "NO_FILES_TO_UPLOAD" }), new Error("NO_FILES_TO_UPLOAD"); try { isUploading.value = !0; for await (const fileData of filesData.value) { const formData = getFormData(fileData.file); try { fileData.error = !1, fileData.uploading = !0; const response = await uploadFile(formData); emits("upload-success", { file: fileData.file, response }), fileData.success = !0; } catch (error) { fileData.error = !0, emits("upload-error", { file: fileData.file, code: "FILE_UPLOAD_ERROR", error }); } finally { fileData.uploading = !1; } } } finally { isUploading.value = !1, await n(1e3), filesData.value = filesData.value.filter((f) => f.error); } } async function getFileData(file) { const fileData = { file, name: file.name, size: file.size, type: file.type, lastModified: file.lastModified, sizeInMb: (file.size / 1024 / 1024).toFixed(2), lastModifiedDate: new Date(file.lastModified), url: URL.createObjectURL(file), thumbnail: void 0, uploading: !1, success: !1, error: !1 }; return file.type.startsWith("image/") && await new Promise((resolve) => { const reader = new FileReader(); reader.onload = (event) => { const thumbnail = event.target?.result; fileData.thumbnail = thumbnail || void 0, resolve(null); }, reader.readAsDataURL(file); }), __props.maxFileSize && file.size > __props.maxFileSize * 1024 * 1024 && emits("error", { files: [file], event: null, code: "FILE_SIZE_EXCEEDED" }), __props.minFileSize && file.size < __props.minFileSize * 1024 * 1024 && emits("error", { files: [file], event: null, code: "FILE_SIZE_TOO_SMALL" }), fileData; } async function handleFiles(files) { if (__props.disabled || !files) return null; for await (const file of files) { if (!isFileTypeAllowed(file)) { emits("error", { files: [file], event: null, code: "FILE_TYPE_NOT_ALLOWED" }); continue; } if (!hasMultiple.value && filesData.value.length >= 1) { emits("error", { files: [file], event: null, code: "MAX_FILES_EXCEEDED" }); continue; } if (__props.maxFiles && filesData.value.length >= __props.maxFiles) { emits("error", { files: [file], event: null, code: "MAX_FILES_EXCEEDED" }); continue; } if (__props.maxFileSize && file.size > __props.maxFileSize * 1024 * 1024) { emits("error", { files: [file], event: null, code: "FILE_SIZE_EXCEEDED" }); continue; } if (__props.minFileSize && file.size < __props.minFileSize * 1024 * 1024) { emits("error", { files: [file], event: null, code: "FILE_SIZE_TOO_SMALL" }); continue; } if (!__props.allowDuplicates && filesData.value.find((f) => f.name === file.name && f.size === file.size && f.type === file.type)) { emits("error", { files: [file], event: null, code: "FILE_DUPLICATED" }); continue; } const fileData = await getFileData(file); filesData.value = [...filesData.value, fileData], emits("add", fileData.file); } return __props.autoUpload === "single" ? uploadFiles() : __props.autoUpload === "multiple" && uploadFilesMultiple(), filesData.value; } async function onDrop(files, event) { await handleFiles(files), emits("drop", { files, event }); } function onError(files, event) { emits("error", { files, event, code: "FILE_TYPE_NOT_ALLOWED" }); } function onEnter(files, event) { emits("enter", { files, event }); } function onLeave(files, event) { emits("leave", { files, event }); } function onOver(files, event) { emits("over", { files, event }); } const { isOverDropZone, isOverError } = useDropzone(dropZoneRef, { dataTypes: __props.dataTypes, onDrop, preventDefaultForUnhandled: __props.preventDefaultForUnhandled, multiple: hasMultiple.value, onError, onEnter, onLeave, onOver }), fileInput = ref(); function handleFileUpload(event) { const input = event.target, files = input?.files; files && handleFiles(files), setTimeout(() => { input.value = ""; }, 3e3); } const fileIconMap = { jpeg: "photo", png: "photo", tiff: "photo", bmp: "photo", webp: "photo", svg: "photo", ico: "photo", gif: "gif", mp4: "camera", webm: "camera", ogg: "camera", mp3: "speaker-wave", wav: "speaker-wave", m4a: "speaker-wave", aac: "speaker-wave", flac: "speaker-wave", zip: "archive-box", rar: "archive-box", tar: "archive-box", gz: "archive-box", exe: "command-line", dll: "command-line", so: "command-line", dylib: "command-line", dmg: "command-line", deb: "command-line", rpm: "command-line", apk: "command-line", app: "command-line", xls: "document", xlsx: "document", ppt: "document", pptx: "document", pdf: "document", json: "document", xml: "document", csv: "document", tsv: "document", txt: "document", doc: "document", docx: "document", document: "document" }; function getFileIcon(fileData) { const type = fileData.file.type.split("/")?.[1]?.split("+")?.[0]?.toLowerCase(), extension = fileData.file.name.split(".").pop()?.toLowerCase(); return fileIconMap[type] || extension && fileIconMap[extension] || fileIconMap.document; } function handleFileInputClick() { fileInput.value && fileInput.value.click(); } function handleFileRemove(fileData) { fileData.url && URL.revokeObjectURL(fileData.url), filesData.value = filesData.value.filter((f) => f.file !== fileData.file), emits("remove", fileData.file); } const selectAreaCanBeDisplayed = ref(!0), dataTypesString = computed(() => __props.dataTypes?.map((type) => type.split("/")[1]?.split("+")[0]?.toUpperCase()).join(", ")), allFileIsAccepted = computed(() => __props.dataTypes?.length === 1 && __props.dataTypes[0] === "*/*"), { t } = useTranslations(), messages = computed(() => ({ dragAndDrop: __props.translations?.dragAndDrop || t("dropzone.dragAndDrop"), fileMaxCount: __props.translations?.fileMaxCount || __props.maxFiles ? t("dropzone.fileMaxCount", { count: __props.maxFiles }) : void 0, fileMaxSize: __props.translations?.fileMaxSize || __props.maxFileSize ? t("dropzone.fileMaxSize", { size: __props.maxFileSize }) : void 0, fileTypes: __props.translations?.fileTypes || dataTypesString.value ? t("dropzone.fileTypes", { types: dataTypesString.value }) : void 0, selectFile: __props.translations?.selectFile || t("dropzone.selectFile"), divider: __props.translations?.divider || t("dropzone.divider") })); function reset() { filesData.value.forEach((fileData) => { fileData.url && URL.revokeObjectURL(fileData.url); }), filesData.value = []; } function addFile(file) { handleFiles([file]); } function removeFile(file) { filesData.value = filesData.value.filter((f) => f.file !== file); } function isFileTypeAllowed(file) { return !__props.dataTypes || __props.dataTypes.includes("*/*") ? !0 : __props.dataTypes.some((type) => type.endsWith("/*") ? file.type.startsWith(type.slice(0, -1)) : file.type === type); } return __expose({ /** * Upload files * @description With this method, the files are uploaded one by one (a request for each file) * @usage `mazDropzoneInstance.value?.uploadFiles()` */ uploadFiles, /** * Upload multiple files * @description With this method, the files are uploaded all at once in a single request * @usage `mazDropzoneInstance.value?.uploadFilesMultiple()` */ uploadFilesMultiple, /** * Get form data * @description Get the form data of one file * @usage `const formData = mazDropzoneInstance.value?.getFormData(file)` */ getFormData, /** * Get form data multiple * @description Get the form data of all files * @usage `const formData = mazDropzoneInstance.value?.getFormDataMultiple()` */ getFormDataMultiple, /** * Reset the files * @description Remove all files from the dropzone * @usage `mazDropzoneInstance.value?.reset()` */ reset, /** * Check if the files are uploading * @type boolean * @description Check if the files are uploading * @usage `const isUploading = mazDropzoneInstance.value?.isUploading` */ isUploading, /** * Add a file to the dropzone * @description Add a file manually to the dropzone * @usage `mazDropzoneInstance.value?.addFile(file)` */ addFile, /** * Remove a file from the dropzone * @description Remove a file manually from the dropzone * @usage `mazDropzoneInstance.value?.removeFile(file)` */ removeFile }), (_ctx, _cache) => (openBlock(), createElementBlock("label", { ref_key: "dropZoneRef", ref: dropZoneRef, role: "button", tabindex: "0", for: `input-file-uploader-${unref(instanceId)}`, class: normalizeClass(["m-dropzone m-reset-css", { "m-dropzone--disabled": _ctx.disabled, "m-dropzone--is-over-drop-zone": unref(isOverDropZone) && !unref(isOverError), "m-dropzone--is-over-error": unref(isOverError) }]), style: normalizeStyle({ "--active-color": `hsl(var(--maz-${_ctx.color}))` }) }, [ renderSlot(_ctx.$slots, "files-area", { filesData: filesData.value }, () => [ createVNode(TransitionGroup, { name: "file-scale", tag: "div", class: "m-dropzone__files-container", onBeforeEnter: _cache[1] || (_cache[1] = ($event) => selectAreaCanBeDisplayed.value = !1), onAfterLeave: _cache[2] || (_cache[2] = ($event) => selectAreaCanBeDisplayed.value = filesData.value.length === 0) }, { default: withCtx(() => [ (openBlock(!0), createElementBlock(Fragment, null, renderList(filesData.value, (file) => (openBlock(), createElementBlock("div", { key: `${file.name}-${file.size}-${file}`, class: "m-dropzone__file-item group", onClick: _cache[0] || (_cache[0] = withModifiers(() => { }, ["prevent"])) }, [ renderSlot(_ctx.$slots, "file-item", { file }, () => [ file.thumbnail && _ctx.preview ? (openBlock(), createElementBlock("div", { key: 0, style: normalizeStyle({ backgroundImage: `url(${file.thumbnail})`, backgroundSize: "cover", backgroundPosition: "center" }), class: "m-dropzone__thumbnail" }, null, 4)) : createCommentVNode("", !0), _cache[3] || (_cache[3] = createElementVNode("div", { class: "m-dropzone__overlay" }, null, -1)), createElementVNode("div", _hoisted_2, [ createVNode(Transition, { name: "icon-scale" }, { default: withCtx(() => [ file.uploading ? (openBlock(), createBlock(unref(MazSpinner), mergeProps({ key: 0, color: _ctx.color, class: "m-dropzone__spinner" }, { ref_for: !0 }, _ctx.spinnerProps), null, 16, ["color"])) : file.success ? (openBlock(), createBlock(unref(MazCheckCircle), { key: 1, class: "m-dropzone__success-icon" })) : file.error ? (openBlock(), createBlock(unref(MazXCircle), { key: 2, class: "m-dropzone__error-icon" })) : (openBlock(), createBlock(MazIcon, { key: 3, name: getFileIcon(file), class: "m-dropzone__file-icon" }, null, 8, ["name"])) ]), _: 2 }, 1024) ]), createElementVNode("div", _hoisted_3, [ createElementVNode("div", _hoisted_4, [ createElementVNode("span", _hoisted_5, toDisplayString(file.name), 1), createElementVNode("span", _hoisted_6, toDisplayString(file.sizeInMb) + " MB", 1) ]), !file.uploading && !file.success ? (openBlock(), createBlock(MazBtn, mergeProps({ key: 0, size: "xs", "rounded-size": "full", icon: unref(MazTrash), disabled: _ctx.disabled, color: _ctx.color }, { ref_for: !0 }, _ctx.removeFileBtnProps, { onClick: withModifiers(($event) => handleFileRemove(file), ["prevent"]) }), null, 16, ["icon", "disabled", "color", "onClick"])) : createCommentVNode("", !0) ]) ], !0) ]))), 128)) ]), _: 3 }) ], !0), filesData.value.length === 0 && selectAreaCanBeDisplayed.value ? renderSlot(_ctx.$slots, "no-files-area", { key: 0, selectFile: handleFileInputClick }, () => [ createElementVNode("div", _hoisted_7, [ renderSlot(_ctx.$slots, "upload-icon", {}, () => [ createVNode(unref(MazArrowUpOnSquare), { class: "m-dropzone__upload-icon" }) ], !0), createElementVNode("span", _hoisted_8, toDisplayString(messages.value.dragAndDrop), 1) ]), createElementVNode("span", _hoisted_9, toDisplayString(messages.value.divider), 1), createVNode(MazBtn, mergeProps({ disabled: _ctx.disabled, color: _ctx.color }, _ctx.selectFileBtnProps, { onClick: handleFileInputClick }), { default: withCtx(() => [ createTextVNode(toDisplayString(messages.value.selectFile), 1) ]), _: 1 }, 16, ["disabled", "color"]), !allFileIsAccepted.value && (messages.value.fileMaxCount || messages.value.fileMaxSize || messages.value.fileTypes) ? (openBlock(), createElementBlock("p", _hoisted_10, [ messages.value.fileMaxCount ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [ createTextVNode(toDisplayString(messages.value.fileMaxCount) + " ", 1), messages.value.fileMaxSize || messages.value.fileTypes ? (openBlock(), createElementBlock("span", _hoisted_11, " - ")) : createCommentVNode("", !0) ], 64)) : createCommentVNode("", !0), messages.value.fileMaxSize ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [ createTextVNode(toDisplayString(messages.value.fileMaxSize) + " ", 1), messages.value.fileTypes ? (openBlock(), createElementBlock("span", _hoisted_12, " - ")) : createCommentVNode("", !0) ], 64)) : createCommentVNode("", !0), messages.value.fileTypes ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [ createTextVNode(toDisplayString(messages.value.fileTypes), 1) ], 64)) : createCommentVNode("", !0) ])) : createCommentVNode("", !0) ], !0) : createCommentVNode("", !0), createElementVNode("input", { id: `input-file-uploader-${unref(instanceId)}`, ref_key: "fileInput", ref: fileInput, multiple: hasMultiple.value, type: "file", accept: _ctx.dataTypes?.join(","), tabindex: "-1", disabled: _ctx.disabled, class: "m-dropzone__file-input", onChange: handleFileUpload }, null, 40, _hoisted_13) ], 14, _hoisted_1)); } }), MazDropzone = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-69969cc3"]]); export { MazDropzone as default };