@shelamkoff/rector 1.0.1 → 1.0.2
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/README.md +25 -24
- package/dist/core/clipboard/Clipboard.d.ts +2 -0
- package/dist/core/clipboard/Clipboard.js +52 -24
- package/dist/core/index.js +195 -194
- package/dist/core/locale/en.d.ts +1 -0
- package/dist/core/locale/en.js +1 -0
- package/dist/core/locale/ru.d.ts +1 -0
- package/dist/core/locale/ru.js +1 -0
- package/dist/core/themes/light.css +1 -1
- package/dist/core/themes/variables.css +72 -10
- package/dist/locale/en.d.ts +41 -0
- package/dist/locale/ru.d.ts +41 -0
- package/dist/plugins/attaches/attaches.css +16 -14
- package/dist/plugins/attaches/index.js +997 -920
- package/dist/plugins/attaches/locale/en.d.ts +9 -0
- package/dist/plugins/attaches/locale/en.js +11 -2
- package/dist/plugins/attaches/locale/ru.d.ts +9 -0
- package/dist/plugins/attaches/locale/ru.js +11 -2
- package/dist/plugins/carousel/carousel.css +213 -49
- package/dist/plugins/carousel/index.js +918 -858
- package/dist/plugins/carousel/locale/en.d.ts +15 -0
- package/dist/plugins/carousel/locale/en.js +18 -3
- package/dist/plugins/carousel/locale/ru.d.ts +15 -0
- package/dist/plugins/carousel/locale/ru.js +18 -3
- package/dist/plugins/gallery/gallery.css +6 -1
- package/dist/plugins/gallery/index.js +375 -354
- package/dist/plugins/gallery/locale/en.d.ts +8 -0
- package/dist/plugins/gallery/locale/en.js +11 -3
- package/dist/plugins/gallery/locale/ru.d.ts +8 -0
- package/dist/plugins/gallery/locale/ru.js +11 -3
- package/dist/plugins/gallery/view-empty.d.ts +2 -0
- package/dist/plugins/gallery/view-empty.js +21 -15
- package/dist/plugins/gallery/view-filled.d.ts +2 -2
- package/dist/plugins/gallery/view-filled.js +61 -56
- package/dist/plugins/image/image.css +7 -2
- package/dist/plugins/image/index.js +365 -345
- package/dist/plugins/image/locale/en.d.ts +8 -0
- package/dist/plugins/image/locale/en.js +11 -3
- package/dist/plugins/image/locale/ru.d.ts +8 -0
- package/dist/plugins/image/locale/ru.js +11 -3
- package/dist/plugins/image/view-empty.d.ts +2 -0
- package/dist/plugins/image/view-empty.js +21 -15
- package/dist/plugins/image/view-filled.d.ts +2 -2
- package/dist/plugins/image/view-filled.js +77 -72
- package/dist/plugins/shared/dropzone.d.ts +6 -0
- package/dist/plugins/shared/dropzone.js +47 -36
- package/dist/plugins/shared/layer.d.ts +13 -0
- package/dist/plugins/shared/layer.js +45 -0
- package/dist/plugins/shared/sourceEditor.css +165 -0
- package/dist/plugins/shared/sourceEditor.d.ts +97 -0
- package/dist/plugins/shared/sourceEditor.js +260 -0
- package/dist/renderer/renderers/carousel/index.js +24 -23
- package/dist/renderer/renderers/carousel/styles.css +8 -2
- package/package.json +32 -32
|
@@ -1,920 +1,997 @@
|
|
|
1
|
-
import { triggerFileInput } from '../shared/fileInput.js'
|
|
2
|
-
import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
|
|
3
|
-
import { getFileIcon, getExtension, formatSize, EXT_COLORS } from '../../shared/fileUtils.js'
|
|
4
|
-
import { validateAttachesData } from '../../shared/blockDataValidators.js'
|
|
5
|
-
import { sanitizeUrl } from '../../shared/sanitize/sanitizeUrl.js'
|
|
6
|
-
import { normalizeTextValue } from '../../shared/textFormat.js'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
/** @type {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* @
|
|
35
|
-
* @
|
|
36
|
-
* @
|
|
37
|
-
* @property {
|
|
38
|
-
* @property {
|
|
39
|
-
* @
|
|
40
|
-
* @property {string} [
|
|
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
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
*
|
|
160
|
-
* @param {HTMLElement} el
|
|
161
|
-
* @returns {
|
|
162
|
-
*/
|
|
163
|
-
|
|
164
|
-
const s = stateMap.get(el)
|
|
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
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
select.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
* @param {
|
|
517
|
-
* @param {
|
|
518
|
-
* @
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}, { signal })
|
|
574
|
-
return
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
* @param {
|
|
581
|
-
* @
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}, { signal })
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
panel.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
#
|
|
776
|
-
const s = stateMap.get(wrapper)
|
|
777
|
-
if (!s
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
if (!
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1
|
+
import { triggerFileInput } from '../shared/fileInput.js'
|
|
2
|
+
import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
|
|
3
|
+
import { getFileIcon, getExtension, formatSize, EXT_COLORS } from '../../shared/fileUtils.js'
|
|
4
|
+
import { validateAttachesData } from '../../shared/blockDataValidators.js'
|
|
5
|
+
import { sanitizeUrl } from '../../shared/sanitize/sanitizeUrl.js'
|
|
6
|
+
import { normalizeTextValue } from '../../shared/textFormat.js'
|
|
7
|
+
import { createPluginLayer } from '../shared/layer.js'
|
|
8
|
+
import { openSourceEditor, preloadSourceEditor } from '../shared/sourceEditor.js'
|
|
9
|
+
|
|
10
|
+
const editorStyles = new URL('./attaches.css', import.meta.url).href
|
|
11
|
+
const sourceEditorStyles = new URL('../shared/sourceEditor.css', import.meta.url).href
|
|
12
|
+
|
|
13
|
+
const ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1-2-2v-14a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"/><path d="M12 11v6"/><path d="M9.5 13.5l2.5-2.5l2.5 2.5"/></svg>'
|
|
14
|
+
const ICON_SELECT = '<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1-2-2v-14a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"/><path d="M12 11v6"/><path d="M9.5 13.5l2.5-2.5l2.5 2.5"/></svg>'
|
|
15
|
+
const ICON_UPLOAD = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/><path d="M7 9l5-5l5 5"/><path d="M12 4v12"/></svg>'
|
|
16
|
+
const ICON_TRASH = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4 7h16"/><path stroke-linecap="round" stroke-linejoin="round" d="M10 11v6"/><path stroke-linecap="round" stroke-linejoin="round" d="M14 11v6"/><path stroke-linecap="round" stroke-linejoin="round" d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12"/><path stroke-linecap="round" stroke-linejoin="round" d="M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/></svg>'
|
|
17
|
+
const ICON_CHEVRON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>'
|
|
18
|
+
const ICON_SETTINGS = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>'
|
|
19
|
+
const ICON_FILE_DEFAULT = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2"/></svg>'
|
|
20
|
+
const ICON_FILE_SM = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2"/></svg>'
|
|
21
|
+
|
|
22
|
+
/** @type {string[]} */
|
|
23
|
+
const VARIANTS = ['a', 'b', 'f', 'g']
|
|
24
|
+
|
|
25
|
+
/** @type {Record<string, { icon: string, label: string }>} */
|
|
26
|
+
const VARIANT_META = {
|
|
27
|
+
a: { icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21h-10a2 2 0 0 1-2-2v-14a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2"/></svg>', label: 'variantA' },
|
|
28
|
+
b: { icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>', label: 'variantB' },
|
|
29
|
+
f: { icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18"/><path d="M3 6h18"/><path d="M3 18h18"/></svg>', label: 'variantF' },
|
|
30
|
+
g: { icon: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="3"/></svg>', label: 'variantG' },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{ url: string, name: string, size: number, extension: string }} FileEntry
|
|
35
|
+
* @typedef {Object} AttachesAction
|
|
36
|
+
* @property {string} label Plain-text label shown on the source button.
|
|
37
|
+
* @property {string} [icon] Trusted application-owned icon markup. Never pass user-authored HTML.
|
|
38
|
+
* @property {(context: { signal: AbortSignal }) => Promise<Array<{ url: string, name: string, size?: number, extension?: string }> | null>} handler Opens an application-owned source and returns selected files, or `null` when selection is cancelled.
|
|
39
|
+
* @typedef {Object} AttachesConfig
|
|
40
|
+
* @property {(file: File, context: { signal: AbortSignal }) => Promise<{ url: string, size?: number }>} [uploadFile] Uploads a browser file. Without this callback the plugin creates a temporary object URL valid until the editor is destroyed.
|
|
41
|
+
* @property {AttachesAction[]} [actions] Additional file sources such as a media library.
|
|
42
|
+
* @property {boolean} [injectStyles=true] Whether the editor should load the built-in plugin stylesheet.
|
|
43
|
+
* @property {string} [css] Additional or replacement stylesheet URL, depending on `injectStyles`.
|
|
44
|
+
* @typedef {{
|
|
45
|
+
* data: { files: FileEntry[], variant: string },
|
|
46
|
+
* viewController: AbortController | null,
|
|
47
|
+
* taskControllers: Set<AbortController>,
|
|
48
|
+
* expanded: boolean,
|
|
49
|
+
* context: import('../../core/types').BlockMutationContext,
|
|
50
|
+
* }} AttachesState
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** @type {WeakMap<HTMLElement, AttachesState>} */
|
|
54
|
+
const stateMap = new WeakMap()
|
|
55
|
+
let groupSequence = 0
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derive a readable attachment name from a URL without trusting it as markup.
|
|
59
|
+
* @param {string} url Sanitized absolute file URL.
|
|
60
|
+
* @returns {string} Decoded final path segment, or an empty string.
|
|
61
|
+
*/
|
|
62
|
+
function getUrlFileName(url) {
|
|
63
|
+
try {
|
|
64
|
+
const segment = new URL(url).pathname.split('/').filter(Boolean).pop()
|
|
65
|
+
if (!segment) return ''
|
|
66
|
+
try {
|
|
67
|
+
return decodeURIComponent(segment)
|
|
68
|
+
} catch {
|
|
69
|
+
return segment
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return ''
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* File attachment block supporting direct uploads and consumer-defined file
|
|
78
|
+
* source actions such as a media library.
|
|
79
|
+
* @extends {BlockPluginAbstract<AttachesConfig>}
|
|
80
|
+
*/
|
|
81
|
+
export class Attaches extends BlockPluginAbstract {
|
|
82
|
+
static isTextBlock = false
|
|
83
|
+
static styles = [editorStyles, sourceEditorStyles]
|
|
84
|
+
type = 'attaches'
|
|
85
|
+
icon = ICON
|
|
86
|
+
inlineTools = false
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
#objectUrls = new Set()
|
|
90
|
+
/**
|
|
91
|
+
* Create an Attaches instance with the supplied consumer configuration.
|
|
92
|
+
* @param {AttachesConfig} [config]
|
|
93
|
+
*/
|
|
94
|
+
constructor(config) {
|
|
95
|
+
super(config)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @returns {AttachesConfig} Consumer configuration for this plugin instance. */
|
|
99
|
+
get #config() {
|
|
100
|
+
return /** @type {AttachesConfig} */ (this._config)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Return the localized toolbox label for this block.
|
|
105
|
+
* @returns {string} Localized toolbox title.
|
|
106
|
+
*/
|
|
107
|
+
get title() { return this._t('title', 'File') }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create the editable DOM owned by this block instance.
|
|
111
|
+
* @param {Record<string, unknown>} data
|
|
112
|
+
* @param {import('../../core/types').BlockMutationContext} context
|
|
113
|
+
* @returns {HTMLElement}
|
|
114
|
+
*/
|
|
115
|
+
render(data, context) {
|
|
116
|
+
const normalizeSize = (value) => {
|
|
117
|
+
const size = Number(value)
|
|
118
|
+
return Number.isFinite(size) && size >= 0 ? size : 0
|
|
119
|
+
}
|
|
120
|
+
let files
|
|
121
|
+
if (data?.file && !data?.files) {
|
|
122
|
+
const f = /** @type {any} */ (data.file)
|
|
123
|
+
const url = sanitizeUrl(normalizeTextValue(f.url), { policy: 'download', fallback: '' })
|
|
124
|
+
const name = normalizeTextValue(f.name)
|
|
125
|
+
files = url ? [{
|
|
126
|
+
url,
|
|
127
|
+
name,
|
|
128
|
+
size: normalizeSize(f.size),
|
|
129
|
+
extension: normalizeTextValue(f.extension) || getExtension(name),
|
|
130
|
+
}] : []
|
|
131
|
+
} else {
|
|
132
|
+
const inputFiles = Array.isArray(data?.files) ? data.files : []
|
|
133
|
+
files = /** @type {any[]} */ (inputFiles).map(f => ({
|
|
134
|
+
url: sanitizeUrl(normalizeTextValue(f?.url), { policy: 'download', fallback: '' }),
|
|
135
|
+
name: normalizeTextValue(f?.name),
|
|
136
|
+
size: normalizeSize(f?.size),
|
|
137
|
+
extension: normalizeTextValue(f?.extension) || getExtension(normalizeTextValue(f?.name)),
|
|
138
|
+
})).filter(f => f.url)
|
|
139
|
+
}
|
|
140
|
+
const requestedVariant = normalizeTextValue(data?.variant) || 'f'
|
|
141
|
+
const variant = VARIANTS.includes(requestedVariant) ? requestedVariant : 'f'
|
|
142
|
+
const wrapper = document.createElement('div')
|
|
143
|
+
wrapper.classList.add('oe-attaches')
|
|
144
|
+
wrapper.contentEditable = 'false'
|
|
145
|
+
wrapper.tabIndex = -1
|
|
146
|
+
stateMap.set(wrapper, {
|
|
147
|
+
data: { files, variant },
|
|
148
|
+
viewController: null,
|
|
149
|
+
taskControllers: new Set(),
|
|
150
|
+
expanded: false,
|
|
151
|
+
context,
|
|
152
|
+
})
|
|
153
|
+
if (files.length > 0) this.#renderFilled(wrapper)
|
|
154
|
+
else this.#renderSelect(wrapper)
|
|
155
|
+
return wrapper
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Serialize the current block DOM into document data.
|
|
160
|
+
* @param {HTMLElement} el
|
|
161
|
+
* @returns {{ files: FileEntry[], variant: string }}
|
|
162
|
+
*/
|
|
163
|
+
save(el) {
|
|
164
|
+
const s = stateMap.get(el)
|
|
165
|
+
if (!s) return { files: [], variant: 'f' }
|
|
166
|
+
this.#syncNames(el)
|
|
167
|
+
return { files: s.data.files.map(f => ({ ...f })), variant: s.data.variant }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check whether serialized data satisfies this block's schema.
|
|
172
|
+
* @param {Record<string, unknown>} data
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
validate(data) {
|
|
176
|
+
return validateAttachesData(data)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check whether the block has no meaningful user content.
|
|
181
|
+
* @param {HTMLElement} el
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
isEmpty(el) {
|
|
185
|
+
const s = stateMap.get(el)
|
|
186
|
+
return !s || s.data.files.length === 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extract neutral text that can initialize another block type.
|
|
191
|
+
* @param {HTMLElement} el
|
|
192
|
+
* @returns {{ text: string }}
|
|
193
|
+
*/
|
|
194
|
+
exportData(el) {
|
|
195
|
+
const s = stateMap.get(el)
|
|
196
|
+
return { text: s ? s.data.files.map(f => f.name).join(', ') : '' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Abort work and listeners owned by one rendered block.
|
|
201
|
+
* @param {HTMLElement} el
|
|
202
|
+
* @returns {void}
|
|
203
|
+
*/
|
|
204
|
+
destroy(el) {
|
|
205
|
+
const s = stateMap.get(el)
|
|
206
|
+
if (s) {
|
|
207
|
+
s.viewController?.abort()
|
|
208
|
+
for (const controller of s.taskControllers) controller.abort()
|
|
209
|
+
s.taskControllers.clear()
|
|
210
|
+
stateMap.delete(el)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Release temporary local URLs after all block snapshots and undo history
|
|
216
|
+
* owned by this editor have become unreachable.
|
|
217
|
+
* @returns {void}
|
|
218
|
+
*/
|
|
219
|
+
dispose() {
|
|
220
|
+
for (const url of this.#objectUrls) URL.revokeObjectURL(url)
|
|
221
|
+
this.#objectUrls.clear()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── sync editable names ────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
227
|
+
#syncNames(wrapper) {
|
|
228
|
+
const s = stateMap.get(wrapper)
|
|
229
|
+
if (!s) return
|
|
230
|
+
wrapper.querySelectorAll('.oe-attaches__name:not(.oe-attaches__name--static)').forEach((el, i) => {
|
|
231
|
+
if (s.data.files[i]) {
|
|
232
|
+
const t = String(el.textContent ?? '').trim()
|
|
233
|
+
if (t) s.data.files[i].name = t
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── dropzone ───────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
241
|
+
#renderSelect(wrapper) {
|
|
242
|
+
const s = stateMap.get(wrapper)
|
|
243
|
+
if (!s) return
|
|
244
|
+
wrapper.innerHTML = ''
|
|
245
|
+
wrapper.classList.remove('oe-attaches--filled')
|
|
246
|
+
s.viewController?.abort()
|
|
247
|
+
s.viewController = new AbortController()
|
|
248
|
+
const { signal } = s.viewController
|
|
249
|
+
|
|
250
|
+
const select = document.createElement('div')
|
|
251
|
+
select.className = 'oe-attaches__select'
|
|
252
|
+
|
|
253
|
+
if (s.context.readOnly) {
|
|
254
|
+
select.textContent = this._t('emptyReadonly', 'No files')
|
|
255
|
+
wrapper.appendChild(select)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const icon = document.createElement('div')
|
|
260
|
+
icon.className = 'oe-attaches__select-icon'
|
|
261
|
+
icon.innerHTML = ICON_SELECT
|
|
262
|
+
|
|
263
|
+
const text = document.createElement('div')
|
|
264
|
+
text.className = 'oe-attaches__select-text'
|
|
265
|
+
const link = document.createElement('button')
|
|
266
|
+
link.type = 'button'
|
|
267
|
+
link.className = 'oe-attaches__select-link'
|
|
268
|
+
link.textContent = this._t('dropzoneUpload', 'Upload')
|
|
269
|
+
link.addEventListener('click', (e) => { e.stopPropagation(); this.#triggerFileInput(wrapper) }, { signal })
|
|
270
|
+
const urlLink = document.createElement('button')
|
|
271
|
+
urlLink.type = 'button'
|
|
272
|
+
urlLink.className = 'oe-attaches__select-link'
|
|
273
|
+
urlLink.textContent = this._t('dropzoneUrl', 'insert a URL')
|
|
274
|
+
urlLink.addEventListener('click', (event) => {
|
|
275
|
+
event.preventDefault()
|
|
276
|
+
event.stopPropagation()
|
|
277
|
+
this.#openUrlEditor(wrapper)
|
|
278
|
+
}, { signal })
|
|
279
|
+
text.append(
|
|
280
|
+
link,
|
|
281
|
+
document.createTextNode(' ' + this._t('dropzoneText', 'files from your device or drag and drop them here')),
|
|
282
|
+
document.createTextNode(` ${this._t('dropzoneUrlPrefix', 'or')} `),
|
|
283
|
+
urlLink,
|
|
284
|
+
)
|
|
285
|
+
select.append(icon, text)
|
|
286
|
+
|
|
287
|
+
const configuredActions = this.#config.actions ?? []
|
|
288
|
+
if (configuredActions.length) {
|
|
289
|
+
const sources = document.createElement('div')
|
|
290
|
+
sources.className = 'oe-attaches__select-actions'
|
|
291
|
+
for (const action of configuredActions) {
|
|
292
|
+
const button = document.createElement('button')
|
|
293
|
+
button.type = 'button'
|
|
294
|
+
button.className = 'oe-attaches__select-action'
|
|
295
|
+
if (action.icon) button.insertAdjacentHTML('afterbegin', action.icon)
|
|
296
|
+
button.append(document.createTextNode(action.label))
|
|
297
|
+
button.addEventListener('click', event => {
|
|
298
|
+
event.stopPropagation()
|
|
299
|
+
void this.#runCustomAction(wrapper, action.handler)
|
|
300
|
+
}, { signal })
|
|
301
|
+
sources.appendChild(button)
|
|
302
|
+
}
|
|
303
|
+
select.appendChild(sources)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
wrapper.addEventListener('dragover', (e) => { e.preventDefault(); select.classList.add('oe-attaches__select--dragover') }, { signal })
|
|
307
|
+
wrapper.addEventListener('dragleave', () => select.classList.remove('oe-attaches__select--dragover'), { signal })
|
|
308
|
+
wrapper.addEventListener('drop', (e) => {
|
|
309
|
+
e.preventDefault()
|
|
310
|
+
select.classList.remove('oe-attaches__select--dragover')
|
|
311
|
+
if (e.dataTransfer?.files?.length) void this.#handleFiles(wrapper, e.dataTransfer.files)
|
|
312
|
+
}, { signal })
|
|
313
|
+
|
|
314
|
+
wrapper.appendChild(select)
|
|
315
|
+
preloadSourceEditor(wrapper, signal, ['url'])
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── filled state (dispatches by variant) ──────────────────────────────────
|
|
319
|
+
|
|
320
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
321
|
+
#renderFilled(wrapper) {
|
|
322
|
+
const s = stateMap.get(wrapper)
|
|
323
|
+
if (!s) return
|
|
324
|
+
this.#syncNames(wrapper)
|
|
325
|
+
wrapper.innerHTML = ''
|
|
326
|
+
wrapper.classList.add('oe-attaches--filled')
|
|
327
|
+
s.viewController?.abort()
|
|
328
|
+
s.viewController = new AbortController()
|
|
329
|
+
const { signal } = s.viewController
|
|
330
|
+
const files = s.data.files
|
|
331
|
+
const v = s.data.variant || 'a'
|
|
332
|
+
|
|
333
|
+
switch (v) {
|
|
334
|
+
case 'b':
|
|
335
|
+
this.#renderPills(wrapper, files, signal)
|
|
336
|
+
break
|
|
337
|
+
case 'f':
|
|
338
|
+
this.#renderNotion(wrapper, files, signal)
|
|
339
|
+
break
|
|
340
|
+
case 'g':
|
|
341
|
+
this.#renderMaterial(wrapper, files, signal)
|
|
342
|
+
break
|
|
343
|
+
default: // 'a'
|
|
344
|
+
if (files.length === 1) wrapper.appendChild(this.#buildCardA(wrapper, files[0], 0, signal))
|
|
345
|
+
else wrapper.appendChild(this.#buildGroupA(wrapper, signal))
|
|
346
|
+
break
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!s.context.readOnly) this.#renderActions(wrapper, signal)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Variant A: Minimal ────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @param {HTMLElement} w Block wrapper that owns the file state.
|
|
356
|
+
* @param {FileEntry} file File represented by this card.
|
|
357
|
+
* @param {number} i File index in the block data.
|
|
358
|
+
* @param {AbortSignal} sig Current view lifecycle signal.
|
|
359
|
+
* @returns {HTMLDivElement}
|
|
360
|
+
*/
|
|
361
|
+
#buildCardA(w, file, i, sig) {
|
|
362
|
+
const card = document.createElement('div')
|
|
363
|
+
card.className = 'oe-attaches__card'
|
|
364
|
+
card.appendChild(this.#buildIconA(file))
|
|
365
|
+
const info = document.createElement('div')
|
|
366
|
+
info.className = 'oe-attaches__info'
|
|
367
|
+
info.appendChild(this.#buildNameEl(w, i, file, sig))
|
|
368
|
+
if (file.size) { const m = document.createElement('div'); m.className = 'oe-attaches__meta'; m.textContent = formatSize(file.size); info.appendChild(m) }
|
|
369
|
+
card.append(info, this.#buildRemoveBtn(w, i, sig))
|
|
370
|
+
return card
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @param {HTMLElement} w Block wrapper that owns the group.
|
|
375
|
+
* @param {AbortSignal} sig Current view lifecycle signal.
|
|
376
|
+
* @returns {HTMLDivElement}
|
|
377
|
+
*/
|
|
378
|
+
#buildGroupA(w, sig) {
|
|
379
|
+
const s = stateMap.get(w)
|
|
380
|
+
if (!s) return document.createElement('div')
|
|
381
|
+
const files = s.data.files
|
|
382
|
+
const group = document.createElement('div')
|
|
383
|
+
group.className = 'oe-attaches__group'
|
|
384
|
+
|
|
385
|
+
const header = document.createElement('div')
|
|
386
|
+
header.className = 'oe-attaches__group-header'
|
|
387
|
+
const iconWrap = document.createElement('div')
|
|
388
|
+
iconWrap.className = 'oe-attaches__icon'
|
|
389
|
+
iconWrap.innerHTML = ICON_FILE_DEFAULT
|
|
390
|
+
const headerInfo = document.createElement('div')
|
|
391
|
+
headerInfo.className = 'oe-attaches__info'
|
|
392
|
+
const countText = document.createElement('div')
|
|
393
|
+
countText.className = 'oe-attaches__name oe-attaches__name--static'
|
|
394
|
+
countText.textContent = `${files.length} ${this._p('filesCount', files.length, 'files')}`
|
|
395
|
+
headerInfo.appendChild(countText)
|
|
396
|
+
const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0)
|
|
397
|
+
if (totalSize > 0) { const m = document.createElement('span'); m.className = 'oe-attaches__meta'; m.textContent = formatSize(totalSize); headerInfo.appendChild(m) }
|
|
398
|
+
const chevron = document.createElement('button')
|
|
399
|
+
chevron.type = 'button'
|
|
400
|
+
chevron.className = 'oe-attaches__chevron'
|
|
401
|
+
chevron.innerHTML = ICON_CHEVRON
|
|
402
|
+
chevron.setAttribute('aria-label', this._t('toggleGroup', 'Show or hide files'))
|
|
403
|
+
chevron.setAttribute('aria-expanded', String(s.expanded))
|
|
404
|
+
header.append(iconWrap, headerInfo, chevron)
|
|
405
|
+
|
|
406
|
+
const body = document.createElement('div')
|
|
407
|
+
body.className = 'oe-attaches__group-body'
|
|
408
|
+
body.id = `rector-editor-attaches-group-${++groupSequence}`
|
|
409
|
+
chevron.setAttribute('aria-controls', body.id)
|
|
410
|
+
if (s.expanded) { body.classList.add('oe-attaches__group-body--open'); chevron.classList.add('oe-attaches__chevron--open') }
|
|
411
|
+
for (let i = 0; i < files.length; i++) {
|
|
412
|
+
const row = document.createElement('div')
|
|
413
|
+
row.className = 'oe-attaches__row'
|
|
414
|
+
row.append(this.#buildNameEl(w, i, files[i], sig))
|
|
415
|
+
if (files[i].size) { const sz = document.createElement('span'); sz.className = 'oe-attaches__row-size'; sz.textContent = formatSize(files[i].size); row.appendChild(sz) }
|
|
416
|
+
row.appendChild(this.#buildRemoveBtn(w, i, sig))
|
|
417
|
+
body.appendChild(row)
|
|
418
|
+
}
|
|
419
|
+
const toggle = () => {
|
|
420
|
+
const st = stateMap.get(w)
|
|
421
|
+
if (!st) return
|
|
422
|
+
st.expanded = !st.expanded
|
|
423
|
+
body.classList.toggle('oe-attaches__group-body--open', st.expanded)
|
|
424
|
+
chevron.classList.toggle('oe-attaches__chevron--open', st.expanded)
|
|
425
|
+
chevron.setAttribute('aria-expanded', String(st.expanded))
|
|
426
|
+
}
|
|
427
|
+
header.addEventListener('click', (event) => {
|
|
428
|
+
const target = /** @type {HTMLElement | null} */ (event.target instanceof HTMLElement ? event.target : null)
|
|
429
|
+
if (target?.closest('button, [contenteditable="true"]')) return
|
|
430
|
+
toggle()
|
|
431
|
+
}, { signal: sig })
|
|
432
|
+
chevron.addEventListener('click', (event) => {
|
|
433
|
+
event.stopPropagation()
|
|
434
|
+
toggle()
|
|
435
|
+
}, { signal: sig })
|
|
436
|
+
group.append(header, body)
|
|
437
|
+
return group
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Variant A icon: always default file SVG + extension badge. @param {FileEntry} file @returns {HTMLDivElement} */
|
|
441
|
+
#buildIconA(file) {
|
|
442
|
+
const wrap = document.createElement('div')
|
|
443
|
+
wrap.className = 'oe-attaches__icon'
|
|
444
|
+
wrap.innerHTML = ICON_FILE_DEFAULT
|
|
445
|
+
if (file.extension) {
|
|
446
|
+
const badge = document.createElement('span')
|
|
447
|
+
badge.className = 'oe-attaches__ext'
|
|
448
|
+
badge.textContent = file.extension.toUpperCase()
|
|
449
|
+
const color = EXT_COLORS[file.extension.toLowerCase()]
|
|
450
|
+
if (color) badge.style.backgroundColor = color
|
|
451
|
+
wrap.appendChild(badge)
|
|
452
|
+
}
|
|
453
|
+
return wrap
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Variant B: Pill ────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* @param {HTMLElement} w Block wrapper that receives the pill list.
|
|
460
|
+
* @param {FileEntry[]} files Files to render.
|
|
461
|
+
* @param {AbortSignal} sig Current view lifecycle signal.
|
|
462
|
+
* @returns {void}
|
|
463
|
+
*/
|
|
464
|
+
#renderPills(w, files, sig) {
|
|
465
|
+
const container = document.createElement('div')
|
|
466
|
+
container.className = 'oe-attaches__pills'
|
|
467
|
+
for (let i = 0; i < files.length; i++) {
|
|
468
|
+
const pill = document.createElement('div')
|
|
469
|
+
pill.className = 'oe-attaches__pill'
|
|
470
|
+
const ic = document.createElement('div')
|
|
471
|
+
ic.className = 'oe-attaches__pill-icon'
|
|
472
|
+
ic.innerHTML = ICON_FILE_SM
|
|
473
|
+
pill.appendChild(ic)
|
|
474
|
+
pill.appendChild(this.#buildNameEl(w, i, files[i], sig))
|
|
475
|
+
if (files[i].size) { const sz = document.createElement('span'); sz.className = 'oe-attaches__pill-size'; sz.textContent = formatSize(files[i].size); pill.appendChild(sz) }
|
|
476
|
+
pill.appendChild(this.#buildRemoveBtn(w, i, sig))
|
|
477
|
+
container.appendChild(pill)
|
|
478
|
+
}
|
|
479
|
+
w.appendChild(container)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── Variant F: Notion ──────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @param {HTMLElement} w Block wrapper that receives the table-like list.
|
|
486
|
+
* @param {FileEntry[]} files Files to render.
|
|
487
|
+
* @param {AbortSignal} sig Current view lifecycle signal.
|
|
488
|
+
* @returns {void}
|
|
489
|
+
*/
|
|
490
|
+
#renderNotion(w, files, sig) {
|
|
491
|
+
const table = document.createElement('div')
|
|
492
|
+
table.className = 'oe-attaches__notion'
|
|
493
|
+
for (let i = 0; i < files.length; i++) {
|
|
494
|
+
const row = document.createElement('div')
|
|
495
|
+
row.className = 'oe-attaches__notion-row'
|
|
496
|
+
row.appendChild(this.#buildNameEl(w, i, files[i], sig))
|
|
497
|
+
const ext = (files[i].extension || '').toUpperCase()
|
|
498
|
+
if (ext) {
|
|
499
|
+
const tag = document.createElement('span')
|
|
500
|
+
tag.className = 'oe-attaches__notion-tag'
|
|
501
|
+
tag.textContent = ext
|
|
502
|
+
const color = EXT_COLORS[files[i].extension?.toLowerCase()] || null
|
|
503
|
+
if (color) { tag.style.backgroundColor = `${color}20`; tag.style.color = color }
|
|
504
|
+
row.appendChild(tag)
|
|
505
|
+
}
|
|
506
|
+
if (files[i].size) { const sz = document.createElement('span'); sz.className = 'oe-attaches__notion-size'; sz.textContent = formatSize(files[i].size); row.appendChild(sz) }
|
|
507
|
+
row.appendChild(this.#buildRemoveBtn(w, i, sig))
|
|
508
|
+
table.appendChild(row)
|
|
509
|
+
}
|
|
510
|
+
w.appendChild(table)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Variant G: Material ────────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {HTMLElement} w Block wrapper that receives the material cards.
|
|
517
|
+
* @param {FileEntry[]} files Files to render.
|
|
518
|
+
* @param {AbortSignal} sig Current view lifecycle signal.
|
|
519
|
+
* @returns {void}
|
|
520
|
+
*/
|
|
521
|
+
#renderMaterial(w, files, sig) {
|
|
522
|
+
const stack = document.createElement('div')
|
|
523
|
+
stack.className = 'oe-attaches__material'
|
|
524
|
+
for (let i = 0; i < files.length; i++) {
|
|
525
|
+
const card = document.createElement('div')
|
|
526
|
+
card.className = 'oe-attaches__material-card'
|
|
527
|
+
card.appendChild(this.#buildIconG(files[i]))
|
|
528
|
+
const info = document.createElement('div')
|
|
529
|
+
info.className = 'oe-attaches__info'
|
|
530
|
+
info.appendChild(this.#buildNameEl(w, i, files[i], sig))
|
|
531
|
+
if (files[i].size) { const m = document.createElement('div'); m.className = 'oe-attaches__meta'; m.textContent = formatSize(files[i].size); info.appendChild(m) }
|
|
532
|
+
card.append(info, this.#buildRemoveBtn(w, i, sig))
|
|
533
|
+
stack.appendChild(card)
|
|
534
|
+
}
|
|
535
|
+
w.appendChild(stack)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Variant G icon: custom type-specific icon, no badge. @param {FileEntry} file @returns {HTMLDivElement} */
|
|
539
|
+
#buildIconG(file) {
|
|
540
|
+
const wrap = document.createElement('div')
|
|
541
|
+
wrap.className = 'oe-attaches__material-icon'
|
|
542
|
+
const { svg } = getFileIcon(file.extension)
|
|
543
|
+
wrap.innerHTML = svg
|
|
544
|
+
return wrap
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── shared builders ────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @param {HTMLElement} wrapper
|
|
551
|
+
* @param {number} index
|
|
552
|
+
* @param {FileEntry} file
|
|
553
|
+
* @param {AbortSignal} signal
|
|
554
|
+
* @returns {HTMLDivElement}
|
|
555
|
+
*/
|
|
556
|
+
#buildNameEl(wrapper, index, file, signal) {
|
|
557
|
+
const state = stateMap.get(wrapper)
|
|
558
|
+
const editable = !state?.context.readOnly
|
|
559
|
+
const el = document.createElement('div')
|
|
560
|
+
el.className = `oe-attaches__name${editable ? '' : ' oe-attaches__name--static'}`
|
|
561
|
+
el.contentEditable = String(editable)
|
|
562
|
+
const fallbackName = this._t('untitled', 'File')
|
|
563
|
+
el.textContent = file.name || fallbackName
|
|
564
|
+
if (!editable) return el
|
|
565
|
+
|
|
566
|
+
el.addEventListener('keydown', (e) => {
|
|
567
|
+
if (e.key === 'Enter') { e.preventDefault(); el.blur() }
|
|
568
|
+
if (!e.ctrlKey && !e.metaKey) e.stopPropagation()
|
|
569
|
+
}, { signal })
|
|
570
|
+
el.addEventListener('blur', () => {
|
|
571
|
+
const s = stateMap.get(wrapper)
|
|
572
|
+
if (s?.data.files[index]) s.data.files[index].name = String(el.textContent ?? '').trim() || fallbackName
|
|
573
|
+
}, { signal })
|
|
574
|
+
return el
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* @param {HTMLElement} wrapper
|
|
579
|
+
* @param {number} index
|
|
580
|
+
* @param {AbortSignal} signal
|
|
581
|
+
* @returns {HTMLButtonElement}
|
|
582
|
+
*/
|
|
583
|
+
#buildRemoveBtn(wrapper, index, signal) {
|
|
584
|
+
const btn = document.createElement('button')
|
|
585
|
+
btn.type = 'button'
|
|
586
|
+
btn.className = 'oe-attaches__remove'
|
|
587
|
+
btn.innerHTML = '×'
|
|
588
|
+
btn.setAttribute('aria-label', this._t('delete', 'Delete'))
|
|
589
|
+
const state = stateMap.get(wrapper)
|
|
590
|
+
if (state?.context.readOnly) {
|
|
591
|
+
btn.hidden = true
|
|
592
|
+
btn.disabled = true
|
|
593
|
+
return btn
|
|
594
|
+
}
|
|
595
|
+
btn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
596
|
+
btn.addEventListener('click', (e) => {
|
|
597
|
+
e.stopPropagation()
|
|
598
|
+
const st = stateMap.get(wrapper)
|
|
599
|
+
if (!st) return
|
|
600
|
+
st.context.mutate(() => {
|
|
601
|
+
st.data.files.splice(index, 1)
|
|
602
|
+
if (st.data.files.length > 0) {
|
|
603
|
+
this.#renderFilled(wrapper)
|
|
604
|
+
} else {
|
|
605
|
+
this.#cancelTasks(wrapper)
|
|
606
|
+
this.#renderSelect(wrapper)
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
}, { signal })
|
|
610
|
+
return btn
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ── actions ────────────────────────────────────────────────────────────────
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @param {HTMLElement} wrapper Block wrapper that owns the action bar.
|
|
617
|
+
* @param {AbortSignal} signal Current view lifecycle signal.
|
|
618
|
+
* @returns {void}
|
|
619
|
+
*/
|
|
620
|
+
#renderActions(wrapper, signal) {
|
|
621
|
+
const actions = document.createElement('div')
|
|
622
|
+
actions.className = 'oe-attaches__actions'
|
|
623
|
+
|
|
624
|
+
// Settings dropdown (variant selector)
|
|
625
|
+
const dropdown = document.createElement('div')
|
|
626
|
+
dropdown.className = 'oe-attaches__dropdown'
|
|
627
|
+
|
|
628
|
+
const settingsBtn = document.createElement('button')
|
|
629
|
+
settingsBtn.type = 'button'
|
|
630
|
+
settingsBtn.className = 'oe-attaches__action-btn'
|
|
631
|
+
settingsBtn.innerHTML = `${ICON_SETTINGS} ${this._t('settings', 'Settings')}`
|
|
632
|
+
settingsBtn.setAttribute('aria-haspopup', 'true')
|
|
633
|
+
settingsBtn.setAttribute('aria-expanded', 'false')
|
|
634
|
+
|
|
635
|
+
const panel = this.#buildVariantPanel(wrapper, signal)
|
|
636
|
+
const layer = createPluginLayer(wrapper, signal)
|
|
637
|
+
const setOpen = (open) => {
|
|
638
|
+
dropdown.classList.toggle('oe-attaches__dropdown--open', open)
|
|
639
|
+
settingsBtn.setAttribute('aria-expanded', String(open))
|
|
640
|
+
if (open) layer.open()
|
|
641
|
+
else layer.close()
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
settingsBtn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
645
|
+
settingsBtn.addEventListener('click', (e) => {
|
|
646
|
+
e.stopPropagation()
|
|
647
|
+
setOpen(!dropdown.classList.contains('oe-attaches__dropdown--open'))
|
|
648
|
+
}, { signal })
|
|
649
|
+
|
|
650
|
+
document.addEventListener('click', (e) => {
|
|
651
|
+
const target = e.target
|
|
652
|
+
if (!(target instanceof globalThis.Node)) return
|
|
653
|
+
if (!dropdown.contains(/** @type {import('../../core/types').DOMNode} */ (target))) setOpen(false)
|
|
654
|
+
}, { signal })
|
|
655
|
+
document.addEventListener('keydown', (event) => {
|
|
656
|
+
if (event.key !== 'Escape' || !dropdown.classList.contains('oe-attaches__dropdown--open')) return
|
|
657
|
+
event.preventDefault()
|
|
658
|
+
setOpen(false)
|
|
659
|
+
settingsBtn.focus()
|
|
660
|
+
}, { signal })
|
|
661
|
+
|
|
662
|
+
dropdown.append(settingsBtn, panel)
|
|
663
|
+
actions.appendChild(dropdown)
|
|
664
|
+
|
|
665
|
+
actions.appendChild(this.#sep())
|
|
666
|
+
|
|
667
|
+
// Add files
|
|
668
|
+
const addBtn = document.createElement('button')
|
|
669
|
+
addBtn.type = 'button'
|
|
670
|
+
addBtn.className = 'oe-attaches__action-btn'
|
|
671
|
+
addBtn.innerHTML = `${ICON_UPLOAD} ${this._t('addFiles', 'Add files')}`
|
|
672
|
+
addBtn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
673
|
+
addBtn.addEventListener('click', (e) => { e.stopPropagation(); this.#triggerFileInput(wrapper) }, { signal })
|
|
674
|
+
actions.appendChild(addBtn)
|
|
675
|
+
|
|
676
|
+
for (const action of this.#config.actions ?? []) {
|
|
677
|
+
const sourceBtn = document.createElement('button')
|
|
678
|
+
sourceBtn.type = 'button'
|
|
679
|
+
sourceBtn.className = 'oe-attaches__action-btn'
|
|
680
|
+
if (action.icon) sourceBtn.insertAdjacentHTML('afterbegin', action.icon)
|
|
681
|
+
sourceBtn.append(document.createTextNode(` ${action.label}`))
|
|
682
|
+
sourceBtn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
683
|
+
sourceBtn.addEventListener('click', (e) => {
|
|
684
|
+
e.stopPropagation()
|
|
685
|
+
void this.#runCustomAction(wrapper, action.handler)
|
|
686
|
+
}, { signal })
|
|
687
|
+
actions.appendChild(sourceBtn)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
actions.appendChild(this.#sep())
|
|
691
|
+
|
|
692
|
+
// Delete all
|
|
693
|
+
const delBtn = document.createElement('button')
|
|
694
|
+
delBtn.type = 'button'
|
|
695
|
+
delBtn.className = 'oe-attaches__action-btn oe-attaches__action-btn--danger'
|
|
696
|
+
delBtn.innerHTML = ICON_TRASH
|
|
697
|
+
delBtn.setAttribute('aria-label', this._t('deleteAll', 'Delete all'))
|
|
698
|
+
delBtn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
699
|
+
delBtn.addEventListener('click', (e) => {
|
|
700
|
+
e.stopPropagation()
|
|
701
|
+
const st = stateMap.get(wrapper)
|
|
702
|
+
if (!st) return
|
|
703
|
+
st.context.mutate(() => {
|
|
704
|
+
this.#cancelTasks(wrapper)
|
|
705
|
+
st.data = { files: [], variant: st.data.variant }
|
|
706
|
+
this.#renderSelect(wrapper)
|
|
707
|
+
})
|
|
708
|
+
}, { signal })
|
|
709
|
+
actions.appendChild(delBtn)
|
|
710
|
+
|
|
711
|
+
wrapper.appendChild(actions)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Build the variant selector dropdown panel.
|
|
716
|
+
* @param {HTMLElement} wrapper
|
|
717
|
+
* @param {AbortSignal} signal
|
|
718
|
+
* @returns {HTMLDivElement}
|
|
719
|
+
*/
|
|
720
|
+
#buildVariantPanel(wrapper, signal) {
|
|
721
|
+
const s = stateMap.get(wrapper)
|
|
722
|
+
const panel = document.createElement('div')
|
|
723
|
+
panel.className = 'oe-attaches__dropdown-panel'
|
|
724
|
+
panel.setAttribute('role', 'group')
|
|
725
|
+
panel.setAttribute('aria-label', this._t('template', 'Template'))
|
|
726
|
+
panel.addEventListener('click', (e) => e.stopPropagation(), { signal })
|
|
727
|
+
|
|
728
|
+
const title = document.createElement('div')
|
|
729
|
+
title.className = 'oe-attaches__tpl-title'
|
|
730
|
+
title.textContent = this._t('template', 'Template')
|
|
731
|
+
panel.appendChild(title)
|
|
732
|
+
|
|
733
|
+
const grid = document.createElement('div')
|
|
734
|
+
grid.className = 'oe-attaches__tpl-grid'
|
|
735
|
+
|
|
736
|
+
for (const v of VARIANTS) {
|
|
737
|
+
const meta = VARIANT_META[v]
|
|
738
|
+
const btn = document.createElement('button')
|
|
739
|
+
btn.type = 'button'
|
|
740
|
+
btn.className = 'oe-attaches__tpl-btn' + (s?.data.variant === v ? ' oe-attaches__tpl-btn--active' : '')
|
|
741
|
+
btn.innerHTML = meta.icon
|
|
742
|
+
btn.title = this._t(meta.label, meta.label)
|
|
743
|
+
btn.setAttribute('aria-label', btn.title)
|
|
744
|
+
btn.setAttribute('aria-pressed', String(s?.data.variant === v))
|
|
745
|
+
btn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
746
|
+
btn.addEventListener('click', () => {
|
|
747
|
+
const st = stateMap.get(wrapper)
|
|
748
|
+
if (!st) return
|
|
749
|
+
st.context.mutate(() => {
|
|
750
|
+
this.#syncNames(wrapper)
|
|
751
|
+
st.data.variant = v
|
|
752
|
+
grid.querySelectorAll('.oe-attaches__tpl-btn').forEach(b => {
|
|
753
|
+
b.classList.remove('oe-attaches__tpl-btn--active')
|
|
754
|
+
b.setAttribute('aria-pressed', 'false')
|
|
755
|
+
})
|
|
756
|
+
btn.classList.add('oe-attaches__tpl-btn--active')
|
|
757
|
+
btn.setAttribute('aria-pressed', 'true')
|
|
758
|
+
// Re-render content only, keep actions intact
|
|
759
|
+
this.#rerenderContent(wrapper)
|
|
760
|
+
})
|
|
761
|
+
}, { signal })
|
|
762
|
+
grid.appendChild(btn)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
panel.appendChild(grid)
|
|
766
|
+
return panel
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Re-render file content without rebuilding action bar.
|
|
771
|
+
* Preserves the actions element and its event listeners.
|
|
772
|
+
* @param {HTMLElement} wrapper
|
|
773
|
+
* @returns {void}
|
|
774
|
+
*/
|
|
775
|
+
#rerenderContent(wrapper) {
|
|
776
|
+
const s = stateMap.get(wrapper)
|
|
777
|
+
if (!s) return
|
|
778
|
+
|
|
779
|
+
// Remove only content elements, keep .oe-attaches__actions intact
|
|
780
|
+
const actions = wrapper.querySelector('.oe-attaches__actions')
|
|
781
|
+
for (const child of [...wrapper.children]) {
|
|
782
|
+
if (child !== actions) child.remove()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const signal = s.viewController?.signal
|
|
786
|
+
if (!signal || signal.aborted) return
|
|
787
|
+
const files = s.data.files
|
|
788
|
+
const v = s.data.variant || 'a'
|
|
789
|
+
|
|
790
|
+
switch (v) {
|
|
791
|
+
case 'b': this.#renderPills(wrapper, files, signal); break
|
|
792
|
+
case 'f': this.#renderNotion(wrapper, files, signal); break
|
|
793
|
+
case 'g': this.#renderMaterial(wrapper, files, signal); break
|
|
794
|
+
default:
|
|
795
|
+
if (files.length === 1) wrapper.appendChild(this.#buildCardA(wrapper, files[0], 0, signal))
|
|
796
|
+
else wrapper.appendChild(this.#buildGroupA(wrapper, signal))
|
|
797
|
+
break
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Move actions back to the end
|
|
801
|
+
if (actions) wrapper.appendChild(actions)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/** @returns {HTMLDivElement} */
|
|
805
|
+
#sep() {
|
|
806
|
+
const s = document.createElement('div')
|
|
807
|
+
s.className = 'oe-attaches__actions-sep'
|
|
808
|
+
return s
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ── file handling ──────────────────────────────────────────────────────────
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Open the shared in-editor URL form and append one remote file.
|
|
815
|
+
* @param {HTMLElement} wrapper Block wrapper that owns the form.
|
|
816
|
+
* @returns {void}
|
|
817
|
+
*/
|
|
818
|
+
#openUrlEditor(wrapper) {
|
|
819
|
+
const state = stateMap.get(wrapper)
|
|
820
|
+
const signal = state?.viewController?.signal
|
|
821
|
+
if (!state || state.context.readOnly || !signal || signal.aborted) return
|
|
822
|
+
|
|
823
|
+
openSourceEditor({
|
|
824
|
+
wrapper,
|
|
825
|
+
signal,
|
|
826
|
+
kind: 'url',
|
|
827
|
+
title: this._t('urlEditorTitle', 'Add file by URL'),
|
|
828
|
+
label: this._t('urlEditorLabel', 'File URL'),
|
|
829
|
+
placeholder: this._t('urlEditorPlaceholder', 'https://example.com/file.pdf'),
|
|
830
|
+
submitText: this._t('sourceSubmit', 'Add'),
|
|
831
|
+
cancelText: this._t('sourceCancel', 'Cancel'),
|
|
832
|
+
invalidText: this._t('invalidUrl', 'Enter a valid file URL.'),
|
|
833
|
+
normalize: value => sanitizeUrl(value, { policy: 'download', fallback: '' }),
|
|
834
|
+
onSubmit: url => {
|
|
835
|
+
const current = stateMap.get(wrapper)
|
|
836
|
+
if (current !== state || current.context.readOnly) return
|
|
837
|
+
const name = getUrlFileName(url) || this._t('urlFileName', 'Linked file')
|
|
838
|
+
current.context.mutate(() => {
|
|
839
|
+
current.data.files.push({
|
|
840
|
+
url,
|
|
841
|
+
name,
|
|
842
|
+
size: 0,
|
|
843
|
+
extension: getExtension(name),
|
|
844
|
+
})
|
|
845
|
+
this.#renderFilled(wrapper)
|
|
846
|
+
})
|
|
847
|
+
},
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
852
|
+
#triggerFileInput(wrapper) {
|
|
853
|
+
const s = stateMap.get(wrapper)
|
|
854
|
+
if (!s || s.context.readOnly) return
|
|
855
|
+
triggerFileInput({
|
|
856
|
+
multiple: true,
|
|
857
|
+
signal: s.viewController?.signal,
|
|
858
|
+
onFiles: (files) => { void this.#handleFiles(wrapper, files) },
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Start an asynchronous additive operation owned by one block.
|
|
864
|
+
* View re-renders do not cancel it; block destruction or explicit removal does.
|
|
865
|
+
* @param {HTMLElement} wrapper
|
|
866
|
+
* @returns {{ state: AttachesState, controller: AbortController } | null}
|
|
867
|
+
*/
|
|
868
|
+
#startTask(wrapper) {
|
|
869
|
+
const state = stateMap.get(wrapper)
|
|
870
|
+
if (!state || state.context.readOnly) return null
|
|
871
|
+
const controller = new AbortController()
|
|
872
|
+
state.taskControllers.add(controller)
|
|
873
|
+
wrapper.classList.add('oe-attaches--loading')
|
|
874
|
+
return { state, controller }
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* @param {HTMLElement} wrapper Block wrapper whose loading state is updated.
|
|
879
|
+
* @param {AttachesState} state State that owns the asynchronous task.
|
|
880
|
+
* @param {AbortController} controller Completed task controller.
|
|
881
|
+
* @returns {void}
|
|
882
|
+
*/
|
|
883
|
+
#finishTask(wrapper, state, controller) {
|
|
884
|
+
state.taskControllers.delete(controller)
|
|
885
|
+
if (state.taskControllers.size === 0) wrapper.classList.remove('oe-attaches--loading')
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
889
|
+
#cancelTasks(wrapper) {
|
|
890
|
+
const state = stateMap.get(wrapper)
|
|
891
|
+
if (!state) return
|
|
892
|
+
for (const controller of state.taskControllers) controller.abort()
|
|
893
|
+
state.taskControllers.clear()
|
|
894
|
+
wrapper.classList.remove('oe-attaches--loading')
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* @param {HTMLElement} wrapper Block wrapper that receives uploaded files.
|
|
899
|
+
* @param {FileList | File[]} fileList Browser files selected or dropped by the user.
|
|
900
|
+
* @returns {Promise<void>}
|
|
901
|
+
*/
|
|
902
|
+
async #handleFiles(wrapper, fileList) {
|
|
903
|
+
const files = /** @type {File[]} */ (Array.from(fileList))
|
|
904
|
+
if (files.length === 0) return
|
|
905
|
+
const task = this.#startTask(wrapper)
|
|
906
|
+
if (!task) return
|
|
907
|
+
const { state: s, controller } = task
|
|
908
|
+
const { signal } = controller
|
|
909
|
+
|
|
910
|
+
try {
|
|
911
|
+
const uploadFile = this.#config.uploadFile
|
|
912
|
+
if (uploadFile) {
|
|
913
|
+
const added = []
|
|
914
|
+
for (const file of files) {
|
|
915
|
+
if (signal.aborted) break
|
|
916
|
+
try {
|
|
917
|
+
const ext = getExtension(file.name)
|
|
918
|
+
const result = await uploadFile(file, { signal })
|
|
919
|
+
const url = sanitizeUrl(String(result?.url || ''), { policy: 'download', fallback: '' })
|
|
920
|
+
const resultSize = Number(result?.size)
|
|
921
|
+
const size = Number.isFinite(resultSize) && resultSize >= 0 ? resultSize : file.size
|
|
922
|
+
if (url) added.push({ url, name: file.name, size, extension: ext })
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (signal.aborted) break
|
|
925
|
+
console.warn(`[Attaches] Failed to upload "${file.name}":`, error)
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (!signal.aborted && stateMap.get(wrapper) === s && added.length > 0) {
|
|
929
|
+
s.context.mutate(() => {
|
|
930
|
+
s.data.files.push(...added)
|
|
931
|
+
this.#renderFilled(wrapper)
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
} else {
|
|
935
|
+
const added = []
|
|
936
|
+
for (const file of files) {
|
|
937
|
+
if (signal.aborted) break
|
|
938
|
+
const ext = getExtension(file.name)
|
|
939
|
+
try {
|
|
940
|
+
const url = URL.createObjectURL(file)
|
|
941
|
+
this.#objectUrls.add(url)
|
|
942
|
+
added.push({ url, name: file.name, size: file.size, extension: ext })
|
|
943
|
+
} catch (error) {
|
|
944
|
+
console.warn(`[Attaches] Failed to open "${file.name}":`, error)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (!signal.aborted && stateMap.get(wrapper) === s && added.length > 0) {
|
|
948
|
+
s.context.mutate(() => {
|
|
949
|
+
s.data.files.push(...added)
|
|
950
|
+
this.#renderFilled(wrapper)
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} finally {
|
|
955
|
+
this.#finishTask(wrapper, s, controller)
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Add files selected by an application source such as a media library.
|
|
961
|
+
* One completed selection becomes one editor history operation.
|
|
962
|
+
* @param {HTMLElement} wrapper
|
|
963
|
+
* @param {(context: { signal: AbortSignal }) => Promise<Array<{ url: string, name: string, size?: number, extension?: string }> | null>} handler
|
|
964
|
+
* @returns {Promise<void>}
|
|
965
|
+
*/
|
|
966
|
+
async #runCustomAction(wrapper, handler) {
|
|
967
|
+
const task = this.#startTask(wrapper)
|
|
968
|
+
if (!task) return
|
|
969
|
+
const { state: s, controller } = task
|
|
970
|
+
const { signal } = controller
|
|
971
|
+
try {
|
|
972
|
+
const result = await handler({ signal })
|
|
973
|
+
if (!Array.isArray(result) || signal.aborted || stateMap.get(wrapper) !== s) return
|
|
974
|
+
const added = result.flatMap(item => {
|
|
975
|
+
const url = sanitizeUrl(String(item?.url || ''), { policy: 'download', fallback: '' })
|
|
976
|
+
const name = String(item?.name || '').trim()
|
|
977
|
+
if (!url || !name) return []
|
|
978
|
+
const size = Number(item?.size)
|
|
979
|
+
return [{
|
|
980
|
+
url,
|
|
981
|
+
name,
|
|
982
|
+
size: Number.isFinite(size) && size >= 0 ? size : 0,
|
|
983
|
+
extension: String(item?.extension || getExtension(name)),
|
|
984
|
+
}]
|
|
985
|
+
})
|
|
986
|
+
if (!added.length) return
|
|
987
|
+
s.context.mutate(() => {
|
|
988
|
+
s.data.files.push(...added)
|
|
989
|
+
this.#renderFilled(wrapper)
|
|
990
|
+
})
|
|
991
|
+
} catch (error) {
|
|
992
|
+
if (!signal.aborted) console.warn('[Attaches] Application source failed:', error)
|
|
993
|
+
} finally {
|
|
994
|
+
this.#finishTask(wrapper, s, controller)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|