@seorii/tiptap 0.3.0 → 0.4.1
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/en-us/index.d.ts +4 -0
- package/dist/i18n/en-us/index.js +4 -0
- package/dist/i18n/index.d.ts +8 -0
- package/dist/i18n/ko-kr/index.d.ts +4 -0
- package/dist/i18n/ko-kr/index.js +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/columns/index.d.ts +14 -0
- package/dist/plugin/columns/index.js +88 -0
- package/dist/plugin/columns/style.css +37 -0
- package/dist/plugin/command/suggest.js +40 -4
- package/dist/plugin/embed.js +1 -1
- package/dist/plugin/iframe.js +1 -1
- package/dist/plugin/image/dragdrop.js +63 -16
- package/dist/plugin/image/index.js +1 -1
- package/dist/plugin/resize/index.d.ts +8 -0
- package/dist/plugin/resize/index.js +455 -0
- 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 +139 -34
- package/dist/tiptap/Bubble.svelte.d.ts +1 -0
- package/dist/tiptap/TipTap.svelte +95 -9
- package/dist/tiptap/TipTap.svelte.d.ts +3 -0
- package/dist/tiptap/tiptap.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
import { NodeSelection } from '@tiptap/pm/state';
|
|
3
|
+
import { SvelteNodeViewRenderer } from 'svelte-tiptap';
|
|
4
|
+
import UploadSkeleton from './UploadSkeleton.svelte';
|
|
5
|
+
export const UPLOAD_SKELETON_NODE = 'tiptap-upload-skeleton';
|
|
6
|
+
const defaultHeight = {
|
|
7
|
+
image: 220,
|
|
8
|
+
file: 56,
|
|
9
|
+
pdf: 420,
|
|
10
|
+
embed: 420,
|
|
11
|
+
block: 180
|
|
12
|
+
};
|
|
13
|
+
function findUploadSkeleton(doc, id) {
|
|
14
|
+
let foundPos = null;
|
|
15
|
+
let foundNode = null;
|
|
16
|
+
doc.descendants((node, pos) => {
|
|
17
|
+
if (node.type.name !== UPLOAD_SKELETON_NODE)
|
|
18
|
+
return;
|
|
19
|
+
if (node.attrs.uploadId !== id)
|
|
20
|
+
return;
|
|
21
|
+
foundPos = pos;
|
|
22
|
+
foundNode = node;
|
|
23
|
+
return false;
|
|
24
|
+
});
|
|
25
|
+
if (foundPos === null || foundNode === null)
|
|
26
|
+
return null;
|
|
27
|
+
return { pos: foundPos, node: foundNode };
|
|
28
|
+
}
|
|
29
|
+
function tryCreateNodeSelection(doc, pos) {
|
|
30
|
+
if (pos < 0 || pos > doc.content.size)
|
|
31
|
+
return null;
|
|
32
|
+
const node = doc.nodeAt(pos);
|
|
33
|
+
if (!node || node.type.spec.selectable === false)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return NodeSelection.create(doc, pos);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function insertUploadSkeleton(editor, { kind = 'block', height = defaultHeight[kind], at, select = true, insertParagraph = true } = {}) {
|
|
43
|
+
const skeletonType = editor.state.schema.nodes[UPLOAD_SKELETON_NODE];
|
|
44
|
+
if (!skeletonType)
|
|
45
|
+
return null;
|
|
46
|
+
const clampedHeight = Math.max(44, Math.min(1200, Math.round(height)));
|
|
47
|
+
const uploadId = `upload-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
48
|
+
const node = skeletonType.create({ uploadId, kind, height: clampedHeight });
|
|
49
|
+
const paragraph = editor.state.schema.nodes.paragraph?.create();
|
|
50
|
+
const safePos = Math.max(0, Math.min(at ?? editor.state.selection.from, editor.state.doc.content.size));
|
|
51
|
+
const tr = editor.state.tr.insert(safePos, node);
|
|
52
|
+
if (insertParagraph && paragraph) {
|
|
53
|
+
tr.insert(safePos + node.nodeSize, paragraph);
|
|
54
|
+
}
|
|
55
|
+
if (select) {
|
|
56
|
+
const nodeSelection = tryCreateNodeSelection(tr.doc, safePos);
|
|
57
|
+
if (nodeSelection)
|
|
58
|
+
tr.setSelection(nodeSelection);
|
|
59
|
+
}
|
|
60
|
+
editor.view.dispatch(tr);
|
|
61
|
+
return {
|
|
62
|
+
id: uploadId,
|
|
63
|
+
exists: () => Boolean(findUploadSkeleton(editor.state.doc, uploadId)),
|
|
64
|
+
replaceWith: (content, options = {}) => {
|
|
65
|
+
const target = findUploadSkeleton(editor.state.doc, uploadId);
|
|
66
|
+
if (!target)
|
|
67
|
+
return false;
|
|
68
|
+
if (!content?.type)
|
|
69
|
+
return false;
|
|
70
|
+
let nextNode;
|
|
71
|
+
try {
|
|
72
|
+
nextNode = editor.state.schema.nodeFromJSON(content);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const tr = editor.state.tr.replaceWith(target.pos, target.pos + target.node.nodeSize, nextNode);
|
|
78
|
+
if (options.select ?? true) {
|
|
79
|
+
const nodeSelection = tryCreateNodeSelection(tr.doc, target.pos);
|
|
80
|
+
if (nodeSelection)
|
|
81
|
+
tr.setSelection(nodeSelection);
|
|
82
|
+
}
|
|
83
|
+
editor.view.dispatch(tr);
|
|
84
|
+
return true;
|
|
85
|
+
},
|
|
86
|
+
remove: () => {
|
|
87
|
+
const target = findUploadSkeleton(editor.state.doc, uploadId);
|
|
88
|
+
if (!target)
|
|
89
|
+
return false;
|
|
90
|
+
const removeFrom = target.pos;
|
|
91
|
+
let removeTo = target.pos + target.node.nodeSize;
|
|
92
|
+
const nextNode = editor.state.doc.nodeAt(removeTo);
|
|
93
|
+
if (nextNode?.type.name === 'paragraph' && nextNode.content.size === 0) {
|
|
94
|
+
removeTo += nextNode.nodeSize;
|
|
95
|
+
}
|
|
96
|
+
editor.view.dispatch(editor.state.tr.deleteRange(removeFrom, removeTo));
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export default Node.create({
|
|
102
|
+
name: UPLOAD_SKELETON_NODE,
|
|
103
|
+
group: 'block',
|
|
104
|
+
atom: true,
|
|
105
|
+
draggable: false,
|
|
106
|
+
selectable: true,
|
|
107
|
+
addAttributes() {
|
|
108
|
+
return {
|
|
109
|
+
uploadId: {
|
|
110
|
+
default: null,
|
|
111
|
+
parseHTML: (element) => element.getAttribute('data-upload-id'),
|
|
112
|
+
renderHTML: (attributes) => attributes.uploadId ? { 'data-upload-id': attributes.uploadId } : {}
|
|
113
|
+
},
|
|
114
|
+
kind: {
|
|
115
|
+
default: 'block',
|
|
116
|
+
parseHTML: (element) => element.getAttribute('data-upload-kind') || 'block',
|
|
117
|
+
renderHTML: (attributes) => attributes.kind ? { 'data-upload-kind': attributes.kind } : {}
|
|
118
|
+
},
|
|
119
|
+
height: {
|
|
120
|
+
default: defaultHeight.block,
|
|
121
|
+
parseHTML: (element) => {
|
|
122
|
+
const value = Number.parseInt(element.getAttribute('data-upload-height') || '', 10);
|
|
123
|
+
return Number.isFinite(value) ? value : defaultHeight.block;
|
|
124
|
+
},
|
|
125
|
+
renderHTML: (attributes) => {
|
|
126
|
+
const raw = Number.parseFloat(String(attributes.height ?? defaultHeight.block));
|
|
127
|
+
const height = Number.isFinite(raw) ? Math.max(44, Math.min(1200, Math.round(raw))) : 180;
|
|
128
|
+
return { 'data-upload-height': String(height) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
parseHTML() {
|
|
134
|
+
return [{ tag: UPLOAD_SKELETON_NODE }];
|
|
135
|
+
},
|
|
136
|
+
renderHTML({ HTMLAttributes }) {
|
|
137
|
+
return [
|
|
138
|
+
UPLOAD_SKELETON_NODE,
|
|
139
|
+
mergeAttributes(HTMLAttributes, {
|
|
140
|
+
'data-bubble-menu': 'false'
|
|
141
|
+
})
|
|
142
|
+
];
|
|
143
|
+
},
|
|
144
|
+
addNodeView() {
|
|
145
|
+
return SvelteNodeViewRenderer(UploadSkeleton);
|
|
146
|
+
}
|
|
147
|
+
});
|
package/dist/plugin/youtube.js
CHANGED
|
@@ -5,7 +5,7 @@ const youtubeExtractId = (url) => {
|
|
|
5
5
|
const match = youtubeRegExp.exec(url.trim());
|
|
6
6
|
return match ? match[1] : false;
|
|
7
7
|
};
|
|
8
|
-
const videoPlayerStaticAttributes = { nocookie: true };
|
|
8
|
+
const videoPlayerStaticAttributes = { nocookie: true, 'data-bubble-menu': 'false' };
|
|
9
9
|
export default Node.create({
|
|
10
10
|
name: 'lite-youtube',
|
|
11
11
|
content: '',
|
|
@@ -10,23 +10,33 @@
|
|
|
10
10
|
import defaultI18n, { I18N_CONTEXT, type I18nTranslate } from '../i18n';
|
|
11
11
|
import ColorPicker from 'svelte-awesome-color-picker';
|
|
12
12
|
import { isTextSelection } from '@tiptap/core';
|
|
13
|
-
import type
|
|
13
|
+
import { NodeSelection, type EditorState, type Selection } from '@tiptap/pm/state';
|
|
14
14
|
import type { EditorView } from '@tiptap/pm/view';
|
|
15
15
|
|
|
16
16
|
type Props = {
|
|
17
17
|
colors?: string[];
|
|
18
18
|
editable?: boolean;
|
|
19
19
|
override?: any;
|
|
20
|
+
docked?: boolean;
|
|
20
21
|
children?: any;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
let { colors = [], editable, override, children }: Props = $props();
|
|
24
|
+
let { colors = [], editable, override, docked = false, children }: Props = $props();
|
|
24
25
|
|
|
25
26
|
const editor = getContext<{ v: any; c: number }>('editor');
|
|
26
27
|
const i18nFromContext = getContext<I18nTranslate | undefined>(I18N_CONTEXT);
|
|
27
28
|
const i18n: I18nTranslate = (...args) =>
|
|
28
29
|
i18nFromContext ? i18nFromContext(...args) : defaultI18n(...args);
|
|
29
30
|
const tiptap = $derived(editor.v);
|
|
31
|
+
const currentTextColor = $derived.by(() => {
|
|
32
|
+
editor.c;
|
|
33
|
+
const color = tiptap?.getAttributes('textStyle')?.color;
|
|
34
|
+
return typeof color === 'string' ? color.trim().toLowerCase() : '';
|
|
35
|
+
});
|
|
36
|
+
const hasTextColor = $derived.by(() => {
|
|
37
|
+
editor.c;
|
|
38
|
+
return !!tiptap?.getAttributes('textStyle')?.color;
|
|
39
|
+
});
|
|
30
40
|
|
|
31
41
|
let selection = $state<Selection | null>(null);
|
|
32
42
|
let table = $state<number[] | false>(false);
|
|
@@ -72,6 +82,7 @@
|
|
|
72
82
|
|
|
73
83
|
const shouldShow = ({
|
|
74
84
|
state,
|
|
85
|
+
view,
|
|
75
86
|
from,
|
|
76
87
|
to
|
|
77
88
|
}: {
|
|
@@ -85,6 +96,18 @@
|
|
|
85
96
|
const { doc, selection } = state;
|
|
86
97
|
const { empty } = selection;
|
|
87
98
|
|
|
99
|
+
if (selection instanceof NodeSelection && selection.node.isBlock) {
|
|
100
|
+
const nodeDom = view.nodeDOM(from);
|
|
101
|
+
if (
|
|
102
|
+
nodeDom instanceof Element &&
|
|
103
|
+
(nodeDom.hasAttribute('data-hide-bubble-menu') ||
|
|
104
|
+
nodeDom.getAttribute('data-bubble-menu') === 'false' ||
|
|
105
|
+
Boolean(nodeDom.querySelector('[data-hide-bubble-menu], [data-bubble-menu="false"]')))
|
|
106
|
+
) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
88
111
|
const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection);
|
|
89
112
|
|
|
90
113
|
return !(empty || isEmptyTextBlock);
|
|
@@ -92,22 +115,13 @@
|
|
|
92
115
|
</script>
|
|
93
116
|
|
|
94
117
|
{#if tiptap}
|
|
95
|
-
|
|
96
|
-
editor={tiptap}
|
|
97
|
-
updateDelay={50}
|
|
98
|
-
{shouldShow}
|
|
99
|
-
tippyOptions={{
|
|
100
|
-
moveTransition: 'transform 0.2s cubic-bezier(1,.5,0,.85)',
|
|
101
|
-
animation: 'shift-away-subtle',
|
|
102
|
-
duration: [200, 50]
|
|
103
|
-
}}
|
|
104
|
-
>
|
|
118
|
+
{#snippet toolbar()}
|
|
105
119
|
{#if override}
|
|
106
|
-
<main>
|
|
120
|
+
<main class:docked>
|
|
107
121
|
<Render it={override} />
|
|
108
122
|
</main>
|
|
109
123
|
{:else}
|
|
110
|
-
<main>
|
|
124
|
+
<main class:docked>
|
|
111
125
|
{#if link}
|
|
112
126
|
<div class="link">
|
|
113
127
|
<p>
|
|
@@ -157,11 +171,11 @@
|
|
|
157
171
|
<ToolbarButton icon="close" handler={() => deleteTable({ editor: tiptap })} />
|
|
158
172
|
{:else}
|
|
159
173
|
{#if editable}
|
|
160
|
-
<Paper hover bl>
|
|
174
|
+
<Paper hover bl remap>
|
|
161
175
|
{#snippet target()}
|
|
162
176
|
<IconButton size="1.2em" icon="format_align_left" />
|
|
163
177
|
{/snippet}
|
|
164
|
-
<div
|
|
178
|
+
<div class="menu-list align-menu">
|
|
165
179
|
<List>
|
|
166
180
|
<OneLine
|
|
167
181
|
icon="format_align_left"
|
|
@@ -201,13 +215,19 @@
|
|
|
201
215
|
{#if editable}
|
|
202
216
|
<ToolbarButton icon="functions" prop="math_inline" handler={() => setMath(tiptap)} />
|
|
203
217
|
{/if}
|
|
204
|
-
<Paper bl
|
|
218
|
+
<Paper bl remap>
|
|
205
219
|
{#snippet target()}
|
|
206
220
|
<IconButton size="1.2em" icon="palette" />
|
|
207
221
|
{/snippet}
|
|
208
|
-
<div
|
|
222
|
+
<div class="menu-list">
|
|
209
223
|
<div class="colors">
|
|
210
|
-
<Button
|
|
224
|
+
<Button
|
|
225
|
+
small
|
|
226
|
+
outlined
|
|
227
|
+
active={!hasTextColor}
|
|
228
|
+
class={!hasTextColor ? 'color-active' : undefined}
|
|
229
|
+
onclick={() => tiptap.chain().focus().unsetColor().run()}
|
|
230
|
+
>
|
|
211
231
|
{i18n('default')}
|
|
212
232
|
</Button>
|
|
213
233
|
<Paper bl remap block>
|
|
@@ -216,15 +236,14 @@
|
|
|
216
236
|
small
|
|
217
237
|
full
|
|
218
238
|
outlined
|
|
239
|
+
active={hasTextColor}
|
|
240
|
+
class={hasTextColor ? 'color-active' : undefined}
|
|
219
241
|
onclick={() => tiptap.chain().focus().unsetColor().run()}
|
|
220
242
|
>
|
|
221
243
|
<Icon icon="palette" />
|
|
222
244
|
</Button>
|
|
223
245
|
{/snippet}
|
|
224
|
-
<div
|
|
225
|
-
onclick={(event) => event.stopPropagation()}
|
|
226
|
-
onmousedown={(event) => event.stopPropagation()}
|
|
227
|
-
>
|
|
246
|
+
<div role="presentation" onmousedown={(event) => event.stopPropagation()}>
|
|
228
247
|
<ColorPicker
|
|
229
248
|
isDialog={false}
|
|
230
249
|
onInput={(event) => {
|
|
@@ -233,10 +252,12 @@
|
|
|
233
252
|
/>
|
|
234
253
|
</div>
|
|
235
254
|
</Paper>
|
|
236
|
-
{#each colors as color}
|
|
255
|
+
{#each colors as color (color)}
|
|
237
256
|
<Button
|
|
238
257
|
small
|
|
239
258
|
outlined
|
|
259
|
+
active={currentTextColor === color.toLowerCase()}
|
|
260
|
+
class={currentTextColor === color.toLowerCase() ? 'color-active' : undefined}
|
|
240
261
|
onclick={() => tiptap.chain().focus().setColor(color).run()}
|
|
241
262
|
>
|
|
242
263
|
<span style:background={color} class="pal"></span>
|
|
@@ -253,30 +274,104 @@
|
|
|
253
274
|
<Render it={children} />
|
|
254
275
|
</main>
|
|
255
276
|
{/if}
|
|
256
|
-
|
|
277
|
+
{/snippet}
|
|
278
|
+
|
|
279
|
+
{#if docked && tiptap.isEditable}
|
|
280
|
+
<div class="docked-menu">
|
|
281
|
+
{@render toolbar()}
|
|
282
|
+
</div>
|
|
283
|
+
{:else}
|
|
284
|
+
<BubbleMenu
|
|
285
|
+
editor={tiptap}
|
|
286
|
+
updateDelay={50}
|
|
287
|
+
{shouldShow}
|
|
288
|
+
tippyOptions={{
|
|
289
|
+
moveTransition: 'transform 0.2s cubic-bezier(1,.5,0,.85)',
|
|
290
|
+
animation: 'shift-away-subtle',
|
|
291
|
+
duration: [200, 50]
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
{@render toolbar()}
|
|
295
|
+
</BubbleMenu>
|
|
296
|
+
{/if}
|
|
257
297
|
{/if}
|
|
258
298
|
|
|
259
299
|
<style>
|
|
260
300
|
main {
|
|
261
|
-
box-shadow:
|
|
301
|
+
box-shadow:
|
|
302
|
+
0 10px 24px rgba(22, 37, 63, 0.14),
|
|
303
|
+
0 2px 8px rgba(22, 37, 63, 0.1);
|
|
262
304
|
background: var(--surface, #fff);
|
|
305
|
+
border: 1px solid var(--primary-light2, #d5dff3);
|
|
263
306
|
color: var(--on-surface, #000);
|
|
264
|
-
padding: 8px;
|
|
265
|
-
border-radius:
|
|
307
|
+
padding: 8px 10px;
|
|
308
|
+
border-radius: 12px;
|
|
266
309
|
display: flex;
|
|
310
|
+
flex-wrap: nowrap;
|
|
311
|
+
gap: 4px;
|
|
267
312
|
align-items: center;
|
|
268
313
|
justify-content: center;
|
|
269
314
|
font-size: 1.2em;
|
|
270
315
|
& > :global(*) {
|
|
271
|
-
margin: 0
|
|
316
|
+
margin: 0;
|
|
272
317
|
}
|
|
318
|
+
}
|
|
273
319
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
.docked-menu {
|
|
321
|
+
position: sticky;
|
|
322
|
+
top: var(--tiptap-toolbar-sticky-top, 8px);
|
|
323
|
+
z-index: 40;
|
|
324
|
+
display: flex;
|
|
325
|
+
justify-content: center;
|
|
326
|
+
margin-bottom: 10px;
|
|
327
|
+
padding: 4px 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
main.docked {
|
|
331
|
+
width: 100%;
|
|
332
|
+
justify-content: flex-start;
|
|
333
|
+
border-radius: 14px;
|
|
334
|
+
overflow-x: auto;
|
|
335
|
+
overflow-y: hidden;
|
|
336
|
+
backdrop-filter: blur(10px) saturate(1.15);
|
|
337
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.15);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
main:not(.docked) {
|
|
341
|
+
overflow-x: auto;
|
|
342
|
+
overflow-y: hidden;
|
|
343
|
+
}
|
|
277
344
|
|
|
278
|
-
|
|
279
|
-
|
|
345
|
+
main.docked > :global(*) {
|
|
346
|
+
flex-shrink: 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main.docked :global(button) {
|
|
350
|
+
font-size: 1.08em;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
main.docked > :global(main.i) {
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
line-height: 1;
|
|
357
|
+
transform: translateY(-1px);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.menu-list {
|
|
361
|
+
font-size: 0.6em;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.align-menu {
|
|
365
|
+
margin: -6px;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main.docked .menu-list {
|
|
369
|
+
font-size: 0.72em;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
@media (max-width: 768px) {
|
|
373
|
+
main {
|
|
374
|
+
font-size: 1.05em;
|
|
280
375
|
}
|
|
281
376
|
}
|
|
282
377
|
|
|
@@ -304,6 +399,16 @@
|
|
|
304
399
|
& > :global(:first-child) {
|
|
305
400
|
grid-column: 1/3;
|
|
306
401
|
}
|
|
402
|
+
|
|
403
|
+
& :global(button.color-active) {
|
|
404
|
+
box-shadow: inset 0 0 0 1px var(--primary-dark2, #2d4b8f);
|
|
405
|
+
background: color-mix(in srgb, var(--primary-light1, #eef3ff) 65%, var(--surface, #fff));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
& :global(button.color-active .pal) {
|
|
409
|
+
outline: 1px solid color-mix(in srgb, var(--on-surface, #111) 65%, transparent);
|
|
410
|
+
outline-offset: 1px;
|
|
411
|
+
}
|
|
307
412
|
}
|
|
308
413
|
|
|
309
414
|
.pal {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
} from '../i18n';
|
|
20
20
|
import type { UploadFn } from '../plugin/image/dragdrop';
|
|
21
21
|
import { fallbackUpload } from '../plugin/image/dragdrop';
|
|
22
|
+
import MediaResize, { type ResizeOptions } from '../plugin/resize';
|
|
22
23
|
import { Render } from 'nunui';
|
|
23
24
|
|
|
24
25
|
type Props = {
|
|
@@ -36,9 +37,11 @@
|
|
|
36
37
|
sanitize?: Record<string, any>;
|
|
37
38
|
colors?: string[];
|
|
38
39
|
bubble?: any;
|
|
40
|
+
bubbleDocked?: boolean;
|
|
39
41
|
preloader?: any;
|
|
40
42
|
crossorigin?: 'anonymous' | 'use-credentials';
|
|
41
43
|
codeBlockLanguageLabels?: Record<string, string>;
|
|
44
|
+
resize?: boolean | ResizeOptions;
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
let {
|
|
@@ -65,12 +68,22 @@
|
|
|
65
68
|
'#ab47bc' //purple
|
|
66
69
|
],
|
|
67
70
|
bubble = null,
|
|
71
|
+
bubbleDocked = false,
|
|
68
72
|
preloader,
|
|
69
73
|
crossorigin = 'anonymous',
|
|
70
|
-
codeBlockLanguageLabels = {}
|
|
74
|
+
codeBlockLanguageLabels = {},
|
|
75
|
+
resize = true
|
|
71
76
|
}: Props = $props();
|
|
72
77
|
|
|
73
78
|
const scopedI18n: I18nTranslate = (...args) => translateWithLocale(locale, ...args);
|
|
79
|
+
const resizeDataAttrs = [
|
|
80
|
+
'data-resize-handler',
|
|
81
|
+
'data-resize-target',
|
|
82
|
+
'data-resize-min-height',
|
|
83
|
+
'data-resize-max-height',
|
|
84
|
+
'data-bubble-menu',
|
|
85
|
+
'data-hide-bubble-menu'
|
|
86
|
+
];
|
|
74
87
|
|
|
75
88
|
const san = (body: string) =>
|
|
76
89
|
sanitizeHtml(body || '', {
|
|
@@ -85,18 +98,26 @@
|
|
|
85
98
|
'embed',
|
|
86
99
|
'mark',
|
|
87
100
|
'code',
|
|
101
|
+
'tiptap-upload-skeleton',
|
|
88
102
|
...(sanitize.allowedTags || [])
|
|
89
103
|
]),
|
|
90
104
|
allowedStyles: '*' as any,
|
|
91
105
|
allowedAttributes: {
|
|
92
106
|
'*': ['style', 'class'],
|
|
93
107
|
a: ['href', 'name', 'target'],
|
|
94
|
-
img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'],
|
|
95
|
-
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
|
108
|
+
img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading', ...resizeDataAttrs],
|
|
109
|
+
iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen', ...resizeDataAttrs],
|
|
96
110
|
th: ['colwidth', 'colspan', 'rowspan'],
|
|
97
111
|
td: ['colwidth', 'colspan', 'rowspan'],
|
|
98
|
-
'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider'],
|
|
99
|
-
embed: ['src', 'type', 'frameborder', 'allowfullscreen'],
|
|
112
|
+
'lite-youtube': ['videoid', 'params', 'nocookie', 'title', 'provider', ...resizeDataAttrs],
|
|
113
|
+
embed: ['src', 'type', 'frameborder', 'allowfullscreen', ...resizeDataAttrs],
|
|
114
|
+
'tiptap-upload-skeleton': [
|
|
115
|
+
'data-upload-id',
|
|
116
|
+
'data-upload-kind',
|
|
117
|
+
'data-upload-height',
|
|
118
|
+
'data-bubble-menu',
|
|
119
|
+
'data-hide-bubble-menu'
|
|
120
|
+
],
|
|
100
121
|
mark: ['style', 'data-color'],
|
|
101
122
|
code: ['class'],
|
|
102
123
|
...(sanitize.allowedAttributes || [])
|
|
@@ -133,6 +154,15 @@
|
|
|
133
154
|
([{ default: tt }]) => {
|
|
134
155
|
if (!untrack(() => mounted)) return;
|
|
135
156
|
const editorPlaceholder = placeholder ?? scopedI18n('placeholder');
|
|
157
|
+
const optionPlugins = Array.isArray(options.plugins)
|
|
158
|
+
? [...options.plugins]
|
|
159
|
+
: options.plugins
|
|
160
|
+
? [options.plugins]
|
|
161
|
+
: [];
|
|
162
|
+
if (resize) {
|
|
163
|
+
const resizeOptions = typeof resize === 'object' ? resize : {};
|
|
164
|
+
optionPlugins.unshift(MediaResize.configure(resizeOptions));
|
|
165
|
+
}
|
|
136
166
|
tiptap.v = ref = tt(element, r, {
|
|
137
167
|
placeholder: editorPlaceholder,
|
|
138
168
|
editable,
|
|
@@ -142,7 +172,8 @@
|
|
|
142
172
|
},
|
|
143
173
|
crossorigin,
|
|
144
174
|
codeBlockLanguageLabels,
|
|
145
|
-
...options
|
|
175
|
+
...options,
|
|
176
|
+
plugins: optionPlugins
|
|
146
177
|
});
|
|
147
178
|
tiptap.v.on('update', ({ editor: tiptap }: any) => {
|
|
148
179
|
let content = tiptap.getHTML(),
|
|
@@ -217,6 +248,11 @@
|
|
|
217
248
|
</script>
|
|
218
249
|
|
|
219
250
|
<main class:fullscreen class:editable {style}>
|
|
251
|
+
{#if bubbleDocked && (editable || mark)}
|
|
252
|
+
<Bubble {colors} {editable} override={bubble} docked={bubbleDocked}>
|
|
253
|
+
<Render it={bubble} />
|
|
254
|
+
</Bubble>
|
|
255
|
+
{/if}
|
|
220
256
|
<div class="wrapper">
|
|
221
257
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
222
258
|
<div bind:this={element} class="target" onkeydown={handleKeydown}></div>
|
|
@@ -236,8 +272,8 @@
|
|
|
236
272
|
<Command />
|
|
237
273
|
<Floating />
|
|
238
274
|
{/if}
|
|
239
|
-
{#if editable || mark}
|
|
240
|
-
<Bubble {colors} {editable} override={bubble}>
|
|
275
|
+
{#if !bubbleDocked && (editable || mark)}
|
|
276
|
+
<Bubble {colors} {editable} override={bubble} docked={bubbleDocked}>
|
|
241
277
|
<Render it={bubble} />
|
|
242
278
|
</Bubble>
|
|
243
279
|
{/if}
|
|
@@ -291,7 +327,9 @@
|
|
|
291
327
|
|
|
292
328
|
.editable :global(.ProseMirror-selectednode img) {
|
|
293
329
|
transition: all 0.2s ease-in-out;
|
|
294
|
-
|
|
330
|
+
outline: 3px solid var(--primary);
|
|
331
|
+
outline-offset: 2px;
|
|
332
|
+
filter: none;
|
|
295
333
|
}
|
|
296
334
|
|
|
297
335
|
.editable :global(.iframe-wrapper.ProseMirror-selectednode) {
|
|
@@ -302,6 +340,53 @@
|
|
|
302
340
|
outline: 3px solid var(--primary);
|
|
303
341
|
}
|
|
304
342
|
|
|
343
|
+
.editable :global(.tiptap-media-resize-anchor) {
|
|
344
|
+
width: 100%;
|
|
345
|
+
display: flex;
|
|
346
|
+
justify-content: center;
|
|
347
|
+
margin: 6px 0 2px;
|
|
348
|
+
line-height: 0;
|
|
349
|
+
pointer-events: none;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.editable :global(.tiptap-media-resize-handle) {
|
|
353
|
+
appearance: none;
|
|
354
|
+
-webkit-appearance: none;
|
|
355
|
+
display: block;
|
|
356
|
+
width: 42px;
|
|
357
|
+
height: 8px;
|
|
358
|
+
margin: 0;
|
|
359
|
+
padding: 0;
|
|
360
|
+
border: 1px solid var(--primary-light3, rgba(120, 120, 120, 0.45));
|
|
361
|
+
border-radius: 999px;
|
|
362
|
+
background: var(--primary-light6, rgba(120, 120, 120, 0.2));
|
|
363
|
+
cursor: ns-resize;
|
|
364
|
+
pointer-events: auto;
|
|
365
|
+
transition:
|
|
366
|
+
background-color 0.15s ease,
|
|
367
|
+
border-color 0.15s ease,
|
|
368
|
+
transform 0.15s ease;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.editable :global(.tiptap-media-resize-handle:hover),
|
|
372
|
+
.editable :global(.tiptap-media-resize-handle:focus-visible) {
|
|
373
|
+
background: var(--primary-light4, rgba(120, 120, 120, 0.35));
|
|
374
|
+
border-color: var(--primary-light2, rgba(100, 100, 100, 0.55));
|
|
375
|
+
outline: none;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.editable :global(.tiptap-media-resize-handle:active) {
|
|
379
|
+
transform: translateY(1px);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.editable :global(.tiptap-media-resize-proxy) {
|
|
383
|
+
width: 100%;
|
|
384
|
+
border-radius: 12px;
|
|
385
|
+
background: var(--primary-light4, rgba(120, 120, 120, 0.35));
|
|
386
|
+
opacity: 0.55;
|
|
387
|
+
pointer-events: none;
|
|
388
|
+
}
|
|
389
|
+
|
|
305
390
|
div > :global(div) {
|
|
306
391
|
outline: none !important;
|
|
307
392
|
& :global(.ProseMirror) :global(p.is-editor-empty:first-child::before) {
|
|
@@ -325,6 +410,7 @@
|
|
|
325
410
|
& :global(img) {
|
|
326
411
|
transition: all 0.2s ease-in-out;
|
|
327
412
|
max-width: 100%;
|
|
413
|
+
object-fit: contain;
|
|
328
414
|
border-radius: 12px;
|
|
329
415
|
position: relative;
|
|
330
416
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '@seorii/prosemirror-math/style.css';
|
|
2
2
|
import type { UploadFn } from '../plugin/image/dragdrop';
|
|
3
|
+
import { type ResizeOptions } from '../plugin/resize';
|
|
3
4
|
type Props = {
|
|
4
5
|
body: string;
|
|
5
6
|
editable?: boolean;
|
|
@@ -15,9 +16,11 @@ type Props = {
|
|
|
15
16
|
sanitize?: Record<string, any>;
|
|
16
17
|
colors?: string[];
|
|
17
18
|
bubble?: any;
|
|
19
|
+
bubbleDocked?: boolean;
|
|
18
20
|
preloader?: any;
|
|
19
21
|
crossorigin?: 'anonymous' | 'use-credentials';
|
|
20
22
|
codeBlockLanguageLabels?: Record<string, string>;
|
|
23
|
+
resize?: boolean | ResizeOptions;
|
|
21
24
|
};
|
|
22
25
|
declare const TipTap: import("svelte").Component<Props, {}, "body" | "ref" | "loaded">;
|
|
23
26
|
type TipTap = ReturnType<typeof TipTap>;
|
package/dist/tiptap/tiptap.js
CHANGED
|
@@ -21,10 +21,12 @@ import { Color } from '@tiptap/extension-color';
|
|
|
21
21
|
import { TextStyle } from '@tiptap/extension-text-style';
|
|
22
22
|
import Iframe from '../plugin/iframe';
|
|
23
23
|
import Embed from '../plugin/embed';
|
|
24
|
+
import UploadSkeleton from '../plugin/upload/skeleton';
|
|
24
25
|
// @ts-ignore
|
|
25
26
|
import { MathInline, MathBlock } from '@seorii/prosemirror-math/tiptap';
|
|
26
27
|
import Youtube from '../plugin/youtube';
|
|
27
28
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
29
|
+
import columns from '../plugin/columns';
|
|
28
30
|
import command from '../plugin/command/suggest';
|
|
29
31
|
import emoji from '../plugin/command/emoji';
|
|
30
32
|
import { countSlashItems, moveSlashSelection, runSlashItemAt, slashState } from '../plugin/command/stores.svelte';
|
|
@@ -238,6 +240,7 @@ const extensions = (placeholder, plugins, crossorigin, codeBlockLanguageLabels)
|
|
|
238
240
|
orderedlist,
|
|
239
241
|
MathInline,
|
|
240
242
|
MathBlock,
|
|
243
|
+
...columns,
|
|
241
244
|
table,
|
|
242
245
|
tableHeader,
|
|
243
246
|
tableRow,
|
|
@@ -247,6 +250,7 @@ const extensions = (placeholder, plugins, crossorigin, codeBlockLanguageLabels)
|
|
|
247
250
|
Indent,
|
|
248
251
|
Color,
|
|
249
252
|
TextStyle,
|
|
253
|
+
UploadSkeleton,
|
|
250
254
|
Iframe,
|
|
251
255
|
Embed,
|
|
252
256
|
Code.extend({
|