@intinyagroup/rich-text 0.1.1-alpha.5
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.
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { Editor, Node, mergeAttributes } from '@tiptap/core';
|
|
4
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
5
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
6
|
+
import Underline from '@tiptap/extension-underline';
|
|
7
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
8
|
+
import Link from '@tiptap/extension-link';
|
|
9
|
+
import Image from '@tiptap/extension-image';
|
|
10
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
11
|
+
import Table from '@tiptap/extension-table';
|
|
12
|
+
import TableRow from '@tiptap/extension-table-row';
|
|
13
|
+
import TableCell from '@tiptap/extension-table-cell';
|
|
14
|
+
import TableHeader from '@tiptap/extension-table-header';
|
|
15
|
+
import Typography from '@tiptap/extension-typography';
|
|
16
|
+
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
|
17
|
+
import {
|
|
18
|
+
Bold, Italic, Underline as UnderlineIcon, Strikethrough, Highlighter,
|
|
19
|
+
AlignLeft, AlignCenter, AlignRight, AlignJustify,
|
|
20
|
+
List, ListOrdered, Quote, Code, Minus, Link as LinkIcon, Image as ImageIcon,
|
|
21
|
+
Undo, Redo, Heading1, Heading2, Heading3, TableIcon, Plus, Trash2,
|
|
22
|
+
Video,
|
|
23
|
+
} from 'lucide-svelte';
|
|
24
|
+
import { Button, Separator } from '@intinyagroup/ui';
|
|
25
|
+
import { cn } from '@intinyagroup/grid-core/utils';
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
content = '',
|
|
29
|
+
placeholder = 'Start writing...',
|
|
30
|
+
editable = true,
|
|
31
|
+
height = 400,
|
|
32
|
+
class: className,
|
|
33
|
+
onUpdate,
|
|
34
|
+
onImageUpload,
|
|
35
|
+
}: {
|
|
36
|
+
content?: string;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
editable?: boolean;
|
|
39
|
+
height?: number;
|
|
40
|
+
class?: string;
|
|
41
|
+
onUpdate?: (html: string) => void;
|
|
42
|
+
/** Called when paste/drop provides an image file. Return a URL to insert. */
|
|
43
|
+
onImageUpload?: (file: File) => Promise<string>;
|
|
44
|
+
} = $props();
|
|
45
|
+
|
|
46
|
+
let editorEl: HTMLDivElement | null = null;
|
|
47
|
+
let bubbleMenuEl: HTMLDivElement | null = null;
|
|
48
|
+
let editor: Editor | null = null;
|
|
49
|
+
let isActive = $state<Record<string, boolean>>({});
|
|
50
|
+
let uploadingCount = $state(0);
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Custom Extensions
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/** Image with href/alt/title attributes — renders as <a><img></a> when href set */
|
|
57
|
+
const CustomImage = Image.extend({
|
|
58
|
+
addAttributes() {
|
|
59
|
+
return {
|
|
60
|
+
...this.parent?.(),
|
|
61
|
+
href: { default: null },
|
|
62
|
+
alt: { default: '' },
|
|
63
|
+
title: { default: '' },
|
|
64
|
+
target: { default: '_blank' },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
renderHTML({ HTMLAttributes }) {
|
|
68
|
+
const { href, target, alt, title, ...rest } = HTMLAttributes;
|
|
69
|
+
const img = ['img', mergeAttributes(this.options.HTMLAttributes, rest, { alt, title })];
|
|
70
|
+
if (href) {
|
|
71
|
+
return ['a', { href, target, class: 'image-link' }, img];
|
|
72
|
+
}
|
|
73
|
+
return img;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** YouTube iframe embed node */
|
|
78
|
+
const CustomYoutube = Node.create({
|
|
79
|
+
name: 'youtube',
|
|
80
|
+
group: 'block',
|
|
81
|
+
atom: true,
|
|
82
|
+
addAttributes() {
|
|
83
|
+
return { src: { default: null } };
|
|
84
|
+
},
|
|
85
|
+
parseHTML() {
|
|
86
|
+
return [
|
|
87
|
+
{ tag: "iframe[src*='youtube.com']" },
|
|
88
|
+
{ tag: "iframe[src*='youtu.be']" },
|
|
89
|
+
];
|
|
90
|
+
},
|
|
91
|
+
renderHTML({ HTMLAttributes }) {
|
|
92
|
+
return [
|
|
93
|
+
'div',
|
|
94
|
+
{ class: 'video-wrapper' },
|
|
95
|
+
[
|
|
96
|
+
'iframe',
|
|
97
|
+
mergeAttributes(HTMLAttributes, {
|
|
98
|
+
width: '100%',
|
|
99
|
+
height: '315',
|
|
100
|
+
allowfullscreen: 'true',
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
];
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Image Compression
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
async function compressImage(file: File): Promise<File> {
|
|
112
|
+
if (file.size < 100 * 1024) return file;
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const reader = new FileReader();
|
|
116
|
+
reader.onload = (e) => {
|
|
117
|
+
const img = new Image();
|
|
118
|
+
img.onload = () => {
|
|
119
|
+
const canvas = document.createElement('canvas');
|
|
120
|
+
const ctx = canvas.getContext('2d');
|
|
121
|
+
if (!ctx) {
|
|
122
|
+
resolve(file);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let { width, height } = img;
|
|
127
|
+
const maxDim = 2048;
|
|
128
|
+
|
|
129
|
+
if (width > maxDim || height > maxDim) {
|
|
130
|
+
if (width > height) {
|
|
131
|
+
height = (height / width) * maxDim;
|
|
132
|
+
width = maxDim;
|
|
133
|
+
} else {
|
|
134
|
+
width = (width / height) * maxDim;
|
|
135
|
+
height = maxDim;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
canvas.width = width;
|
|
140
|
+
canvas.height = height;
|
|
141
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
142
|
+
|
|
143
|
+
canvas.toBlob(
|
|
144
|
+
(blob) => {
|
|
145
|
+
if (blob) {
|
|
146
|
+
resolve(new File([blob], file.name, { type: 'image/jpeg' }));
|
|
147
|
+
} else {
|
|
148
|
+
resolve(file);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
'image/jpeg',
|
|
152
|
+
0.85
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
img.src = e.target?.result as string;
|
|
156
|
+
};
|
|
157
|
+
reader.readAsDataURL(file);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Image Upload (paste / drop)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
async function uploadAndInsertImage(file: File) {
|
|
166
|
+
if (!editor) return;
|
|
167
|
+
uploadingCount++;
|
|
168
|
+
try {
|
|
169
|
+
let src: string;
|
|
170
|
+
if (onImageUpload) {
|
|
171
|
+
const compressed = await compressImage(file);
|
|
172
|
+
src = await onImageUpload(compressed);
|
|
173
|
+
} else {
|
|
174
|
+
// Fallback: inline data URL (no server upload)
|
|
175
|
+
src = await new Promise<string>((resolve) => {
|
|
176
|
+
const reader = new FileReader();
|
|
177
|
+
reader.onload = (ev) => resolve(ev.target?.result as string);
|
|
178
|
+
reader.readAsDataURL(file);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
editor.chain().focus().setImage({ src }).run();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('Failed to upload image:', err);
|
|
184
|
+
} finally {
|
|
185
|
+
uploadingCount--;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleFiles(files: File[]) {
|
|
190
|
+
const images = Array.from(files).filter((f) => f.type.startsWith('image/'));
|
|
191
|
+
await Promise.all(images.map(uploadAndInsertImage));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// YouTube Embed
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
function addYoutube() {
|
|
199
|
+
const url = window.prompt('Enter YouTube URL:');
|
|
200
|
+
if (!url || !editor) return;
|
|
201
|
+
const match = url.match(
|
|
202
|
+
/^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|shorts\/)([^#&?]*).*/
|
|
203
|
+
);
|
|
204
|
+
const id = match && match[2].length === 11 ? match[2] : null;
|
|
205
|
+
if (id) {
|
|
206
|
+
editor
|
|
207
|
+
.chain()
|
|
208
|
+
.focus()
|
|
209
|
+
.insertContent({
|
|
210
|
+
type: 'youtube',
|
|
211
|
+
attrs: { src: `https://www.youtube.com/embed/${id}` },
|
|
212
|
+
})
|
|
213
|
+
.run();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Toolbar Helpers
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
function getActiveStates(e: Editor): Record<string, boolean> {
|
|
222
|
+
return {
|
|
223
|
+
bold: e.isActive('bold'),
|
|
224
|
+
italic: e.isActive('italic'),
|
|
225
|
+
underline: e.isActive('underline'),
|
|
226
|
+
strike: e.isActive('strike'),
|
|
227
|
+
highlight: e.isActive('highlight'),
|
|
228
|
+
h1: e.isActive('heading', { level: 1 }),
|
|
229
|
+
h2: e.isActive('heading', { level: 2 }),
|
|
230
|
+
h3: e.isActive('heading', { level: 3 }),
|
|
231
|
+
bulletList: e.isActive('bulletList'),
|
|
232
|
+
orderedList: e.isActive('orderedList'),
|
|
233
|
+
blockquote: e.isActive('blockquote'),
|
|
234
|
+
codeBlock: e.isActive('codeBlock'),
|
|
235
|
+
alignLeft: e.isActive({ textAlign: 'left' }),
|
|
236
|
+
alignCenter: e.isActive({ textAlign: 'center' }),
|
|
237
|
+
alignRight: e.isActive({ textAlign: 'right' }),
|
|
238
|
+
alignJustify: e.isActive({ textAlign: 'justify' }),
|
|
239
|
+
link: e.isActive('link'),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function toggleBold() { editor?.chain().focus().toggleBold().run(); }
|
|
244
|
+
function toggleItalic() { editor?.chain().focus().toggleItalic().run(); }
|
|
245
|
+
function toggleUnderline() { editor?.chain().focus().toggleUnderline().run(); }
|
|
246
|
+
function toggleStrike() { editor?.chain().focus().toggleStrike().run(); }
|
|
247
|
+
function setHeading(level: 1 | 2 | 3) { editor?.chain().focus().toggleHeading({ level }).run(); }
|
|
248
|
+
function toggleBulletList() { editor?.chain().focus().toggleBulletList().run(); }
|
|
249
|
+
function toggleOrderedList() { editor?.chain().focus().toggleOrderedList().run(); }
|
|
250
|
+
function toggleBlockquote() { editor?.chain().focus().toggleBlockquote().run(); }
|
|
251
|
+
function toggleCodeBlock() { editor?.chain().focus().toggleCodeBlock().run(); }
|
|
252
|
+
function setAlign(align: 'left' | 'center' | 'right' | 'justify') {
|
|
253
|
+
editor?.chain().focus().setTextAlign(align).run();
|
|
254
|
+
}
|
|
255
|
+
function setLink() {
|
|
256
|
+
const url = window.prompt('Enter URL:');
|
|
257
|
+
if (url) editor?.chain().focus().setLink({ href: url }).run();
|
|
258
|
+
}
|
|
259
|
+
function setImage() {
|
|
260
|
+
const url = window.prompt('Enter image URL:');
|
|
261
|
+
if (url) editor?.chain().focus().setImage({ src: url }).run();
|
|
262
|
+
}
|
|
263
|
+
function insertHorizontalRule() { editor?.chain().focus().setHorizontalRule().run(); }
|
|
264
|
+
function insertTable() {
|
|
265
|
+
editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
266
|
+
}
|
|
267
|
+
function toggleHighlight(color?: string) {
|
|
268
|
+
if (color) {
|
|
269
|
+
editor?.chain().focus().toggleHighlight({ color }).run();
|
|
270
|
+
} else {
|
|
271
|
+
editor?.chain().focus().toggleHighlight().run();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function addColumnBefore() { editor?.chain().focus().addColumnBefore().run(); }
|
|
275
|
+
function addColumnAfter() { editor?.chain().focus().addColumnAfter().run(); }
|
|
276
|
+
function deleteColumn() { editor?.chain().focus().deleteColumn().run(); }
|
|
277
|
+
function addRowBefore() { editor?.chain().focus().addRowBefore().run(); }
|
|
278
|
+
function addRowAfter() { editor?.chain().focus().addRowAfter().run(); }
|
|
279
|
+
function deleteRow() { editor?.chain().focus().deleteRow().run(); }
|
|
280
|
+
function deleteTable() { editor?.chain().focus().deleteTable().run(); }
|
|
281
|
+
function undo() { editor?.chain().focus().undo().run(); }
|
|
282
|
+
function redo() { editor?.chain().focus().redo().run(); }
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Lifecycle
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
onMount(() => {
|
|
289
|
+
if (!editorEl) return;
|
|
290
|
+
|
|
291
|
+
editor = new Editor({
|
|
292
|
+
element: editorEl,
|
|
293
|
+
extensions: [
|
|
294
|
+
StarterKit,
|
|
295
|
+
Placeholder.configure({ placeholder }),
|
|
296
|
+
Underline,
|
|
297
|
+
TextAlign.configure({ types: ['heading', 'paragraph'] }),
|
|
298
|
+
Link.configure({ openOnClick: false }),
|
|
299
|
+
CustomImage.configure({
|
|
300
|
+
inline: false,
|
|
301
|
+
HTMLAttributes: {
|
|
302
|
+
class: 'rounded-lg mx-auto block max-w-full h-auto cursor-pointer',
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
Highlight.configure({ multicolor: true }),
|
|
306
|
+
Typography,
|
|
307
|
+
Table.configure({ resizable: true }),
|
|
308
|
+
TableRow,
|
|
309
|
+
TableCell,
|
|
310
|
+
TableHeader,
|
|
311
|
+
BubbleMenu.configure({
|
|
312
|
+
element: bubbleMenuEl!,
|
|
313
|
+
pluginKey: 'bubbleMenu',
|
|
314
|
+
shouldShow: ({ editor: e }) =>
|
|
315
|
+
e.isEditable &&
|
|
316
|
+
!e.isActive('table') &&
|
|
317
|
+
e.view.state.selection.content().size > 0,
|
|
318
|
+
}),
|
|
319
|
+
CustomYoutube,
|
|
320
|
+
],
|
|
321
|
+
content,
|
|
322
|
+
editable,
|
|
323
|
+
onUpdate: ({ editor: e }) => {
|
|
324
|
+
const html = e.getHTML();
|
|
325
|
+
isActive = getActiveStates(e);
|
|
326
|
+
onUpdate?.(html);
|
|
327
|
+
},
|
|
328
|
+
onSelectionUpdate: ({ editor: e }) => {
|
|
329
|
+
isActive = getActiveStates(e);
|
|
330
|
+
},
|
|
331
|
+
editorProps: {
|
|
332
|
+
attributes: {
|
|
333
|
+
class: 'prose prose-sm max-w-none focus:outline-none',
|
|
334
|
+
},
|
|
335
|
+
handleDOMEvents: {
|
|
336
|
+
paste: (_view, event) => {
|
|
337
|
+
const items = event.clipboardData?.items;
|
|
338
|
+
if (!items) return false;
|
|
339
|
+
const imageFiles: File[] = [];
|
|
340
|
+
for (let i = 0; i < items.length; i++) {
|
|
341
|
+
if (items[i].type.startsWith('image/')) {
|
|
342
|
+
const file = items[i].getAsFile();
|
|
343
|
+
if (file) imageFiles.push(file);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (imageFiles.length) {
|
|
347
|
+
event.preventDefault();
|
|
348
|
+
handleFiles(imageFiles);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
},
|
|
353
|
+
drop: (_view, event) => {
|
|
354
|
+
const files = event.dataTransfer?.files;
|
|
355
|
+
if (!files?.length) return false;
|
|
356
|
+
const imageFiles = Array.from(files).filter((f) =>
|
|
357
|
+
f.type.startsWith('image/')
|
|
358
|
+
);
|
|
359
|
+
if (imageFiles.length) {
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
handleFiles(imageFiles);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
onDestroy(() => {
|
|
372
|
+
if (editor) editor.destroy();
|
|
373
|
+
});
|
|
374
|
+
</script>
|
|
375
|
+
|
|
376
|
+
<div
|
|
377
|
+
class={cn(
|
|
378
|
+
'rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] overflow-hidden',
|
|
379
|
+
className
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
<!-- Toolbar -->
|
|
383
|
+
{#if editable}
|
|
384
|
+
<div
|
|
385
|
+
class="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b border-[var(--ui-border)] bg-[var(--ui-secondary)]/30"
|
|
386
|
+
>
|
|
387
|
+
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={undo}>
|
|
388
|
+
<Undo class="size-4" />
|
|
389
|
+
</Button>
|
|
390
|
+
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={redo}>
|
|
391
|
+
<Redo class="size-4" />
|
|
392
|
+
</Button>
|
|
393
|
+
|
|
394
|
+
<Separator orientation="vertical" class="h-6 mx-1" />
|
|
395
|
+
|
|
396
|
+
<Button
|
|
397
|
+
variant="ghost"
|
|
398
|
+
size="sm"
|
|
399
|
+
class={cn('size-8 p-0', isActive.h1 && 'bg-[var(--ui-primary)]/10')}
|
|
400
|
+
onclick={() => setHeading(1)}
|
|
401
|
+
>
|
|
402
|
+
<Heading1 class="size-4" />
|
|
403
|
+
</Button>
|
|
404
|
+
<Button
|
|
405
|
+
variant="ghost"
|
|
406
|
+
size="sm"
|
|
407
|
+
class={cn('size-8 p-0', isActive.h2 && 'bg-[var(--ui-primary)]/10')}
|
|
408
|
+
onclick={() => setHeading(2)}
|
|
409
|
+
>
|
|
410
|
+
<Heading2 class="size-4" />
|
|
411
|
+
</Button>
|
|
412
|
+
<Button
|
|
413
|
+
variant="ghost"
|
|
414
|
+
size="sm"
|
|
415
|
+
class={cn('size-8 p-0', isActive.h3 && 'bg-[var(--ui-primary)]/10')}
|
|
416
|
+
onclick={() => setHeading(3)}
|
|
417
|
+
>
|
|
418
|
+
<Heading3 class="size-4" />
|
|
419
|
+
</Button>
|
|
420
|
+
|
|
421
|
+
<Separator orientation="vertical" class="h-6 mx-1" />
|
|
422
|
+
|
|
423
|
+
<Button
|
|
424
|
+
variant="ghost"
|
|
425
|
+
size="sm"
|
|
426
|
+
class={cn('size-8 p-0', isActive.bold && 'bg-[var(--ui-primary)]/10')}
|
|
427
|
+
onclick={toggleBold}
|
|
428
|
+
>
|
|
429
|
+
<Bold class="size-4" />
|
|
430
|
+
</Button>
|
|
431
|
+
<Button
|
|
432
|
+
variant="ghost"
|
|
433
|
+
size="sm"
|
|
434
|
+
class={cn('size-8 p-0', isActive.italic && 'bg-[var(--ui-primary)]/10')}
|
|
435
|
+
onclick={toggleItalic}
|
|
436
|
+
>
|
|
437
|
+
<Italic class="size-4" />
|
|
438
|
+
</Button>
|
|
439
|
+
<Button
|
|
440
|
+
variant="ghost"
|
|
441
|
+
size="sm"
|
|
442
|
+
class={cn('size-8 p-0', isActive.underline && 'bg-[var(--ui-primary)]/10')}
|
|
443
|
+
onclick={toggleUnderline}
|
|
444
|
+
>
|
|
445
|
+
<UnderlineIcon class="size-4" />
|
|
446
|
+
</Button>
|
|
447
|
+
<Button
|
|
448
|
+
variant="ghost"
|
|
449
|
+
size="sm"
|
|
450
|
+
class={cn('size-8 p-0', isActive.strike && 'bg-[var(--ui-primary)]/10')}
|
|
451
|
+
onclick={toggleStrike}
|
|
452
|
+
>
|
|
453
|
+
<Strikethrough class="size-4" />
|
|
454
|
+
</Button>
|
|
455
|
+
<Button
|
|
456
|
+
variant="ghost"
|
|
457
|
+
size="sm"
|
|
458
|
+
class={cn(
|
|
459
|
+
'size-8 p-0',
|
|
460
|
+
isActive.highlight && 'bg-[var(--ui-primary)]/10'
|
|
461
|
+
)}
|
|
462
|
+
onclick={() => toggleHighlight('#fef08a')}
|
|
463
|
+
>
|
|
464
|
+
<Highlighter class="size-4" />
|
|
465
|
+
</Button>
|
|
466
|
+
|
|
467
|
+
<Separator orientation="vertical" class="h-6 mx-1" />
|
|
468
|
+
|
|
469
|
+
<Button
|
|
470
|
+
variant="ghost"
|
|
471
|
+
size="sm"
|
|
472
|
+
class={cn(
|
|
473
|
+
'size-8 p-0',
|
|
474
|
+
isActive.bulletList && 'bg-[var(--ui-primary)]/10'
|
|
475
|
+
)}
|
|
476
|
+
onclick={toggleBulletList}
|
|
477
|
+
>
|
|
478
|
+
<List class="size-4" />
|
|
479
|
+
</Button>
|
|
480
|
+
<Button
|
|
481
|
+
variant="ghost"
|
|
482
|
+
size="sm"
|
|
483
|
+
class={cn(
|
|
484
|
+
'size-8 p-0',
|
|
485
|
+
isActive.orderedList && 'bg-[var(--ui-primary)]/10'
|
|
486
|
+
)}
|
|
487
|
+
onclick={toggleOrderedList}
|
|
488
|
+
>
|
|
489
|
+
<ListOrdered class="size-4" />
|
|
490
|
+
</Button>
|
|
491
|
+
<Button
|
|
492
|
+
variant="ghost"
|
|
493
|
+
size="sm"
|
|
494
|
+
class={cn(
|
|
495
|
+
'size-8 p-0',
|
|
496
|
+
isActive.blockquote && 'bg-[var(--ui-primary)]/10'
|
|
497
|
+
)}
|
|
498
|
+
onclick={toggleBlockquote}
|
|
499
|
+
>
|
|
500
|
+
<Quote class="size-4" />
|
|
501
|
+
</Button>
|
|
502
|
+
<Button
|
|
503
|
+
variant="ghost"
|
|
504
|
+
size="sm"
|
|
505
|
+
class={cn(
|
|
506
|
+
'size-8 p-0',
|
|
507
|
+
isActive.codeBlock && 'bg-[var(--ui-primary)]/10'
|
|
508
|
+
)}
|
|
509
|
+
onclick={toggleCodeBlock}
|
|
510
|
+
>
|
|
511
|
+
<Code class="size-4" />
|
|
512
|
+
</Button>
|
|
513
|
+
|
|
514
|
+
<Separator orientation="vertical" class="h-6 mx-1" />
|
|
515
|
+
|
|
516
|
+
<Button
|
|
517
|
+
variant="ghost"
|
|
518
|
+
size="sm"
|
|
519
|
+
class={cn(
|
|
520
|
+
'size-8 p-0',
|
|
521
|
+
isActive.alignLeft && 'bg-[var(--ui-primary)]/10'
|
|
522
|
+
)}
|
|
523
|
+
onclick={() => setAlign('left')}
|
|
524
|
+
>
|
|
525
|
+
<AlignLeft class="size-4" />
|
|
526
|
+
</Button>
|
|
527
|
+
<Button
|
|
528
|
+
variant="ghost"
|
|
529
|
+
size="sm"
|
|
530
|
+
class={cn(
|
|
531
|
+
'size-8 p-0',
|
|
532
|
+
isActive.alignCenter && 'bg-[var(--ui-primary)]/10'
|
|
533
|
+
)}
|
|
534
|
+
onclick={() => setAlign('center')}
|
|
535
|
+
>
|
|
536
|
+
<AlignCenter class="size-4" />
|
|
537
|
+
</Button>
|
|
538
|
+
<Button
|
|
539
|
+
variant="ghost"
|
|
540
|
+
size="sm"
|
|
541
|
+
class={cn(
|
|
542
|
+
'size-8 p-0',
|
|
543
|
+
isActive.alignRight && 'bg-[var(--ui-primary)]/10'
|
|
544
|
+
)}
|
|
545
|
+
onclick={() => setAlign('right')}
|
|
546
|
+
>
|
|
547
|
+
<AlignRight class="size-4" />
|
|
548
|
+
</Button>
|
|
549
|
+
<Button
|
|
550
|
+
variant="ghost"
|
|
551
|
+
size="sm"
|
|
552
|
+
class={cn(
|
|
553
|
+
'size-8 p-0',
|
|
554
|
+
isActive.alignJustify && 'bg-[var(--ui-primary)]/10'
|
|
555
|
+
)}
|
|
556
|
+
onclick={() => setAlign('justify')}
|
|
557
|
+
>
|
|
558
|
+
<AlignJustify class="size-4" />
|
|
559
|
+
</Button>
|
|
560
|
+
|
|
561
|
+
<Separator orientation="vertical" class="h-6 mx-1" />
|
|
562
|
+
|
|
563
|
+
<Button
|
|
564
|
+
variant="ghost"
|
|
565
|
+
size="sm"
|
|
566
|
+
class={cn('size-8 p-0', isActive.link && 'bg-[var(--ui-primary)]/10')}
|
|
567
|
+
onclick={setLink}
|
|
568
|
+
>
|
|
569
|
+
<LinkIcon class="size-4" />
|
|
570
|
+
</Button>
|
|
571
|
+
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={setImage}>
|
|
572
|
+
<ImageIcon class="size-4" />
|
|
573
|
+
</Button>
|
|
574
|
+
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={insertTable}>
|
|
575
|
+
<TableIcon class="size-4" />
|
|
576
|
+
</Button>
|
|
577
|
+
<Button
|
|
578
|
+
variant="ghost"
|
|
579
|
+
size="sm"
|
|
580
|
+
class="size-8 p-0"
|
|
581
|
+
onclick={addYoutube}
|
|
582
|
+
title="Embed YouTube video"
|
|
583
|
+
>
|
|
584
|
+
<Video class="size-4" />
|
|
585
|
+
</Button>
|
|
586
|
+
<Button variant="ghost" size="sm" class="size-8 p-0" onclick={insertHorizontalRule}>
|
|
587
|
+
<Minus class="size-4" />
|
|
588
|
+
</Button>
|
|
589
|
+
|
|
590
|
+
{#if uploadingCount > 0}
|
|
591
|
+
<span class="ml-auto text-xs text-[var(--ui-muted-foreground)]">
|
|
592
|
+
Uploading {uploadingCount} image{uploadingCount > 1 ? 's' : ''}...
|
|
593
|
+
</span>
|
|
594
|
+
{/if}
|
|
595
|
+
</div>
|
|
596
|
+
{/if}
|
|
597
|
+
|
|
598
|
+
<!-- Editor -->
|
|
599
|
+
<div
|
|
600
|
+
bind:this={editorEl}
|
|
601
|
+
class="prose prose-sm max-w-none p-4 focus:outline-none"
|
|
602
|
+
style="min-height: {height}px;"
|
|
603
|
+
></div>
|
|
604
|
+
|
|
605
|
+
<!-- BubbleMenu: floating toolbar on text selection (element bound for TipTap) -->
|
|
606
|
+
<div
|
|
607
|
+
bind:this={bubbleMenuEl}
|
|
608
|
+
class="flex items-center gap-0.5 rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] p-1 shadow-lg backdrop-blur-sm"
|
|
609
|
+
>
|
|
610
|
+
{#if editor}
|
|
611
|
+
<Button
|
|
612
|
+
variant="ghost"
|
|
613
|
+
size="sm"
|
|
614
|
+
class={cn('size-7 p-0', editor.isActive('bold') && 'bg-[var(--ui-primary)]/10')}
|
|
615
|
+
onclick={toggleBold}
|
|
616
|
+
>
|
|
617
|
+
<Bold class="size-3.5" />
|
|
618
|
+
</Button>
|
|
619
|
+
<Button
|
|
620
|
+
variant="ghost"
|
|
621
|
+
size="sm"
|
|
622
|
+
class={cn('size-7 p-0', editor.isActive('italic') && 'bg-[var(--ui-primary)]/10')}
|
|
623
|
+
onclick={toggleItalic}
|
|
624
|
+
>
|
|
625
|
+
<Italic class="size-3.5" />
|
|
626
|
+
</Button>
|
|
627
|
+
<Button
|
|
628
|
+
variant="ghost"
|
|
629
|
+
size="sm"
|
|
630
|
+
class={cn('size-7 p-0', editor.isActive('underline') && 'bg-[var(--ui-primary)]/10')}
|
|
631
|
+
onclick={toggleUnderline}
|
|
632
|
+
>
|
|
633
|
+
<UnderlineIcon class="size-3.5" />
|
|
634
|
+
</Button>
|
|
635
|
+
<Button
|
|
636
|
+
variant="ghost"
|
|
637
|
+
size="sm"
|
|
638
|
+
class={cn('size-7 p-0', editor.isActive('strike') && 'bg-[var(--ui-primary)]/10')}
|
|
639
|
+
onclick={toggleStrike}
|
|
640
|
+
>
|
|
641
|
+
<Strikethrough class="size-3.5" />
|
|
642
|
+
</Button>
|
|
643
|
+
<Button
|
|
644
|
+
variant="ghost"
|
|
645
|
+
size="sm"
|
|
646
|
+
class={cn(
|
|
647
|
+
'size-7 p-0',
|
|
648
|
+
editor.isActive('highlight') && 'bg-[var(--ui-primary)]/10'
|
|
649
|
+
)}
|
|
650
|
+
onclick={() => toggleHighlight('#fef08a')}
|
|
651
|
+
>
|
|
652
|
+
<Highlighter class="size-3.5" />
|
|
653
|
+
</Button>
|
|
654
|
+
<Button
|
|
655
|
+
variant="ghost"
|
|
656
|
+
size="sm"
|
|
657
|
+
class={cn('size-7 p-0', editor.isActive('link') && 'bg-[var(--ui-primary)]/10')}
|
|
658
|
+
onclick={setLink}
|
|
659
|
+
>
|
|
660
|
+
<LinkIcon class="size-3.5" />
|
|
661
|
+
</Button>
|
|
662
|
+
{/if}
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
<style>
|
|
667
|
+
:global(.tiptap) {
|
|
668
|
+
outline: none;
|
|
669
|
+
}
|
|
670
|
+
:global(.tiptap p.is-editor-empty:first-child::before) {
|
|
671
|
+
content: attr(data-placeholder);
|
|
672
|
+
float: left;
|
|
673
|
+
color: var(--ui-muted-foreground);
|
|
674
|
+
pointer-events: none;
|
|
675
|
+
height: 0;
|
|
676
|
+
}
|
|
677
|
+
:global(.tiptap h1) {
|
|
678
|
+
font-size: 1.5rem;
|
|
679
|
+
font-weight: 700;
|
|
680
|
+
margin: 0.5rem 0;
|
|
681
|
+
}
|
|
682
|
+
:global(.tiptap h2) {
|
|
683
|
+
font-size: 1.25rem;
|
|
684
|
+
font-weight: 600;
|
|
685
|
+
margin: 0.5rem 0;
|
|
686
|
+
}
|
|
687
|
+
:global(.tiptap h3) {
|
|
688
|
+
font-size: 1.125rem;
|
|
689
|
+
font-weight: 600;
|
|
690
|
+
margin: 0.5rem 0;
|
|
691
|
+
}
|
|
692
|
+
:global(.tiptap ul) {
|
|
693
|
+
list-style-type: disc;
|
|
694
|
+
padding-left: 1.5rem;
|
|
695
|
+
}
|
|
696
|
+
:global(.tiptap ol) {
|
|
697
|
+
list-style-type: decimal;
|
|
698
|
+
padding-left: 1.5rem;
|
|
699
|
+
}
|
|
700
|
+
:global(.tiptap blockquote) {
|
|
701
|
+
border-left: 3px solid var(--ui-primary);
|
|
702
|
+
padding-left: 1rem;
|
|
703
|
+
margin-left: 0;
|
|
704
|
+
color: var(--ui-muted-foreground);
|
|
705
|
+
}
|
|
706
|
+
:global(.tiptap pre) {
|
|
707
|
+
background: var(--ui-secondary);
|
|
708
|
+
border-radius: 0.5rem;
|
|
709
|
+
padding: 0.75rem 1rem;
|
|
710
|
+
font-family: monospace;
|
|
711
|
+
font-size: 0.875rem;
|
|
712
|
+
}
|
|
713
|
+
:global(.tiptap code) {
|
|
714
|
+
background: var(--ui-secondary);
|
|
715
|
+
border-radius: 0.25rem;
|
|
716
|
+
padding: 0.125rem 0.25rem;
|
|
717
|
+
font-size: 0.875em;
|
|
718
|
+
}
|
|
719
|
+
:global(.tiptap pre code) {
|
|
720
|
+
background: none;
|
|
721
|
+
padding: 0;
|
|
722
|
+
}
|
|
723
|
+
:global(.tiptap img) {
|
|
724
|
+
max-width: 100%;
|
|
725
|
+
border-radius: 0.5rem;
|
|
726
|
+
}
|
|
727
|
+
:global(.tiptap a.image-link) {
|
|
728
|
+
display: block;
|
|
729
|
+
text-align: center;
|
|
730
|
+
}
|
|
731
|
+
:global(.tiptap a.image-link img) {
|
|
732
|
+
display: block;
|
|
733
|
+
margin: 0 auto;
|
|
734
|
+
}
|
|
735
|
+
:global(.tiptap hr) {
|
|
736
|
+
border: none;
|
|
737
|
+
border-top: 1px solid var(--ui-border);
|
|
738
|
+
margin: 1rem 0;
|
|
739
|
+
}
|
|
740
|
+
:global(.tiptap table) {
|
|
741
|
+
border-collapse: collapse;
|
|
742
|
+
width: 100%;
|
|
743
|
+
margin: 1em 0;
|
|
744
|
+
overflow: hidden;
|
|
745
|
+
}
|
|
746
|
+
:global(.tiptap th),
|
|
747
|
+
:global(.tiptap td) {
|
|
748
|
+
border: 1px solid var(--ui-border);
|
|
749
|
+
padding: 0.5rem 0.75rem;
|
|
750
|
+
text-align: left;
|
|
751
|
+
position: relative;
|
|
752
|
+
min-width: 80px;
|
|
753
|
+
}
|
|
754
|
+
:global(.tiptap th) {
|
|
755
|
+
background: var(--ui-secondary);
|
|
756
|
+
font-weight: 600;
|
|
757
|
+
}
|
|
758
|
+
:global(.tiptap td.selectedCell) {
|
|
759
|
+
background: color-mix(in srgb, var(--ui-primary) 10%, transparent);
|
|
760
|
+
}
|
|
761
|
+
:global(.tiptap .selectedCell::after) {
|
|
762
|
+
content: '';
|
|
763
|
+
position: absolute;
|
|
764
|
+
inset: 0;
|
|
765
|
+
background: rgba(0, 0, 0, 0.05);
|
|
766
|
+
pointer-events: none;
|
|
767
|
+
}
|
|
768
|
+
:global(.tiptap .column-resize-handle) {
|
|
769
|
+
position: absolute;
|
|
770
|
+
right: -2px;
|
|
771
|
+
top: 0;
|
|
772
|
+
bottom: 0;
|
|
773
|
+
width: 4px;
|
|
774
|
+
background: var(--ui-primary);
|
|
775
|
+
cursor: col-resize;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/* YouTube / Video embed */
|
|
779
|
+
:global(.tiptap .video-wrapper) {
|
|
780
|
+
position: relative;
|
|
781
|
+
padding-bottom: 56.25%;
|
|
782
|
+
height: 0;
|
|
783
|
+
overflow: hidden;
|
|
784
|
+
margin: 1.5rem 0;
|
|
785
|
+
border-radius: 0.75rem;
|
|
786
|
+
background: var(--ui-secondary);
|
|
787
|
+
}
|
|
788
|
+
:global(.tiptap .video-wrapper iframe) {
|
|
789
|
+
position: absolute;
|
|
790
|
+
top: 0;
|
|
791
|
+
left: 0;
|
|
792
|
+
width: 100%;
|
|
793
|
+
height: 100%;
|
|
794
|
+
border: 0;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/* Typography smart-quotes styling (cosmetic, TipTap handles conversion) */
|
|
798
|
+
:global(.tiptap mark) {
|
|
799
|
+
background-color: #fef08a;
|
|
800
|
+
padding: 0.1em 0.2em;
|
|
801
|
+
border-radius: 2px;
|
|
802
|
+
}
|
|
803
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
content?: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
editable?: boolean;
|
|
5
|
+
height?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
onUpdate?: (html: string) => void;
|
|
8
|
+
/** Called when paste/drop provides an image file. Return a URL to insert. */
|
|
9
|
+
onImageUpload?: (file: File) => Promise<string>;
|
|
10
|
+
};
|
|
11
|
+
declare const RichTextEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type RichTextEditor = ReturnType<typeof RichTextEditor>;
|
|
13
|
+
export default RichTextEditor;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as RichTextEditor } from './components/RichTextEditor.svelte';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as RichTextEditor } from './components/RichTextEditor.svelte';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type TrackedChange = {
|
|
2
|
+
id: string;
|
|
3
|
+
type: 'insertion' | 'deletion';
|
|
4
|
+
userId: string;
|
|
5
|
+
userName: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
content: string;
|
|
8
|
+
accepted: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type TrackedChangeState = {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
changes: TrackedChange[];
|
|
13
|
+
currentUserId: string;
|
|
14
|
+
currentUserName: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function createTrackedChangeState(userId: string, userName: string): TrackedChangeState;
|
|
17
|
+
export declare function addTrackedChange(state: TrackedChangeState, type: 'insertion' | 'deletion', content: string): TrackedChange;
|
|
18
|
+
export declare function acceptChange(state: TrackedChangeState, changeId: string): TrackedChangeState;
|
|
19
|
+
export declare function rejectChange(state: TrackedChangeState, changeId: string): TrackedChangeState;
|
|
20
|
+
export declare function acceptAllChanges(state: TrackedChangeState): TrackedChangeState;
|
|
21
|
+
export declare function rejectAllChanges(state: TrackedChangeState): TrackedChangeState;
|
|
22
|
+
export declare function getPendingChangesCount(state: TrackedChangeState): number;
|
|
23
|
+
export declare function getChangesByUser(state: TrackedChangeState, userId: string): TrackedChange[];
|
|
24
|
+
/**
|
|
25
|
+
* Render tracked changes as HTML with colored marks
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderTrackedChanges(html: string, changes: TrackedChange[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Extract tracked changes from HTML
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractTrackedChanges(html: string): TrackedChange[];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// Tracked Changes utilities — track insertions/deletions
|
|
3
|
+
// ============================================
|
|
4
|
+
export function createTrackedChangeState(userId, userName) {
|
|
5
|
+
return {
|
|
6
|
+
enabled: false,
|
|
7
|
+
changes: [],
|
|
8
|
+
currentUserId: userId,
|
|
9
|
+
currentUserName: userName,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function addTrackedChange(state, type, content) {
|
|
13
|
+
const change = {
|
|
14
|
+
id: `tc-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
15
|
+
type,
|
|
16
|
+
userId: state.currentUserId,
|
|
17
|
+
userName: state.currentUserName,
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
content,
|
|
20
|
+
accepted: false,
|
|
21
|
+
};
|
|
22
|
+
return change;
|
|
23
|
+
}
|
|
24
|
+
export function acceptChange(state, changeId) {
|
|
25
|
+
return {
|
|
26
|
+
...state,
|
|
27
|
+
changes: state.changes.map((c) => c.id === changeId ? { ...c, accepted: true } : c),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function rejectChange(state, changeId) {
|
|
31
|
+
return {
|
|
32
|
+
...state,
|
|
33
|
+
changes: state.changes.filter((c) => c.id !== changeId),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function acceptAllChanges(state) {
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
changes: state.changes.map((c) => ({ ...c, accepted: true })),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function rejectAllChanges(state) {
|
|
43
|
+
return {
|
|
44
|
+
...state,
|
|
45
|
+
changes: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function getPendingChangesCount(state) {
|
|
49
|
+
return state.changes.filter((c) => !c.accepted).length;
|
|
50
|
+
}
|
|
51
|
+
export function getChangesByUser(state, userId) {
|
|
52
|
+
return state.changes.filter((c) => c.userId === userId);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render tracked changes as HTML with colored marks
|
|
56
|
+
*/
|
|
57
|
+
export function renderTrackedChanges(html, changes) {
|
|
58
|
+
let result = html;
|
|
59
|
+
for (const change of changes) {
|
|
60
|
+
if (change.accepted)
|
|
61
|
+
continue;
|
|
62
|
+
if (change.type === 'insertion') {
|
|
63
|
+
// Mark insertions with green background
|
|
64
|
+
result = result.replace(change.content, `<span class="tracked-insertion" data-change-id="${change.id}" style="background: #dcfce7; text-decoration: none;">${change.content}</span>`);
|
|
65
|
+
}
|
|
66
|
+
else if (change.type === 'deletion') {
|
|
67
|
+
// Mark deletions with red strikethrough
|
|
68
|
+
result = result.replace(change.content, `<span class="tracked-deletion" data-change-id="${change.id}" style="background: #fee2e2; text-decoration: line-through; color: #991b1b;">${change.content}</span>`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extract tracked changes from HTML
|
|
75
|
+
*/
|
|
76
|
+
export function extractTrackedChanges(html) {
|
|
77
|
+
const changes = [];
|
|
78
|
+
// Extract insertions
|
|
79
|
+
const insertionRegex = /<span[^>]*class="tracked-insertion"[^>]*data-change-id="([^"]*)"[^>]*>(.*?)<\/span>/gi;
|
|
80
|
+
let match;
|
|
81
|
+
while ((match = insertionRegex.exec(html)) !== null) {
|
|
82
|
+
changes.push({
|
|
83
|
+
id: match[1],
|
|
84
|
+
type: 'insertion',
|
|
85
|
+
userId: 'unknown',
|
|
86
|
+
userName: 'Unknown',
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
content: match[2],
|
|
89
|
+
accepted: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Extract deletions
|
|
93
|
+
const deletionRegex = /<span[^>]*class="tracked-deletion"[^>]*data-change-id="([^"]*)"[^>]*>(.*?)<\/span>/gi;
|
|
94
|
+
while ((match = deletionRegex.exec(html)) !== null) {
|
|
95
|
+
changes.push({
|
|
96
|
+
id: match[1],
|
|
97
|
+
type: 'deletion',
|
|
98
|
+
userId: 'unknown',
|
|
99
|
+
userName: 'Unknown',
|
|
100
|
+
timestamp: new Date().toISOString(),
|
|
101
|
+
content: match[2],
|
|
102
|
+
accepted: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return changes;
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intinyagroup/rich-text",
|
|
3
|
+
"version": "0.1.1-alpha.5",
|
|
4
|
+
"description": "Rich text editor component powered by TipTap",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"svelte": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "svelte-package -o dist",
|
|
18
|
+
"check": "echo 'no check'",
|
|
19
|
+
"dev": "svelte-package -o dist -w"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@intinyagroup/ui": "0.1.1-alpha.5",
|
|
23
|
+
"@intinyagroup/grid-core": "0.1.1-alpha.5",
|
|
24
|
+
"@tiptap/core": "^3.22.5",
|
|
25
|
+
"@tiptap/starter-kit": "^3.22.5",
|
|
26
|
+
"@tiptap/extension-placeholder": "^3.22.5",
|
|
27
|
+
"@tiptap/extension-underline": "^3.22.5",
|
|
28
|
+
"@tiptap/extension-text-align": "^3.22.5",
|
|
29
|
+
"@tiptap/extension-link": "^3.22.5",
|
|
30
|
+
"@tiptap/extension-image": "^3.22.5",
|
|
31
|
+
"@tiptap/extension-code-block-lowlight": "^3.22.5",
|
|
32
|
+
"@tiptap/pm": "^3.22.5"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@sveltejs/package": "^2.5.8",
|
|
36
|
+
"svelte": "^5.55.2",
|
|
37
|
+
"typescript": "^6.0.2",
|
|
38
|
+
"@sveltejs/vite-plugin-svelte": "^5.1.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"svelte": "^5.0.0"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/intinyagroup/ui.git",
|
|
47
|
+
"directory": "packages/rich-text"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|