@schukai/monster 4.60.0 → 4.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/source/components/datatable/filter.mjs +0 -7
- package/source/components/form/dropzone.mjs +1282 -0
- package/source/components/form/style/dropzone.pcss +213 -0
- package/source/components/form/stylesheet/dropzone.mjs +41 -0
- package/source/monster.mjs +1 -0
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
|
|
3
|
+
* Node module: @schukai/monster
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
|
|
6
|
+
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
|
|
7
|
+
*
|
|
8
|
+
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
|
|
9
|
+
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
|
|
10
|
+
* For more information about purchasing a commercial license, please contact Volker Schukai.
|
|
11
|
+
*
|
|
12
|
+
* SPDX-License-Identifier: AGPL-3.0
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { instanceSymbol } from "../../constants.mjs";
|
|
16
|
+
import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
|
|
17
|
+
import {
|
|
18
|
+
assembleMethodSymbol,
|
|
19
|
+
CustomElement,
|
|
20
|
+
registerCustomElement,
|
|
21
|
+
} from "../../dom/customelement.mjs";
|
|
22
|
+
import {
|
|
23
|
+
findTargetElementFromEvent,
|
|
24
|
+
fireCustomEvent,
|
|
25
|
+
} from "../../dom/events.mjs";
|
|
26
|
+
import { addErrorAttribute, resetErrorAttribute } from "../../dom/error.mjs";
|
|
27
|
+
import { getDocument } from "../../dom/util.mjs";
|
|
28
|
+
import { getLocaleOfDocument } from "../../dom/locale.mjs";
|
|
29
|
+
import { isFunction, isObject, isString } from "../../types/is.mjs";
|
|
30
|
+
import { DropzoneStyleSheet } from "./stylesheet/dropzone.mjs";
|
|
31
|
+
|
|
32
|
+
import "./button.mjs";
|
|
33
|
+
|
|
34
|
+
export { Dropzone };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @private
|
|
38
|
+
* @type {symbol}
|
|
39
|
+
*/
|
|
40
|
+
const dropzoneElementSymbol = Symbol("dropzoneElement");
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @private
|
|
44
|
+
* @type {symbol}
|
|
45
|
+
*/
|
|
46
|
+
const inputElementSymbol = Symbol("inputElement");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @private
|
|
50
|
+
* @type {symbol}
|
|
51
|
+
*/
|
|
52
|
+
const buttonElementSymbol = Symbol("buttonElement");
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @private
|
|
56
|
+
* @type {symbol}
|
|
57
|
+
*/
|
|
58
|
+
const statusElementSymbol = Symbol("statusElement");
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @private
|
|
62
|
+
* @type {symbol}
|
|
63
|
+
*/
|
|
64
|
+
const dragCounterSymbol = Symbol("dragCounter");
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @private
|
|
68
|
+
* @type {symbol}
|
|
69
|
+
*/
|
|
70
|
+
const listElementSymbol = Symbol("listElement");
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @private
|
|
74
|
+
* @type {symbol}
|
|
75
|
+
*/
|
|
76
|
+
const fileItemMapSymbol = Symbol("fileItemMap");
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @private
|
|
80
|
+
* @type {symbol}
|
|
81
|
+
*/
|
|
82
|
+
const fileRequestMapSymbol = Symbol("fileRequestMap");
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @private
|
|
86
|
+
* @type {symbol}
|
|
87
|
+
*/
|
|
88
|
+
const fileTimeoutMapSymbol = Symbol("fileTimeoutMap");
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A Dropzone control
|
|
92
|
+
*
|
|
93
|
+
* @fragments /fragments/components/form/dropzone/
|
|
94
|
+
*
|
|
95
|
+
* @example /examples/components/form/dropzone-simple
|
|
96
|
+
*
|
|
97
|
+
* @since 4.40.0
|
|
98
|
+
* @copyright Volker Schukai
|
|
99
|
+
* @summary A dropzone control for uploading documents via click or drag and drop.
|
|
100
|
+
*
|
|
101
|
+
* @fires monster-dropzone-selected
|
|
102
|
+
* @fires monster-dropzone-file-added
|
|
103
|
+
* @fires monster-dropzone-file-removed
|
|
104
|
+
* @fires monster-dropzone-file-retry
|
|
105
|
+
* @fires monster-dropzone-file-upload-start
|
|
106
|
+
* @fires monster-dropzone-file-upload-success
|
|
107
|
+
* @fires monster-dropzone-file-upload-error
|
|
108
|
+
* @fires monster-dropzone-upload-start
|
|
109
|
+
* @fires monster-dropzone-upload-success
|
|
110
|
+
* @fires monster-dropzone-upload-error
|
|
111
|
+
*/
|
|
112
|
+
class Dropzone extends CustomElement {
|
|
113
|
+
/**
|
|
114
|
+
* This method is called by the `instanceof` operator.
|
|
115
|
+
* @return {symbol}
|
|
116
|
+
*/
|
|
117
|
+
static get [instanceSymbol]() {
|
|
118
|
+
return Symbol.for("@schukai/monster/components/form/dropzone@@instance");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
|
|
123
|
+
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
|
|
124
|
+
*
|
|
125
|
+
* The individual configuration values can be found in the table.
|
|
126
|
+
*
|
|
127
|
+
* @property {Object} templates Template definitions
|
|
128
|
+
* @property {string} templates.main Main template
|
|
129
|
+
* @property {Object} labels Label definitions
|
|
130
|
+
* @property {string} labels.title Title text
|
|
131
|
+
* @property {string} labels.hint Hint text
|
|
132
|
+
* @property {string} labels.button Button label
|
|
133
|
+
* @property {string} labels.statusIdle Status text for idle state
|
|
134
|
+
* @property {string} labels.statusUploading Status text for uploading state
|
|
135
|
+
* @property {string} labels.statusSuccess Status text for success state
|
|
136
|
+
* @property {string} labels.statusError Status text for error state
|
|
137
|
+
* @property {string} labels.statusMissingUrl Status text for missing URL
|
|
138
|
+
* @property {Object} classes Class definitions
|
|
139
|
+
* @property {string} classes.dropzone Dropzone CSS class
|
|
140
|
+
* @property {string} classes.button Monster button class
|
|
141
|
+
* @property {string} url Upload URL
|
|
142
|
+
* @property {string} fieldName="files" FormData field name for files
|
|
143
|
+
* @property {string} accept File input accept attribute
|
|
144
|
+
* @property {boolean} multiple Allow multiple file selection
|
|
145
|
+
* @property {boolean} disabled Disable interaction
|
|
146
|
+
* @property {Object} data Additional data appended to the FormData
|
|
147
|
+
* @property {Object} features Feature flags
|
|
148
|
+
* @property {boolean} features.autoUpload Automatically upload after selection
|
|
149
|
+
* @property {boolean} features.previewImages Show image previews
|
|
150
|
+
* @property {boolean} features.disappear Enable auto-removal of finished items
|
|
151
|
+
* @property {Object} disappear Disappear settings
|
|
152
|
+
* @property {number} disappear.time Delay before auto-removal (ms)
|
|
153
|
+
* @property {number} disappear.duration Delay before auto-removal (ms)
|
|
154
|
+
* @property {Object} actions Action definitions for custom event handling
|
|
155
|
+
* @property {Function} actions.fileAdded Called after a file is added to the list
|
|
156
|
+
* @property {Function} actions.fileRemoved Called after a file is removed
|
|
157
|
+
* @property {Function} actions.fileRetry Called before retrying a failed upload
|
|
158
|
+
* @property {Function} actions.uploadStart Called when uploads start
|
|
159
|
+
* @property {Function} actions.uploadSuccess Called after successful upload
|
|
160
|
+
* @property {Function} actions.uploadError Called when upload fails
|
|
161
|
+
* @property {Function} actions.beforeUpload Called before upload, return false to cancel
|
|
162
|
+
* @property {Object} fetch Fetch options
|
|
163
|
+
* @property {string} fetch.method="POST"
|
|
164
|
+
* @property {string} fetch.redirect="error"
|
|
165
|
+
* @property {string} fetch.mode="same-origin"
|
|
166
|
+
* @property {string} fetch.credentials="same-origin"
|
|
167
|
+
* @property {Object} fetch.headers={"accept":"application/json"}
|
|
168
|
+
*/
|
|
169
|
+
get defaults() {
|
|
170
|
+
return Object.assign({}, super.defaults, {
|
|
171
|
+
templates: {
|
|
172
|
+
main: getTemplate(),
|
|
173
|
+
},
|
|
174
|
+
labels: getTranslations(),
|
|
175
|
+
classes: {
|
|
176
|
+
dropzone: "monster-dropzone",
|
|
177
|
+
button: "monster-button-outline-primary",
|
|
178
|
+
},
|
|
179
|
+
url: "",
|
|
180
|
+
fieldName: "files",
|
|
181
|
+
accept: "",
|
|
182
|
+
multiple: true,
|
|
183
|
+
disabled: false,
|
|
184
|
+
data: {},
|
|
185
|
+
features: {
|
|
186
|
+
autoUpload: true,
|
|
187
|
+
previewImages: true,
|
|
188
|
+
disappear: true,
|
|
189
|
+
},
|
|
190
|
+
disappear: {
|
|
191
|
+
duration: 3000,
|
|
192
|
+
},
|
|
193
|
+
actions: {
|
|
194
|
+
fileAdded: null,
|
|
195
|
+
fileRemoved: null,
|
|
196
|
+
fileRetry: null,
|
|
197
|
+
uploadStart: null,
|
|
198
|
+
uploadSuccess: null,
|
|
199
|
+
uploadError: null,
|
|
200
|
+
beforeUpload: null,
|
|
201
|
+
},
|
|
202
|
+
fetch: {
|
|
203
|
+
method: "POST",
|
|
204
|
+
redirect: "error",
|
|
205
|
+
mode: "same-origin",
|
|
206
|
+
credentials: "same-origin",
|
|
207
|
+
headers: {
|
|
208
|
+
accept: "application/json",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
*
|
|
216
|
+
*/
|
|
217
|
+
[assembleMethodSymbol]() {
|
|
218
|
+
super[assembleMethodSymbol]();
|
|
219
|
+
initControlReferences.call(this);
|
|
220
|
+
initEventHandler.call(this);
|
|
221
|
+
this[fileItemMapSymbol] = new Map();
|
|
222
|
+
this[fileRequestMapSymbol] = new Map();
|
|
223
|
+
this[fileTimeoutMapSymbol] = new Map();
|
|
224
|
+
setStatus.call(this, this.getOption("labels.statusIdle"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
*
|
|
229
|
+
* @return {CSSStyleSheet[]}
|
|
230
|
+
*/
|
|
231
|
+
static getCSSStyleSheet() {
|
|
232
|
+
return [DropzoneStyleSheet];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
*
|
|
237
|
+
* @return {string}
|
|
238
|
+
*/
|
|
239
|
+
static getTag() {
|
|
240
|
+
return "monster-dropzone";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Open the native file picker.
|
|
245
|
+
*
|
|
246
|
+
* @return {void}
|
|
247
|
+
*/
|
|
248
|
+
open() {
|
|
249
|
+
if (this.getOption("disabled") === true) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const input = this[inputElementSymbol];
|
|
254
|
+
if (input && typeof input.click === "function") {
|
|
255
|
+
input.click();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Upload files programmatically.
|
|
261
|
+
*
|
|
262
|
+
* @param {FileList|File[]} files
|
|
263
|
+
* @return {Promise<void>}
|
|
264
|
+
*/
|
|
265
|
+
upload(files) {
|
|
266
|
+
const normalized = normalizeFiles(files);
|
|
267
|
+
if (normalized.length === 0) {
|
|
268
|
+
return Promise.resolve();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return uploadFiles.call(this, normalized);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
function initControlReferences() {
|
|
279
|
+
this[dropzoneElementSymbol] = this.shadowRoot.querySelector(
|
|
280
|
+
`[${ATTRIBUTE_ROLE}=dropzone]`,
|
|
281
|
+
);
|
|
282
|
+
this[inputElementSymbol] = this.shadowRoot.querySelector(
|
|
283
|
+
`[${ATTRIBUTE_ROLE}=input]`,
|
|
284
|
+
);
|
|
285
|
+
this[buttonElementSymbol] = this.shadowRoot.querySelector(
|
|
286
|
+
`[${ATTRIBUTE_ROLE}=button]`,
|
|
287
|
+
);
|
|
288
|
+
this[statusElementSymbol] = this.shadowRoot.querySelector(
|
|
289
|
+
`[${ATTRIBUTE_ROLE}=status]`,
|
|
290
|
+
);
|
|
291
|
+
this[listElementSymbol] = this.shadowRoot.querySelector(
|
|
292
|
+
`[${ATTRIBUTE_ROLE}=list]`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
function initEventHandler() {
|
|
300
|
+
this[dragCounterSymbol] = 0;
|
|
301
|
+
|
|
302
|
+
const dropzone = this[dropzoneElementSymbol];
|
|
303
|
+
const input = this[inputElementSymbol];
|
|
304
|
+
const button = this[buttonElementSymbol];
|
|
305
|
+
|
|
306
|
+
if (dropzone) {
|
|
307
|
+
dropzone.addEventListener("dragenter", (event) => {
|
|
308
|
+
if (this.getOption("disabled") === true) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
this[dragCounterSymbol] += 1;
|
|
313
|
+
setDropActive.call(this, true);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
dropzone.addEventListener("dragover", (event) => {
|
|
317
|
+
if (this.getOption("disabled") === true) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
setDropActive.call(this, true);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
dropzone.addEventListener("dragleave", (event) => {
|
|
325
|
+
if (this.getOption("disabled") === true) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
this[dragCounterSymbol] = Math.max(0, this[dragCounterSymbol] - 1);
|
|
330
|
+
if (this[dragCounterSymbol] === 0) {
|
|
331
|
+
setDropActive.call(this, false);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
dropzone.addEventListener("drop", (event) => {
|
|
336
|
+
if (this.getOption("disabled") === true) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
this[dragCounterSymbol] = 0;
|
|
341
|
+
setDropActive.call(this, false);
|
|
342
|
+
const files = event.dataTransfer?.files;
|
|
343
|
+
handleFiles.call(this, files);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
dropzone.addEventListener("keydown", (event) => {
|
|
347
|
+
if (this.getOption("disabled") === true) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
this.open();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (input) {
|
|
358
|
+
input.addEventListener("change", (event) => {
|
|
359
|
+
const files = event.target?.files;
|
|
360
|
+
handleFiles.call(this, files);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (button) {
|
|
365
|
+
button.addEventListener("monster-button-clicked", (event) => {
|
|
366
|
+
if (this.getOption("disabled") === true) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const element = findTargetElementFromEvent(
|
|
371
|
+
event,
|
|
372
|
+
ATTRIBUTE_ROLE,
|
|
373
|
+
"button",
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (!(element instanceof Node && this.hasNode(element))) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.open();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* @private
|
|
387
|
+
* @param {FileList|File[]} files
|
|
388
|
+
*/
|
|
389
|
+
function handleFiles(files) {
|
|
390
|
+
const normalized = normalizeFiles(files);
|
|
391
|
+
if (normalized.length === 0) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const file of normalized) {
|
|
396
|
+
addFileItem.call(this, file);
|
|
397
|
+
fireCustomEvent(this, "monster-dropzone-file-added", { file });
|
|
398
|
+
triggerAction.call(this, "fileAdded", { file });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fireCustomEvent(this, "monster-dropzone-selected", {
|
|
402
|
+
files: normalized,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (this.getOption("features.autoUpload") === true) {
|
|
406
|
+
uploadFiles.call(this, normalized);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @private
|
|
412
|
+
* @param {FileList|File[]} files
|
|
413
|
+
* @return {File[]}
|
|
414
|
+
*/
|
|
415
|
+
function normalizeFiles(files) {
|
|
416
|
+
if (!files) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (Array.isArray(files)) {
|
|
421
|
+
return files.filter((file) => file instanceof File);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (files instanceof FileList) {
|
|
425
|
+
return Array.from(files);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @private
|
|
433
|
+
* @param {File[]} files
|
|
434
|
+
* @return {Promise<void>}
|
|
435
|
+
*/
|
|
436
|
+
function uploadFiles(files) {
|
|
437
|
+
let url = this.getOption("url");
|
|
438
|
+
if (!isString(url) || url === "") {
|
|
439
|
+
const message = this.getOption("labels.statusMissingUrl");
|
|
440
|
+
setStatus.call(this, message);
|
|
441
|
+
addErrorAttribute(this, message);
|
|
442
|
+
fireCustomEvent(this, "monster-dropzone-upload-error", {
|
|
443
|
+
files,
|
|
444
|
+
error: new Error(message),
|
|
445
|
+
});
|
|
446
|
+
return Promise.reject(new Error(message));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
url = new URL(url, getDocument().location).toString();
|
|
451
|
+
} catch (error) {
|
|
452
|
+
addErrorAttribute(this, error);
|
|
453
|
+
setStatus.call(this, this.getOption("labels.statusError"));
|
|
454
|
+
fireCustomEvent(this, "monster-dropzone-upload-error", { files, error });
|
|
455
|
+
return Promise.reject(error);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
setStatus.call(this, this.getOption("labels.statusUploading"));
|
|
459
|
+
fireCustomEvent(this, "monster-dropzone-upload-start", { files, url });
|
|
460
|
+
triggerAction.call(this, "uploadStart", { files, url });
|
|
461
|
+
|
|
462
|
+
const uploads = files.map((file) => {
|
|
463
|
+
const item = this[fileItemMapSymbol]?.get(file);
|
|
464
|
+
return uploadSingleFile.call(this, file, url, item);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return Promise.all(uploads)
|
|
468
|
+
.then((responses) => {
|
|
469
|
+
resetErrorAttribute(this);
|
|
470
|
+
setStatus.call(this, this.getOption("labels.statusSuccess"));
|
|
471
|
+
fireCustomEvent(this, "monster-dropzone-upload-success", {
|
|
472
|
+
files,
|
|
473
|
+
url,
|
|
474
|
+
response: responses,
|
|
475
|
+
});
|
|
476
|
+
triggerAction.call(this, "uploadSuccess", {
|
|
477
|
+
files,
|
|
478
|
+
url,
|
|
479
|
+
response: responses,
|
|
480
|
+
});
|
|
481
|
+
})
|
|
482
|
+
.catch((error) => {
|
|
483
|
+
addErrorAttribute(this, error);
|
|
484
|
+
setStatus.call(this, this.getOption("labels.statusError"));
|
|
485
|
+
fireCustomEvent(this, "monster-dropzone-upload-error", {
|
|
486
|
+
files,
|
|
487
|
+
url,
|
|
488
|
+
error,
|
|
489
|
+
});
|
|
490
|
+
triggerAction.call(this, "uploadError", { files, url, error });
|
|
491
|
+
|
|
492
|
+
throw error;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* @private
|
|
498
|
+
* @param {File} file
|
|
499
|
+
* @param {string} url
|
|
500
|
+
* @param {HTMLElement|null} item
|
|
501
|
+
* @return {Promise<*>}
|
|
502
|
+
*/
|
|
503
|
+
function uploadSingleFile(file, url, item) {
|
|
504
|
+
let formData = new FormData();
|
|
505
|
+
const fieldName = this.getOption("fieldName") || "files";
|
|
506
|
+
formData.append(fieldName, file, file.name);
|
|
507
|
+
|
|
508
|
+
const extraData = this.getOption("data");
|
|
509
|
+
if (isObject(extraData)) {
|
|
510
|
+
for (const [key, value] of Object.entries(extraData)) {
|
|
511
|
+
if (value === undefined || value === null) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
formData.append(key, `${value}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const beforeUpload = this.getOption("actions.beforeUpload");
|
|
519
|
+
if (isFunction(beforeUpload)) {
|
|
520
|
+
const result = beforeUpload.call(this, { file, formData, url });
|
|
521
|
+
if (result === false) {
|
|
522
|
+
return Promise.resolve(null);
|
|
523
|
+
}
|
|
524
|
+
if (result instanceof FormData) {
|
|
525
|
+
formData = result;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return new Promise((resolve, reject) => {
|
|
530
|
+
const xhr = new XMLHttpRequest();
|
|
531
|
+
const fetchOptions = Object.assign({}, this.getOption("fetch", {}));
|
|
532
|
+
const method = fetchOptions.method || "POST";
|
|
533
|
+
|
|
534
|
+
xhr.open(method, url);
|
|
535
|
+
|
|
536
|
+
if (fetchOptions.headers && isObject(fetchOptions.headers)) {
|
|
537
|
+
for (const [key, value] of Object.entries(fetchOptions.headers)) {
|
|
538
|
+
if (key.toLowerCase() === "content-type") {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
xhr.setRequestHeader(key, String(value));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const credentials = fetchOptions.credentials || "same-origin";
|
|
546
|
+
xhr.withCredentials = credentials === "include";
|
|
547
|
+
|
|
548
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
549
|
+
if (event.lengthComputable) {
|
|
550
|
+
const percent = Math.round((event.loaded / event.total) * 100);
|
|
551
|
+
updateFileProgress.call(this, item, percent);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
xhr.addEventListener("load", () => {
|
|
556
|
+
const status = xhr.status;
|
|
557
|
+
const response = parseXhrResponse(xhr);
|
|
558
|
+
this[fileRequestMapSymbol]?.delete(file);
|
|
559
|
+
if (status >= 200 && status < 300) {
|
|
560
|
+
updateFileProgress.call(this, item, 100);
|
|
561
|
+
setItemState.call(this, item, "success");
|
|
562
|
+
fireCustomEvent(this, "monster-dropzone-file-upload-success", {
|
|
563
|
+
file,
|
|
564
|
+
url,
|
|
565
|
+
response,
|
|
566
|
+
});
|
|
567
|
+
triggerAction.call(this, "uploadSuccess", {
|
|
568
|
+
files: [file],
|
|
569
|
+
url,
|
|
570
|
+
response,
|
|
571
|
+
});
|
|
572
|
+
resolve(response);
|
|
573
|
+
} else {
|
|
574
|
+
setItemState.call(this, item, "error");
|
|
575
|
+
fireCustomEvent(this, "monster-dropzone-file-upload-error", {
|
|
576
|
+
file,
|
|
577
|
+
url,
|
|
578
|
+
error: new Error(
|
|
579
|
+
`upload failed (${status} ${xhr.statusText || "error"})`,
|
|
580
|
+
),
|
|
581
|
+
});
|
|
582
|
+
triggerAction.call(this, "uploadError", {
|
|
583
|
+
files: [file],
|
|
584
|
+
url,
|
|
585
|
+
error: new Error(
|
|
586
|
+
`upload failed (${status} ${xhr.statusText || "error"})`,
|
|
587
|
+
),
|
|
588
|
+
});
|
|
589
|
+
reject(
|
|
590
|
+
new Error(`upload failed (${status} ${xhr.statusText || "error"})`),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
xhr.addEventListener("error", () => {
|
|
596
|
+
this[fileRequestMapSymbol]?.delete(file);
|
|
597
|
+
setItemState.call(this, item, "error");
|
|
598
|
+
fireCustomEvent(this, "monster-dropzone-file-upload-error", {
|
|
599
|
+
file,
|
|
600
|
+
url,
|
|
601
|
+
error: new Error("upload failed"),
|
|
602
|
+
});
|
|
603
|
+
triggerAction.call(this, "uploadError", {
|
|
604
|
+
files: [file],
|
|
605
|
+
url,
|
|
606
|
+
error: new Error("upload failed"),
|
|
607
|
+
});
|
|
608
|
+
reject(new Error("upload failed"));
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
xhr.addEventListener("abort", () => {
|
|
612
|
+
this[fileRequestMapSymbol]?.delete(file);
|
|
613
|
+
setItemState.call(this, item, "error");
|
|
614
|
+
fireCustomEvent(this, "monster-dropzone-file-upload-error", {
|
|
615
|
+
file,
|
|
616
|
+
url,
|
|
617
|
+
error: new Error("upload aborted"),
|
|
618
|
+
});
|
|
619
|
+
triggerAction.call(this, "uploadError", {
|
|
620
|
+
files: [file],
|
|
621
|
+
url,
|
|
622
|
+
error: new Error("upload aborted"),
|
|
623
|
+
});
|
|
624
|
+
reject(new Error("upload aborted"));
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
setItemState.call(this, item, "uploading");
|
|
628
|
+
fireCustomEvent(this, "monster-dropzone-file-upload-start", { file, url });
|
|
629
|
+
triggerAction.call(this, "uploadStart", { files: [file], url });
|
|
630
|
+
this[fileRequestMapSymbol].set(file, xhr);
|
|
631
|
+
xhr.send(formData);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* @private
|
|
637
|
+
* @param {XMLHttpRequest} xhr
|
|
638
|
+
* @return {*}
|
|
639
|
+
*/
|
|
640
|
+
function parseXhrResponse(xhr) {
|
|
641
|
+
const contentType = xhr.getResponseHeader("content-type") || "";
|
|
642
|
+
if (contentType.includes("application/json")) {
|
|
643
|
+
try {
|
|
644
|
+
return JSON.parse(xhr.responseText || "{}");
|
|
645
|
+
} catch {
|
|
646
|
+
return {};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return xhr.responseText;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* @private
|
|
654
|
+
* @param {boolean} active
|
|
655
|
+
*/
|
|
656
|
+
function setDropActive(active) {
|
|
657
|
+
const dropzone = this[dropzoneElementSymbol];
|
|
658
|
+
if (!dropzone) {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
dropzone.classList.toggle("is-dragover", active);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* @private
|
|
666
|
+
* @param {string} text
|
|
667
|
+
*/
|
|
668
|
+
function setStatus(text) {
|
|
669
|
+
const status = this[statusElementSymbol];
|
|
670
|
+
if (!status) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
status.textContent = isString(text) ? text : "";
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* @private
|
|
678
|
+
* @param {File} file
|
|
679
|
+
*/
|
|
680
|
+
function addFileItem(file) {
|
|
681
|
+
const list = this[listElementSymbol];
|
|
682
|
+
if (!list || !file) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const item = document.createElement("li");
|
|
687
|
+
item.setAttribute("data-monster-role", "item");
|
|
688
|
+
|
|
689
|
+
const preview = document.createElement("div");
|
|
690
|
+
preview.setAttribute("data-monster-role", "preview");
|
|
691
|
+
|
|
692
|
+
const meta = document.createElement("div");
|
|
693
|
+
meta.setAttribute("data-monster-role", "meta");
|
|
694
|
+
|
|
695
|
+
const name = document.createElement("div");
|
|
696
|
+
name.setAttribute("data-monster-role", "name");
|
|
697
|
+
name.textContent = file.name;
|
|
698
|
+
|
|
699
|
+
const info = document.createElement("div");
|
|
700
|
+
info.setAttribute("data-monster-role", "info");
|
|
701
|
+
info.textContent = `${formatFileType(file)} | ${formatFileSize(file.size)}`;
|
|
702
|
+
|
|
703
|
+
const progress = document.createElement("div");
|
|
704
|
+
progress.setAttribute("data-monster-role", "progress");
|
|
705
|
+
|
|
706
|
+
const bar = document.createElement("div");
|
|
707
|
+
bar.setAttribute("data-monster-role", "bar");
|
|
708
|
+
progress.appendChild(bar);
|
|
709
|
+
|
|
710
|
+
const percent = document.createElement("div");
|
|
711
|
+
percent.setAttribute("data-monster-role", "percent");
|
|
712
|
+
percent.textContent = "0%";
|
|
713
|
+
|
|
714
|
+
const stateIcon = document.createElement("div");
|
|
715
|
+
stateIcon.setAttribute("data-monster-role", "state-icon");
|
|
716
|
+
|
|
717
|
+
const removeButton = document.createElement("button");
|
|
718
|
+
removeButton.setAttribute("type", "button");
|
|
719
|
+
removeButton.setAttribute("data-monster-role", "remove");
|
|
720
|
+
removeButton.setAttribute("aria-label", "remove");
|
|
721
|
+
removeButton.innerHTML = getIconMarkup("cancel");
|
|
722
|
+
|
|
723
|
+
const retryButton = document.createElement("button");
|
|
724
|
+
retryButton.setAttribute("type", "button");
|
|
725
|
+
retryButton.setAttribute("data-monster-role", "retry");
|
|
726
|
+
retryButton.setAttribute("aria-label", "retry");
|
|
727
|
+
retryButton.innerHTML = getIconMarkup("reload");
|
|
728
|
+
|
|
729
|
+
removeButton.addEventListener("click", () => {
|
|
730
|
+
handleItemRemove.call(this, file);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
retryButton.addEventListener("click", () => {
|
|
734
|
+
handleItemRetry.call(this, file);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
meta.appendChild(name);
|
|
738
|
+
meta.appendChild(info);
|
|
739
|
+
|
|
740
|
+
item.appendChild(preview);
|
|
741
|
+
item.appendChild(meta);
|
|
742
|
+
item.appendChild(progress);
|
|
743
|
+
item.appendChild(percent);
|
|
744
|
+
item.appendChild(stateIcon);
|
|
745
|
+
item.appendChild(retryButton);
|
|
746
|
+
item.appendChild(removeButton);
|
|
747
|
+
list.appendChild(item);
|
|
748
|
+
|
|
749
|
+
const showPreview =
|
|
750
|
+
this.getOption("features.previewImages") === true &&
|
|
751
|
+
isString(file.type) &&
|
|
752
|
+
file.type.startsWith("image/");
|
|
753
|
+
|
|
754
|
+
if (showPreview) {
|
|
755
|
+
const img = document.createElement("img");
|
|
756
|
+
img.setAttribute("data-monster-role", "preview-image");
|
|
757
|
+
img.alt = file.name;
|
|
758
|
+
const url = URL.createObjectURL(file);
|
|
759
|
+
img.src = url;
|
|
760
|
+
img.addEventListener(
|
|
761
|
+
"load",
|
|
762
|
+
() => {
|
|
763
|
+
URL.revokeObjectURL(url);
|
|
764
|
+
},
|
|
765
|
+
{ once: true },
|
|
766
|
+
);
|
|
767
|
+
preview.appendChild(img);
|
|
768
|
+
} else {
|
|
769
|
+
const icon = document.createElement("div");
|
|
770
|
+
icon.setAttribute("data-monster-role", "preview-icon");
|
|
771
|
+
icon.textContent = formatFileExtension(file);
|
|
772
|
+
preview.appendChild(icon);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
this[fileItemMapSymbol].set(file, item);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* @private
|
|
780
|
+
* @param {HTMLElement|null} item
|
|
781
|
+
* @param {number} percent
|
|
782
|
+
*/
|
|
783
|
+
function updateFileProgress(item, percent) {
|
|
784
|
+
if (!item) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const bar = item.querySelector(`[${ATTRIBUTE_ROLE}=bar]`);
|
|
788
|
+
if (bar instanceof HTMLElement) {
|
|
789
|
+
bar.style.width = `${percent}%`;
|
|
790
|
+
}
|
|
791
|
+
const label = item.querySelector(`[${ATTRIBUTE_ROLE}=percent]`);
|
|
792
|
+
if (label instanceof HTMLElement) {
|
|
793
|
+
label.textContent = `${percent}%`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @private
|
|
799
|
+
* @param {HTMLElement|null} item
|
|
800
|
+
* @param {string} icon
|
|
801
|
+
*/
|
|
802
|
+
/**
|
|
803
|
+
* @private
|
|
804
|
+
* @param {HTMLElement|null} item
|
|
805
|
+
* @param {string} state
|
|
806
|
+
*/
|
|
807
|
+
function setItemState(item, state) {
|
|
808
|
+
if (!item) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
item.setAttribute("data-monster-state", state);
|
|
812
|
+
setStateIcon.call(this, item, state);
|
|
813
|
+
if (state === "success" || state === "error") {
|
|
814
|
+
scheduleDisappear.call(this, item);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* @private
|
|
820
|
+
* @param {HTMLElement|null} item
|
|
821
|
+
* @param {string} state
|
|
822
|
+
*/
|
|
823
|
+
function setStateIcon(item, state) {
|
|
824
|
+
if (!item) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const target = item.querySelector(`[${ATTRIBUTE_ROLE}=state-icon]`);
|
|
828
|
+
if (!(target instanceof HTMLElement)) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (state === "success") {
|
|
832
|
+
target.innerHTML = getIconMarkup("success");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (state === "error") {
|
|
836
|
+
target.innerHTML = getIconMarkup("error");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
target.innerHTML = "";
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* @private
|
|
844
|
+
* @param {File} file
|
|
845
|
+
*/
|
|
846
|
+
function handleItemRemove(file) {
|
|
847
|
+
const item = this[fileItemMapSymbol]?.get(file);
|
|
848
|
+
const xhr = this[fileRequestMapSymbol]?.get(file);
|
|
849
|
+
const timeout = this[fileTimeoutMapSymbol]?.get(file);
|
|
850
|
+
|
|
851
|
+
if (xhr instanceof XMLHttpRequest) {
|
|
852
|
+
xhr.abort();
|
|
853
|
+
this[fileRequestMapSymbol]?.delete(file);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (timeout) {
|
|
857
|
+
clearTimeout(timeout);
|
|
858
|
+
this[fileTimeoutMapSymbol]?.delete(file);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (item && item.parentElement) {
|
|
862
|
+
item.parentElement.removeChild(item);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
this[fileItemMapSymbol]?.delete(file);
|
|
866
|
+
fireCustomEvent(this, "monster-dropzone-file-removed", { file });
|
|
867
|
+
triggerAction.call(this, "fileRemoved", { file });
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* @private
|
|
872
|
+
* @param {File} file
|
|
873
|
+
*/
|
|
874
|
+
function handleItemRetry(file) {
|
|
875
|
+
const item = this[fileItemMapSymbol]?.get(file);
|
|
876
|
+
const timeout = this[fileTimeoutMapSymbol]?.get(file);
|
|
877
|
+
if (!item) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (timeout) {
|
|
881
|
+
clearTimeout(timeout);
|
|
882
|
+
this[fileTimeoutMapSymbol]?.delete(file);
|
|
883
|
+
}
|
|
884
|
+
updateFileProgress.call(this, item, 0);
|
|
885
|
+
setItemState.call(this, item, "uploading");
|
|
886
|
+
fireCustomEvent(this, "monster-dropzone-file-retry", { file });
|
|
887
|
+
triggerAction.call(this, "fileRetry", { file });
|
|
888
|
+
uploadSingleFile
|
|
889
|
+
.call(this, file, this.getOption("url"), item)
|
|
890
|
+
.catch(() => {});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* @private
|
|
895
|
+
* @param {string} name
|
|
896
|
+
* @param {object} payload
|
|
897
|
+
*/
|
|
898
|
+
function triggerAction(name, payload) {
|
|
899
|
+
const action = this.getOption(`actions.${name}`);
|
|
900
|
+
if (isFunction(action)) {
|
|
901
|
+
action.call(this, payload);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* @private
|
|
907
|
+
* @param {HTMLElement} item
|
|
908
|
+
*/
|
|
909
|
+
function scheduleDisappear(item) {
|
|
910
|
+
if (this.getOption("features.disappear") !== true) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const delay =
|
|
915
|
+
this.getOption("disappear.duration") ??
|
|
916
|
+
this.getOption("disappear.time") ??
|
|
917
|
+
3000;
|
|
918
|
+
|
|
919
|
+
const file = [...this[fileItemMapSymbol].entries()].find(
|
|
920
|
+
([, value]) => value === item,
|
|
921
|
+
)?.[0];
|
|
922
|
+
|
|
923
|
+
if (!file) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (this[fileTimeoutMapSymbol].has(file)) {
|
|
928
|
+
clearTimeout(this[fileTimeoutMapSymbol].get(file));
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const timeout = setTimeout(() => {
|
|
932
|
+
item.classList.add("is-disappearing");
|
|
933
|
+
setTimeout(() => {
|
|
934
|
+
handleItemRemove.call(this, file);
|
|
935
|
+
}, 250);
|
|
936
|
+
}, delay);
|
|
937
|
+
|
|
938
|
+
this[fileTimeoutMapSymbol].set(file, timeout);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* @private
|
|
942
|
+
* @param {File} file
|
|
943
|
+
* @return {string}
|
|
944
|
+
*/
|
|
945
|
+
function formatFileType(file) {
|
|
946
|
+
if (isString(file.type) && file.type !== "") {
|
|
947
|
+
return file.type;
|
|
948
|
+
}
|
|
949
|
+
return "unknown";
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* @private
|
|
954
|
+
* @param {File} file
|
|
955
|
+
* @return {string}
|
|
956
|
+
*/
|
|
957
|
+
function formatFileExtension(file) {
|
|
958
|
+
const parts = file.name.split(".");
|
|
959
|
+
if (parts.length < 2) {
|
|
960
|
+
return "file";
|
|
961
|
+
}
|
|
962
|
+
const ext = parts.pop() || "file";
|
|
963
|
+
return ext.slice(0, 4).toLowerCase();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* @private
|
|
968
|
+
* @param {number} bytes
|
|
969
|
+
* @return {string}
|
|
970
|
+
*/
|
|
971
|
+
function formatFileSize(bytes) {
|
|
972
|
+
if (!Number.isFinite(bytes)) {
|
|
973
|
+
return "0 B";
|
|
974
|
+
}
|
|
975
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
976
|
+
let size = bytes;
|
|
977
|
+
let unitIndex = 0;
|
|
978
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
979
|
+
size /= 1024;
|
|
980
|
+
unitIndex += 1;
|
|
981
|
+
}
|
|
982
|
+
return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* @private
|
|
987
|
+
* @param {string} kind
|
|
988
|
+
* @return {string}
|
|
989
|
+
*/
|
|
990
|
+
function getIconMarkup(kind) {
|
|
991
|
+
switch (kind) {
|
|
992
|
+
case "reload":
|
|
993
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
994
|
+
<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"/>
|
|
995
|
+
<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"/>
|
|
996
|
+
</svg>`;
|
|
997
|
+
case "success":
|
|
998
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
999
|
+
<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"/>
|
|
1000
|
+
<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"/>
|
|
1001
|
+
</svg>`;
|
|
1002
|
+
case "error":
|
|
1003
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
1004
|
+
<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"/>
|
|
1005
|
+
<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"/>
|
|
1006
|
+
</svg>`;
|
|
1007
|
+
case "cancel":
|
|
1008
|
+
default:
|
|
1009
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
1010
|
+
<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"/>
|
|
1011
|
+
<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"/>
|
|
1012
|
+
</svg>`;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* @private
|
|
1018
|
+
* @return {string}
|
|
1019
|
+
*/
|
|
1020
|
+
function getTemplate() {
|
|
1021
|
+
// language=HTML
|
|
1022
|
+
return `
|
|
1023
|
+
<div data-monster-role="control" part="control">
|
|
1024
|
+
<div data-monster-role="dropzone" part="dropzone" tabindex="0"
|
|
1025
|
+
data-monster-attributes="
|
|
1026
|
+
class path:classes.dropzone,
|
|
1027
|
+
data-monster-disabled path:disabled | if:true">
|
|
1028
|
+
<div data-monster-role="content" part="content">
|
|
1029
|
+
<div data-monster-role="title" part="title"
|
|
1030
|
+
data-monster-replace="path:labels.title"></div>
|
|
1031
|
+
<div data-monster-role="hint" part="hint"
|
|
1032
|
+
data-monster-replace="path:labels.hint"></div>
|
|
1033
|
+
</div>
|
|
1034
|
+
<monster-button data-monster-role="button" part="button"
|
|
1035
|
+
data-monster-attributes="
|
|
1036
|
+
data-monster-button-class path:classes.button,
|
|
1037
|
+
disabled path:disabled | if:true">
|
|
1038
|
+
<span data-monster-replace="path:labels.button"></span>
|
|
1039
|
+
</monster-button>
|
|
1040
|
+
<input data-monster-role="input" part="input" type="file"
|
|
1041
|
+
data-monster-attributes="
|
|
1042
|
+
accept path:accept,
|
|
1043
|
+
multiple path:multiple | if:true,
|
|
1044
|
+
disabled path:disabled | if:true">
|
|
1045
|
+
</div>
|
|
1046
|
+
<div data-monster-role="status" part="status"></div>
|
|
1047
|
+
<ul data-monster-role="list" part="list"></ul>
|
|
1048
|
+
</div>`;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* @private
|
|
1053
|
+
* @returns {object}
|
|
1054
|
+
*/
|
|
1055
|
+
function getTranslations() {
|
|
1056
|
+
const locale = getLocaleOfDocument();
|
|
1057
|
+
switch (locale.language) {
|
|
1058
|
+
case "de":
|
|
1059
|
+
return {
|
|
1060
|
+
title: "Dokumente hochladen",
|
|
1061
|
+
hint: "oder Dateien hierhin ziehen",
|
|
1062
|
+
button: "Dateien wählen",
|
|
1063
|
+
statusIdle: "",
|
|
1064
|
+
statusUploading: "Upload läuft ...",
|
|
1065
|
+
statusSuccess: "Upload abgeschlossen.",
|
|
1066
|
+
statusError: "Upload fehlgeschlagen.",
|
|
1067
|
+
statusMissingUrl: "Keine Upload-URL konfiguriert.",
|
|
1068
|
+
};
|
|
1069
|
+
case "es":
|
|
1070
|
+
return {
|
|
1071
|
+
title: "Subir documentos",
|
|
1072
|
+
hint: "o arrastre archivos aquí",
|
|
1073
|
+
button: "Seleccionar archivos",
|
|
1074
|
+
statusIdle: "",
|
|
1075
|
+
statusUploading: "Cargando ...",
|
|
1076
|
+
statusSuccess: "Subida completa.",
|
|
1077
|
+
statusError: "Error al subir.",
|
|
1078
|
+
statusMissingUrl: "No se ha configurado la URL de carga.",
|
|
1079
|
+
};
|
|
1080
|
+
case "zh":
|
|
1081
|
+
return {
|
|
1082
|
+
title: "上传文档",
|
|
1083
|
+
hint: "或将文件拖到此处",
|
|
1084
|
+
button: "选择文件",
|
|
1085
|
+
statusIdle: "",
|
|
1086
|
+
statusUploading: "正在上传...",
|
|
1087
|
+
statusSuccess: "上传完成。",
|
|
1088
|
+
statusError: "上传失败。",
|
|
1089
|
+
statusMissingUrl: "未配置上传URL。",
|
|
1090
|
+
};
|
|
1091
|
+
case "hi":
|
|
1092
|
+
return {
|
|
1093
|
+
title: "दस्तावेज़ अपलोड करें",
|
|
1094
|
+
hint: "या फ़ाइलें यहाँ खींचें",
|
|
1095
|
+
button: "फ़ाइलें चुनें",
|
|
1096
|
+
statusIdle: "",
|
|
1097
|
+
statusUploading: "अपलोड हो रहा है...",
|
|
1098
|
+
statusSuccess: "अपलोड पूर्ण हुआ।",
|
|
1099
|
+
statusError: "अपलोड विफल।",
|
|
1100
|
+
statusMissingUrl: "अपलोड URL कॉन्फ़िगर नहीं है।",
|
|
1101
|
+
};
|
|
1102
|
+
case "bn":
|
|
1103
|
+
return {
|
|
1104
|
+
title: "ডকুমেন্ট আপলোড করুন",
|
|
1105
|
+
hint: "অথবা এখানে ফাইল টেনে আনুন",
|
|
1106
|
+
button: "ফাইল নির্বাচন করুন",
|
|
1107
|
+
statusIdle: "",
|
|
1108
|
+
statusUploading: "আপলোড হচ্ছে...",
|
|
1109
|
+
statusSuccess: "আপলোড সম্পন্ন।",
|
|
1110
|
+
statusError: "আপলোড ব্যর্থ।",
|
|
1111
|
+
statusMissingUrl: "আপলোড URL কনফিগার করা নেই।",
|
|
1112
|
+
};
|
|
1113
|
+
case "pt":
|
|
1114
|
+
return {
|
|
1115
|
+
title: "Enviar documentos",
|
|
1116
|
+
hint: "ou solte os arquivos aqui",
|
|
1117
|
+
button: "Selecionar arquivos",
|
|
1118
|
+
statusIdle: "",
|
|
1119
|
+
statusUploading: "Enviando...",
|
|
1120
|
+
statusSuccess: "Envio concluído.",
|
|
1121
|
+
statusError: "Falha no envio.",
|
|
1122
|
+
statusMissingUrl: "URL de envio não configurada.",
|
|
1123
|
+
};
|
|
1124
|
+
case "ru":
|
|
1125
|
+
return {
|
|
1126
|
+
title: "Загрузить документы",
|
|
1127
|
+
hint: "или перетащите файлы сюда",
|
|
1128
|
+
button: "Выбрать файлы",
|
|
1129
|
+
statusIdle: "",
|
|
1130
|
+
statusUploading: "Загрузка...",
|
|
1131
|
+
statusSuccess: "Загрузка завершена.",
|
|
1132
|
+
statusError: "Ошибка загрузки.",
|
|
1133
|
+
statusMissingUrl: "URL загрузки не настроен.",
|
|
1134
|
+
};
|
|
1135
|
+
case "ja":
|
|
1136
|
+
return {
|
|
1137
|
+
title: "ドキュメントをアップロード",
|
|
1138
|
+
hint: "またはここにファイルをドロップ",
|
|
1139
|
+
button: "ファイルを選択",
|
|
1140
|
+
statusIdle: "",
|
|
1141
|
+
statusUploading: "アップロード中...",
|
|
1142
|
+
statusSuccess: "アップロード完了。",
|
|
1143
|
+
statusError: "アップロードに失敗しました。",
|
|
1144
|
+
statusMissingUrl: "アップロードURLが設定されていません。",
|
|
1145
|
+
};
|
|
1146
|
+
case "pa":
|
|
1147
|
+
return {
|
|
1148
|
+
title: "ਦਸਤਾਵੇਜ਼ ਅਪਲੋਡ ਕਰੋ",
|
|
1149
|
+
hint: "ਜਾਂ ਫਾਈਲਾਂ ਇੱਥੇ ਖਿੱਚੋ",
|
|
1150
|
+
button: "ਫਾਈਲਾਂ ਚੁਣੋ",
|
|
1151
|
+
statusIdle: "",
|
|
1152
|
+
statusUploading: "ਅਪਲੋਡ ਹੋ ਰਿਹਾ ਹੈ...",
|
|
1153
|
+
statusSuccess: "ਅਪਲੋਡ ਪੂਰਾ ਹੋ ਗਿਆ।",
|
|
1154
|
+
statusError: "ਅਪਲੋਡ ਫੇਲ੍ਹ ਹੋ ਗਿਆ।",
|
|
1155
|
+
statusMissingUrl: "ਅਪਲੋਡ URL ਸੰਰਚਿਤ ਨਹੀਂ ਹੈ।",
|
|
1156
|
+
};
|
|
1157
|
+
case "mr":
|
|
1158
|
+
return {
|
|
1159
|
+
title: "दस्तऐवज अपलोड करा",
|
|
1160
|
+
hint: "किंवा फायली येथे ओढा",
|
|
1161
|
+
button: "फायली निवडा",
|
|
1162
|
+
statusIdle: "",
|
|
1163
|
+
statusUploading: "अपलोड होत आहे...",
|
|
1164
|
+
statusSuccess: "अपलोड पूर्ण झाले.",
|
|
1165
|
+
statusError: "अपलोड अयशस्वी.",
|
|
1166
|
+
statusMissingUrl: "अपलोड URL संरचीत केलेले नाही.",
|
|
1167
|
+
};
|
|
1168
|
+
case "fr":
|
|
1169
|
+
return {
|
|
1170
|
+
title: "Téléverser des documents",
|
|
1171
|
+
hint: "ou déposer les fichiers ici",
|
|
1172
|
+
button: "Choisir des fichiers",
|
|
1173
|
+
statusIdle: "",
|
|
1174
|
+
statusUploading: "Téléversement en cours...",
|
|
1175
|
+
statusSuccess: "Téléversement terminé.",
|
|
1176
|
+
statusError: "Échec du téléversement.",
|
|
1177
|
+
statusMissingUrl: "URL de téléversement non configurée.",
|
|
1178
|
+
};
|
|
1179
|
+
case "it":
|
|
1180
|
+
return {
|
|
1181
|
+
title: "Carica documenti",
|
|
1182
|
+
hint: "oppure trascina i file qui",
|
|
1183
|
+
button: "Seleziona file",
|
|
1184
|
+
statusIdle: "",
|
|
1185
|
+
statusUploading: "Caricamento in corso...",
|
|
1186
|
+
statusSuccess: "Caricamento completato.",
|
|
1187
|
+
statusError: "Caricamento non riuscito.",
|
|
1188
|
+
statusMissingUrl: "URL di caricamento non configurata.",
|
|
1189
|
+
};
|
|
1190
|
+
case "nl":
|
|
1191
|
+
return {
|
|
1192
|
+
title: "Documenten uploaden",
|
|
1193
|
+
hint: "of sleep bestanden hierheen",
|
|
1194
|
+
button: "Bestanden kiezen",
|
|
1195
|
+
statusIdle: "",
|
|
1196
|
+
statusUploading: "Uploaden...",
|
|
1197
|
+
statusSuccess: "Upload voltooid.",
|
|
1198
|
+
statusError: "Upload mislukt.",
|
|
1199
|
+
statusMissingUrl: "Upload-URL niet geconfigureerd.",
|
|
1200
|
+
};
|
|
1201
|
+
case "sv":
|
|
1202
|
+
return {
|
|
1203
|
+
title: "Ladda upp dokument",
|
|
1204
|
+
hint: "eller dra filer hit",
|
|
1205
|
+
button: "Välj filer",
|
|
1206
|
+
statusIdle: "",
|
|
1207
|
+
statusUploading: "Uppladdning pågår...",
|
|
1208
|
+
statusSuccess: "Uppladdning klar.",
|
|
1209
|
+
statusError: "Uppladdning misslyckades.",
|
|
1210
|
+
statusMissingUrl: "Ingen uppladdnings-URL konfigurerad.",
|
|
1211
|
+
};
|
|
1212
|
+
case "pl":
|
|
1213
|
+
return {
|
|
1214
|
+
title: "Prześlij dokumenty",
|
|
1215
|
+
hint: "lub upuść pliki tutaj",
|
|
1216
|
+
button: "Wybierz pliki",
|
|
1217
|
+
statusIdle: "",
|
|
1218
|
+
statusUploading: "Przesyłanie...",
|
|
1219
|
+
statusSuccess: "Przesyłanie zakończone.",
|
|
1220
|
+
statusError: "Przesyłanie nieudane.",
|
|
1221
|
+
statusMissingUrl: "Brak skonfigurowanego URL przesyłania.",
|
|
1222
|
+
};
|
|
1223
|
+
case "da":
|
|
1224
|
+
return {
|
|
1225
|
+
title: "Upload dokumenter",
|
|
1226
|
+
hint: "eller træk filer her",
|
|
1227
|
+
button: "Vælg filer",
|
|
1228
|
+
statusIdle: "",
|
|
1229
|
+
statusUploading: "Uploader...",
|
|
1230
|
+
statusSuccess: "Upload fuldført.",
|
|
1231
|
+
statusError: "Upload mislykkedes.",
|
|
1232
|
+
statusMissingUrl: "Ingen upload-URL konfigureret.",
|
|
1233
|
+
};
|
|
1234
|
+
case "fi":
|
|
1235
|
+
return {
|
|
1236
|
+
title: "Lataa asiakirjoja",
|
|
1237
|
+
hint: "tai pudota tiedostot tähän",
|
|
1238
|
+
button: "Valitse tiedostot",
|
|
1239
|
+
statusIdle: "",
|
|
1240
|
+
statusUploading: "Siirretään...",
|
|
1241
|
+
statusSuccess: "Siirto valmis.",
|
|
1242
|
+
statusError: "Siirto epäonnistui.",
|
|
1243
|
+
statusMissingUrl: "Siirto-URL ei ole määritetty.",
|
|
1244
|
+
};
|
|
1245
|
+
case "no":
|
|
1246
|
+
return {
|
|
1247
|
+
title: "Last opp dokumenter",
|
|
1248
|
+
hint: "eller dra filer hit",
|
|
1249
|
+
button: "Velg filer",
|
|
1250
|
+
statusIdle: "",
|
|
1251
|
+
statusUploading: "Laster opp...",
|
|
1252
|
+
statusSuccess: "Opplasting fullført.",
|
|
1253
|
+
statusError: "Opplasting feilet.",
|
|
1254
|
+
statusMissingUrl: "Ingen opplastings-URL konfigurert.",
|
|
1255
|
+
};
|
|
1256
|
+
case "cs":
|
|
1257
|
+
return {
|
|
1258
|
+
title: "Nahrát dokumenty",
|
|
1259
|
+
hint: "nebo přetáhněte soubory sem",
|
|
1260
|
+
button: "Vybrat soubory",
|
|
1261
|
+
statusIdle: "",
|
|
1262
|
+
statusUploading: "Nahrávání...",
|
|
1263
|
+
statusSuccess: "Nahrávání dokončeno.",
|
|
1264
|
+
statusError: "Nahrávání selhalo.",
|
|
1265
|
+
statusMissingUrl: "URL pro nahrávání není nastavena.",
|
|
1266
|
+
};
|
|
1267
|
+
default:
|
|
1268
|
+
case "en":
|
|
1269
|
+
return {
|
|
1270
|
+
title: "Upload documents",
|
|
1271
|
+
hint: "or drop files here",
|
|
1272
|
+
button: "Choose files",
|
|
1273
|
+
statusIdle: "",
|
|
1274
|
+
statusUploading: "Uploading ...",
|
|
1275
|
+
statusSuccess: "Upload complete.",
|
|
1276
|
+
statusError: "Upload failed.",
|
|
1277
|
+
statusMissingUrl: "No upload URL configured.",
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
registerCustomElement(Dropzone);
|