UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

1,284 lines (1,178 loc) 35.9 kB
/** * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved. * Node module: @schukai/monster * * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3). * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html * * For those who do not wish to adhere to the AGPLv3, a commercial license is available. * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms. * For more information about purchasing a commercial license, please contact Volker Schukai. * * SPDX-License-Identifier: AGPL-3.0 */ import { instanceSymbol } from "../../constants.mjs"; import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs"; import { assembleMethodSymbol, CustomElement, registerCustomElement, } from "../../dom/customelement.mjs"; import { findTargetElementFromEvent, fireCustomEvent, } from "../../dom/events.mjs"; import { addErrorAttribute, resetErrorAttribute } from "../../dom/error.mjs"; import { getDocument } from "../../dom/util.mjs"; import { getLocaleOfDocument } from "../../dom/locale.mjs"; import { isFunction, isObject, isString } from "../../types/is.mjs"; import { DropzoneStyleSheet } from "./stylesheet/dropzone.mjs"; import "./button.mjs"; export { Dropzone }; /** * @private * @type {symbol} */ const dropzoneElementSymbol = Symbol("dropzoneElement"); /** * @private * @type {symbol} */ const inputElementSymbol = Symbol("inputElement"); /** * @private * @type {symbol} */ const buttonElementSymbol = Symbol("buttonElement"); /** * @private * @type {symbol} */ const statusElementSymbol = Symbol("statusElement"); /** * @private * @type {symbol} */ const dragCounterSymbol = Symbol("dragCounter"); /** * @private * @type {symbol} */ const listElementSymbol = Symbol("listElement"); /** * @private * @type {symbol} */ const fileItemMapSymbol = Symbol("fileItemMap"); /** * @private * @type {symbol} */ const fileRequestMapSymbol = Symbol("fileRequestMap"); /** * @private * @type {symbol} */ const fileTimeoutMapSymbol = Symbol("fileTimeoutMap"); /** * A Dropzone control * * @fragments /fragments/components/form/dropzone/ * * @example /examples/components/form/dropzone-simple Simple dropzone * @example /examples/components/form/dropzone-avatar Profile image upload * * @since 4.40.0 * @copyright Volker Schukai * @summary A dropzone control for uploading documents via click or drag and drop. * * @fires monster-dropzone-selected * @fires monster-dropzone-file-added * @fires monster-dropzone-file-removed * @fires monster-dropzone-file-retry * @fires monster-dropzone-file-upload-start * @fires monster-dropzone-file-upload-success * @fires monster-dropzone-file-upload-error * @fires monster-dropzone-upload-start * @fires monster-dropzone-upload-success * @fires monster-dropzone-upload-error */ class Dropzone extends CustomElement { /** * This method is called by the `instanceof` operator. * @return {symbol} */ static get [instanceSymbol]() { return Symbol.for("@schukai/monster/components/form/dropzone@@instance"); } /** * To set the options via the HTML tag, the attribute `data-monster-options` must be used. * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control} * * The individual configuration values can be found in the table. * * @property {Object} templates Template definitions * @property {string} templates.main Main template * @property {Object} labels Label definitions * @property {string} labels.title Title text * @property {string} labels.hint Hint text * @property {string} labels.button Button label * @property {string} labels.statusIdle Status text for idle state * @property {string} labels.statusUploading Status text for uploading state * @property {string} labels.statusSuccess Status text for success state * @property {string} labels.statusError Status text for error state * @property {string} labels.statusMissingUrl Status text for missing URL * @property {Object} classes Class definitions * @property {string} classes.dropzone Dropzone CSS class * @property {string} classes.button Monster button class * @property {string} url Upload URL * @property {string} fieldName="files" FormData field name for files * @property {string} accept File input accept attribute * @property {boolean} multiple Allow multiple file selection * @property {boolean} disabled Disable interaction * @property {Object} data Additional data appended to the FormData * @property {Object} features Feature flags * @property {boolean} features.autoUpload Automatically upload after selection * @property {boolean} features.previewImages Show image previews * @property {boolean} features.disappear Enable auto-removal of finished items * @property {Object} disappear Disappear settings * @property {number} disappear.time Delay before auto-removal (ms) * @property {number} disappear.duration Delay before auto-removal (ms) * @property {Object} actions Action definitions for custom event handling * @property {Function} actions.fileAdded Called after a file is added to the list * @property {Function} actions.fileRemoved Called after a file is removed * @property {Function} actions.fileRetry Called before retrying a failed upload * @property {Function} actions.uploadStart Called when uploads start * @property {Function} actions.uploadSuccess Called after successful upload * @property {Function} actions.uploadError Called when upload fails * @property {Function} actions.beforeUpload Called before upload, return false to cancel * @property {Object} fetch Fetch options * @property {string} fetch.method="POST" * @property {string} fetch.redirect="error" * @property {string} fetch.mode="same-origin" * @property {string} fetch.credentials="same-origin" * @property {Object} fetch.headers={"accept":"application/json"} */ get defaults() { return Object.assign({}, super.defaults, { templates: { main: getTemplate(), }, labels: getTranslations(), classes: { dropzone: "monster-dropzone", button: "monster-button-outline-primary", }, url: "", fieldName: "files", accept: "", multiple: true, disabled: false, data: {}, features: { autoUpload: true, previewImages: true, disappear: true, }, disappear: { duration: 3000, }, actions: { fileAdded: null, fileRemoved: null, fileRetry: null, uploadStart: null, uploadSuccess: null, uploadError: null, beforeUpload: null, }, fetch: { method: "POST", redirect: "error", mode: "same-origin", credentials: "same-origin", headers: { accept: "application/json", }, }, }); } /** * */ [assembleMethodSymbol]() { super[assembleMethodSymbol](); initControlReferences.call(this); initEventHandler.call(this); this[fileItemMapSymbol] = new Map(); this[fileRequestMapSymbol] = new Map(); this[fileTimeoutMapSymbol] = new Map(); setStatus.call(this, this.getOption("labels.statusIdle")); } /** * * @return {CSSStyleSheet[]} */ static getCSSStyleSheet() { return [DropzoneStyleSheet]; } /** * * @return {string} */ static getTag() { return "monster-dropzone"; } /** * Open the native file picker. * * @return {void} */ open() { if (this.getOption("disabled") === true) { return; } const input = this[inputElementSymbol]; if (input && typeof input.click === "function") { input.click(); } } /** * Upload files programmatically. * * @param {FileList|File[]} files * @return {Promise<void>} */ upload(files) { const normalized = normalizeFiles(files); if (normalized.length === 0) { return Promise.resolve(); } return uploadFiles.call(this, normalized); } } /** * @private */ function initControlReferences() { this[dropzoneElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=dropzone]`, ); this[inputElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=input]`, ); this[buttonElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=button]`, ); this[statusElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=status]`, ); this[listElementSymbol] = this.shadowRoot.querySelector( `[${ATTRIBUTE_ROLE}=list]`, ); } /** * @private */ function initEventHandler() { this[dragCounterSymbol] = 0; const dropzone = this[dropzoneElementSymbol]; const input = this[inputElementSymbol]; const button = this[buttonElementSymbol]; if (dropzone) { dropzone.addEventListener("dragenter", (event) => { if (this.getOption("disabled") === true) { return; } event.preventDefault(); this[dragCounterSymbol] += 1; setDropActive.call(this, true); }); dropzone.addEventListener("dragover", (event) => { if (this.getOption("disabled") === true) { return; } event.preventDefault(); setDropActive.call(this, true); }); dropzone.addEventListener("dragleave", (event) => { if (this.getOption("disabled") === true) { return; } event.preventDefault(); this[dragCounterSymbol] = Math.max(0, this[dragCounterSymbol] - 1); if (this[dragCounterSymbol] === 0) { setDropActive.call(this, false); } }); dropzone.addEventListener("drop", (event) => { if (this.getOption("disabled") === true) { return; } event.preventDefault(); this[dragCounterSymbol] = 0; setDropActive.call(this, false); const files = event.dataTransfer?.files; handleFiles.call(this, files); }); dropzone.addEventListener("keydown", (event) => { if (this.getOption("disabled") === true) { return; } if (event.key === "Enter" || event.key === " ") { event.preventDefault(); this.open(); } }); } if (input) { input.addEventListener("change", (event) => { const files = event.target?.files; handleFiles.call(this, files); }); } if (button) { button.addEventListener("monster-button-clicked", (event) => { if (this.getOption("disabled") === true) { return; } const element = findTargetElementFromEvent( event, ATTRIBUTE_ROLE, "button", ); if (!(element instanceof Node && this.hasNode(element))) { return; } this.open(); }); } } /** * @private * @param {FileList|File[]} files */ function handleFiles(files) { const normalized = normalizeFiles(files); if (normalized.length === 0) { return; } for (const file of normalized) { addFileItem.call(this, file); fireCustomEvent(this, "monster-dropzone-file-added", { file }); triggerAction.call(this, "fileAdded", { file }); } fireCustomEvent(this, "monster-dropzone-selected", { files: normalized, }); if (this.getOption("features.autoUpload") === true) { uploadFiles.call(this, normalized); } } /** * @private * @param {FileList|File[]} files * @return {File[]} */ function normalizeFiles(files) { if (!files) { return []; } if (Array.isArray(files)) { return files.filter((file) => file instanceof File); } if (files instanceof FileList) { return Array.from(files); } return []; } /** * @private * @param {File[]} files * @return {Promise<void>} */ function uploadFiles(files) { let url = this.getOption("url"); if (!isString(url) || url === "") { const message = this.getOption("labels.statusMissingUrl"); setStatus.call(this, message); addErrorAttribute(this, message); fireCustomEvent(this, "monster-dropzone-upload-error", { files, error: new Error(message), }); return Promise.reject(new Error(message)); } try { url = new URL(url, getDocument().location).toString(); } catch (error) { addErrorAttribute(this, error); setStatus.call(this, this.getOption("labels.statusError")); fireCustomEvent(this, "monster-dropzone-upload-error", { files, error }); return Promise.reject(error); } setStatus.call(this, this.getOption("labels.statusUploading")); fireCustomEvent(this, "monster-dropzone-upload-start", { files, url }); triggerAction.call(this, "uploadStart", { files, url }); const uploads = files.map((file) => { const item = this[fileItemMapSymbol]?.get(file); return uploadSingleFile.call(this, file, url, item); }); return Promise.all(uploads) .then((responses) => { resetErrorAttribute(this); setStatus.call(this, this.getOption("labels.statusSuccess")); fireCustomEvent(this, "monster-dropzone-upload-success", { files, url, response: responses, }); triggerAction.call(this, "uploadSuccess", { files, url, response: responses, }); }) .catch((error) => { addErrorAttribute(this, error); setStatus.call(this, this.getOption("labels.statusError")); fireCustomEvent(this, "monster-dropzone-upload-error", { files, url, error, }); triggerAction.call(this, "uploadError", { files, url, error }); throw error; }); } /** * @private * @param {File} file * @param {string} url * @param {HTMLElement|null} item * @return {Promise<*>} */ function uploadSingleFile(file, url, item) { let formData = new FormData(); const fieldName = this.getOption("fieldName") || "files"; formData.append(fieldName, file, file.name); const extraData = this.getOption("data"); if (isObject(extraData)) { for (const [key, value] of Object.entries(extraData)) { if (value === undefined || value === null) { continue; } formData.append(key, `${value}`); } } const beforeUpload = this.getOption("actions.beforeUpload"); if (isFunction(beforeUpload)) { const result = beforeUpload.call(this, { file, formData, url }); if (result === false) { return Promise.resolve(null); } if (result instanceof FormData) { formData = result; } } return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const fetchOptions = Object.assign({}, this.getOption("fetch", {})); const method = fetchOptions.method || "POST"; xhr.open(method, url); if (fetchOptions.headers && isObject(fetchOptions.headers)) { for (const [key, value] of Object.entries(fetchOptions.headers)) { if (key.toLowerCase() === "content-type") { continue; } xhr.setRequestHeader(key, String(value)); } } const credentials = fetchOptions.credentials || "same-origin"; xhr.withCredentials = credentials === "include"; xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { const percent = Math.round((event.loaded / event.total) * 100); updateFileProgress.call(this, item, percent); } }); xhr.addEventListener("load", () => { const status = xhr.status; const response = parseXhrResponse(xhr); this[fileRequestMapSymbol]?.delete(file); if (status >= 200 && status < 300) { updateFileProgress.call(this, item, 100); setItemState.call(this, item, "success"); fireCustomEvent(this, "monster-dropzone-file-upload-success", { file, url, response, }); triggerAction.call(this, "uploadSuccess", { files: [file], url, response, }); resolve(response); } else { setItemState.call(this, item, "error"); fireCustomEvent(this, "monster-dropzone-file-upload-error", { file, url, error: new Error( `upload failed (${status} ${xhr.statusText || "error"})`, ), }); triggerAction.call(this, "uploadError", { files: [file], url, error: new Error( `upload failed (${status} ${xhr.statusText || "error"})`, ), }); reject( new Error(`upload failed (${status} ${xhr.statusText || "error"})`), ); } }); xhr.addEventListener("error", () => { this[fileRequestMapSymbol]?.delete(file); setItemState.call(this, item, "error"); fireCustomEvent(this, "monster-dropzone-file-upload-error", { file, url, error: new Error("upload failed"), }); triggerAction.call(this, "uploadError", { files: [file], url, error: new Error("upload failed"), }); reject(new Error("upload failed")); }); xhr.addEventListener("abort", () => { this[fileRequestMapSymbol]?.delete(file); setItemState.call(this, item, "error"); fireCustomEvent(this, "monster-dropzone-file-upload-error", { file, url, error: new Error("upload aborted"), }); triggerAction.call(this, "uploadError", { files: [file], url, error: new Error("upload aborted"), }); reject(new Error("upload aborted")); }); setItemState.call(this, item, "uploading"); fireCustomEvent(this, "monster-dropzone-file-upload-start", { file, url }); triggerAction.call(this, "uploadStart", { files: [file], url }); this[fileRequestMapSymbol].set(file, xhr); xhr.send(formData); }); } /** * @private * @param {XMLHttpRequest} xhr * @return {*} */ function parseXhrResponse(xhr) { const contentType = xhr.getResponseHeader("content-type") || ""; if (contentType.includes("application/json")) { try { return JSON.parse(xhr.responseText || "{}"); } catch { return {}; } } return xhr.responseText; } /** * @private * @param {boolean} active */ function setDropActive(active) { const dropzone = this[dropzoneElementSymbol]; if (!dropzone) { return; } dropzone.classList.toggle("is-dragover", active); } /** * @private * @param {string} text */ function setStatus(text) { const status = this[statusElementSymbol]; if (!status) { return; } status.textContent = isString(text) ? text : ""; } /** * @private * @param {File} file */ function addFileItem(file) { const list = this[listElementSymbol]; if (!list || !file) { return; } const item = document.createElement("li"); item.setAttribute("data-monster-role", "item"); const preview = document.createElement("div"); preview.setAttribute("data-monster-role", "preview"); const meta = document.createElement("div"); meta.setAttribute("data-monster-role", "meta"); const name = document.createElement("div"); name.setAttribute("data-monster-role", "name"); name.textContent = file.name; const info = document.createElement("div"); info.setAttribute("data-monster-role", "info"); info.textContent = `${formatFileType(file)} | ${formatFileSize(file.size)}`; const progress = document.createElement("div"); progress.setAttribute("data-monster-role", "progress"); const bar = document.createElement("div"); bar.setAttribute("data-monster-role", "bar"); progress.appendChild(bar); const percent = document.createElement("div"); percent.setAttribute("data-monster-role", "percent"); percent.textContent = "0%"; const stateIcon = document.createElement("div"); stateIcon.setAttribute("data-monster-role", "state-icon"); const removeButton = document.createElement("button"); removeButton.setAttribute("type", "button"); removeButton.setAttribute("data-monster-role", "remove"); removeButton.setAttribute("aria-label", "remove"); removeButton.innerHTML = getIconMarkup("cancel"); const retryButton = document.createElement("button"); retryButton.setAttribute("type", "button"); retryButton.setAttribute("data-monster-role", "retry"); retryButton.setAttribute("aria-label", "retry"); retryButton.innerHTML = getIconMarkup("reload"); removeButton.addEventListener("click", () => { handleItemRemove.call(this, file); }); retryButton.addEventListener("click", () => { handleItemRetry.call(this, file); }); meta.appendChild(name); meta.appendChild(info); item.appendChild(preview); item.appendChild(meta); item.appendChild(progress); item.appendChild(percent); item.appendChild(stateIcon); item.appendChild(retryButton); item.appendChild(removeButton); list.appendChild(item); const showPreview = this.getOption("features.previewImages") === true && isString(file.type) && file.type.startsWith("image/"); if (showPreview) { const img = document.createElement("img"); img.setAttribute("data-monster-role", "preview-image"); img.alt = file.name; const url = URL.createObjectURL(file); img.src = url; img.addEventListener( "load", () => { URL.revokeObjectURL(url); }, { once: true }, ); preview.appendChild(img); } else { const icon = document.createElement("div"); icon.setAttribute("data-monster-role", "preview-icon"); icon.textContent = formatFileExtension(file); preview.appendChild(icon); } this[fileItemMapSymbol].set(file, item); } /** * @private * @param {HTMLElement|null} item * @param {number} percent */ function updateFileProgress(item, percent) { if (!item) { return; } const bar = item.querySelector(`[${ATTRIBUTE_ROLE}=bar]`); if (bar instanceof HTMLElement) { bar.style.width = `${percent}%`; } const label = item.querySelector(`[${ATTRIBUTE_ROLE}=percent]`); if (label instanceof HTMLElement) { label.textContent = `${percent}%`; } } /** * @private * @param {HTMLElement|null} item * @param {string} icon */ /** * @private * @param {HTMLElement|null} item * @param {string} state */ function setItemState(item, state) { if (!item) { return; } item.setAttribute("data-monster-state", state); setStateIcon.call(this, item, state); if (state === "success" || state === "error") { scheduleDisappear.call(this, item); } } /** * @private * @param {HTMLElement|null} item * @param {string} state */ function setStateIcon(item, state) { if (!item) { return; } const target = item.querySelector(`[${ATTRIBUTE_ROLE}=state-icon]`); if (!(target instanceof HTMLElement)) { return; } if (state === "success") { target.innerHTML = getIconMarkup("success"); return; } if (state === "error") { target.innerHTML = getIconMarkup("error"); return; } target.innerHTML = ""; } /** * @private * @param {File} file */ function handleItemRemove(file) { const item = this[fileItemMapSymbol]?.get(file); const xhr = this[fileRequestMapSymbol]?.get(file); const timeout = this[fileTimeoutMapSymbol]?.get(file); if (xhr instanceof XMLHttpRequest) { xhr.abort(); this[fileRequestMapSymbol]?.delete(file); } if (timeout) { clearTimeout(timeout); this[fileTimeoutMapSymbol]?.delete(file); } if (item && item.parentElement) { item.parentElement.removeChild(item); } this[fileItemMapSymbol]?.delete(file); fireCustomEvent(this, "monster-dropzone-file-removed", { file }); triggerAction.call(this, "fileRemoved", { file }); } /** * @private * @param {File} file */ function handleItemRetry(file) { const item = this[fileItemMapSymbol]?.get(file); const timeout = this[fileTimeoutMapSymbol]?.get(file); if (!item) { return; } if (timeout) { clearTimeout(timeout); this[fileTimeoutMapSymbol]?.delete(file); } updateFileProgress.call(this, item, 0); setItemState.call(this, item, "uploading"); fireCustomEvent(this, "monster-dropzone-file-retry", { file }); triggerAction.call(this, "fileRetry", { file }); uploadSingleFile .call(this, file, this.getOption("url"), item) .catch(() => {}); } /** * @private * @param {string} name * @param {object} payload */ function triggerAction(name, payload) { const action = this.getOption(`actions.${name}`); if (isFunction(action)) { action.call(this, payload); } } /** * @private * @param {HTMLElement} item */ function scheduleDisappear(item) { if (this.getOption("features.disappear") !== true) { return; } const delay = this.getOption("disappear.duration") ?? this.getOption("disappear.time") ?? 3000; const file = [...this[fileItemMapSymbol].entries()].find( ([, value]) => value === item, )?.[0]; if (!file) { return; } if (this[fileTimeoutMapSymbol].has(file)) { clearTimeout(this[fileTimeoutMapSymbol].get(file)); } const timeout = setTimeout(() => { item.classList.add("is-disappearing"); setTimeout(() => { handleItemRemove.call(this, file); }, 250); }, delay); this[fileTimeoutMapSymbol].set(file, timeout); } /** * @private * @param {File} file * @return {string} */ function formatFileType(file) { if (isString(file.type) && file.type !== "") { return file.type; } return "unknown"; } /** * @private * @param {File} file * @return {string} */ function formatFileExtension(file) { const parts = file.name.split("."); if (parts.length < 2) { return "file"; } const ext = parts.pop() || "file"; return ext.slice(0, 4).toLowerCase(); } /** * @private * @param {number} bytes * @return {string} */ function formatFileSize(bytes) { if (!Number.isFinite(bytes)) { return "0 B"; } const units = ["B", "KB", "MB", "GB"]; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex += 1; } return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`; } /** * @private * @param {string} kind * @return {string} */ function getIconMarkup(kind) { switch (kind) { case "reload": return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/> <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/> </svg>`; case "success": return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> <path d="m10.97 4.97-.02.022-3.473 4.425-2.093-2.094a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-1.071-1.05"/> </svg>`; case "error": return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/> </svg>`; case "cancel": default: return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708"/> </svg>`; } } /** * @private * @return {string} */ function getTemplate() { // language=HTML return ` <div data-monster-role="control" part="control"> <div data-monster-role="dropzone" part="dropzone" tabindex="0" data-monster-attributes=" class path:classes.dropzone, data-monster-disabled path:disabled | if:true"> <div data-monster-role="content" part="content"> <div data-monster-role="title" part="title" data-monster-replace="path:labels.title"></div> <div data-monster-role="hint" part="hint" data-monster-replace="path:labels.hint"></div> </div> <monster-button data-monster-role="button" part="button" data-monster-attributes=" data-monster-button-class path:classes.button, disabled path:disabled | if:true"> <span data-monster-replace="path:labels.button"></span> </monster-button> <input data-monster-role="input" part="input" type="file" data-monster-attributes=" accept path:accept, multiple path:multiple | if:true, disabled path:disabled | if:true"> </div> <div data-monster-role="status" part="status"></div> <ul data-monster-role="list" part="list"></ul> </div>`; } /** * @private * @returns {object} */ function getTranslations() { const locale = getLocaleOfDocument(); switch (locale.language) { case "de": return { title: "Dokumente hochladen", hint: "oder Dateien hierhin ziehen", button: "Dateien wählen", statusIdle: "", statusUploading: "Upload läuft ...", statusSuccess: "Upload abgeschlossen.", statusError: "Upload fehlgeschlagen.", statusMissingUrl: "Keine Upload-URL konfiguriert.", }; case "es": return { title: "Subir documentos", hint: "o arrastre archivos aquí", button: "Seleccionar archivos", statusIdle: "", statusUploading: "Cargando ...", statusSuccess: "Subida completa.", statusError: "Error al subir.", statusMissingUrl: "No se ha configurado la URL de carga.", }; case "zh": return { title: "上传文档", hint: "或将文件拖到此处", button: "选择文件", statusIdle: "", statusUploading: "正在上传...", statusSuccess: "上传完成。", statusError: "上传失败。", statusMissingUrl: "未配置上传URL。", }; case "hi": return { title: "दस्तावेज़ अपलोड करें", hint: "या फ़ाइलें यहाँ खींचें", button: "फ़ाइलें चुनें", statusIdle: "", statusUploading: "अपलोड हो रहा है...", statusSuccess: "अपलोड पूर्ण हुआ।", statusError: "अपलोड विफल।", statusMissingUrl: "अपलोड URL कॉन्फ़िगर नहीं है।", }; case "bn": return { title: "ডকুমেন্ট আপলোড করুন", hint: "অথবা এখানে ফাইল টেনে আনুন", button: "ফাইল নির্বাচন করুন", statusIdle: "", statusUploading: "আপলোড হচ্ছে...", statusSuccess: "আপলোড সম্পন্ন।", statusError: "আপলোড ব্যর্থ।", statusMissingUrl: "আপলোড URL কনফিগার করা নেই।", }; case "pt": return { title: "Enviar documentos", hint: "ou solte os arquivos aqui", button: "Selecionar arquivos", statusIdle: "", statusUploading: "Enviando...", statusSuccess: "Envio concluído.", statusError: "Falha no envio.", statusMissingUrl: "URL de envio não configurada.", }; case "ru": return { title: "Загрузить документы", hint: "или перетащите файлы сюда", button: "Выбрать файлы", statusIdle: "", statusUploading: "Загрузка...", statusSuccess: "Загрузка завершена.", statusError: "Ошибка загрузки.", statusMissingUrl: "URL загрузки не настроен.", }; case "ja": return { title: "ドキュメントをアップロード", hint: "またはここにファイルをドロップ", button: "ファイルを選択", statusIdle: "", statusUploading: "アップロード中...", statusSuccess: "アップロード完了。", statusError: "アップロードに失敗しました。", statusMissingUrl: "アップロードURLが設定されていません。", }; case "pa": return { title: "ਦਸਤਾਵੇਜ਼ ਅਪਲੋਡ ਕਰੋ", hint: "ਜਾਂ ਫਾਈਲਾਂ ਇੱਥੇ ਖਿੱਚੋ", button: "ਫਾਈਲਾਂ ਚੁਣੋ", statusIdle: "", statusUploading: "ਅਪਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...", statusSuccess: "ਅਪਲੋਡ ਪੂਰਾ ਹੋ ਗਿਆ।", statusError: "ਅਪਲੋਡ ਫੇਲ੍ਹ ਹੋ ਗਿਆ।", statusMissingUrl: "ਅਪਲੋਡ URL ਸੰਰਚਿਤ ਨਹੀਂ ਹੈ।", }; case "mr": return { title: "दस्तऐवज अपलोड करा", hint: "किंवा फायली येथे ओढा", button: "फायली निवडा", statusIdle: "", statusUploading: "अपलोड होत आहे...", statusSuccess: "अपलोड पूर्ण झाले.", statusError: "अपलोड अयशस्वी.", statusMissingUrl: "अपलोड URL संरचीत केलेले नाही.", }; case "fr": return { title: "Téléverser des documents", hint: "ou déposer les fichiers ici", button: "Choisir des fichiers", statusIdle: "", statusUploading: "Téléversement en cours...", statusSuccess: "Téléversement terminé.", statusError: "Échec du téléversement.", statusMissingUrl: "URL de téléversement non configurée.", }; case "it": return { title: "Carica documenti", hint: "oppure trascina i file qui", button: "Seleziona file", statusIdle: "", statusUploading: "Caricamento in corso...", statusSuccess: "Caricamento completato.", statusError: "Caricamento non riuscito.", statusMissingUrl: "URL di caricamento non configurata.", }; case "nl": return { title: "Documenten uploaden", hint: "of sleep bestanden hierheen", button: "Bestanden kiezen", statusIdle: "", statusUploading: "Uploaden...", statusSuccess: "Upload voltooid.", statusError: "Upload mislukt.", statusMissingUrl: "Upload-URL niet geconfigureerd.", }; case "sv": return { title: "Ladda upp dokument", hint: "eller dra filer hit", button: "Välj filer", statusIdle: "", statusUploading: "Uppladdning pågår...", statusSuccess: "Uppladdning klar.", statusError: "Uppladdning misslyckades.", statusMissingUrl: "Ingen uppladdnings-URL konfigurerad.", }; case "pl": return { title: "Prześlij dokumenty", hint: "lub upuść pliki tutaj", button: "Wybierz pliki", statusIdle: "", statusUploading: "Przesyłanie...", statusSuccess: "Przesyłanie zakończone.", statusError: "Przesyłanie nieudane.", statusMissingUrl: "Brak skonfigurowanego URL przesyłania.", }; case "da": return { title: "Upload dokumenter", hint: "eller træk filer her", button: "Vælg filer", statusIdle: "", statusUploading: "Uploader...", statusSuccess: "Upload fuldført.", statusError: "Upload mislykkedes.", statusMissingUrl: "Ingen upload-URL konfigureret.", }; case "fi": return { title: "Lataa asiakirjoja", hint: "tai pudota tiedostot tähän", button: "Valitse tiedostot", statusIdle: "", statusUploading: "Siirretään...", statusSuccess: "Siirto valmis.", statusError: "Siirto epäonnistui.", statusMissingUrl: "Siirto-URL ei ole määritetty.", }; case "no": return { title: "Last opp dokumenter", hint: "eller dra filer hit", button: "Velg filer", statusIdle: "", statusUploading: "Laster opp...", statusSuccess: "Opplasting fullført.", statusError: "Opplasting feilet.", statusMissingUrl: "Ingen opplastings-URL konfigurert.", }; case "cs": return { title: "Nahrát dokumenty", hint: "nebo přetáhněte soubory sem", button: "Vybrat soubory", statusIdle: "", statusUploading: "Nahrávání...", statusSuccess: "Nahrávání dokončeno.", statusError: "Nahrávání selhalo.", statusMissingUrl: "URL pro nahrávání není nastavena.", }; default: case "en": return { title: "Upload documents", hint: "or drop files here", button: "Choose files", statusIdle: "", statusUploading: "Uploading ...", statusSuccess: "Upload complete.", statusError: "Upload failed.", statusMissingUrl: "No upload URL configured.", }; } } registerCustomElement(Dropzone);