maz-ui
Version:
A standalone components library for Vue.Js 3 & Nuxt.Js 3
505 lines (504 loc) • 22.1 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 { 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
};