maz-ui
Version:
A standalone components library for Vue.Js 3 & Nuxt.Js 3
534 lines (533 loc) • 24.7 kB
JavaScript
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
};