@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,284 lines (1,178 loc) • 35.9 kB
JavaScript
/**
* 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);