@seorii/tiptap 0.3.0-next.9 → 0.4.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/dist/i18n/index.d.ts +106 -6
- package/dist/i18n/index.js +56 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/command/emoji.d.ts +3 -17
- package/dist/plugin/command/emoji.js +51 -24
- package/dist/plugin/command/stores.svelte.d.ts +52 -0
- package/dist/plugin/command/stores.svelte.js +69 -0
- package/dist/plugin/command/suggest.d.ts +6 -19
- package/dist/plugin/command/suggest.js +149 -49
- package/dist/plugin/embed.d.ts +2 -2
- package/dist/plugin/embed.js +6 -2
- package/dist/plugin/iframe.js +1 -1
- package/dist/plugin/image/dragdrop.d.ts +2 -0
- package/dist/plugin/image/dragdrop.js +126 -15
- package/dist/plugin/image/index.js +4 -3
- package/dist/plugin/indent.js +0 -1
- package/dist/plugin/orderedlist/index.d.ts +1 -1
- package/dist/plugin/orderedlist/index.js +1 -1
- package/dist/plugin/orderedlist/{korean.scss → korean.css} +2 -2
- package/dist/plugin/resize/index.d.ts +8 -0
- package/dist/plugin/resize/index.js +454 -0
- package/dist/plugin/table/index.d.ts +1 -1
- package/dist/plugin/table/index.js +19 -11
- package/dist/plugin/table/style/{cell.scss → cell.css} +6 -5
- package/dist/plugin/table/style/{grip.scss → grip.css} +14 -19
- package/dist/plugin/table/style/resize.css +28 -0
- package/dist/plugin/table/style/{table.scss → table.css} +15 -17
- package/dist/plugin/table/style.css +4 -0
- package/dist/plugin/table/tableCell/index.js +2 -4
- package/dist/plugin/table/tableHeader/index.js +1 -2
- package/dist/plugin/upload/skeleton/UploadSkeleton.svelte +97 -0
- package/dist/plugin/upload/skeleton/UploadSkeleton.svelte.d.ts +5 -0
- package/dist/plugin/upload/skeleton/index.d.ts +30 -0
- package/dist/plugin/upload/skeleton/index.js +147 -0
- package/dist/plugin/youtube.js +1 -1
- package/dist/tiptap/Bubble.svelte +231 -92
- package/dist/tiptap/Bubble.svelte.d.ts +9 -6
- package/dist/tiptap/Command.svelte +160 -158
- package/dist/tiptap/Command.svelte.d.ts +2 -3
- package/dist/tiptap/Floating.svelte +51 -24
- package/dist/tiptap/Floating.svelte.d.ts +1 -0
- package/dist/tiptap/TipTap.svelte +302 -140
- package/dist/tiptap/TipTap.svelte.d.ts +10 -3
- package/dist/tiptap/ToolbarButton.svelte +30 -10
- package/dist/tiptap/ToolbarButton.svelte.d.ts +10 -6
- package/dist/tiptap/setMath.d.ts +2 -1
- package/dist/tiptap/setMath.js +74 -12
- package/dist/tiptap/tiptap.d.ts +9 -1
- package/dist/tiptap/tiptap.js +172 -16
- package/package.json +63 -57
- package/dist/plugin/command/stores.d.ts +0 -13
- package/dist/plugin/command/stores.js +0 -7
- package/dist/plugin/table/style/resize.scss +0 -26
- package/dist/plugin/table/style.scss +0 -4
|
@@ -1,18 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeSlash, moveSlashSelection, runSlashItemAt, setSlashItems, setSlashLocation, setSlashProps, slashState } from './stores.svelte';
|
|
2
2
|
import i18n from '../../i18n';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import enUs from '../../i18n/en-us/index';
|
|
4
|
+
import koKr from '../../i18n/ko-kr/index';
|
|
5
|
+
import { fallbackUpload, releaseObjectUrlOnImageSettled } from '../image/dragdrop';
|
|
6
|
+
import { insertUploadSkeleton } from '../upload/skeleton';
|
|
7
|
+
import { PluginKey, TextSelection } from '@tiptap/pm/state';
|
|
8
|
+
import Suggestion, {} from '@tiptap/suggestion';
|
|
9
|
+
const normalizeSearch = (value) => value.toLowerCase().trim();
|
|
10
|
+
const compactSearch = (value) => normalizeSearch(value).replace(/\s+/g, '');
|
|
11
|
+
function localeText(key, locale) {
|
|
12
|
+
const value = locale[key];
|
|
13
|
+
return typeof value === 'string' ? value : '';
|
|
14
|
+
}
|
|
15
|
+
function createKeywords(keys, extra = []) {
|
|
16
|
+
return [
|
|
17
|
+
...new Set([...extra, ...keys.flatMap((key) => [localeText(key, enUs), localeText(key, koKr)])]
|
|
18
|
+
.map(normalizeSearch)
|
|
19
|
+
.filter(Boolean))
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function matchQuery(value, query, compactQuery) {
|
|
23
|
+
const normalizedValue = normalizeSearch(value);
|
|
24
|
+
return normalizedValue.includes(query) || compactSearch(normalizedValue).includes(compactQuery);
|
|
25
|
+
}
|
|
26
|
+
function matchItem(item, query, compactQuery) {
|
|
27
|
+
if (!query)
|
|
28
|
+
return true;
|
|
29
|
+
return [item.title, item.subtitle ?? '', ...(item.keywords ?? [])].some((value) => matchQuery(value, query, compactQuery));
|
|
30
|
+
}
|
|
31
|
+
function fixRange(editor, rawRange, split = '/') {
|
|
32
|
+
const range = { ...rawRange };
|
|
33
|
+
const { state } = editor.view;
|
|
34
|
+
const { selection, doc } = state;
|
|
9
35
|
if (selection.$to.nodeBefore?.text?.includes?.(split)) {
|
|
10
36
|
range.from = range.to;
|
|
11
37
|
while (range.from > 0 && doc.textBetween(range.from - 1, range.from) !== split) {
|
|
12
38
|
try {
|
|
13
39
|
range.from -= 1;
|
|
14
40
|
}
|
|
15
|
-
catch
|
|
41
|
+
catch {
|
|
16
42
|
range.from += 2;
|
|
17
43
|
break;
|
|
18
44
|
}
|
|
@@ -23,29 +49,35 @@ function fixRange(editor, range, split = '/') {
|
|
|
23
49
|
try {
|
|
24
50
|
range.to += 1;
|
|
25
51
|
}
|
|
26
|
-
catch
|
|
52
|
+
catch {
|
|
27
53
|
range.to -= 1;
|
|
28
54
|
break;
|
|
29
55
|
}
|
|
30
56
|
}
|
|
31
57
|
return range;
|
|
32
58
|
}
|
|
33
|
-
export function getDetail(editor, range,
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
export function getDetail(editor, range, option) {
|
|
60
|
+
slashState.selection = () => {
|
|
61
|
+
editor.chain().focus().deleteRange(fixRange(editor, range)).run();
|
|
62
|
+
};
|
|
63
|
+
slashState.detail = option;
|
|
36
64
|
}
|
|
37
65
|
export const suggest = {
|
|
38
66
|
pluginKey: new PluginKey('slash-suggest'),
|
|
39
67
|
char: '/',
|
|
40
68
|
items: ({ query }) => {
|
|
69
|
+
const blocks = typeof window !== 'undefined'
|
|
70
|
+
? (window.__tiptap_blocks ?? [])
|
|
71
|
+
: [];
|
|
41
72
|
const raw = [
|
|
42
73
|
{
|
|
43
74
|
section: i18n('text'),
|
|
44
75
|
list: [
|
|
45
76
|
{
|
|
46
77
|
icon: 'title',
|
|
47
|
-
title: i18n('title')
|
|
78
|
+
title: `${i18n('title')} 1`,
|
|
48
79
|
subtitle: i18n('title1Info'),
|
|
80
|
+
keywords: createKeywords(['title', 'title1Info'], ['heading 1', 'h1']),
|
|
49
81
|
command: ({ editor, range }) => {
|
|
50
82
|
editor
|
|
51
83
|
.chain()
|
|
@@ -57,8 +89,9 @@ export const suggest = {
|
|
|
57
89
|
},
|
|
58
90
|
{
|
|
59
91
|
icon: 'title',
|
|
60
|
-
title: i18n('title')
|
|
92
|
+
title: `${i18n('title')} 2`,
|
|
61
93
|
subtitle: i18n('title2Info'),
|
|
94
|
+
keywords: createKeywords(['title', 'title2Info'], ['heading 2', 'h2']),
|
|
62
95
|
command: ({ editor, range }) => {
|
|
63
96
|
editor
|
|
64
97
|
.chain()
|
|
@@ -70,8 +103,9 @@ export const suggest = {
|
|
|
70
103
|
},
|
|
71
104
|
{
|
|
72
105
|
icon: 'title',
|
|
73
|
-
title: i18n('title')
|
|
106
|
+
title: `${i18n('title')} 3`,
|
|
74
107
|
subtitle: i18n('title3Info'),
|
|
108
|
+
keywords: createKeywords(['title', 'title3Info'], ['heading 3', 'h3']),
|
|
75
109
|
command: ({ editor, range }) => {
|
|
76
110
|
editor
|
|
77
111
|
.chain()
|
|
@@ -85,6 +119,7 @@ export const suggest = {
|
|
|
85
119
|
icon: 'format_list_bulleted',
|
|
86
120
|
title: i18n('unorderedList'),
|
|
87
121
|
subtitle: i18n('unorderedListInfo'),
|
|
122
|
+
keywords: createKeywords(['unorderedList', 'unorderedListInfo'], ['bullet list', 'ul']),
|
|
88
123
|
command: ({ editor, range }) => {
|
|
89
124
|
editor.commands.deleteRange(fixRange(editor, range));
|
|
90
125
|
editor.commands.toggleBulletList();
|
|
@@ -94,6 +129,7 @@ export const suggest = {
|
|
|
94
129
|
icon: 'format_list_numbered',
|
|
95
130
|
title: i18n('numberList'),
|
|
96
131
|
subtitle: i18n('numberListInfo'),
|
|
132
|
+
keywords: createKeywords(['numberList', 'numberListInfo'], ['ordered list', 'ol']),
|
|
97
133
|
command: ({ editor, range }) => {
|
|
98
134
|
editor.commands.deleteRange(fixRange(editor, range));
|
|
99
135
|
editor.commands.toggleOrderedList();
|
|
@@ -104,24 +140,43 @@ export const suggest = {
|
|
|
104
140
|
{
|
|
105
141
|
section: i18n('block'),
|
|
106
142
|
list: [
|
|
107
|
-
...
|
|
143
|
+
...blocks,
|
|
108
144
|
{
|
|
109
145
|
icon: 'image',
|
|
110
146
|
title: i18n('image'),
|
|
111
147
|
subtitle: i18n('imageInfo'),
|
|
148
|
+
keywords: createKeywords(['image', 'imageInfo']),
|
|
112
149
|
command: ({ editor, range }) => {
|
|
113
150
|
editor.chain().focus().deleteRange(fixRange(editor, range)).run();
|
|
114
151
|
const input = document.createElement('input');
|
|
115
152
|
input.type = 'file';
|
|
116
153
|
input.accept = 'image/*';
|
|
117
154
|
input.onchange = async () => {
|
|
118
|
-
if (input.files)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
155
|
+
if (!input.files?.length)
|
|
156
|
+
return;
|
|
157
|
+
const file = input.files[0];
|
|
158
|
+
if (!file)
|
|
159
|
+
return;
|
|
160
|
+
const skeleton = insertUploadSkeleton(editor, {
|
|
161
|
+
kind: 'image',
|
|
162
|
+
height: 220
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
const upload = window.__image_uploader ?? fallbackUpload;
|
|
166
|
+
const src = await upload(file);
|
|
167
|
+
if (skeleton) {
|
|
168
|
+
skeleton.replaceWith({
|
|
169
|
+
type: 'image',
|
|
170
|
+
attrs: { src }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
editor.chain().focus().setImage({ src }).run();
|
|
124
175
|
}
|
|
176
|
+
releaseObjectUrlOnImageSettled(editor.view, src);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
skeleton?.remove();
|
|
125
180
|
}
|
|
126
181
|
};
|
|
127
182
|
input.click();
|
|
@@ -131,24 +186,42 @@ export const suggest = {
|
|
|
131
186
|
icon: 'code',
|
|
132
187
|
title: i18n('codeBlock'),
|
|
133
188
|
subtitle: i18n('codeBlockInfo'),
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
keywords: createKeywords(['codeBlock', 'codeBlockInfo'], ['code']),
|
|
190
|
+
command: ({ editor, range }) => {
|
|
191
|
+
editor
|
|
192
|
+
.chain()
|
|
193
|
+
.focus()
|
|
194
|
+
.deleteRange(fixRange(editor, range))
|
|
195
|
+
.setNode('codeBlock', { language: null })
|
|
196
|
+
.command(({ tr }) => {
|
|
197
|
+
const { from } = tr.selection;
|
|
198
|
+
const $from = tr.doc.resolve(from);
|
|
199
|
+
if ($from.parent.type.name === 'codeBlock') {
|
|
200
|
+
tr.setSelection(TextSelection.create(tr.doc, $from.start()));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
const before = $from.nodeBefore;
|
|
204
|
+
if (before?.type.name === 'codeBlock') {
|
|
205
|
+
const positionInsideCodeBlock = from - before.nodeSize + 1;
|
|
206
|
+
tr.setSelection(TextSelection.create(tr.doc, positionInsideCodeBlock));
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const after = $from.nodeAfter;
|
|
210
|
+
if (after?.type.name === 'codeBlock') {
|
|
211
|
+
tr.setSelection(TextSelection.create(tr.doc, from + 1));
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
})
|
|
215
|
+
.focus()
|
|
216
|
+
.run();
|
|
217
|
+
}
|
|
145
218
|
},
|
|
146
219
|
{
|
|
147
220
|
icon: 'functions',
|
|
148
221
|
title: i18n('mathBlock'),
|
|
149
222
|
subtitle: i18n('mathBlockInfo'),
|
|
223
|
+
keywords: createKeywords(['mathBlock', 'mathBlockInfo'], ['latex', 'equation']),
|
|
150
224
|
command: ({ editor, range }) => {
|
|
151
|
-
const { to } = range;
|
|
152
225
|
editor
|
|
153
226
|
.chain()
|
|
154
227
|
.focus()
|
|
@@ -162,6 +235,7 @@ export const suggest = {
|
|
|
162
235
|
icon: 'table_chart',
|
|
163
236
|
title: i18n('table'),
|
|
164
237
|
subtitle: i18n('tableInfo'),
|
|
238
|
+
keywords: createKeywords(['table', 'tableInfo']),
|
|
165
239
|
command: ({ editor, range }) => {
|
|
166
240
|
editor
|
|
167
241
|
.chain()
|
|
@@ -178,6 +252,7 @@ export const suggest = {
|
|
|
178
252
|
icon: 'format_quote',
|
|
179
253
|
title: i18n('blockquote'),
|
|
180
254
|
subtitle: i18n('blockquoteInfo'),
|
|
255
|
+
keywords: createKeywords(['blockquote', 'blockquoteInfo'], ['quote']),
|
|
181
256
|
command: ({ editor, range }) => {
|
|
182
257
|
editor
|
|
183
258
|
.chain()
|
|
@@ -192,6 +267,7 @@ export const suggest = {
|
|
|
192
267
|
icon: 'iframe',
|
|
193
268
|
title: i18n('iframe'),
|
|
194
269
|
subtitle: i18n('iframeInfo'),
|
|
270
|
+
keywords: createKeywords(['iframe', 'iframeInfo'], ['embed', 'url']),
|
|
195
271
|
command: ({ editor, range }) => getDetail(editor, range, {
|
|
196
272
|
title: 'iframe',
|
|
197
273
|
placeholder: 'url',
|
|
@@ -214,6 +290,7 @@ export const suggest = {
|
|
|
214
290
|
icon: 'youtube_activity',
|
|
215
291
|
title: i18n('youtube'),
|
|
216
292
|
subtitle: i18n('youtubeInfo'),
|
|
293
|
+
keywords: createKeywords(['youtube', 'youtubeInfo'], ['video']),
|
|
217
294
|
command: ({ editor, range }) => getDetail(editor, range, {
|
|
218
295
|
title: 'youtube',
|
|
219
296
|
placeholder: 'url',
|
|
@@ -230,38 +307,61 @@ export const suggest = {
|
|
|
230
307
|
]
|
|
231
308
|
}
|
|
232
309
|
];
|
|
233
|
-
const
|
|
310
|
+
const normalizedQuery = normalizeSearch(query);
|
|
311
|
+
const compactQuery = compactSearch(query);
|
|
312
|
+
return raw
|
|
234
313
|
.map(({ section, list }) => ({
|
|
235
314
|
section,
|
|
236
|
-
list: list.filter((item) => item
|
|
237
|
-
item.subtitle.toLowerCase().includes(query.toLowerCase()))
|
|
315
|
+
list: list.filter((item) => matchItem(item, normalizedQuery, compactQuery))
|
|
238
316
|
}))
|
|
239
317
|
.filter(({ list }) => list.length > 0);
|
|
240
|
-
return filtered;
|
|
241
318
|
},
|
|
242
319
|
render: () => {
|
|
243
320
|
return {
|
|
244
321
|
onStart: (props) => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
322
|
+
const { editor, range } = props;
|
|
323
|
+
setSlashProps({ editor, range });
|
|
324
|
+
slashState.visible = true;
|
|
325
|
+
slashState.selectedIndex = 0;
|
|
326
|
+
setSlashItems(props.items);
|
|
327
|
+
slashState.detail = null;
|
|
328
|
+
const location = props.clientRect?.();
|
|
329
|
+
if (location) {
|
|
330
|
+
setSlashLocation({ x: location.x, y: location.y, height: location.height });
|
|
331
|
+
}
|
|
253
332
|
},
|
|
254
333
|
onUpdate(props) {
|
|
255
|
-
|
|
334
|
+
setSlashProps({ editor: props.editor, range: props.range });
|
|
335
|
+
setSlashItems(props.items);
|
|
256
336
|
},
|
|
257
337
|
onKeyDown(props) {
|
|
338
|
+
if (props.event.key === 'ArrowUp') {
|
|
339
|
+
props.event.preventDefault();
|
|
340
|
+
moveSlashSelection(-1);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
if (props.event.key === 'ArrowDown') {
|
|
344
|
+
props.event.preventDefault();
|
|
345
|
+
moveSlashSelection(1);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
if (props.event.key === 'Tab') {
|
|
349
|
+
props.event.preventDefault();
|
|
350
|
+
moveSlashSelection(props.event.shiftKey ? -1 : 1);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
if (props.event.key === 'Enter') {
|
|
354
|
+
props.event.preventDefault();
|
|
355
|
+
return runSlashItemAt(slashState.selectedIndex);
|
|
356
|
+
}
|
|
258
357
|
if (props.event.key === 'Escape') {
|
|
259
|
-
|
|
358
|
+
closeSlash();
|
|
260
359
|
return true;
|
|
261
360
|
}
|
|
361
|
+
return false;
|
|
262
362
|
},
|
|
263
363
|
onExit() {
|
|
264
|
-
|
|
364
|
+
closeSlash();
|
|
265
365
|
}
|
|
266
366
|
};
|
|
267
367
|
}
|
package/dist/plugin/embed.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Node } from '@tiptap/core';
|
|
2
2
|
export interface EmbedOptions {
|
|
3
3
|
allowFullscreen: boolean;
|
|
4
|
-
type: string;
|
|
5
4
|
HTMLAttributes: {
|
|
6
5
|
[key: string]: any;
|
|
7
6
|
};
|
|
@@ -12,7 +11,8 @@ declare module '@tiptap/core' {
|
|
|
12
11
|
setEmbed: (options: {
|
|
13
12
|
src: string;
|
|
14
13
|
type: string;
|
|
15
|
-
width
|
|
14
|
+
width?: string;
|
|
15
|
+
height?: string;
|
|
16
16
|
}) => ReturnType;
|
|
17
17
|
};
|
|
18
18
|
}
|
package/dist/plugin/embed.js
CHANGED
|
@@ -34,7 +34,7 @@ export default Node.create({
|
|
|
34
34
|
renderHTML({ HTMLAttributes }) {
|
|
35
35
|
return [
|
|
36
36
|
'div',
|
|
37
|
-
this.options.HTMLAttributes,
|
|
37
|
+
mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
|
|
38
38
|
['embed', mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })]
|
|
39
39
|
];
|
|
40
40
|
},
|
|
@@ -42,7 +42,11 @@ export default Node.create({
|
|
|
42
42
|
return {
|
|
43
43
|
setEmbed: (options) => ({ tr, dispatch }) => {
|
|
44
44
|
const { selection } = tr;
|
|
45
|
-
const node = this.type.create(
|
|
45
|
+
const node = this.type.create({
|
|
46
|
+
width: options.width ?? '100%',
|
|
47
|
+
height: options.height ?? '800px',
|
|
48
|
+
...options
|
|
49
|
+
});
|
|
46
50
|
if (dispatch)
|
|
47
51
|
tr.replaceRangeWith(selection.from, selection.to, node);
|
|
48
52
|
return true;
|
package/dist/plugin/iframe.js
CHANGED
|
@@ -31,7 +31,7 @@ export default Node.create({
|
|
|
31
31
|
renderHTML({ HTMLAttributes }) {
|
|
32
32
|
return [
|
|
33
33
|
'div',
|
|
34
|
-
this.options.HTMLAttributes,
|
|
34
|
+
mergeAttributes(this.options.HTMLAttributes, { 'data-bubble-menu': 'false' }),
|
|
35
35
|
[
|
|
36
36
|
'iframe',
|
|
37
37
|
mergeAttributes(HTMLAttributes, { credentialless: true, crossorigin: 'anonymous' })
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import type { EditorView } from 'prosemirror-view';
|
|
2
3
|
export type UploadFn = (image: File) => Promise<string>;
|
|
3
4
|
export declare const fallbackUpload: (image: File) => Promise<string>;
|
|
5
|
+
export declare const releaseObjectUrlOnImageSettled: (view: EditorView, src: string) => void;
|
|
4
6
|
export declare const dropImagePlugin: () => Plugin<any>;
|
|
@@ -1,5 +1,67 @@
|
|
|
1
|
-
import { Plugin
|
|
1
|
+
import { Plugin } from 'prosemirror-state';
|
|
2
|
+
import { insertUploadSkeleton } from '../upload/skeleton';
|
|
2
3
|
export const fallbackUpload = async (image) => URL.createObjectURL(image);
|
|
4
|
+
const OBJECT_URL_PREFIX = 'blob:';
|
|
5
|
+
const OBJECT_URL_REVOKE_TIMEOUT_MS = 30_000;
|
|
6
|
+
const isObjectUrl = (src) => src.startsWith(OBJECT_URL_PREFIX);
|
|
7
|
+
const revokeObjectUrl = (src) => {
|
|
8
|
+
try {
|
|
9
|
+
URL.revokeObjectURL(src);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// no-op
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const releaseObjectUrlOnImageSettled = (view, src) => {
|
|
16
|
+
if (!isObjectUrl(src))
|
|
17
|
+
return;
|
|
18
|
+
let released = false;
|
|
19
|
+
const cleanups = [];
|
|
20
|
+
const release = () => {
|
|
21
|
+
if (released)
|
|
22
|
+
return;
|
|
23
|
+
released = true;
|
|
24
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
25
|
+
cleanups.length = 0;
|
|
26
|
+
revokeObjectUrl(src);
|
|
27
|
+
};
|
|
28
|
+
const timer = setTimeout(release, OBJECT_URL_REVOKE_TIMEOUT_MS);
|
|
29
|
+
cleanups.push(() => clearTimeout(timer));
|
|
30
|
+
const bind = () => {
|
|
31
|
+
const images = Array.from(view.dom.querySelectorAll('img')).filter((image) => image.getAttribute('src') === src);
|
|
32
|
+
if (!images.length)
|
|
33
|
+
return false;
|
|
34
|
+
let pending = 0;
|
|
35
|
+
images.forEach((image) => {
|
|
36
|
+
if (image.complete)
|
|
37
|
+
return;
|
|
38
|
+
pending += 1;
|
|
39
|
+
const settle = () => {
|
|
40
|
+
image.removeEventListener('load', settle);
|
|
41
|
+
image.removeEventListener('error', settle);
|
|
42
|
+
pending -= 1;
|
|
43
|
+
if (pending <= 0)
|
|
44
|
+
release();
|
|
45
|
+
};
|
|
46
|
+
image.addEventListener('load', settle);
|
|
47
|
+
image.addEventListener('error', settle);
|
|
48
|
+
cleanups.push(() => {
|
|
49
|
+
image.removeEventListener('load', settle);
|
|
50
|
+
image.removeEventListener('error', settle);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
if (pending <= 0)
|
|
54
|
+
release();
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
queueMicrotask(() => {
|
|
58
|
+
if (bind())
|
|
59
|
+
return;
|
|
60
|
+
requestAnimationFrame(() => {
|
|
61
|
+
bind();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
3
65
|
export const dropImagePlugin = () => {
|
|
4
66
|
return new Plugin({
|
|
5
67
|
props: {
|
|
@@ -12,13 +74,31 @@ export const dropImagePlugin = () => {
|
|
|
12
74
|
const image = item.getAsFile();
|
|
13
75
|
if (item.type.indexOf('image') === 0) {
|
|
14
76
|
event.preventDefault();
|
|
77
|
+
const skeleton = insertUploadSkeleton({
|
|
78
|
+
state: view.state,
|
|
79
|
+
view
|
|
80
|
+
}, {
|
|
81
|
+
kind: 'image',
|
|
82
|
+
height: 220
|
|
83
|
+
});
|
|
15
84
|
if (upload && image) {
|
|
16
|
-
upload(image)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
85
|
+
upload(image)
|
|
86
|
+
.then((src) => {
|
|
87
|
+
if (skeleton) {
|
|
88
|
+
skeleton.replaceWith({
|
|
89
|
+
type: 'image',
|
|
90
|
+
attrs: { src }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const node = schema.nodes.image.create({ src });
|
|
95
|
+
const transaction = view.state.tr.replaceSelectionWith(node);
|
|
96
|
+
view.dispatch(transaction);
|
|
97
|
+
}
|
|
98
|
+
releaseObjectUrlOnImageSettled(view, src);
|
|
99
|
+
})
|
|
100
|
+
.catch(() => {
|
|
101
|
+
skeleton?.remove();
|
|
22
102
|
});
|
|
23
103
|
}
|
|
24
104
|
}
|
|
@@ -58,18 +138,49 @@ export const dropImagePlugin = () => {
|
|
|
58
138
|
return false;
|
|
59
139
|
images.forEach(async (image) => {
|
|
60
140
|
const reader = new FileReader();
|
|
141
|
+
const skeleton = insertUploadSkeleton({
|
|
142
|
+
state: view.state,
|
|
143
|
+
view
|
|
144
|
+
}, {
|
|
145
|
+
kind: 'image',
|
|
146
|
+
height: 220,
|
|
147
|
+
at: coordinates.pos
|
|
148
|
+
});
|
|
61
149
|
if (upload) {
|
|
62
|
-
|
|
63
|
-
src
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
150
|
+
try {
|
|
151
|
+
const src = await upload(image);
|
|
152
|
+
if (skeleton) {
|
|
153
|
+
skeleton.replaceWith({
|
|
154
|
+
type: 'image',
|
|
155
|
+
attrs: { src }
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const node = schema.nodes.image.create({ src });
|
|
160
|
+
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
161
|
+
view.dispatch(transaction);
|
|
162
|
+
}
|
|
163
|
+
releaseObjectUrlOnImageSettled(view, src);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
skeleton?.remove();
|
|
167
|
+
}
|
|
67
168
|
}
|
|
68
169
|
else {
|
|
69
170
|
reader.onload = (readerEvent) => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
171
|
+
const src = readerEvent.target?.result;
|
|
172
|
+
if (typeof src !== 'string') {
|
|
173
|
+
skeleton?.remove();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (skeleton) {
|
|
177
|
+
skeleton.replaceWith({
|
|
178
|
+
type: 'image',
|
|
179
|
+
attrs: { src }
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const node = schema.nodes.image.create({ src });
|
|
73
184
|
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
74
185
|
view.dispatch(transaction);
|
|
75
186
|
};
|
|
@@ -3,17 +3,18 @@ import { mergeAttributes } from '@tiptap/core';
|
|
|
3
3
|
import { dropImagePlugin } from './dragdrop';
|
|
4
4
|
export default (crossorigin = 'anonymous') => Image.extend({
|
|
5
5
|
addOptions() {
|
|
6
|
+
const parentOptions = this.parent?.() ?? {};
|
|
6
7
|
return {
|
|
7
|
-
...
|
|
8
|
+
...parentOptions,
|
|
8
9
|
sizes: ['inline', 'block', 'left', 'right']
|
|
9
10
|
};
|
|
10
11
|
},
|
|
11
12
|
parseHTML: () => [{ tag: 'img' }],
|
|
12
13
|
renderHTML({ HTMLAttributes }) {
|
|
13
|
-
const
|
|
14
|
+
const style = HTMLAttributes.style;
|
|
14
15
|
return [
|
|
15
16
|
'figure',
|
|
16
|
-
{ style },
|
|
17
|
+
{ style, 'data-bubble-menu': 'false' },
|
|
17
18
|
['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
|
|
18
19
|
];
|
|
19
20
|
},
|
package/dist/plugin/indent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { wrappingInputRule } from '@tiptap/core';
|
|
2
2
|
import OrderedListBase from '@tiptap/extension-ordered-list';
|
|
3
3
|
import toggleList from './toggleList';
|
|
4
|
-
import './korean.
|
|
4
|
+
import './korean.css';
|
|
5
5
|
export default OrderedListBase.extend({
|
|
6
6
|
priority: 20,
|
|
7
7
|
addAttributes() {
|