@tuturuuu/ui 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/package.json +3 -3
- package/src/components/ui/calendar.tsx +6 -6
- package/src/components/ui/date-time-picker-layout.ts +6 -0
- package/src/components/ui/date-time-picker.test.tsx +25 -0
- package/src/components/ui/date-time-picker.tsx +5 -5
- package/src/components/ui/text-editor/__tests__/image-extension-clipboard.test.ts +87 -0
- package/src/components/ui/text-editor/__tests__/inline-task-conversion.test.tsx +116 -5
- package/src/components/ui/text-editor/__tests__/markdown-paste-extension.test.ts +419 -2
- package/src/components/ui/text-editor/clipboard-image-files.ts +41 -0
- package/src/components/ui/text-editor/clipboard-serialization.ts +249 -0
- package/src/components/ui/text-editor/color-controls.tsx +1 -1
- package/src/components/ui/text-editor/copy-menu.tsx +129 -0
- package/src/components/ui/text-editor/editor.tsx +7 -0
- package/src/components/ui/text-editor/image-extension.ts +3 -14
- package/src/components/ui/text-editor/markdown-paste-extension.ts +62 -14
- package/src/components/ui/text-editor/tool-bar.tsx +17 -69
- package/src/components/ui/text-editor/toolbar-controls.tsx +59 -0
|
@@ -1,7 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
serializeClipboardPlainText,
|
|
5
|
+
serializeClipboardText,
|
|
6
|
+
} from '../clipboard-serialization';
|
|
7
|
+
import { getEditorExtensions } from '../extensions';
|
|
2
8
|
import { __markdownPastePrivate } from '../markdown-paste-extension';
|
|
3
9
|
|
|
4
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
markdownToHtml,
|
|
12
|
+
looksLikeMarkdown,
|
|
13
|
+
normalizePastedPlainText,
|
|
14
|
+
shouldConvertPastedText,
|
|
15
|
+
} = __markdownPastePrivate;
|
|
16
|
+
|
|
17
|
+
const editors: Editor[] = [];
|
|
18
|
+
|
|
19
|
+
function createEditor(content: Record<string, unknown>) {
|
|
20
|
+
const editor = new Editor({ content, extensions: getEditorExtensions() });
|
|
21
|
+
editors.push(editor);
|
|
22
|
+
return editor;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function copyDocument(editor: Editor) {
|
|
26
|
+
return serializeClipboardText(
|
|
27
|
+
editor.state.doc.slice(0, editor.state.doc.content.size)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function copyDocumentAsPlainText(editor: Editor) {
|
|
32
|
+
return serializeClipboardPlainText(
|
|
33
|
+
editor.state.doc.slice(0, editor.state.doc.content.size)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
for (const editor of editors.splice(0)) editor.destroy();
|
|
39
|
+
});
|
|
5
40
|
|
|
6
41
|
describe('markdownToHtml', () => {
|
|
7
42
|
it('should convert headings', () => {
|
|
@@ -47,6 +82,65 @@ describe('markdownToHtml', () => {
|
|
|
47
82
|
expect(html).toContain('<li><p>item 2</p></li>');
|
|
48
83
|
});
|
|
49
84
|
|
|
85
|
+
it('normalizes copied visual bullets into structured list markers', () => {
|
|
86
|
+
const text = normalizePastedPlainText(
|
|
87
|
+
'Next steps\r\n• Draft the interview plan\r\n◦ Share it with the team'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(text).toBe(
|
|
91
|
+
'Next steps\n- Draft the interview plan\n- Share it with the team'
|
|
92
|
+
);
|
|
93
|
+
expect(looksLikeMarkdown(text)).toBe(true);
|
|
94
|
+
expect(markdownToHtml(text)).toContain(
|
|
95
|
+
'<ul><li><p>Draft the interview plan</p></li><li><p>Share it with the team</p></li></ul>'
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('normalizes copied visual checkboxes into task list markers', () => {
|
|
100
|
+
const text = normalizePastedPlainText('☐ Plan interviews\n☑ Invite team');
|
|
101
|
+
|
|
102
|
+
expect(text).toBe('- [ ] Plan interviews\n- [x] Invite team');
|
|
103
|
+
expect(markdownToHtml(text)).toContain('data-type="taskList"');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('collapses pathological blank-line runs without removing section spacing', () => {
|
|
107
|
+
const text = normalizePastedPlainText(
|
|
108
|
+
'First section\r\n\r\n\r\n\r\nSecond section\n\n\n- Item'
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(text).toBe('First section\n\nSecond section\n\n- Item');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('does not collapse intentional blank lines inside fenced code', () => {
|
|
115
|
+
const text = normalizePastedPlainText(
|
|
116
|
+
'Before\n\n\n```ts\nfirst()\n\n\nsecond()\n```\n\n\nAfter'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(text).toBe('Before\n\n```ts\nfirst()\n\n\nsecond()\n```\n\nAfter');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('keeps semantic rich HTML for long structured task descriptions', () => {
|
|
123
|
+
const text = normalizePastedPlainText(
|
|
124
|
+
'KEEPING FROM THE NEW VERSION:\n• The new About Me UI editor: I like the compactness of it more than the older version.'
|
|
125
|
+
);
|
|
126
|
+
const html = [
|
|
127
|
+
'<h1>KEEPING FROM THE NEW VERSION:</h1>',
|
|
128
|
+
'<ul><li><p><strong>The new About Me UI editor:</strong> ',
|
|
129
|
+
'I like the compactness of it more than the older version.</p></li></ul>',
|
|
130
|
+
].join('');
|
|
131
|
+
|
|
132
|
+
expect(looksLikeMarkdown(text)).toBe(true);
|
|
133
|
+
expect(shouldConvertPastedText({ html, text })).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('converts Markdown when clipboard HTML is only a visual wrapper', () => {
|
|
137
|
+
const text = '# Heading\n- Item';
|
|
138
|
+
const html = '<div># Heading<br>- Item</div>';
|
|
139
|
+
|
|
140
|
+
expect(shouldConvertPastedText({ html, text })).toBe(true);
|
|
141
|
+
expect(shouldConvertPastedText({ html: '', text })).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
50
144
|
it('should convert ordered lists', () => {
|
|
51
145
|
const md = '1. first\n2. second';
|
|
52
146
|
const html = markdownToHtml(md);
|
|
@@ -63,6 +157,16 @@ describe('markdownToHtml', () => {
|
|
|
63
157
|
expect(html).toContain('data-checked="true"');
|
|
64
158
|
});
|
|
65
159
|
|
|
160
|
+
it('keeps adjacent regular and task lists as separate structures', () => {
|
|
161
|
+
const html = markdownToHtml(
|
|
162
|
+
'- Regular item\n\n- [ ] Pending task\n- [x] Finished task'
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(html).toContain('<ul><li><p>Regular item</p></li></ul>');
|
|
166
|
+
expect(html).toContain('<ul data-type="taskList">');
|
|
167
|
+
expect(html).not.toContain('data-checked="null"');
|
|
168
|
+
});
|
|
169
|
+
|
|
66
170
|
it('should convert nested unordered lists', () => {
|
|
67
171
|
const md = '- Item A\n - Nested A1\n - Nested A2\n- Item B';
|
|
68
172
|
const html = markdownToHtml(md);
|
|
@@ -86,6 +190,12 @@ describe('markdownToHtml', () => {
|
|
|
86
190
|
);
|
|
87
191
|
});
|
|
88
192
|
|
|
193
|
+
it('preserves a copied ordered-list start number', () => {
|
|
194
|
+
const html = markdownToHtml('3. Third\n4. Fourth');
|
|
195
|
+
|
|
196
|
+
expect(html).toContain('<ol start="3">');
|
|
197
|
+
});
|
|
198
|
+
|
|
89
199
|
it('should convert deeply nested lists', () => {
|
|
90
200
|
const md = '- A\n - B\n - C\n - D';
|
|
91
201
|
const html = markdownToHtml(md);
|
|
@@ -210,6 +320,12 @@ describe('markdownToHtml', () => {
|
|
|
210
320
|
expect(html).toContain('<p>Hello world</p>');
|
|
211
321
|
});
|
|
212
322
|
|
|
323
|
+
it('preserves intentional line breaks inside pasted paragraphs', () => {
|
|
324
|
+
const html = markdownToHtml('Line one\nLine two');
|
|
325
|
+
|
|
326
|
+
expect(html).toContain('<p>Line one<br>Line two</p>');
|
|
327
|
+
});
|
|
328
|
+
|
|
213
329
|
it('should escape HTML in plain text', () => {
|
|
214
330
|
const md = '<script>alert(1)</script>';
|
|
215
331
|
const html = markdownToHtml(md);
|
|
@@ -264,3 +380,304 @@ describe('markdownToHtml', () => {
|
|
|
264
380
|
expect(html).not.toContain('<div>');
|
|
265
381
|
});
|
|
266
382
|
});
|
|
383
|
+
|
|
384
|
+
describe('task-description clipboard serialization', () => {
|
|
385
|
+
it('removes duplicate empty blocks while retaining one section gap', () => {
|
|
386
|
+
const editor = createEditor({
|
|
387
|
+
type: 'doc',
|
|
388
|
+
content: [
|
|
389
|
+
{
|
|
390
|
+
type: 'heading',
|
|
391
|
+
attrs: { level: 1 },
|
|
392
|
+
content: [{ type: 'text', text: 'KEEPING FROM THE NEW VERSION:' }],
|
|
393
|
+
},
|
|
394
|
+
{ type: 'paragraph' },
|
|
395
|
+
{ type: 'paragraph' },
|
|
396
|
+
{ type: 'paragraph' },
|
|
397
|
+
{
|
|
398
|
+
type: 'paragraph',
|
|
399
|
+
content: [{ type: 'text', text: 'Intro' }],
|
|
400
|
+
},
|
|
401
|
+
{ type: 'paragraph' },
|
|
402
|
+
{ type: 'paragraph' },
|
|
403
|
+
{
|
|
404
|
+
type: 'heading',
|
|
405
|
+
attrs: { level: 2 },
|
|
406
|
+
content: [{ type: 'text', text: 'Next section' }],
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(copyDocument(editor)).toBe(
|
|
412
|
+
'# KEEPING FROM THE NEW VERSION:\n\nIntro\n\n## Next section'
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('retains unordered, nested ordered, and task-list markers', () => {
|
|
417
|
+
const editor = createEditor({
|
|
418
|
+
type: 'doc',
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: 'bulletList',
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: 'listItem',
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: 'paragraph',
|
|
428
|
+
content: [{ type: 'text', text: 'Parent bullet' }],
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: 'orderedList',
|
|
432
|
+
attrs: { start: 3 },
|
|
433
|
+
content: [
|
|
434
|
+
{
|
|
435
|
+
type: 'listItem',
|
|
436
|
+
content: [
|
|
437
|
+
{
|
|
438
|
+
type: 'paragraph',
|
|
439
|
+
content: [{ type: 'text', text: 'Nested step' }],
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
],
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
type: 'taskList',
|
|
451
|
+
content: [
|
|
452
|
+
{
|
|
453
|
+
type: 'taskItem',
|
|
454
|
+
attrs: { checked: false },
|
|
455
|
+
content: [
|
|
456
|
+
{
|
|
457
|
+
type: 'paragraph',
|
|
458
|
+
content: [{ type: 'text', text: 'Pending' }],
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
type: 'taskItem',
|
|
464
|
+
attrs: { checked: true },
|
|
465
|
+
content: [
|
|
466
|
+
{
|
|
467
|
+
type: 'paragraph',
|
|
468
|
+
content: [{ type: 'text', text: 'Finished' }],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expect(copyDocument(editor)).toBe(
|
|
478
|
+
[
|
|
479
|
+
'- Parent bullet',
|
|
480
|
+
' 3. Nested step',
|
|
481
|
+
'',
|
|
482
|
+
'- [ ] Pending',
|
|
483
|
+
'- [x] Finished',
|
|
484
|
+
].join('\n')
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('retains headings, inline formatting, links, and hard breaks', () => {
|
|
489
|
+
const editor = createEditor({
|
|
490
|
+
type: 'doc',
|
|
491
|
+
content: [
|
|
492
|
+
{
|
|
493
|
+
type: 'heading',
|
|
494
|
+
attrs: { level: 2 },
|
|
495
|
+
content: [
|
|
496
|
+
{ type: 'text', text: 'Bold', marks: [{ type: 'bold' }] },
|
|
497
|
+
{ type: 'text', text: ' and ' },
|
|
498
|
+
{ type: 'text', text: 'italic', marks: [{ type: 'italic' }] },
|
|
499
|
+
],
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
type: 'paragraph',
|
|
503
|
+
content: [
|
|
504
|
+
{
|
|
505
|
+
type: 'text',
|
|
506
|
+
text: 'Docs',
|
|
507
|
+
marks: [{ type: 'link', attrs: { href: 'https://example.com' } }],
|
|
508
|
+
},
|
|
509
|
+
{ type: 'hardBreak' },
|
|
510
|
+
{ type: 'text', text: 'deleted', marks: [{ type: 'strike' }] },
|
|
511
|
+
],
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
});
|
|
515
|
+
const slice = editor.state.doc.slice(0, editor.state.doc.content.size);
|
|
516
|
+
let fromClipboardProp = '';
|
|
517
|
+
editor.view.someProp('clipboardTextSerializer', (serializer) => {
|
|
518
|
+
fromClipboardProp = serializer(slice, editor.view);
|
|
519
|
+
return true;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
expect(fromClipboardProp).toBe(
|
|
523
|
+
'## **Bold** and *italic*\n\n[Docs](https://example.com)\n~~deleted~~'
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('creates clean plain text without Markdown formatting delimiters', () => {
|
|
528
|
+
const editor = createEditor({
|
|
529
|
+
type: 'doc',
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: 'heading',
|
|
533
|
+
attrs: { level: 2 },
|
|
534
|
+
content: [
|
|
535
|
+
{ type: 'text', text: 'Important', marks: [{ type: 'bold' }] },
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
type: 'paragraph',
|
|
540
|
+
content: [
|
|
541
|
+
{
|
|
542
|
+
type: 'text',
|
|
543
|
+
text: 'Read the docs',
|
|
544
|
+
marks: [{ type: 'link', attrs: { href: 'https://example.com' } }],
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
type: 'bulletList',
|
|
550
|
+
content: [
|
|
551
|
+
{
|
|
552
|
+
type: 'listItem',
|
|
553
|
+
content: [
|
|
554
|
+
{
|
|
555
|
+
type: 'paragraph',
|
|
556
|
+
content: [{ type: 'text', text: 'First point' }],
|
|
557
|
+
},
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(copyDocumentAsPlainText(editor)).toBe(
|
|
566
|
+
'Important\n\nRead the docs\n\n• First point'
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('uses readable checkbox markers and preserves ordered-list starts in plain text', () => {
|
|
571
|
+
const editor = createEditor({
|
|
572
|
+
type: 'doc',
|
|
573
|
+
content: [
|
|
574
|
+
{
|
|
575
|
+
type: 'taskList',
|
|
576
|
+
content: [
|
|
577
|
+
{
|
|
578
|
+
type: 'taskItem',
|
|
579
|
+
attrs: { checked: false },
|
|
580
|
+
content: [
|
|
581
|
+
{
|
|
582
|
+
type: 'paragraph',
|
|
583
|
+
content: [{ type: 'text', text: 'Todo' }],
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
type: 'taskItem',
|
|
589
|
+
attrs: { checked: true },
|
|
590
|
+
content: [
|
|
591
|
+
{
|
|
592
|
+
type: 'paragraph',
|
|
593
|
+
content: [{ type: 'text', text: 'Done' }],
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
},
|
|
597
|
+
],
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
type: 'orderedList',
|
|
601
|
+
attrs: { start: 4 },
|
|
602
|
+
content: [
|
|
603
|
+
{
|
|
604
|
+
type: 'listItem',
|
|
605
|
+
content: [
|
|
606
|
+
{
|
|
607
|
+
type: 'paragraph',
|
|
608
|
+
content: [{ type: 'text', text: 'Continue' }],
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
expect(copyDocumentAsPlainText(editor)).toBe(
|
|
618
|
+
'☐ Todo\n☑ Done\n\n4. Continue'
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('round-trips the reported task shape through plain clipboard text', () => {
|
|
623
|
+
const source = createEditor({
|
|
624
|
+
type: 'doc',
|
|
625
|
+
content: [
|
|
626
|
+
{
|
|
627
|
+
type: 'heading',
|
|
628
|
+
attrs: { level: 1 },
|
|
629
|
+
content: [{ type: 'text', text: 'CHANGES FOR THE SITE' }],
|
|
630
|
+
},
|
|
631
|
+
{ type: 'paragraph' },
|
|
632
|
+
{ type: 'paragraph' },
|
|
633
|
+
{
|
|
634
|
+
type: 'bulletList',
|
|
635
|
+
content: [
|
|
636
|
+
{
|
|
637
|
+
type: 'listItem',
|
|
638
|
+
content: [
|
|
639
|
+
{
|
|
640
|
+
type: 'paragraph',
|
|
641
|
+
content: [
|
|
642
|
+
{
|
|
643
|
+
type: 'text',
|
|
644
|
+
text: 'The new editor:',
|
|
645
|
+
marks: [{ type: 'bold' }],
|
|
646
|
+
},
|
|
647
|
+
{ type: 'text', text: ' keep the compact layout.' },
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
});
|
|
656
|
+
const clipboardText = copyDocument(source);
|
|
657
|
+
const destination = createEditor({ type: 'doc', content: [] });
|
|
658
|
+
destination.commands.setContent(markdownToHtml(clipboardText));
|
|
659
|
+
const content = destination.getJSON().content ?? [];
|
|
660
|
+
|
|
661
|
+
expect(clipboardText).toBe(
|
|
662
|
+
'# CHANGES FOR THE SITE\n\n- **The new editor:** keep the compact layout.'
|
|
663
|
+
);
|
|
664
|
+
expect(content.map((node) => node.type).slice(0, 2)).toEqual([
|
|
665
|
+
'heading',
|
|
666
|
+
'bulletList',
|
|
667
|
+
]);
|
|
668
|
+
expect(content[1]).toMatchObject({
|
|
669
|
+
content: [
|
|
670
|
+
{
|
|
671
|
+
content: [
|
|
672
|
+
{
|
|
673
|
+
content: [
|
|
674
|
+
{ marks: [{ type: 'bold' }], text: 'The new editor:' },
|
|
675
|
+
{ text: ' keep the compact layout.' },
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const IMAGE_EXTENSIONS_BY_MIME_TYPE: Readonly<Record<string, string>> = {
|
|
2
|
+
'image/avif': 'avif',
|
|
3
|
+
'image/gif': 'gif',
|
|
4
|
+
'image/jpeg': 'jpg',
|
|
5
|
+
'image/jpg': 'jpg',
|
|
6
|
+
'image/png': 'png',
|
|
7
|
+
'image/svg+xml': 'svg',
|
|
8
|
+
'image/webp': 'webp',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function normalizeClipboardImageFile(file: File, clipboardType: string): File {
|
|
12
|
+
const type = file.type || clipboardType;
|
|
13
|
+
const normalizedType = type.split(';', 1)[0]?.trim().toLowerCase() ?? '';
|
|
14
|
+
const extension = IMAGE_EXTENSIONS_BY_MIME_TYPE[normalizedType];
|
|
15
|
+
const currentName = file.name.trim();
|
|
16
|
+
const name =
|
|
17
|
+
currentName && (/\.[a-z0-9]+$/i.test(currentName) || !extension)
|
|
18
|
+
? currentName
|
|
19
|
+
: `${currentName || 'pasted-image'}${extension ? `.${extension}` : ''}`;
|
|
20
|
+
|
|
21
|
+
if (name === file.name && type === file.type) {
|
|
22
|
+
return file;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new File([file], name, {
|
|
26
|
+
lastModified: file.lastModified,
|
|
27
|
+
type,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getClipboardImageFiles(
|
|
32
|
+
items: DataTransferItemList | DataTransferItem[]
|
|
33
|
+
): File[] {
|
|
34
|
+
return Array.from(items)
|
|
35
|
+
.map((item) => {
|
|
36
|
+
if (!item.type.startsWith('image/')) return null;
|
|
37
|
+
const file = item.getAsFile();
|
|
38
|
+
return file ? normalizeClipboardImageFile(file, item.type) : null;
|
|
39
|
+
})
|
|
40
|
+
.filter((file): file is File => file !== null);
|
|
41
|
+
}
|