UNPKG

maz-ui

Version:

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

534 lines (533 loc) 24.7 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 { MazLogoJs, MazLogoTypescript, MazLogoVue, MazLogoReact, MazLogoJson, MazLogoXml, MazLogoHtml, MazLogoMarkdown, MazLogoProperties, MazLogoTxt, MazLogoCsv, MazLogoXls, MazLogoXliff, MazLogoApple, MazLogoAndroid, MazPhoto, MazFilm, MazSpeakerWave, MazArchiveBox, MazCommandLine, MazCodeBracket, MazCog, MazPresentationChartBar, MazDocumentText, MazPencilSquare, MazPaintBrush, MazDocumentIcon, MazCheckCircle, MazXCircle, MazTrash, MazArrowUpOnSquare } from "@maz-ui/icons"; import { useTranslations } from "@maz-ui/translations"; import { s as sleep } from "../chunks/sleep.BLRH1qZG.js"; import { useInstanceUniqId } from "../composables/useInstanceUniqId.js"; import { useDropzone } from "../composables/useDropzone.js"; import MazBtn from "./MazBtn.js"; import MazIcon from "./MazIcon.js"; import MazLink from "./MazLink.js"; import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper.B--vMWp3.js"; import '../assets/MazDropzone.Ch5EPpDi.css';const _hoisted_1 = ["for"], _hoisted_2 = { class: "m-dropzone__icon-container" }, _hoisted_3 = { key: 3, class: "m-dropzone__file-icon-wrapper" }, _hoisted_4 = { class: "m-dropzone__description" }, _hoisted_5 = { class: "m-dropzone__file-info" }, _hoisted_6 = { class: "m-dropzone__file-name" }, _hoisted_7 = { class: "m-dropzone__file-size" }, _hoisted_8 = { class: "m-dropzone__content" }, _hoisted_9 = { class: "m-dropzone__upload-text" }, _hoisted_10 = { key: 0, class: "m-dropzone__info-text" }, _hoisted_11 = { key: 0 }, _hoisted_12 = { key: 1 }, _hoisted_13 = { key: 2 }, _hoisted_14 = ["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" }, removeFileBtnProps: { default: () => ({}) }, spinnerProps: { default: () => ({}) }, autoUpload: { type: [String, Boolean], default: !1 }, url: {}, requestOptions: {}, transformBody: { type: Function }, maxConcurrentUploads: { default: 5 } }, { 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 sleep(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; const queue = [...filesData.value], activeUploads = []; async function uploadFileData(fileData) { 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; } } async function processQueue() { for (; queue.length > 0; ) { const fileData = queue.shift(); fileData && await uploadFileData(fileData); } } for (let i = 0; i < Math.min(__props.maxConcurrentUploads, filesData.value.length); i++) activeUploads.push(processQueue()); await Promise.allSettled(activeUploads); } finally { isUploading.value = !1, await sleep(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 extensionIconMap = (() => { const map = /* @__PURE__ */ new Map(), groups = [ [MazLogoJs, ["js", "mjs", "cjs"]], [MazLogoTypescript, ["ts", "tsx", "mts", "cts"]], [MazLogoVue, ["vue"]], [MazLogoReact, ["jsx"]], [MazLogoJson, ["json", "jsonc", "json5"]], [MazLogoXml, ["xml"]], [MazLogoHtml, ["html", "htm"]], [MazLogoMarkdown, ["md", "markdown", "mdx"]], [MazLogoProperties, ["properties"]], [MazLogoTxt, ["txt"]], [MazLogoCsv, ["csv"]], [MazLogoXls, ["xls", "xlsx"]], [MazLogoXliff, ["xliff", "xlf"]], [MazLogoApple, ["strings", "stringsdict", "xcstrings"]], [MazLogoAndroid, ["apk"]], [MazPhoto, ["jpeg", "jpg", "png", "tiff", "bmp", "webp", "svg", "ico", "gif", "heic", "heif", "avif"]], [MazFilm, ["mp4", "webm", "avi", "mov", "mkv", "flv", "wmv", "m4v", "mpeg", "mpg", "3gp", "ogv"]], [MazSpeakerWave, ["mp3", "wav", "m4a", "aac", "flac", "ogg", "oga", "opus", "wma", "alac", "aiff"]], [MazArchiveBox, ["zip", "rar", "tar", "gz", "7z", "bz2", "xz"]], [MazCommandLine, ["exe", "dll", "so", "dylib", "dmg", "deb", "rpm", "app", "bat", "cmd"]], [MazCodeBracket, ["css", "scss", "sass", "less", "py", "java", "cpp", "c", "h", "hpp", "go", "rs", "rust", "php", "rb", "swift", "kt", "kotlin", "sql", "sh", "bash", "zsh", "yaml", "yml", "toml", "ini"]], [MazCog, ["conf", "config", "env", "cfg"]], [MazPresentationChartBar, ["ppt", "pptx", "key", "odp"]], [MazDocumentText, ["rtf", "odt"]], [MazPencilSquare, ["ttf", "otf", "woff", "woff2", "eot"]], [MazPaintBrush, ["ai", "psd", "sketch", "fig", "xd", "eps"]], [MazDocumentIcon, ["pdf", "doc", "docx", "tsv", "document"]] ]; for (const [icon, extensions] of groups) for (const ext of extensions) map.set(ext, icon); return map; })(); function getIconComponent(fileData) { const type = fileData.file.type.split("/")?.[1]?.split("+")?.[0]?.toLowerCase(), extension = fileData.file.name.split(".").pop()?.toLowerCase(); return extensionIconMap.get(type) || extension && extensionIconMap.get(extension) || extensionIconMap.get("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(formatReadable).join(", ")), allFileIsAccepted = computed(() => __props.dataTypes?.length === 1 && __props.dataTypes[0] === "*/*"), { t } = useTranslations(), messages = computed(() => { const customTranslations = __props.translations || {}; return { dragAndDrop: customTranslations.dragAndDrop ?? t("dropzone.dragAndDrop"), fileMaxCount: __props.maxFiles ? customTranslations.fileMaxCount ?? t("dropzone.fileMaxCount", { count: __props.maxFiles }) : void 0, fileMaxSize: __props.maxFileSize ? customTranslations.fileMaxSize ?? t("dropzone.fileMaxSize", { size: __props.maxFileSize }) : void 0, fileTypes: dataTypesString.value ? customTranslations.fileTypes ?? t("dropzone.fileTypes", { types: dataTypesString.value }) : void 0, selectFile: customTranslations.selectFile ?? t("dropzone.selectFile"), divider: customTranslations.divider ?? t("dropzone.divider") }; }); function formatReadable(fmt) { if (fmt.startsWith(".")) return fmt; if (fmt.includes("/")) { const [type, subtype] = fmt.split("/"); if (subtype === "*") { const customTypes = __props.translations?.types || {}; switch (type) { case "image": return customTypes.image ?? t("dropzone.types.image"); case "video": return customTypes.video ?? t("dropzone.types.video"); case "audio": return customTypes.audio ?? t("dropzone.types.audio"); case "text": return customTypes.text ?? t("dropzone.types.text"); default: return fmt; } } return subtype; } return fmt; } 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.startsWith(".") ? file.name.toLowerCase().endsWith(type.toLowerCase()) : 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": __props.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-${__props.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 && __props.preview ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [ createElementVNode("div", { style: normalizeStyle({ backgroundImage: `url(${file.thumbnail})`, backgroundSize: "cover", backgroundPosition: "center" }), class: "m-dropzone__thumbnail" }, null, 4), _cache[4] || (_cache[4] = createElementVNode("div", { class: "m-dropzone__overlay" }, null, -1)) ], 64)) : createCommentVNode("", !0), createElementVNode("div", _hoisted_2, [ createVNode(Transition, { name: "icon-scale" }, { default: withCtx(() => [ file.uploading ? (openBlock(), createBlock(unref(MazSpinner), mergeProps({ key: 0, color: __props.color, class: "m-dropzone__spinner" }, { ref_for: !0 }, __props.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(), createElementBlock("div", _hoisted_3, [ createVNode(MazIcon, { icon: getIconComponent(file), size: "lg", class: "m-dropzone__file-icon" }, null, 8, ["icon"]) ])) ]), _: 2 }, 1024) ]), createElementVNode("div", _hoisted_4, [ createElementVNode("div", _hoisted_5, [ createElementVNode("span", _hoisted_6, toDisplayString(file.name), 1), createElementVNode("span", _hoisted_7, toDisplayString(file.sizeInMb) + " MB", 1) ]), !file.uploading && !file.success ? (openBlock(), createBlock(MazBtn, mergeProps({ key: 0, size: "xs", icon: unref(MazTrash), disabled: __props.disabled, color: "destructive", pastel: "" }, { ref_for: !0 }, __props.removeFileBtnProps, { onClick: withModifiers(($event) => handleFileRemove(file), ["prevent"]) }), null, 16, ["icon", "disabled", "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_8, [ renderSlot(_ctx.$slots, "upload-icon", {}, () => [ createVNode(unref(MazArrowUpOnSquare), { class: "m-dropzone__upload-icon" }) ], !0), createElementVNode("span", _hoisted_9, [ createTextVNode(toDisplayString(messages.value.dragAndDrop) + " " + toDisplayString(messages.value.divider) + " ", 1), createVNode(MazLink, { color: "inherit", underline: "", onClick: _cache[3] || (_cache[3] = withModifiers(($event) => !__props.disabled && handleFileInputClick(), ["prevent"])) }, { default: withCtx(() => [ createTextVNode(toDisplayString(messages.value.selectFile), 1) ]), _: 1 }) ]) ]), !allFileIsAccepted.value && (messages.value.fileMaxCount || messages.value.fileMaxSize || messages.value.fileTypes) ? (openBlock(), createElementBlock("p", _hoisted_10, [ messages.value.fileMaxCount ? (openBlock(), createElementBlock("span", _hoisted_11, toDisplayString(messages.value.fileMaxCount), 1)) : createCommentVNode("", !0), messages.value.fileMaxSize ? (openBlock(), createElementBlock("span", _hoisted_12, toDisplayString(messages.value.fileMaxSize), 1)) : createCommentVNode("", !0), messages.value.fileTypes ? (openBlock(), createElementBlock("span", _hoisted_13, toDisplayString(messages.value.fileTypes), 1)) : 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: __props.dataTypes?.join(","), tabindex: "-1", disabled: __props.disabled, class: "m-dropzone__file-input", onChange: handleFileUpload }, null, 40, _hoisted_14) ], 14, _hoisted_1)); } }), MazDropzone = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-50e5b169"]]); export { MazDropzone as default };