@foxui/text 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/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/components/advanced-text-area/AdvancedTextArea.svelte +58 -0
- package/dist/components/advanced-text-area/AdvancedTextArea.svelte.d.ts +12 -0
- package/dist/components/advanced-text-area/index.d.ts +1 -0
- package/dist/components/advanced-text-area/index.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/plain-text-editor/PlainTextEditor.svelte +133 -0
- package/dist/components/plain-text-editor/PlainTextEditor.svelte.d.ts +14 -0
- package/dist/components/plain-text-editor/index.d.ts +1 -0
- package/dist/components/plain-text-editor/index.js +1 -0
- package/dist/components/rich-text-editor/Icon.svelte +120 -0
- package/dist/components/rich-text-editor/Icon.svelte.d.ts +7 -0
- package/dist/components/rich-text-editor/RichTextEditor.svelte +427 -0
- package/dist/components/rich-text-editor/RichTextEditor.svelte.d.ts +18 -0
- package/dist/components/rich-text-editor/RichTextEditorLinkMenu.svelte +95 -0
- package/dist/components/rich-text-editor/RichTextEditorLinkMenu.svelte.d.ts +10 -0
- package/dist/components/rich-text-editor/RichTextEditorMenu.svelte +184 -0
- package/dist/components/rich-text-editor/RichTextEditorMenu.svelte.d.ts +19 -0
- package/dist/components/rich-text-editor/RichTextLink.d.ts +13 -0
- package/dist/components/rich-text-editor/RichTextLink.js +105 -0
- package/dist/components/rich-text-editor/Select.svelte +72 -0
- package/dist/components/rich-text-editor/Select.svelte.d.ts +13 -0
- package/dist/components/rich-text-editor/code.css +142 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadComponent.svelte +47 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadComponent.svelte.d.ts +4 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadNode.d.ts +45 -0
- package/dist/components/rich-text-editor/image-upload/ImageUploadNode.js +56 -0
- package/dist/components/rich-text-editor/index.d.ts +2 -0
- package/dist/components/rich-text-editor/index.js +1 -0
- package/dist/components/rich-text-editor/slash-menu/SuggestionSelect.svelte +88 -0
- package/dist/components/rich-text-editor/slash-menu/SuggestionSelect.svelte.d.ts +22 -0
- package/dist/components/rich-text-editor/slash-menu/index.d.ts +32 -0
- package/dist/components/rich-text-editor/slash-menu/index.js +168 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { Editor, mergeAttributes, type Content } from '@tiptap/core';
|
|
4
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
5
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
6
|
+
import Image from '@tiptap/extension-image';
|
|
7
|
+
import { all, createLowlight } from 'lowlight';
|
|
8
|
+
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
|
9
|
+
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
|
10
|
+
import Underline from '@tiptap/extension-underline';
|
|
11
|
+
import RichTextEditorMenu from './RichTextEditorMenu.svelte';
|
|
12
|
+
import type { RichTextTypes } from '.';
|
|
13
|
+
import RichTextEditorLinkMenu from './RichTextEditorLinkMenu.svelte';
|
|
14
|
+
import Slash, { suggestion } from './slash-menu';
|
|
15
|
+
import Typography from '@tiptap/extension-typography';
|
|
16
|
+
import { RichTextLink } from './RichTextLink';
|
|
17
|
+
|
|
18
|
+
import './code.css';
|
|
19
|
+
import { cn } from '@foxui/core';
|
|
20
|
+
import { ImageUploadNode } from './image-upload/ImageUploadNode';
|
|
21
|
+
import { Transaction } from '@tiptap/pm/state';
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
content = $bindable({}),
|
|
25
|
+
placeholder = 'Write or press / for commands',
|
|
26
|
+
editor = $bindable(null),
|
|
27
|
+
ref = $bindable(null),
|
|
28
|
+
class: className,
|
|
29
|
+
onupdate,
|
|
30
|
+
ontransaction
|
|
31
|
+
}: {
|
|
32
|
+
content?: Content;
|
|
33
|
+
placeholder?: string;
|
|
34
|
+
editor?: Editor | null;
|
|
35
|
+
ref?: HTMLDivElement | null;
|
|
36
|
+
class?: string;
|
|
37
|
+
onupdate?: (content: Content, context: { editor: Editor; transaction: Transaction }) => void;
|
|
38
|
+
ontransaction?: () => void;
|
|
39
|
+
} = $props();
|
|
40
|
+
|
|
41
|
+
const lowlight = createLowlight(all);
|
|
42
|
+
|
|
43
|
+
let hasFocus = true;
|
|
44
|
+
|
|
45
|
+
let menu: HTMLElement | null = $state(null);
|
|
46
|
+
let menuLink: HTMLElement | null = $state(null);
|
|
47
|
+
|
|
48
|
+
let selectedType: RichTextTypes = $state('paragraph');
|
|
49
|
+
|
|
50
|
+
let isBold = $state(false);
|
|
51
|
+
let isItalic = $state(false);
|
|
52
|
+
let isUnderline = $state(false);
|
|
53
|
+
let isStrikethrough = $state(false);
|
|
54
|
+
let isLink = $state(false);
|
|
55
|
+
let isImage = $state(false);
|
|
56
|
+
|
|
57
|
+
const CustomImage = Image.extend({
|
|
58
|
+
// addAttributes(this) {
|
|
59
|
+
// return {
|
|
60
|
+
// inline: true,
|
|
61
|
+
// allowBase64: true,
|
|
62
|
+
// HTMLAttributes: {},
|
|
63
|
+
// uploadImageHandler: { default: undefined }
|
|
64
|
+
// };
|
|
65
|
+
// },
|
|
66
|
+
renderHTML({ HTMLAttributes }) {
|
|
67
|
+
const attrs = mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
|
68
|
+
uploadImageHandler: undefined
|
|
69
|
+
});
|
|
70
|
+
const isLocal = attrs['data-local'] === 'true';
|
|
71
|
+
|
|
72
|
+
// if (isLocal) {
|
|
73
|
+
// // For local images, wrap in a container with a label
|
|
74
|
+
// return [
|
|
75
|
+
// 'div',
|
|
76
|
+
// { class: 'image-container' },
|
|
77
|
+
// ['img', { ...attrs, class: `${attrs.class || ''} local-image` }],
|
|
78
|
+
// ['span', { class: 'local-image-label' }, 'Local preview']
|
|
79
|
+
// ];
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
console.log('attrs', attrs);
|
|
83
|
+
|
|
84
|
+
// For regular images, just return the img tag
|
|
85
|
+
return ['img', attrs];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
onMount(() => {
|
|
90
|
+
if (!ref) return;
|
|
91
|
+
|
|
92
|
+
let extensions = [
|
|
93
|
+
StarterKit.configure({
|
|
94
|
+
dropcursor: {
|
|
95
|
+
class: 'text-accent-500/30 rounded-2xl',
|
|
96
|
+
width: 2
|
|
97
|
+
},
|
|
98
|
+
codeBlock: false,
|
|
99
|
+
heading: {
|
|
100
|
+
levels: [1, 2, 3]
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
Placeholder.configure({
|
|
104
|
+
placeholder: ({ node }) => {
|
|
105
|
+
// only show in paragraphs
|
|
106
|
+
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
|
107
|
+
return placeholder;
|
|
108
|
+
}
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
CustomImage.configure({
|
|
113
|
+
HTMLAttributes: {
|
|
114
|
+
class: 'max-w-full object-contain relative rounded-2xl'
|
|
115
|
+
},
|
|
116
|
+
allowBase64: true
|
|
117
|
+
}),
|
|
118
|
+
CodeBlockLowlight.configure({
|
|
119
|
+
lowlight,
|
|
120
|
+
defaultLanguage: 'js'
|
|
121
|
+
}),
|
|
122
|
+
BubbleMenu.configure({
|
|
123
|
+
element: menu,
|
|
124
|
+
shouldShow: ({ editor }) => {
|
|
125
|
+
// dont show if image selected or no selection or is code block
|
|
126
|
+
return (
|
|
127
|
+
!editor.isActive('image') &&
|
|
128
|
+
!editor.view.state.selection.empty &&
|
|
129
|
+
!editor.isActive('codeBlock') &&
|
|
130
|
+
!editor.isActive('link') &&
|
|
131
|
+
!editor.isActive('imageUpload')
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
pluginKey: 'bubble-menu-marks'
|
|
135
|
+
}),
|
|
136
|
+
BubbleMenu.configure({
|
|
137
|
+
element: menuLink,
|
|
138
|
+
shouldShow: ({ editor }) => {
|
|
139
|
+
// only show if link is selected
|
|
140
|
+
return editor.isActive('link') && !editor.view.state.selection.empty;
|
|
141
|
+
},
|
|
142
|
+
pluginKey: 'bubble-menu-links'
|
|
143
|
+
}),
|
|
144
|
+
Underline.configure({}),
|
|
145
|
+
RichTextLink.configure({
|
|
146
|
+
openOnClick: false,
|
|
147
|
+
autolink: true,
|
|
148
|
+
defaultProtocol: 'https'
|
|
149
|
+
}),
|
|
150
|
+
Slash.configure({
|
|
151
|
+
suggestion: suggestion({
|
|
152
|
+
char: '/',
|
|
153
|
+
pluginKey: 'slash',
|
|
154
|
+
switchTo,
|
|
155
|
+
processImageFile
|
|
156
|
+
})
|
|
157
|
+
}),
|
|
158
|
+
Typography.configure(),
|
|
159
|
+
ImageUploadNode.configure({
|
|
160
|
+
upload: async (file, onProgress, abortSignal) => {
|
|
161
|
+
console.log('uploading image', file);
|
|
162
|
+
// wait 2 seconds
|
|
163
|
+
for (let i = 0; i < 10; i++) {
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
165
|
+
onProgress?.({ progress: i / 10 });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return 'https://picsum.photos/200/300';
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
editor = new Editor({
|
|
174
|
+
element: ref,
|
|
175
|
+
extensions,
|
|
176
|
+
editorProps: {
|
|
177
|
+
attributes: {
|
|
178
|
+
class: 'outline-none'
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
onUpdate: (ctx) => {
|
|
182
|
+
content = ctx.editor.getJSON();
|
|
183
|
+
onupdate?.(content, ctx);
|
|
184
|
+
},
|
|
185
|
+
onFocus: () => {
|
|
186
|
+
hasFocus = true;
|
|
187
|
+
},
|
|
188
|
+
onBlur: () => {
|
|
189
|
+
hasFocus = false;
|
|
190
|
+
},
|
|
191
|
+
onTransaction: (ctx) => {
|
|
192
|
+
isBold = ctx.editor.isActive('bold');
|
|
193
|
+
isItalic = ctx.editor.isActive('italic');
|
|
194
|
+
isUnderline = ctx.editor.isActive('underline');
|
|
195
|
+
isStrikethrough = ctx.editor.isActive('strike');
|
|
196
|
+
isLink = ctx.editor.isActive('link');
|
|
197
|
+
isImage = ctx.editor.isActive('image');
|
|
198
|
+
|
|
199
|
+
if (ctx.editor.isActive('heading', { level: 1 })) {
|
|
200
|
+
selectedType = 'heading-1';
|
|
201
|
+
} else if (ctx.editor.isActive('heading', { level: 2 })) {
|
|
202
|
+
selectedType = 'heading-2';
|
|
203
|
+
} else if (ctx.editor.isActive('heading', { level: 3 })) {
|
|
204
|
+
selectedType = 'heading-3';
|
|
205
|
+
} else if (ctx.editor.isActive('blockquote')) {
|
|
206
|
+
selectedType = 'blockquote';
|
|
207
|
+
} else if (ctx.editor.isActive('code')) {
|
|
208
|
+
selectedType = 'code';
|
|
209
|
+
} else if (ctx.editor.isActive('bulletList')) {
|
|
210
|
+
selectedType = 'bullet-list';
|
|
211
|
+
} else if (ctx.editor.isActive('orderedList')) {
|
|
212
|
+
selectedType = 'ordered-list';
|
|
213
|
+
} else {
|
|
214
|
+
selectedType = 'paragraph';
|
|
215
|
+
}
|
|
216
|
+
ontransaction?.();
|
|
217
|
+
},
|
|
218
|
+
content
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
menu?.classList.remove('hidden');
|
|
222
|
+
menuLink?.classList.remove('hidden');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Flag to track whether a file is being dragged over the drop area
|
|
226
|
+
let isDragOver = $state(false);
|
|
227
|
+
|
|
228
|
+
// Store local image files for later upload
|
|
229
|
+
let localImages: Map<string, File> = $state(new Map());
|
|
230
|
+
|
|
231
|
+
// Track which image URLs in the editor are local previews
|
|
232
|
+
let localImageUrls: Set<string> = $state(new Set());
|
|
233
|
+
|
|
234
|
+
// Process image file to create a local preview
|
|
235
|
+
async function processImageFile(file: File) {
|
|
236
|
+
if (!editor) {
|
|
237
|
+
console.warn('Tiptap editor not initialized');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const localUrl = URL.createObjectURL(file);
|
|
243
|
+
|
|
244
|
+
localImages.set(localUrl, file);
|
|
245
|
+
localImageUrls.add(localUrl);
|
|
246
|
+
|
|
247
|
+
//editor.commands.setImageUploadNode();
|
|
248
|
+
editor
|
|
249
|
+
.chain()
|
|
250
|
+
.focus()
|
|
251
|
+
.setImageUploadNode({
|
|
252
|
+
preview: localUrl
|
|
253
|
+
})
|
|
254
|
+
.run();
|
|
255
|
+
|
|
256
|
+
// wait 2 seconds
|
|
257
|
+
// await new Promise((resolve) => setTimeout(resolve, 500));
|
|
258
|
+
|
|
259
|
+
// content = editor.getJSON();
|
|
260
|
+
|
|
261
|
+
// console.log('replacing image url in content');
|
|
262
|
+
// replaceImageUrlInContent(content, localUrl, 'https://picsum.photos/200/300');
|
|
263
|
+
// editor.commands.setContent(content);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error('Error creating image preview:', error);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const handlePaste = (event: ClipboardEvent) => {
|
|
270
|
+
const items = event.clipboardData?.items;
|
|
271
|
+
if (!items) return;
|
|
272
|
+
// Check for image data in clipboard
|
|
273
|
+
for (const item of Array.from(items)) {
|
|
274
|
+
if (!item.type.startsWith('image/')) continue;
|
|
275
|
+
const file = item.getAsFile();
|
|
276
|
+
if (!file) continue;
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
processImageFile(file);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
function handleDragOver(event: DragEvent) {
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
event.stopPropagation();
|
|
286
|
+
isDragOver = true;
|
|
287
|
+
}
|
|
288
|
+
function handleDragLeave(event: DragEvent) {
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
event.stopPropagation();
|
|
291
|
+
isDragOver = false;
|
|
292
|
+
}
|
|
293
|
+
function handleDrop(event: DragEvent) {
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
event.stopPropagation();
|
|
296
|
+
isDragOver = false;
|
|
297
|
+
if (!event.dataTransfer?.files?.length) return;
|
|
298
|
+
const file = event.dataTransfer.files[0];
|
|
299
|
+
if (file?.type.startsWith('image/')) {
|
|
300
|
+
processImageFile(file);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
onDestroy(() => {
|
|
305
|
+
for (const localUrl of localImageUrls) {
|
|
306
|
+
URL.revokeObjectURL(localUrl);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
editor?.destroy();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
let link = $state('');
|
|
313
|
+
|
|
314
|
+
let linkInput: HTMLInputElement | null = $state(null);
|
|
315
|
+
|
|
316
|
+
function clickedLink() {
|
|
317
|
+
if (isLink) {
|
|
318
|
+
//tiptap?.chain().focus().unsetLink().run();
|
|
319
|
+
// get current link
|
|
320
|
+
link = editor?.getAttributes('link').href;
|
|
321
|
+
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
linkInput?.focus();
|
|
324
|
+
}, 100);
|
|
325
|
+
} else {
|
|
326
|
+
link = '';
|
|
327
|
+
// set link
|
|
328
|
+
editor?.chain().focus().setLink({ href: link }).run();
|
|
329
|
+
|
|
330
|
+
setTimeout(() => {
|
|
331
|
+
linkInput?.focus();
|
|
332
|
+
}, 100);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function switchTo(value: RichTextTypes) {
|
|
337
|
+
editor?.chain().focus().setParagraph().run();
|
|
338
|
+
|
|
339
|
+
if (value === 'heading-1') {
|
|
340
|
+
editor?.chain().focus().setNode('heading', { level: 1 }).run();
|
|
341
|
+
} else if (value === 'heading-2') {
|
|
342
|
+
editor?.chain().focus().setNode('heading', { level: 2 }).run();
|
|
343
|
+
} else if (value === 'heading-3') {
|
|
344
|
+
editor?.chain().focus().setNode('heading', { level: 3 }).run();
|
|
345
|
+
} else if (value === 'blockquote') {
|
|
346
|
+
editor?.chain().focus().setBlockquote().run();
|
|
347
|
+
} else if (value === 'code') {
|
|
348
|
+
editor?.chain().focus().setCodeBlock().run();
|
|
349
|
+
} else if (value === 'bullet-list') {
|
|
350
|
+
editor?.chain().focus().toggleBulletList().run();
|
|
351
|
+
} else if (value === 'ordered-list') {
|
|
352
|
+
editor?.chain().focus().toggleOrderedList().run();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
</script>
|
|
356
|
+
|
|
357
|
+
<div
|
|
358
|
+
bind:this={ref}
|
|
359
|
+
class={cn('relative flex-1', className)}
|
|
360
|
+
onpaste={handlePaste}
|
|
361
|
+
ondragover={handleDragOver}
|
|
362
|
+
ondragleave={handleDragLeave}
|
|
363
|
+
ondrop={handleDrop}
|
|
364
|
+
role="region"
|
|
365
|
+
></div>
|
|
366
|
+
|
|
367
|
+
<RichTextEditorMenu
|
|
368
|
+
bind:ref={menu}
|
|
369
|
+
{editor}
|
|
370
|
+
{isBold}
|
|
371
|
+
{isItalic}
|
|
372
|
+
{isUnderline}
|
|
373
|
+
{isStrikethrough}
|
|
374
|
+
{isLink}
|
|
375
|
+
{isImage}
|
|
376
|
+
{clickedLink}
|
|
377
|
+
{processImageFile}
|
|
378
|
+
{switchTo}
|
|
379
|
+
bind:selectedType
|
|
380
|
+
/>
|
|
381
|
+
|
|
382
|
+
<RichTextEditorLinkMenu bind:ref={menuLink} {editor} bind:link bind:linkInput />
|
|
383
|
+
|
|
384
|
+
<style>
|
|
385
|
+
:global(.tiptap) {
|
|
386
|
+
:first-child {
|
|
387
|
+
margin-top: 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
:global(img) {
|
|
391
|
+
display: block;
|
|
392
|
+
height: auto;
|
|
393
|
+
margin: 1.5rem 0;
|
|
394
|
+
max-width: 100%;
|
|
395
|
+
|
|
396
|
+
&.ProseMirror-selectednode {
|
|
397
|
+
outline: 3px solid var(--color-accent-500);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
:global(div[data-type='image-upload']) {
|
|
402
|
+
&.ProseMirror-selectednode {
|
|
403
|
+
outline: 3px solid var(--color-accent-500);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
:global(blockquote p:first-of-type::before) {
|
|
408
|
+
content: none;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
:global(blockquote p:last-of-type::after) {
|
|
412
|
+
content: none;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
:global(blockquote p) {
|
|
416
|
+
font-style: normal;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
:global(.tiptap .is-empty::before) {
|
|
421
|
+
color: var(--color-base-500);
|
|
422
|
+
content: attr(data-placeholder);
|
|
423
|
+
float: left;
|
|
424
|
+
height: 0;
|
|
425
|
+
pointer-events: none;
|
|
426
|
+
}
|
|
427
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Editor, type Content } from '@tiptap/core';
|
|
2
|
+
import './code.css';
|
|
3
|
+
import { Transaction } from '@tiptap/pm/state';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
content?: Content;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
editor?: Editor | null;
|
|
8
|
+
ref?: HTMLDivElement | null;
|
|
9
|
+
class?: string;
|
|
10
|
+
onupdate?: (content: Content, context: {
|
|
11
|
+
editor: Editor;
|
|
12
|
+
transaction: Transaction;
|
|
13
|
+
}) => void;
|
|
14
|
+
ontransaction?: () => void;
|
|
15
|
+
};
|
|
16
|
+
declare const RichTextEditor: import("svelte").Component<$$ComponentProps, {}, "ref" | "editor" | "content">;
|
|
17
|
+
type RichTextEditor = ReturnType<typeof RichTextEditor>;
|
|
18
|
+
export default RichTextEditor;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Input } from "@foxui/core";
|
|
3
|
+
import type { Editor } from "@tiptap/core";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
editor,
|
|
7
|
+
link = $bindable(''),
|
|
8
|
+
ref = $bindable(null),
|
|
9
|
+
linkInput = $bindable(null)
|
|
10
|
+
}: {
|
|
11
|
+
editor: Editor | null;
|
|
12
|
+
link: string;
|
|
13
|
+
ref: HTMLElement | null;
|
|
14
|
+
linkInput: HTMLInputElement | null;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
function processLink(link: string) {
|
|
18
|
+
return link.includes(':') ? link : `http://${link}`;
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
bind:this={ref}
|
|
24
|
+
class="menu bg-base-50 dark:bg-base-900 relative hidden w-fit rounded-2xl px-1 py-1 shadow-lg backdrop-blur-sm"
|
|
25
|
+
>
|
|
26
|
+
<div class="flex items-center gap-1">
|
|
27
|
+
<Input
|
|
28
|
+
bind:ref={linkInput}
|
|
29
|
+
sizeVariant="sm"
|
|
30
|
+
bind:value={link}
|
|
31
|
+
placeholder="Enter link"
|
|
32
|
+
onblur={() => {
|
|
33
|
+
if (link === '') {
|
|
34
|
+
editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
35
|
+
} else {
|
|
36
|
+
editor?.chain().focus().extendMarkRange('link').setLink({ href: processLink(link) }).run();
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
onkeydown={(e: KeyboardEvent) => {
|
|
40
|
+
if (e.key === 'Enter') {
|
|
41
|
+
if (link === '') {
|
|
42
|
+
editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
43
|
+
} else {
|
|
44
|
+
editor?.chain().focus().extendMarkRange('link').setLink({ href: processLink(link) }).run();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
<Button
|
|
50
|
+
size="iconSm"
|
|
51
|
+
onclick={() => {
|
|
52
|
+
if (link === '') {
|
|
53
|
+
editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
54
|
+
} else {
|
|
55
|
+
editor?.chain().focus().extendMarkRange('link').setLink({ href: processLink(link) }).run();
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<svg
|
|
60
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
61
|
+
fill="none"
|
|
62
|
+
viewBox="0 0 24 24"
|
|
63
|
+
stroke-width="1.5"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
class="size-6"
|
|
66
|
+
>
|
|
67
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
|
68
|
+
</svg>
|
|
69
|
+
|
|
70
|
+
<span class="sr-only">save link</span>
|
|
71
|
+
</Button>
|
|
72
|
+
<Button
|
|
73
|
+
size="iconSm"
|
|
74
|
+
onclick={() => {
|
|
75
|
+
editor?.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
76
|
+
}}
|
|
77
|
+
variant="ghost"
|
|
78
|
+
>
|
|
79
|
+
<svg
|
|
80
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
81
|
+
fill="none"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
stroke-width="1.5"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
>
|
|
86
|
+
<path
|
|
87
|
+
stroke-linecap="round"
|
|
88
|
+
stroke-linejoin="round"
|
|
89
|
+
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
|
|
90
|
+
/>
|
|
91
|
+
</svg>
|
|
92
|
+
<span class="sr-only">remove link</span>
|
|
93
|
+
</Button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
editor: Editor | null;
|
|
4
|
+
link: string;
|
|
5
|
+
ref: HTMLElement | null;
|
|
6
|
+
linkInput: HTMLInputElement | null;
|
|
7
|
+
};
|
|
8
|
+
declare const RichTextEditorLinkMenu: import("svelte").Component<$$ComponentProps, {}, "ref" | "link" | "linkInput">;
|
|
9
|
+
type RichTextEditorLinkMenu = ReturnType<typeof RichTextEditorLinkMenu>;
|
|
10
|
+
export default RichTextEditorLinkMenu;
|