@svadmin/editor 0.1.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/package.json +66 -0
- package/src/components/BubbleMenu.svelte +104 -0
- package/src/components/ColorPicker.svelte +69 -0
- package/src/components/Editor.svelte +223 -0
- package/src/components/FloatingMenu.svelte +77 -0
- package/src/components/ImageUpload.svelte +159 -0
- package/src/components/LinkPopover.svelte +87 -0
- package/src/components/TableMenu.svelte +94 -0
- package/src/components/Toolbar.svelte +312 -0
- package/src/components/ToolbarButton.svelte +27 -0
- package/src/components/ToolbarGroup.svelte +11 -0
- package/src/components/ToolbarSelect.svelte +23 -0
- package/src/editor.svelte.ts +99 -0
- package/src/extensions/index.ts +211 -0
- package/src/extensions/slash-command.ts +150 -0
- package/src/index.ts +65 -0
- package/src/styles/editor.css +697 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @svadmin/editor — Extension presets
|
|
3
|
+
*
|
|
4
|
+
* Pre-configured extension bundles for different use cases.
|
|
5
|
+
* Users can pick a preset or compose their own extension array.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
9
|
+
import Underline from '@tiptap/extension-underline';
|
|
10
|
+
import TextAlign from '@tiptap/extension-text-align';
|
|
11
|
+
import TextStyle from '@tiptap/extension-text-style';
|
|
12
|
+
import Color from '@tiptap/extension-color';
|
|
13
|
+
import Highlight from '@tiptap/extension-highlight';
|
|
14
|
+
import Link from '@tiptap/extension-link';
|
|
15
|
+
import Image from '@tiptap/extension-image';
|
|
16
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
17
|
+
import CharacterCount from '@tiptap/extension-character-count';
|
|
18
|
+
import Typography from '@tiptap/extension-typography';
|
|
19
|
+
import Subscript from '@tiptap/extension-subscript';
|
|
20
|
+
import Superscript from '@tiptap/extension-superscript';
|
|
21
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
22
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
23
|
+
import Table from '@tiptap/extension-table';
|
|
24
|
+
import TableRow from '@tiptap/extension-table-row';
|
|
25
|
+
import TableHeader from '@tiptap/extension-table-header';
|
|
26
|
+
import TableCell from '@tiptap/extension-table-cell';
|
|
27
|
+
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
|
28
|
+
import type { AnyExtension } from '@tiptap/core';
|
|
29
|
+
|
|
30
|
+
export interface EditorPresetOptions {
|
|
31
|
+
/** Placeholder text */
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
/** Max character count (0 = unlimited) */
|
|
34
|
+
maxLength?: number;
|
|
35
|
+
/** Enable code block syntax highlighting */
|
|
36
|
+
codeHighlight?: boolean;
|
|
37
|
+
/** lowlight instance (required if codeHighlight is true) */
|
|
38
|
+
lowlight?: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Full preset: all extensions enabled.
|
|
43
|
+
* Suitable for content management, article editing, knowledge bases.
|
|
44
|
+
*/
|
|
45
|
+
export function fullPreset(options: EditorPresetOptions = {}): AnyExtension[] {
|
|
46
|
+
const extensions: AnyExtension[] = [
|
|
47
|
+
StarterKit.configure({
|
|
48
|
+
// Disable default code block in favor of CodeBlockLowlight
|
|
49
|
+
codeBlock: options.codeHighlight ? false : undefined,
|
|
50
|
+
}),
|
|
51
|
+
Underline,
|
|
52
|
+
TextStyle,
|
|
53
|
+
Color,
|
|
54
|
+
Highlight.configure({ multicolor: true }),
|
|
55
|
+
TextAlign.configure({
|
|
56
|
+
types: ['heading', 'paragraph'],
|
|
57
|
+
}),
|
|
58
|
+
Link.configure({
|
|
59
|
+
openOnClick: false,
|
|
60
|
+
autolink: true,
|
|
61
|
+
HTMLAttributes: {
|
|
62
|
+
rel: 'noopener noreferrer',
|
|
63
|
+
target: '_blank',
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
Image.configure({
|
|
67
|
+
allowBase64: true,
|
|
68
|
+
HTMLAttributes: {
|
|
69
|
+
loading: 'lazy',
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
Subscript,
|
|
73
|
+
Superscript,
|
|
74
|
+
TaskList,
|
|
75
|
+
TaskItem.configure({ nested: true }),
|
|
76
|
+
Table.configure({
|
|
77
|
+
resizable: true,
|
|
78
|
+
HTMLAttributes: {
|
|
79
|
+
class: 'svadmin-table',
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
TableRow,
|
|
83
|
+
TableHeader,
|
|
84
|
+
TableCell,
|
|
85
|
+
Typography,
|
|
86
|
+
CharacterCount.configure({
|
|
87
|
+
limit: options.maxLength || undefined,
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
if (options.placeholder) {
|
|
92
|
+
extensions.push(
|
|
93
|
+
Placeholder.configure({
|
|
94
|
+
placeholder: options.placeholder,
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (options.codeHighlight && options.lowlight) {
|
|
100
|
+
extensions.push(
|
|
101
|
+
CodeBlockLowlight.configure({
|
|
102
|
+
lowlight: options.lowlight as Parameters<typeof CodeBlockLowlight.configure>[0] extends { lowlight: infer L } ? L : never,
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return extensions;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Minimal preset: basic text formatting only.
|
|
112
|
+
* Suitable for comments, short descriptions, inline editing.
|
|
113
|
+
*/
|
|
114
|
+
export function minimalPreset(options: EditorPresetOptions = {}): AnyExtension[] {
|
|
115
|
+
const extensions: AnyExtension[] = [
|
|
116
|
+
StarterKit.configure({
|
|
117
|
+
heading: { levels: [1, 2, 3] },
|
|
118
|
+
codeBlock: false,
|
|
119
|
+
}),
|
|
120
|
+
Underline,
|
|
121
|
+
Link.configure({
|
|
122
|
+
openOnClick: false,
|
|
123
|
+
autolink: true,
|
|
124
|
+
}),
|
|
125
|
+
CharacterCount.configure({
|
|
126
|
+
limit: options.maxLength || undefined,
|
|
127
|
+
}),
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
if (options.placeholder) {
|
|
131
|
+
extensions.push(
|
|
132
|
+
Placeholder.configure({
|
|
133
|
+
placeholder: options.placeholder,
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return extensions;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Prose preset: optimized for long-form writing.
|
|
143
|
+
* Includes typography, highlights, but no tables.
|
|
144
|
+
*/
|
|
145
|
+
export function prosePreset(options: EditorPresetOptions = {}): AnyExtension[] {
|
|
146
|
+
const extensions: AnyExtension[] = [
|
|
147
|
+
StarterKit.configure({
|
|
148
|
+
codeBlock: options.codeHighlight ? false : undefined,
|
|
149
|
+
}),
|
|
150
|
+
Underline,
|
|
151
|
+
TextStyle,
|
|
152
|
+
Color,
|
|
153
|
+
Highlight.configure({ multicolor: true }),
|
|
154
|
+
TextAlign.configure({
|
|
155
|
+
types: ['heading', 'paragraph'],
|
|
156
|
+
}),
|
|
157
|
+
Link.configure({
|
|
158
|
+
openOnClick: false,
|
|
159
|
+
autolink: true,
|
|
160
|
+
}),
|
|
161
|
+
Image.configure({ allowBase64: true }),
|
|
162
|
+
TaskList,
|
|
163
|
+
TaskItem.configure({ nested: true }),
|
|
164
|
+
Typography,
|
|
165
|
+
CharacterCount.configure({
|
|
166
|
+
limit: options.maxLength || undefined,
|
|
167
|
+
}),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
if (options.placeholder) {
|
|
171
|
+
extensions.push(
|
|
172
|
+
Placeholder.configure({
|
|
173
|
+
placeholder: options.placeholder,
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options.codeHighlight && options.lowlight) {
|
|
179
|
+
extensions.push(
|
|
180
|
+
CodeBlockLowlight.configure({
|
|
181
|
+
lowlight: options.lowlight as Parameters<typeof CodeBlockLowlight.configure>[0] extends { lowlight: infer L } ? L : never,
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return extensions;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export {
|
|
190
|
+
// Re-export individual extensions for custom composition
|
|
191
|
+
StarterKit,
|
|
192
|
+
Underline,
|
|
193
|
+
TextAlign,
|
|
194
|
+
TextStyle,
|
|
195
|
+
Color,
|
|
196
|
+
Highlight,
|
|
197
|
+
Link,
|
|
198
|
+
Image,
|
|
199
|
+
Placeholder,
|
|
200
|
+
CharacterCount,
|
|
201
|
+
Typography,
|
|
202
|
+
Subscript,
|
|
203
|
+
Superscript,
|
|
204
|
+
TaskList,
|
|
205
|
+
TaskItem,
|
|
206
|
+
Table,
|
|
207
|
+
TableRow,
|
|
208
|
+
TableHeader,
|
|
209
|
+
TableCell,
|
|
210
|
+
CodeBlockLowlight,
|
|
211
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash command extension for Tiptap
|
|
3
|
+
*
|
|
4
|
+
* Type "/" on an empty line to open a command menu.
|
|
5
|
+
* Uses @tiptap/suggestion under the hood.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Extension } from '@tiptap/core';
|
|
9
|
+
import Suggestion, { type SuggestionOptions } from '@tiptap/suggestion';
|
|
10
|
+
import type { Editor } from '@tiptap/core';
|
|
11
|
+
import { t } from '@svadmin/core';
|
|
12
|
+
|
|
13
|
+
export interface SlashCommandItem {
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
icon?: string;
|
|
17
|
+
category?: string;
|
|
18
|
+
command: (editor: Editor) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default slash command items
|
|
23
|
+
*/
|
|
24
|
+
export function getDefaultSlashItems(): SlashCommandItem[] {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
title: t('editor.h1'),
|
|
28
|
+
description: 'Large section heading',
|
|
29
|
+
category: 'Headings',
|
|
30
|
+
command: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: t('editor.h2'),
|
|
34
|
+
description: 'Medium section heading',
|
|
35
|
+
category: 'Headings',
|
|
36
|
+
command: (editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: t('editor.h3'),
|
|
40
|
+
description: 'Small section heading',
|
|
41
|
+
category: 'Headings',
|
|
42
|
+
command: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: t('editor.bulletList'),
|
|
46
|
+
description: 'Create a bulleted list',
|
|
47
|
+
category: 'Lists',
|
|
48
|
+
command: (editor) => editor.chain().focus().toggleBulletList().run(),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: t('editor.orderedList'),
|
|
52
|
+
description: 'Create a numbered list',
|
|
53
|
+
category: 'Lists',
|
|
54
|
+
command: (editor) => editor.chain().focus().toggleOrderedList().run(),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: t('editor.taskList'),
|
|
58
|
+
description: 'Create a task checklist',
|
|
59
|
+
category: 'Lists',
|
|
60
|
+
command: (editor) => editor.chain().focus().toggleTaskList().run(),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: t('editor.blockquote'),
|
|
64
|
+
description: 'Insert a blockquote',
|
|
65
|
+
category: 'Blocks',
|
|
66
|
+
command: (editor) => editor.chain().focus().toggleBlockquote().run(),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: t('editor.codeBlock'),
|
|
70
|
+
description: 'Insert a code block',
|
|
71
|
+
category: 'Blocks',
|
|
72
|
+
command: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: 'Divider',
|
|
76
|
+
description: 'Insert a horizontal rule',
|
|
77
|
+
category: 'Blocks',
|
|
78
|
+
command: (editor) => editor.chain().focus().setHorizontalRule().run(),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: t('editor.table'),
|
|
82
|
+
description: 'Insert a 3×3 table',
|
|
83
|
+
category: 'Media',
|
|
84
|
+
command: (editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: 'Image',
|
|
88
|
+
description: 'Insert image from URL',
|
|
89
|
+
category: 'Media',
|
|
90
|
+
command: (editor) => {
|
|
91
|
+
const url = window.prompt('Enter image URL:');
|
|
92
|
+
if (url) {
|
|
93
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SlashCommandOptions {
|
|
101
|
+
items?: SlashCommandItem[];
|
|
102
|
+
/** Custom rendering handlers — follows @tiptap/suggestion render interface */
|
|
103
|
+
render?: SuggestionOptions['render'];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a slash command extension.
|
|
108
|
+
*
|
|
109
|
+
* Usage:
|
|
110
|
+
* ```ts
|
|
111
|
+
* import { createSlashCommand } from '@svadmin/editor/extensions/slash-command';
|
|
112
|
+
* const SlashCommand = createSlashCommand();
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function createSlashCommand(options: SlashCommandOptions = {}): Extension {
|
|
116
|
+
const items = options.items ?? getDefaultSlashItems();
|
|
117
|
+
|
|
118
|
+
return Extension.create({
|
|
119
|
+
name: 'slashCommand',
|
|
120
|
+
|
|
121
|
+
addOptions() {
|
|
122
|
+
return {
|
|
123
|
+
suggestion: {
|
|
124
|
+
char: '/',
|
|
125
|
+
command: ({ editor, range, props }: { editor: Editor; range: { from: number; to: number }; props: SlashCommandItem }) => {
|
|
126
|
+
// Delete the slash and trigger text
|
|
127
|
+
editor.chain().focus().deleteRange(range).run();
|
|
128
|
+
// Execute the command
|
|
129
|
+
props.command(editor);
|
|
130
|
+
},
|
|
131
|
+
items: ({ query }: { query: string }) => {
|
|
132
|
+
return items.filter((item) =>
|
|
133
|
+
item.title.toLowerCase().includes(query.toLowerCase())
|
|
134
|
+
);
|
|
135
|
+
},
|
|
136
|
+
...(options.render ? { render: options.render } : {}),
|
|
137
|
+
} as Partial<SuggestionOptions>,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
addProseMirrorPlugins() {
|
|
142
|
+
return [
|
|
143
|
+
Suggestion({
|
|
144
|
+
editor: this.editor,
|
|
145
|
+
...this.options.suggestion,
|
|
146
|
+
}),
|
|
147
|
+
];
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @svadmin/editor — Modern Tiptap-based rich text editor for Svelte 5
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```svelte
|
|
6
|
+
* <script>
|
|
7
|
+
* import { Editor } from '@svadmin/editor';
|
|
8
|
+
*
|
|
9
|
+
* let content = $state('<p>Hello World</p>');
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* <Editor bind:value={content} placeholder="Start writing..." />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ─── Main component ──────────────────────────────────────────────
|
|
17
|
+
export { default as Editor } from './components/Editor.svelte';
|
|
18
|
+
|
|
19
|
+
// ─── Sub-components (for custom layouts) ─────────────────────────
|
|
20
|
+
export { default as Toolbar } from './components/Toolbar.svelte';
|
|
21
|
+
export { default as ToolbarButton } from './components/ToolbarButton.svelte';
|
|
22
|
+
export { default as ToolbarGroup } from './components/ToolbarGroup.svelte';
|
|
23
|
+
export { default as ToolbarSelect } from './components/ToolbarSelect.svelte';
|
|
24
|
+
export { default as BubbleMenu } from './components/BubbleMenu.svelte';
|
|
25
|
+
export { default as FloatingMenu } from './components/FloatingMenu.svelte';
|
|
26
|
+
export { default as ColorPicker } from './components/ColorPicker.svelte';
|
|
27
|
+
export { default as LinkPopover } from './components/LinkPopover.svelte';
|
|
28
|
+
export { default as TableMenu } from './components/TableMenu.svelte';
|
|
29
|
+
export { default as ImageUpload } from './components/ImageUpload.svelte';
|
|
30
|
+
|
|
31
|
+
// ─── Extension presets ───────────────────────────────────────────
|
|
32
|
+
export {
|
|
33
|
+
fullPreset,
|
|
34
|
+
minimalPreset,
|
|
35
|
+
prosePreset,
|
|
36
|
+
type EditorPresetOptions,
|
|
37
|
+
} from './extensions/index.js';
|
|
38
|
+
|
|
39
|
+
// ─── Individual extensions (re-exports for custom composition) ──
|
|
40
|
+
export {
|
|
41
|
+
StarterKit,
|
|
42
|
+
Underline,
|
|
43
|
+
TextAlign,
|
|
44
|
+
TextStyle,
|
|
45
|
+
Color,
|
|
46
|
+
Highlight,
|
|
47
|
+
Link,
|
|
48
|
+
Image,
|
|
49
|
+
Placeholder,
|
|
50
|
+
CharacterCount,
|
|
51
|
+
Typography,
|
|
52
|
+
Subscript,
|
|
53
|
+
Superscript,
|
|
54
|
+
TaskList,
|
|
55
|
+
TaskItem,
|
|
56
|
+
Table,
|
|
57
|
+
TableRow,
|
|
58
|
+
TableHeader,
|
|
59
|
+
TableCell,
|
|
60
|
+
CodeBlockLowlight,
|
|
61
|
+
} from './extensions/index.js';
|
|
62
|
+
|
|
63
|
+
// ─── Tiptap core re-exports (convenience) ────────────────────────
|
|
64
|
+
export { Editor as TiptapEditor } from '@tiptap/core';
|
|
65
|
+
export type { AnyExtension, Extension, Mark, Node } from '@tiptap/core';
|