@liwe3/webcomponents 1.1.0 → 1.1.10
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/dist/AIMarkdownEditor.d.ts +35 -0
- package/dist/AIMarkdownEditor.d.ts.map +1 -0
- package/dist/AIMarkdownEditor.js +412 -0
- package/dist/AIMarkdownEditor.js.map +1 -0
- package/dist/AITextEditor.d.ts +10 -0
- package/dist/AITextEditor.d.ts.map +1 -1
- package/dist/AITextEditor.js +63 -27
- package/dist/AITextEditor.js.map +1 -1
- package/dist/ButtonToolbar.d.ts +35 -0
- package/dist/ButtonToolbar.d.ts.map +1 -0
- package/dist/ButtonToolbar.js +220 -0
- package/dist/ButtonToolbar.js.map +1 -0
- package/dist/CheckList.d.ts +31 -0
- package/dist/CheckList.d.ts.map +1 -0
- package/dist/CheckList.js +336 -0
- package/dist/CheckList.js.map +1 -0
- package/dist/ChunkUploader.d.ts +22 -0
- package/dist/ChunkUploader.d.ts.map +1 -1
- package/dist/ChunkUploader.js +245 -103
- package/dist/ChunkUploader.js.map +1 -1
- package/dist/ComicBalloon.d.ts +82 -0
- package/dist/ComicBalloon.d.ts.map +1 -0
- package/dist/ComicBalloon.js +346 -0
- package/dist/ComicBalloon.js.map +1 -0
- package/dist/Dialog.d.ts +102 -0
- package/dist/Dialog.d.ts.map +1 -0
- package/dist/Dialog.js +299 -0
- package/dist/Dialog.js.map +1 -0
- package/dist/MarkdownPreview.d.ts +25 -0
- package/dist/MarkdownPreview.d.ts.map +1 -0
- package/dist/MarkdownPreview.js +147 -0
- package/dist/MarkdownPreview.js.map +1 -0
- package/dist/ResizableCropper.d.ts +158 -0
- package/dist/ResizableCropper.d.ts.map +1 -0
- package/dist/ResizableCropper.js +562 -0
- package/dist/ResizableCropper.js.map +1 -0
- package/dist/SmartSelect.d.ts +1 -0
- package/dist/SmartSelect.d.ts.map +1 -1
- package/dist/SmartSelect.js +45 -2
- package/dist/SmartSelect.js.map +1 -1
- package/dist/index.d.ts +16 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -29
- package/dist/index.js.map +1 -1
- package/package.json +33 -3
- package/src/AIMarkdownEditor.ts +568 -0
- package/src/AITextEditor.ts +97 -2
- package/src/ButtonToolbar.ts +302 -0
- package/src/CheckList.ts +438 -0
- package/src/ChunkUploader.ts +837 -623
- package/src/ComicBalloon.ts +709 -0
- package/src/Dialog.ts +510 -0
- package/src/MarkdownPreview.ts +213 -0
- package/src/ResizableCropper.ts +1099 -0
- package/src/SmartSelect.ts +48 -2
- package/src/index.ts +110 -47
package/src/ChunkUploader.ts
CHANGED
|
@@ -4,638 +4,786 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export interface UploadedFile {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
id : string;
|
|
8
|
+
file : File;
|
|
9
|
+
status : 'pending' | 'uploading' | 'completed' | 'error' | 'aborted';
|
|
10
|
+
progress : number;
|
|
11
|
+
uploadedBytes : number;
|
|
12
|
+
preview? : string;
|
|
13
|
+
error? : string;
|
|
14
|
+
uploadId? : string; // R2 upload ID for aborting
|
|
15
|
+
key? : string; // R2 key for aborting
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface ChunkUploaderConfig {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
serverURL : string;
|
|
20
|
+
chunkSize : number; // in MB
|
|
21
|
+
authToken? : string;
|
|
22
|
+
validFiletypes? : string[]; // Array of extensions like ['jpg', 'png', 'pdf']
|
|
23
|
+
maxFileSize? : number; // in MB
|
|
24
|
+
labelDropFiles? : string; // Custom text for drop zone (default: "Drop files here")
|
|
25
|
+
labelBrowse? : string; // Custom label for browse button (default: "Browse Files")
|
|
26
|
+
folder? : string; // Destination folder name for uploads
|
|
27
|
+
compact? : boolean; // Compact mode (single file, no preview)
|
|
28
|
+
onfilecomplete? : ( file : UploadedFile ) => void;
|
|
29
|
+
onuploadcomplete? : ( files : UploadedFile[] ) => void;
|
|
30
|
+
parseResponse? : ( response : any, endpoint : 'initiate' | 'part' | 'complete' ) => any; // Transform endpoint responses
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const DEFAULT_CHUNK_SIZE = 5; // 5MB - R2/S3 minimum part size (except last part)
|
|
29
34
|
const DEFAULT_MAX_FILE_SIZE = 5120; // 5GB in MB
|
|
30
35
|
|
|
31
36
|
export class ChunkUploaderElement extends HTMLElement {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
37
|
+
declare shadowRoot : ShadowRoot;
|
|
38
|
+
private files : Map<string, UploadedFile> = new Map();
|
|
39
|
+
private config : ChunkUploaderConfig = {
|
|
40
|
+
serverURL: '',
|
|
41
|
+
chunkSize: DEFAULT_CHUNK_SIZE,
|
|
42
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
43
|
+
};
|
|
44
|
+
private isUploading = false;
|
|
45
|
+
private abortController : AbortController | null = null;
|
|
46
|
+
|
|
47
|
+
constructor () {
|
|
48
|
+
super();
|
|
49
|
+
this.attachShadow( { mode: 'open' } );
|
|
50
|
+
this.render();
|
|
51
|
+
this.bindEvents();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static get observedAttributes () : string[] {
|
|
55
|
+
return [ 'server-url', 'chunk-size', 'auth-token', 'valid-filetypes', 'max-file-size', 'label-drop-files', 'label-browse', 'folder', 'compact' ];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
attributeChangedCallback ( name : string, oldValue : string | null, newValue : string | null ) : void {
|
|
59
|
+
if ( oldValue !== newValue ) {
|
|
60
|
+
switch ( name ) {
|
|
61
|
+
case 'server-url':
|
|
62
|
+
this.config.serverURL = newValue || '';
|
|
63
|
+
break;
|
|
64
|
+
case 'chunk-size':
|
|
65
|
+
this.config.chunkSize = parseFloat( newValue || String( DEFAULT_CHUNK_SIZE ) );
|
|
66
|
+
break;
|
|
67
|
+
case 'auth-token':
|
|
68
|
+
this.config.authToken = newValue || undefined;
|
|
69
|
+
break;
|
|
70
|
+
case 'valid-filetypes':
|
|
71
|
+
this.config.validFiletypes = newValue ? newValue.split( ',' ).map( ( ext ) => ext.trim() ) : undefined;
|
|
72
|
+
break;
|
|
73
|
+
case 'max-file-size':
|
|
74
|
+
this.config.maxFileSize = parseFloat( newValue || String( DEFAULT_MAX_FILE_SIZE ) );
|
|
75
|
+
break;
|
|
76
|
+
case 'label-drop-files':
|
|
77
|
+
this.config.labelDropFiles = newValue || undefined;
|
|
78
|
+
this.updateLabels();
|
|
79
|
+
break;
|
|
80
|
+
case 'label-browse':
|
|
81
|
+
this.config.labelBrowse = newValue || undefined;
|
|
82
|
+
this.updateLabels();
|
|
83
|
+
break;
|
|
84
|
+
case 'folder':
|
|
85
|
+
this.config.folder = newValue || undefined;
|
|
86
|
+
break;
|
|
87
|
+
case 'compact':
|
|
88
|
+
this.config.compact = newValue !== null;
|
|
89
|
+
this.updateCompactMode();
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Property getters and setters
|
|
96
|
+
get serverURL () : string {
|
|
97
|
+
return this.config.serverURL;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
set serverURL ( value : string ) {
|
|
101
|
+
this.config.serverURL = value;
|
|
102
|
+
this.setAttribute( 'server-url', value );
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get chunkSize () : number {
|
|
106
|
+
return this.config.chunkSize;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
set chunkSize ( value : number ) {
|
|
110
|
+
this.config.chunkSize = value;
|
|
111
|
+
this.setAttribute( 'chunk-size', value.toString() );
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get authToken () : string | undefined {
|
|
115
|
+
return this.config.authToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
set authToken ( value : string | undefined ) {
|
|
119
|
+
if ( value ) {
|
|
120
|
+
this.config.authToken = value;
|
|
121
|
+
this.setAttribute( 'auth-token', value );
|
|
122
|
+
} else {
|
|
123
|
+
this.config.authToken = undefined;
|
|
124
|
+
this.removeAttribute( 'auth-token' );
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
get validFiletypes () : string[] | undefined {
|
|
129
|
+
return this.config.validFiletypes;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
set validFiletypes ( value : string[] | undefined ) {
|
|
133
|
+
this.config.validFiletypes = value;
|
|
134
|
+
if ( value ) {
|
|
135
|
+
this.setAttribute( 'valid-filetypes', value.join( ',' ) );
|
|
136
|
+
} else {
|
|
137
|
+
this.removeAttribute( 'valid-filetypes' );
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get maxFileSize () : number {
|
|
142
|
+
return this.config.maxFileSize || DEFAULT_MAX_FILE_SIZE;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
set maxFileSize ( value : number ) {
|
|
146
|
+
this.config.maxFileSize = value;
|
|
147
|
+
this.setAttribute( 'max-file-size', value.toString() );
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get labelDropFiles () : string | undefined {
|
|
151
|
+
return this.config.labelDropFiles;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
set labelDropFiles ( value : string | undefined ) {
|
|
155
|
+
this.config.labelDropFiles = value;
|
|
156
|
+
if ( value ) {
|
|
157
|
+
this.setAttribute( 'label-drop-files', value );
|
|
158
|
+
} else {
|
|
159
|
+
this.removeAttribute( 'label-drop-files' );
|
|
160
|
+
}
|
|
161
|
+
this.updateLabels();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
get labelBrowse () : string | undefined {
|
|
165
|
+
return this.config.labelBrowse;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
set labelBrowse ( value : string | undefined ) {
|
|
169
|
+
this.config.labelBrowse = value;
|
|
170
|
+
if ( value ) {
|
|
171
|
+
this.setAttribute( 'label-browse', value );
|
|
172
|
+
} else {
|
|
173
|
+
this.removeAttribute( 'label-browse' );
|
|
174
|
+
}
|
|
175
|
+
this.updateLabels();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get folder () : string | undefined {
|
|
179
|
+
return this.config.folder;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
set folder ( value : string | undefined ) {
|
|
183
|
+
this.config.folder = value;
|
|
184
|
+
if ( value ) {
|
|
185
|
+
this.setAttribute( 'folder', value );
|
|
186
|
+
} else {
|
|
187
|
+
this.removeAttribute( 'folder' );
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get compact () : boolean {
|
|
192
|
+
return !!this.config.compact;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
set compact ( value : boolean ) {
|
|
196
|
+
this.config.compact = value;
|
|
197
|
+
if ( value ) {
|
|
198
|
+
this.setAttribute( 'compact', '' );
|
|
199
|
+
} else {
|
|
200
|
+
this.removeAttribute( 'compact' );
|
|
201
|
+
}
|
|
202
|
+
this.updateCompactMode();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
set onfilecomplete ( callback : (( file : UploadedFile ) => void) | undefined ) {
|
|
206
|
+
this.config.onfilecomplete = callback;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
set onuploadcomplete ( callback : (( files : UploadedFile[] ) => void) | undefined ) {
|
|
210
|
+
this.config.onuploadcomplete = callback;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
set parseResponse ( callback : (( response : any, endpoint : 'initiate' | 'part' | 'complete' ) => any) | undefined ) {
|
|
214
|
+
this.config.parseResponse = callback;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Updates labels in the DOM when properties change
|
|
219
|
+
*/
|
|
220
|
+
private updateLabels () : void {
|
|
221
|
+
const dropText = this.shadowRoot.querySelector( '.upload-text' );
|
|
222
|
+
const browseBtn = this.shadowRoot.querySelector( '#browseBtn' );
|
|
223
|
+
|
|
224
|
+
if ( dropText ) {
|
|
225
|
+
dropText.textContent = this.config.labelDropFiles || 'Drop files here';
|
|
226
|
+
}
|
|
227
|
+
if ( browseBtn ) {
|
|
228
|
+
browseBtn.textContent = this.config.labelBrowse || 'Browse Files';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Updates compact mode styles and attributes
|
|
234
|
+
*/
|
|
235
|
+
private updateCompactMode () : void {
|
|
236
|
+
const container = this.shadowRoot.querySelector( '.container' );
|
|
237
|
+
const fileInput = this.shadowRoot.querySelector( '#fileInput' ) as HTMLInputElement;
|
|
238
|
+
|
|
239
|
+
if ( this.config.compact ) {
|
|
240
|
+
container?.classList.add( 'compact' );
|
|
241
|
+
fileInput?.removeAttribute( 'multiple' );
|
|
242
|
+
} else {
|
|
243
|
+
container?.classList.remove( 'compact' );
|
|
244
|
+
fileInput?.setAttribute( 'multiple', '' );
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Formats bytes to human readable string
|
|
250
|
+
*/
|
|
251
|
+
private formatBytes ( bytes : number ) : string {
|
|
252
|
+
if ( bytes === 0 ) return '0 Bytes';
|
|
253
|
+
const k = 1024;
|
|
254
|
+
const sizes = [ 'Bytes', 'KB', 'MB', 'GB' ];
|
|
255
|
+
const i = Math.floor( Math.log( bytes ) / Math.log( k ) );
|
|
256
|
+
return Math.round( ( bytes / Math.pow( k, i ) ) * 100 ) / 100 + ' ' + sizes[i];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Generates a unique ID for a file
|
|
261
|
+
*/
|
|
262
|
+
private generateFileId () : string {
|
|
263
|
+
return `file-${Date.now()}-${Math.random().toString( 36 ).substr( 2, 9 )}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Validates a file based on config
|
|
268
|
+
*/
|
|
269
|
+
private validateFile ( file : File ) : { valid : boolean; error? : string } {
|
|
270
|
+
// Check file size
|
|
271
|
+
const maxSizeBytes = this.maxFileSize * 1024 * 1024;
|
|
272
|
+
if ( file.size > maxSizeBytes ) {
|
|
273
|
+
return {
|
|
274
|
+
valid: false,
|
|
275
|
+
error: `File size exceeds maximum of ${this.formatBytes( maxSizeBytes )}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check file type
|
|
280
|
+
if ( this.config.validFiletypes && this.config.validFiletypes.length > 0 ) {
|
|
281
|
+
const extension = file.name.split( '.' ).pop()?.toLowerCase();
|
|
282
|
+
if ( !extension || !this.config.validFiletypes.includes( extension ) ) {
|
|
283
|
+
return {
|
|
284
|
+
valid: false,
|
|
285
|
+
error: `File type .${extension} is not allowed. Valid types: ${this.config.validFiletypes.join( ', ' )}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return { valid: true };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generates a preview for image files
|
|
295
|
+
*/
|
|
296
|
+
private async generatePreview ( file : File ) : Promise<string | undefined> {
|
|
297
|
+
if ( !file.type.startsWith( 'image/' ) ) return undefined;
|
|
298
|
+
|
|
299
|
+
return new Promise( ( resolve ) => {
|
|
300
|
+
const reader = new FileReader();
|
|
301
|
+
reader.onload = ( e ) => resolve( e.target?.result as string );
|
|
302
|
+
reader.onerror = () => resolve( undefined );
|
|
303
|
+
reader.readAsDataURL( file );
|
|
304
|
+
} );
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Adds files to the upload queue
|
|
309
|
+
*/
|
|
310
|
+
private async addFiles ( fileList : FileList ) : Promise<void> {
|
|
311
|
+
if ( this.config.compact ) {
|
|
312
|
+
this.files.clear();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const files = Array.from( fileList );
|
|
316
|
+
const filesToProcess = this.config.compact ? [ files[0] ] : files;
|
|
317
|
+
|
|
318
|
+
for ( const file of filesToProcess ) {
|
|
319
|
+
if ( !file ) continue;
|
|
320
|
+
const validation = this.validateFile( file );
|
|
321
|
+
|
|
322
|
+
const id = this.generateFileId();
|
|
323
|
+
const uploadedFile : UploadedFile = {
|
|
324
|
+
id,
|
|
325
|
+
file,
|
|
326
|
+
status: validation.valid ? 'pending' : 'error',
|
|
327
|
+
progress: 0,
|
|
328
|
+
uploadedBytes: 0,
|
|
329
|
+
error: validation.error,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// Generate preview for images
|
|
333
|
+
if ( validation.valid && file.type.startsWith( 'image/' ) ) {
|
|
334
|
+
uploadedFile.preview = await this.generatePreview( file );
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
this.files.set( id, uploadedFile );
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.renderFileCards();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Removes a file from the queue
|
|
345
|
+
*/
|
|
346
|
+
private removeFile ( id : string ) : void {
|
|
347
|
+
this.files.delete( id );
|
|
348
|
+
this.renderFileCards();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Uploads a single file with chunking
|
|
353
|
+
*/
|
|
354
|
+
private async uploadFile ( uploadedFile : UploadedFile ) : Promise<void> {
|
|
355
|
+
if ( !this.config.serverURL ) {
|
|
356
|
+
throw new Error( 'Server URL is not configured' );
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Remove trailing slash from serverURL if present
|
|
360
|
+
const serverUrl = this.config.serverURL.replace( /\/$/, '' );
|
|
361
|
+
|
|
362
|
+
const { file } = uploadedFile;
|
|
363
|
+
uploadedFile.status = 'uploading';
|
|
364
|
+
uploadedFile.progress = 0;
|
|
365
|
+
this.updateFileCard( uploadedFile.id );
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const headers : HeadersInit = {
|
|
369
|
+
'Content-Type': 'application/json',
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if ( this.config.authToken ) {
|
|
373
|
+
headers['Authorization'] = `Bearer ${this.config.authToken}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Step 1: Initiate multipart upload
|
|
377
|
+
const initResponse = await fetch( `${serverUrl}/api/upload/initiate`, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
mode: 'cors',
|
|
380
|
+
headers,
|
|
381
|
+
body: JSON.stringify( {
|
|
382
|
+
fileName: file.name,
|
|
383
|
+
fileType: file.type,
|
|
384
|
+
folder: this.config.folder,
|
|
385
|
+
} ),
|
|
386
|
+
signal: this.abortController?.signal,
|
|
387
|
+
} );
|
|
388
|
+
|
|
389
|
+
if ( !initResponse.ok ) {
|
|
390
|
+
throw new Error( `Failed to initiate upload: ${await initResponse.text()}` );
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let initData = await initResponse.json();
|
|
394
|
+
if ( this.config.parseResponse ) {
|
|
395
|
+
initData = this.config.parseResponse( initData, 'initiate' );
|
|
396
|
+
}
|
|
397
|
+
const { uploadId, key } = initData;
|
|
398
|
+
|
|
399
|
+
// Store upload metadata for potential abort
|
|
400
|
+
uploadedFile.uploadId = uploadId;
|
|
401
|
+
uploadedFile.key = key;
|
|
402
|
+
|
|
403
|
+
// Step 2: Upload chunks
|
|
404
|
+
const chunkSizeBytes = this.config.chunkSize * 1024 * 1024;
|
|
405
|
+
const totalParts = Math.ceil( file.size / chunkSizeBytes );
|
|
406
|
+
const parts : Array<{ partNumber : number; etag : string }> = [];
|
|
407
|
+
|
|
408
|
+
for ( let partNumber = 1; partNumber <= totalParts; partNumber++ ) {
|
|
409
|
+
// Check if upload was aborted
|
|
410
|
+
if ( this.abortController?.signal.aborted ) {
|
|
411
|
+
throw new Error( 'Upload aborted by user' );
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const start = ( partNumber - 1 ) * chunkSizeBytes;
|
|
415
|
+
const end = Math.min( start + chunkSizeBytes, file.size );
|
|
416
|
+
const chunk = file.slice( start, end );
|
|
417
|
+
|
|
418
|
+
// Create headers for this part upload
|
|
419
|
+
const partHeaders : HeadersInit = {
|
|
420
|
+
'Content-Type': 'application/octet-stream',
|
|
421
|
+
'X-Upload-Id': uploadId,
|
|
422
|
+
'X-Key': key,
|
|
423
|
+
'X-Part-Number': partNumber.toString(),
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
if ( this.config.authToken ) {
|
|
427
|
+
partHeaders['Authorization'] = `Bearer ${this.config.authToken}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const partResponse = await fetch( `${serverUrl}/api/upload/part`, {
|
|
431
|
+
method: 'POST',
|
|
432
|
+
mode: 'cors',
|
|
433
|
+
headers: partHeaders,
|
|
434
|
+
body: chunk,
|
|
435
|
+
signal: this.abortController?.signal,
|
|
436
|
+
} );
|
|
437
|
+
|
|
438
|
+
if ( !partResponse.ok ) {
|
|
439
|
+
throw new Error( `Failed to upload part ${partNumber}` );
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let partData = await partResponse.json();
|
|
443
|
+
if ( this.config.parseResponse ) {
|
|
444
|
+
partData = this.config.parseResponse( partData, 'part' );
|
|
445
|
+
}
|
|
446
|
+
const { etag } = partData;
|
|
447
|
+
parts.push( { partNumber, etag } );
|
|
448
|
+
|
|
449
|
+
// Update progress
|
|
450
|
+
uploadedFile.uploadedBytes = end;
|
|
451
|
+
uploadedFile.progress = ( uploadedFile.uploadedBytes / file.size ) * 100;
|
|
452
|
+
this.updateFileCard( uploadedFile.id );
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Step 3: Complete multipart upload
|
|
456
|
+
const completeResponse = await fetch( `${serverUrl}/api/upload/complete`, {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
mode: 'cors',
|
|
459
|
+
headers,
|
|
460
|
+
body: JSON.stringify( {
|
|
461
|
+
uploadId,
|
|
462
|
+
key,
|
|
463
|
+
parts,
|
|
464
|
+
} ),
|
|
465
|
+
signal: this.abortController?.signal,
|
|
466
|
+
} );
|
|
467
|
+
|
|
468
|
+
if ( !completeResponse.ok ) {
|
|
469
|
+
throw new Error( 'Failed to complete upload' );
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let completeData = await completeResponse.json();
|
|
473
|
+
if ( this.config.parseResponse ) {
|
|
474
|
+
completeData = this.config.parseResponse( completeData, 'complete' );
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
uploadedFile.status = 'completed';
|
|
478
|
+
uploadedFile.progress = 100;
|
|
479
|
+
this.updateFileCard( uploadedFile.id );
|
|
480
|
+
|
|
481
|
+
// Dispatch file complete event
|
|
482
|
+
this.dispatchEvent(
|
|
483
|
+
new CustomEvent( 'filecomplete', {
|
|
484
|
+
detail: uploadedFile,
|
|
485
|
+
bubbles: true,
|
|
486
|
+
composed: true,
|
|
487
|
+
} ),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// Call callback if provided
|
|
491
|
+
if ( this.config.onfilecomplete ) {
|
|
492
|
+
this.config.onfilecomplete( uploadedFile );
|
|
493
|
+
}
|
|
494
|
+
} catch ( error ) {
|
|
495
|
+
// Check if this is an abort error (from AbortController)
|
|
496
|
+
const isAbortError = error instanceof Error
|
|
497
|
+
&& ( error.name === 'AbortError' || error.message === 'Upload aborted by user' );
|
|
498
|
+
|
|
499
|
+
// Only set error status if not aborted
|
|
500
|
+
if ( !isAbortError ) {
|
|
501
|
+
uploadedFile.status = 'error';
|
|
502
|
+
uploadedFile.error = error instanceof Error ? error.message : 'Unknown error';
|
|
503
|
+
this.updateFileCard( uploadedFile.id );
|
|
504
|
+
}
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Starts uploading all pending files
|
|
511
|
+
*/
|
|
512
|
+
private async startUpload () : Promise<void> {
|
|
513
|
+
if ( this.isUploading ) return;
|
|
514
|
+
|
|
515
|
+
const pendingFiles = Array.from( this.files.values() ).filter( ( f ) => f.status === 'pending' );
|
|
516
|
+
if ( pendingFiles.length === 0 ) return;
|
|
517
|
+
|
|
518
|
+
this.isUploading = true;
|
|
519
|
+
this.abortController = new AbortController();
|
|
520
|
+
|
|
521
|
+
const uploadBtn = this.shadowRoot.querySelector( '#uploadBtn' ) as HTMLButtonElement;
|
|
522
|
+
const abortBtn = this.shadowRoot.querySelector( '#abortBtn' ) as HTMLButtonElement;
|
|
523
|
+
|
|
524
|
+
if ( uploadBtn ) {
|
|
525
|
+
uploadBtn.disabled = true;
|
|
526
|
+
uploadBtn.textContent = 'Uploading...';
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if ( abortBtn ) {
|
|
530
|
+
abortBtn.style.display = 'inline-block';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
for ( const file of pendingFiles ) {
|
|
535
|
+
// Check if upload was aborted
|
|
536
|
+
if ( this.abortController.signal.aborted ) {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
await this.uploadFile( file );
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// All uploads complete
|
|
543
|
+
this.dispatchEvent(
|
|
544
|
+
new CustomEvent( 'uploadcomplete', {
|
|
545
|
+
detail: Array.from( this.files.values() ),
|
|
546
|
+
bubbles: true,
|
|
547
|
+
composed: true,
|
|
548
|
+
} ),
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Call callback if provided
|
|
552
|
+
if ( this.config.onuploadcomplete ) {
|
|
553
|
+
this.config.onuploadcomplete( Array.from( this.files.values() ) );
|
|
554
|
+
}
|
|
555
|
+
} catch ( error ) {
|
|
556
|
+
// Only log non-abort errors
|
|
557
|
+
if ( error instanceof Error && error.message !== 'Upload aborted by user' ) {
|
|
558
|
+
console.error( 'Upload error:', error );
|
|
559
|
+
}
|
|
560
|
+
} finally {
|
|
561
|
+
this.isUploading = false;
|
|
562
|
+
if ( uploadBtn ) {
|
|
563
|
+
uploadBtn.disabled = false;
|
|
564
|
+
uploadBtn.textContent = 'Upload Files';
|
|
565
|
+
}
|
|
566
|
+
if ( abortBtn ) {
|
|
567
|
+
abortBtn.style.display = 'none';
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Aborts all pending and uploading files
|
|
574
|
+
*/
|
|
575
|
+
private async abortAllUploads () : Promise<void> {
|
|
576
|
+
// Trigger abort signal to stop ongoing fetch requests
|
|
577
|
+
if ( this.abortController ) {
|
|
578
|
+
this.abortController.abort();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const filesToAbort = Array.from( this.files.values() ).filter(
|
|
582
|
+
( f ) => f.status === 'pending' || f.status === 'uploading',
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
if ( filesToAbort.length === 0 ) return;
|
|
586
|
+
|
|
587
|
+
const headers : HeadersInit = {
|
|
588
|
+
'Content-Type': 'application/json',
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
if ( this.config.authToken ) {
|
|
592
|
+
headers['Authorization'] = `Bearer ${this.config.authToken}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
for ( const file of filesToAbort ) {
|
|
596
|
+
// If upload was initiated, abort it on the server
|
|
597
|
+
if ( file.uploadId && file.key ) {
|
|
598
|
+
try {
|
|
599
|
+
// Remove trailing slash from serverURL if present
|
|
600
|
+
const serverUrl = this.config.serverURL.replace( /\/$/, '' );
|
|
601
|
+
await fetch( `${serverUrl}/api/upload/abort`, {
|
|
602
|
+
method: 'POST',
|
|
603
|
+
mode: 'cors',
|
|
604
|
+
headers,
|
|
605
|
+
body: JSON.stringify( {
|
|
606
|
+
uploadId: file.uploadId,
|
|
607
|
+
key: file.key,
|
|
608
|
+
} ),
|
|
609
|
+
} );
|
|
610
|
+
} catch ( error ) {
|
|
611
|
+
console.error( `Failed to abort upload for ${file.file.name}:`, error );
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Update file status
|
|
616
|
+
file.status = 'aborted';
|
|
617
|
+
file.error = 'Upload aborted by user';
|
|
618
|
+
this.updateFileCard( file.id );
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Dispatch abort event
|
|
622
|
+
this.dispatchEvent(
|
|
623
|
+
new CustomEvent( 'uploadaborted', {
|
|
624
|
+
detail: filesToAbort,
|
|
625
|
+
bubbles: true,
|
|
626
|
+
composed: true,
|
|
627
|
+
} ),
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Reset upload state
|
|
631
|
+
this.isUploading = false;
|
|
632
|
+
const uploadBtn = this.shadowRoot.querySelector( '#uploadBtn' ) as HTMLButtonElement;
|
|
633
|
+
const abortBtn = this.shadowRoot.querySelector( '#abortBtn' ) as HTMLButtonElement;
|
|
634
|
+
|
|
635
|
+
if ( uploadBtn ) {
|
|
636
|
+
uploadBtn.disabled = false;
|
|
637
|
+
uploadBtn.textContent = 'Upload Files';
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if ( abortBtn ) {
|
|
641
|
+
abortBtn.style.display = 'none';
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Updates a single file card in the DOM
|
|
647
|
+
*/
|
|
648
|
+
private updateFileCard ( fileId : string ) : void {
|
|
649
|
+
const file = this.files.get( fileId );
|
|
650
|
+
if ( !file ) return;
|
|
651
|
+
|
|
652
|
+
const card = this.shadowRoot.querySelector( `[data-file-id="${fileId}"]` ) as HTMLElement;
|
|
653
|
+
if ( !card ) return;
|
|
654
|
+
|
|
655
|
+
const progressBar = card.querySelector( '.progress-bar' ) as HTMLElement;
|
|
656
|
+
const progressText = card.querySelector( '.progress-text' ) as HTMLElement;
|
|
657
|
+
const statusDiv = card.querySelector( '.status' ) as HTMLElement;
|
|
658
|
+
|
|
659
|
+
if ( progressBar ) {
|
|
660
|
+
progressBar.style.width = `${file.progress}%`;
|
|
661
|
+
|
|
662
|
+
// Color based on status
|
|
663
|
+
if ( file.status === 'completed' ) {
|
|
664
|
+
progressBar.style.backgroundColor = '#22c55e'; // green
|
|
665
|
+
} else if ( file.status === 'error' ) {
|
|
666
|
+
progressBar.style.backgroundColor = '#ef4444'; // red
|
|
667
|
+
} else if ( file.status === 'aborted' ) {
|
|
668
|
+
progressBar.style.backgroundColor = '#f59e0b'; // orange
|
|
669
|
+
} else {
|
|
670
|
+
progressBar.style.backgroundColor = 'var(--color-primary)';
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if ( progressText ) {
|
|
675
|
+
progressText.textContent = `${Math.round( file.progress )}%`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if ( statusDiv && file.error ) {
|
|
679
|
+
statusDiv.textContent = file.error;
|
|
680
|
+
statusDiv.style.display = 'block';
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Renders all file cards
|
|
686
|
+
*/
|
|
687
|
+
private renderFileCards () : void {
|
|
688
|
+
const container = this.shadowRoot.querySelector( '#fileCardsContainer' );
|
|
689
|
+
if ( !container ) return;
|
|
690
|
+
|
|
691
|
+
if ( this.files.size === 0 ) {
|
|
692
|
+
container.innerHTML = '';
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
container.innerHTML = Array.from( this.files.values() ).map( ( file ) => `
|
|
697
|
+
<div class="file-card" data-file-id="${file.id}">
|
|
698
|
+
<button class="remove-btn" data-file-id="${file.id}">×</button>
|
|
699
|
+
${file.preview ? `<div class="preview"><img src="${file.preview}" alt="Preview"></div>` : '<div class="preview no-preview">📄</div>'}
|
|
554
700
|
<div class="file-info">
|
|
555
|
-
<div class="file-name" title="${
|
|
556
|
-
<div class="file-size">${
|
|
701
|
+
<div class="file-name" title="${file.file.name}">${file.file.name}</div>
|
|
702
|
+
<div class="file-size">${this.formatBytes( file.file.size )}</div>
|
|
557
703
|
</div>
|
|
558
704
|
<div class="progress-container">
|
|
559
705
|
<div class="progress-bar-bg">
|
|
560
|
-
<div class="progress-bar" style="width: ${
|
|
706
|
+
<div class="progress-bar" style="width: ${file.progress}%; background-color: ${
|
|
707
|
+
file.status === 'completed' ? '#22c55e' : file.status === 'error' ? '#ef4444' : 'var(--color-primary)'
|
|
708
|
+
}"></div>
|
|
561
709
|
</div>
|
|
562
|
-
<div class="progress-text">${
|
|
710
|
+
<div class="progress-text">${Math.round( file.progress )}%</div>
|
|
563
711
|
</div>
|
|
564
|
-
${
|
|
712
|
+
${file.error ? `<div class="status error">${file.error}</div>` : ''}
|
|
565
713
|
</div>
|
|
566
714
|
` ).join( '' );
|
|
567
715
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
716
|
+
// Bind remove button events
|
|
717
|
+
const removeButtons = container.querySelectorAll( '.remove-btn' );
|
|
718
|
+
removeButtons.forEach( ( btn ) => {
|
|
719
|
+
btn.addEventListener( 'click', ( e ) => {
|
|
720
|
+
e.stopPropagation();
|
|
721
|
+
const fileId = ( btn as HTMLElement ).dataset.fileId;
|
|
722
|
+
if ( fileId ) this.removeFile( fileId );
|
|
723
|
+
} );
|
|
724
|
+
} );
|
|
725
|
+
|
|
726
|
+
// Show/hide upload button
|
|
727
|
+
const uploadBtn = this.shadowRoot.querySelector( '#uploadBtn' ) as HTMLButtonElement;
|
|
728
|
+
if ( uploadBtn ) {
|
|
729
|
+
const hasPendingFiles = Array.from( this.files.values() ).some( ( f ) => f.status === 'pending' );
|
|
730
|
+
uploadBtn.style.display = hasPendingFiles ? 'block' : 'none';
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Binds event listeners
|
|
736
|
+
*/
|
|
737
|
+
private bindEvents () : void {
|
|
738
|
+
const uploadZone = this.shadowRoot.querySelector( '#uploadZone' ) as HTMLElement;
|
|
739
|
+
const fileInput = this.shadowRoot.querySelector( '#fileInput' ) as HTMLInputElement;
|
|
740
|
+
const browseBtn = this.shadowRoot.querySelector( '#browseBtn' ) as HTMLButtonElement;
|
|
741
|
+
const uploadBtn = this.shadowRoot.querySelector( '#uploadBtn' ) as HTMLButtonElement;
|
|
742
|
+
const abortBtn = this.shadowRoot.querySelector( '#abortBtn' ) as HTMLButtonElement;
|
|
743
|
+
|
|
744
|
+
// Click to browse
|
|
745
|
+
browseBtn?.addEventListener( 'click', () => fileInput?.click() );
|
|
746
|
+
|
|
747
|
+
// Drag and drop
|
|
748
|
+
uploadZone?.addEventListener( 'dragover', ( e ) => {
|
|
749
|
+
e.preventDefault();
|
|
750
|
+
uploadZone.classList.add( 'drag-over' );
|
|
751
|
+
} );
|
|
752
|
+
|
|
753
|
+
uploadZone?.addEventListener( 'dragleave', () => {
|
|
754
|
+
uploadZone.classList.remove( 'drag-over' );
|
|
755
|
+
} );
|
|
756
|
+
|
|
757
|
+
uploadZone?.addEventListener( 'drop', ( e ) => {
|
|
758
|
+
e.preventDefault();
|
|
759
|
+
uploadZone.classList.remove( 'drag-over' );
|
|
760
|
+
if ( e.dataTransfer?.files ) {
|
|
761
|
+
this.addFiles( e.dataTransfer.files );
|
|
762
|
+
}
|
|
763
|
+
} );
|
|
764
|
+
|
|
765
|
+
// File input change
|
|
766
|
+
fileInput?.addEventListener( 'change', ( e ) => {
|
|
767
|
+
const files = ( e.target as HTMLInputElement ).files;
|
|
768
|
+
if ( files ) {
|
|
769
|
+
this.addFiles( files );
|
|
770
|
+
// Reset input so same file can be added again
|
|
771
|
+
fileInput.value = '';
|
|
772
|
+
}
|
|
773
|
+
} );
|
|
774
|
+
|
|
775
|
+
// Upload button
|
|
776
|
+
uploadBtn?.addEventListener( 'click', () => this.startUpload() );
|
|
777
|
+
|
|
778
|
+
// Abort button
|
|
779
|
+
abortBtn?.addEventListener( 'click', () => this.abortAllUploads() );
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Renders the component
|
|
784
|
+
*/
|
|
785
|
+
private render () : void {
|
|
786
|
+
this.shadowRoot.innerHTML = `
|
|
639
787
|
<style>
|
|
640
788
|
:host {
|
|
641
789
|
display: block;
|
|
@@ -653,7 +801,7 @@ export class ChunkUploaderElement extends HTMLElement {
|
|
|
653
801
|
.upload-zone {
|
|
654
802
|
border: 2px dashed #ccc;
|
|
655
803
|
border-radius: 8px;
|
|
656
|
-
padding:
|
|
804
|
+
padding: 10px;
|
|
657
805
|
text-align: center;
|
|
658
806
|
cursor: pointer;
|
|
659
807
|
transition: all 0.3s ease;
|
|
@@ -882,6 +1030,72 @@ export class ChunkUploaderElement extends HTMLElement {
|
|
|
882
1030
|
display: flex;
|
|
883
1031
|
align-items: center;
|
|
884
1032
|
}
|
|
1033
|
+
|
|
1034
|
+
/* Compact Mode Styles */
|
|
1035
|
+
.container.compact .upload-zone {
|
|
1036
|
+
padding: 8px;
|
|
1037
|
+
display: flex;
|
|
1038
|
+
align-items: center;
|
|
1039
|
+
justify-content: center;
|
|
1040
|
+
gap: 8px;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.container.compact .upload-icon,
|
|
1044
|
+
.container.compact .upload-hint {
|
|
1045
|
+
display: none;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.container.compact .upload-text {
|
|
1049
|
+
font-size: 14px;
|
|
1050
|
+
margin-bottom: 0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.container.compact .browse-btn {
|
|
1054
|
+
margin-top: 0;
|
|
1055
|
+
padding: 6px 12px;
|
|
1056
|
+
font-size: 14px;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.container.compact .file-cards-container {
|
|
1060
|
+
margin-top: 12px;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.container.compact .file-card {
|
|
1064
|
+
width: 100%;
|
|
1065
|
+
display: flex;
|
|
1066
|
+
align-items: center;
|
|
1067
|
+
padding: 8px;
|
|
1068
|
+
height: auto;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.container.compact .preview {
|
|
1072
|
+
display: none !important;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.container.compact .file-info {
|
|
1076
|
+
margin-bottom: 0;
|
|
1077
|
+
display: flex;
|
|
1078
|
+
align-items: center;
|
|
1079
|
+
gap: 12px;
|
|
1080
|
+
flex: 0 0 auto;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.container.compact .file-name {
|
|
1084
|
+
margin-bottom: 0;
|
|
1085
|
+
max-width: 150px;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.container.compact .progress-container {
|
|
1089
|
+
margin-top: 0;
|
|
1090
|
+
flex: 1;
|
|
1091
|
+
margin-left: 12px;
|
|
1092
|
+
margin-right: 32px;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.container.compact .remove-btn {
|
|
1096
|
+
top: 50%;
|
|
1097
|
+
transform: translateY(-50%);
|
|
1098
|
+
}
|
|
885
1099
|
</style>
|
|
886
1100
|
|
|
887
1101
|
<div class="container">
|
|
@@ -889,9 +1103,9 @@ export class ChunkUploaderElement extends HTMLElement {
|
|
|
889
1103
|
<input type="file" id="fileInput" multiple>
|
|
890
1104
|
<div class="upload-zone-content">
|
|
891
1105
|
<div class="upload-icon">📁</div>
|
|
892
|
-
<div class="upload-text"
|
|
1106
|
+
<div class="upload-text">${this.config.labelDropFiles || 'Drop files here'}</div>
|
|
893
1107
|
<div class="upload-hint">or</div>
|
|
894
|
-
<button class="browse-btn" id="browseBtn"
|
|
1108
|
+
<button class="browse-btn" id="browseBtn">${this.config.labelBrowse || 'Browse Files'}</button>
|
|
895
1109
|
</div>
|
|
896
1110
|
</div>
|
|
897
1111
|
|
|
@@ -903,19 +1117,19 @@ export class ChunkUploaderElement extends HTMLElement {
|
|
|
903
1117
|
</div>
|
|
904
1118
|
</div>
|
|
905
1119
|
`;
|
|
906
|
-
|
|
1120
|
+
}
|
|
907
1121
|
}
|
|
908
1122
|
|
|
909
1123
|
/**
|
|
910
1124
|
* Defines the custom element if not already defined
|
|
911
1125
|
*/
|
|
912
|
-
export const defineChunkUploader = ( tagName = 'liwe3-chunk-uploader' ): void => {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1126
|
+
export const defineChunkUploader = ( tagName = 'liwe3-chunk-uploader' ) : void => {
|
|
1127
|
+
if ( typeof window !== 'undefined' && !customElements.get( tagName ) ) {
|
|
1128
|
+
customElements.define( tagName, ChunkUploaderElement );
|
|
1129
|
+
}
|
|
916
1130
|
};
|
|
917
1131
|
|
|
918
1132
|
// Auto-register with default tag name
|
|
919
1133
|
if ( typeof window !== 'undefined' ) {
|
|
920
|
-
|
|
1134
|
+
defineChunkUploader();
|
|
921
1135
|
}
|