neuphlo-editor 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Markdown-BAW0cb-U.d.cts +153 -0
- package/dist/Markdown-BAW0cb-U.d.ts +153 -0
- package/dist/{chunk-4L7FHW5M.js → chunk-ABL4D65Z.js} +113 -47
- package/dist/chunk-ABL4D65Z.js.map +1 -0
- package/dist/{chunk-BVFTQKY7.js → chunk-Z2DR2FK3.js} +245 -50
- package/dist/chunk-Z2DR2FK3.js.map +1 -0
- package/dist/headless/components/editor-bubble-item.d.ts +9 -0
- package/dist/headless/components/editor-bubble-item.js +29 -0
- package/dist/headless/components/editor-bubble.d.ts +9 -0
- package/dist/headless/components/editor-bubble.js +9 -0
- package/dist/headless/components/editor-command-item.d.ts +3 -0
- package/dist/headless/components/editor-command-item.js +27 -0
- package/dist/headless/components/editor-command.d.ts +3 -0
- package/dist/headless/components/editor-command.js +136 -0
- package/dist/headless/components/editor.d.ts +24 -0
- package/dist/headless/components/editor.js +13 -0
- package/dist/headless/extensions/AISuggestion/AISuggestion.d.ts +82 -0
- package/dist/headless/extensions/AISuggestion/AISuggestion.js +244 -0
- package/dist/headless/extensions/AISuggestion/index.d.ts +2 -0
- package/dist/headless/extensions/AISuggestion/index.js +1 -0
- package/dist/headless/extensions/CodeBlock/CodeBlock.d.ts +1 -0
- package/dist/headless/extensions/CodeBlock/CodeBlock.js +6 -0
- package/dist/headless/extensions/CodeBlock/index.d.ts +1 -0
- package/dist/headless/extensions/CodeBlock/index.js +1 -0
- package/dist/headless/extensions/DragHandle/DragHandle.d.ts +9 -0
- package/dist/headless/extensions/DragHandle/DragHandle.js +47 -0
- package/dist/headless/extensions/DragHandle/index.d.ts +2 -0
- package/dist/headless/extensions/DragHandle/index.js +1 -0
- package/dist/headless/extensions/Image/Image.d.ts +6 -0
- package/dist/headless/extensions/Image/Image.js +197 -0
- package/dist/headless/extensions/ImageBlock/ImageBlock.d.ts +23 -0
- package/dist/headless/extensions/ImageBlock/ImageBlock.js +242 -0
- package/dist/headless/extensions/ImageBlock/index.d.ts +2 -0
- package/dist/headless/extensions/ImageBlock/index.js +1 -0
- package/dist/headless/extensions/Link/Link.d.ts +2 -0
- package/dist/headless/extensions/Link/Link.js +44 -0
- package/dist/headless/extensions/Link/index.d.ts +1 -0
- package/dist/headless/extensions/Link/index.js +1 -0
- package/dist/headless/extensions/Markdown.d.ts +9 -0
- package/dist/headless/extensions/Markdown.js +138 -0
- package/dist/headless/extensions/MarkdownPaste.d.ts +3 -0
- package/dist/headless/extensions/MarkdownPaste.js +168 -0
- package/dist/headless/extensions/Mention/index.d.ts +4 -0
- package/dist/headless/extensions/Mention/index.js +3 -0
- package/dist/headless/extensions/Mention/mention-command.d.ts +8 -0
- package/dist/headless/extensions/Mention/mention-command.js +165 -0
- package/dist/headless/extensions/Mention/mention-list.d.ts +8 -0
- package/dist/headless/extensions/Mention/mention-list.js +150 -0
- package/dist/headless/extensions/Mention/mention-node-view.d.ts +2 -0
- package/dist/headless/extensions/Mention/mention-node-view.js +92 -0
- package/dist/headless/extensions/Mention/mention.d.ts +46 -0
- package/dist/headless/extensions/Mention/mention.js +218 -0
- package/dist/headless/extensions/Table/index.d.ts +2 -0
- package/dist/headless/extensions/Table/index.js +2 -0
- package/dist/headless/extensions/VideoBlock/VideoBlock.d.ts +17 -0
- package/dist/headless/extensions/VideoBlock/VideoBlock.js +73 -0
- package/dist/headless/extensions/VideoBlock/index.d.ts +2 -0
- package/dist/headless/extensions/VideoBlock/index.js +1 -0
- package/dist/headless/extensions/extension-kit.d.ts +29 -0
- package/dist/headless/extensions/extension-kit.js +132 -0
- package/dist/headless/extensions/index.d.ts +16 -0
- package/dist/headless/extensions/index.js +12 -0
- package/dist/headless/extensions/slash-command.d.ts +24 -0
- package/dist/headless/extensions/slash-command.js +99 -0
- package/dist/headless/index.cjs +143 -2
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +3 -2
- package/dist/headless/index.d.ts +3 -2
- package/dist/headless/index.js +5 -1
- package/dist/headless/utils/atoms.d.ts +13 -0
- package/dist/headless/utils/atoms.js +5 -0
- package/dist/headless/utils/store.d.ts +2 -0
- package/dist/headless/utils/store.js +4 -0
- package/dist/highlight.css +77 -1
- package/dist/react/Editor.d.ts +43 -0
- package/dist/react/Editor.js +119 -0
- package/dist/react/TableOfContents.d.ts +7 -0
- package/dist/react/TableOfContents.js +96 -0
- package/dist/react/index.cjs +156 -9
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +5 -2
- package/dist/react/index.d.ts +5 -2
- package/dist/react/index.js +14 -4
- package/dist/react/index.js.map +1 -1
- package/dist/react/menus/DragHandle/BlockActionMenu.d.ts +7 -0
- package/dist/react/menus/DragHandle/BlockActionMenu.js +111 -0
- package/dist/react/menus/DragHandle/DragHandleMenu.d.ts +7 -0
- package/dist/react/menus/DragHandle/DragHandleMenu.js +97 -0
- package/dist/react/menus/ImageBlock/ImageBlockLoading.d.ts +2 -0
- package/dist/react/menus/ImageBlock/ImageBlockLoading.js +6 -0
- package/dist/react/menus/ImageBlock/ImageBlockMenu.d.ts +8 -0
- package/dist/react/menus/ImageBlock/ImageBlockMenu.js +75 -0
- package/dist/react/menus/ImageBlock/ImageBlockView.d.ts +10 -0
- package/dist/react/menus/ImageBlock/ImageBlockView.js +60 -0
- package/dist/react/menus/ImageBlock/ImageBlockWidth.d.ts +6 -0
- package/dist/react/menus/ImageBlock/ImageBlockWidth.js +29 -0
- package/dist/react/menus/ImageBlock/ImageResizeHandle.d.ts +8 -0
- package/dist/react/menus/ImageBlock/ImageResizeHandle.js +37 -0
- package/dist/react/menus/ImageBlock/ImageUploader.d.ts +7 -0
- package/dist/react/menus/ImageBlock/ImageUploader.js +73 -0
- package/dist/react/menus/ImageBlock/index.d.ts +4 -0
- package/dist/react/menus/ImageBlock/index.js +4 -0
- package/dist/react/menus/ImageMenu.d.ts +10 -0
- package/dist/react/menus/ImageMenu.js +77 -0
- package/dist/react/menus/ImagePopover.d.ts +8 -0
- package/dist/react/menus/ImagePopover.js +146 -0
- package/dist/react/menus/LinkMenu.d.ts +2 -0
- package/dist/react/menus/LinkMenu.js +69 -0
- package/dist/react/menus/MenuList.d.ts +10 -0
- package/dist/react/menus/MenuList.js +94 -0
- package/dist/react/menus/SlashMenu.d.ts +5 -0
- package/dist/react/menus/SlashMenu.js +156 -0
- package/dist/react/menus/TableMenu.d.ts +5 -0
- package/dist/react/menus/TableMenu.js +462 -0
- package/dist/react/menus/TaskItem/TaskItemView.d.ts +12 -0
- package/dist/react/menus/TaskItem/TaskItemView.js +11 -0
- package/dist/react/menus/TextMenu.d.ts +10 -0
- package/dist/react/menus/TextMenu.js +139 -0
- package/dist/react/menus/VideoBlock/VideoBlockMenu.d.ts +7 -0
- package/dist/react/menus/VideoBlock/VideoBlockMenu.js +72 -0
- package/dist/react/menus/VideoBlock/VideoBlockView.d.ts +10 -0
- package/dist/react/menus/VideoBlock/VideoBlockView.js +87 -0
- package/dist/react/menus/VideoBlock/index.d.ts +2 -0
- package/dist/react/menus/VideoBlock/index.js +2 -0
- package/dist/react/menus/index.d.ts +15 -0
- package/dist/react/menus/index.js +9 -0
- package/package.json +3 -1
- package/dist/chunk-2DWEJI45.js +0 -1296
- package/dist/chunk-2DWEJI45.js.map +0 -1
- package/dist/chunk-3CLWAYRP.js +0 -1387
- package/dist/chunk-3CLWAYRP.js.map +0 -1
- package/dist/chunk-457ETWB6.js +0 -1351
- package/dist/chunk-457ETWB6.js.map +0 -1
- package/dist/chunk-4L7FHW5M.js.map +0 -1
- package/dist/chunk-4MV3UUZN.js +0 -1073
- package/dist/chunk-4MV3UUZN.js.map +0 -1
- package/dist/chunk-62DYB7FY.js +0 -1305
- package/dist/chunk-62DYB7FY.js.map +0 -1
- package/dist/chunk-BVFTQKY7.js.map +0 -1
- package/dist/chunk-DWGPGRTQ.js +0 -1302
- package/dist/chunk-DWGPGRTQ.js.map +0 -1
- package/dist/chunk-EG7NQJRA.js +0 -1324
- package/dist/chunk-EG7NQJRA.js.map +0 -1
- package/dist/chunk-FLLPFFI5.js +0 -1296
- package/dist/chunk-FLLPFFI5.js.map +0 -1
- package/dist/chunk-FVQHB6VC.js +0 -1128
- package/dist/chunk-FVQHB6VC.js.map +0 -1
- package/dist/chunk-G5E32MLB.js +0 -1352
- package/dist/chunk-G5E32MLB.js.map +0 -1
- package/dist/chunk-GXJGZHKR.js +0 -1326
- package/dist/chunk-GXJGZHKR.js.map +0 -1
- package/dist/chunk-KCPPTLGY.js +0 -1299
- package/dist/chunk-KCPPTLGY.js.map +0 -1
- package/dist/chunk-LHG2NX6C.js +0 -1123
- package/dist/chunk-LHG2NX6C.js.map +0 -1
- package/dist/chunk-MSNTVITF.js +0 -1352
- package/dist/chunk-MSNTVITF.js.map +0 -1
- package/dist/chunk-N4RDYJ5Z.js +0 -1351
- package/dist/chunk-N4RDYJ5Z.js.map +0 -1
- package/dist/chunk-OCNM37WJ.js +0 -1289
- package/dist/chunk-OCNM37WJ.js.map +0 -1
- package/dist/chunk-P3YFYEUB.js +0 -1351
- package/dist/chunk-P3YFYEUB.js.map +0 -1
- package/dist/chunk-RW6QBMJB.js +0 -1300
- package/dist/chunk-RW6QBMJB.js.map +0 -1
- package/dist/chunk-SJQQVB3M.js +0 -1353
- package/dist/chunk-SJQQVB3M.js.map +0 -1
- package/dist/chunk-SOXTEP7H.js +0 -6705
- package/dist/chunk-SOXTEP7H.js.map +0 -1
- package/dist/chunk-VPI26I4P.js +0 -1350
- package/dist/chunk-VPI26I4P.js.map +0 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TipTap AI Suggestion Extension
|
|
3
|
+
*
|
|
4
|
+
* This extension provides inline AI-powered text completions that appear
|
|
5
|
+
* as ghost text at the cursor position.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { AISuggestion } from 'neuphlo-editor'
|
|
10
|
+
*
|
|
11
|
+
* const editor = new Editor({
|
|
12
|
+
* extensions: [
|
|
13
|
+
* AISuggestion.configure({
|
|
14
|
+
* onFetchSuggestion: async (context) => {
|
|
15
|
+
* // Call your AI API here
|
|
16
|
+
* const response = await fetch('/api/complete', {
|
|
17
|
+
* method: 'POST',
|
|
18
|
+
* body: JSON.stringify({ text: context }),
|
|
19
|
+
* })
|
|
20
|
+
* const data = await response.json()
|
|
21
|
+
* return data.completion
|
|
22
|
+
* },
|
|
23
|
+
* debounceMs: 2000,
|
|
24
|
+
* }),
|
|
25
|
+
* ],
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
import { Extension } from "@tiptap/core";
|
|
30
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
31
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
32
|
+
export const AISuggestionPluginKey = new PluginKey("aiSuggestion");
|
|
33
|
+
export const AISuggestion = Extension.create({
|
|
34
|
+
name: "aiSuggestion",
|
|
35
|
+
addOptions() {
|
|
36
|
+
return {
|
|
37
|
+
onFetchSuggestion: async () => null,
|
|
38
|
+
debounceMs: 2000,
|
|
39
|
+
minChars: 10,
|
|
40
|
+
suggestionClass: "ai-suggestion",
|
|
41
|
+
enabled: true,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
addCommands() {
|
|
45
|
+
return {
|
|
46
|
+
acceptAISuggestion: () => ({ editor, tr, dispatch }) => {
|
|
47
|
+
const pluginState = AISuggestionPluginKey.getState(editor.state);
|
|
48
|
+
if (!pluginState?.suggestion)
|
|
49
|
+
return false;
|
|
50
|
+
if (dispatch) {
|
|
51
|
+
// Insert the suggestion text at cursor
|
|
52
|
+
const { from } = editor.state.selection;
|
|
53
|
+
tr.insertText(pluginState.suggestion, from);
|
|
54
|
+
// Clear the suggestion
|
|
55
|
+
tr.setMeta(AISuggestionPluginKey, { suggestion: null });
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
dismissAISuggestion: () => ({ tr, dispatch }) => {
|
|
60
|
+
if (dispatch) {
|
|
61
|
+
tr.setMeta(AISuggestionPluginKey, { suggestion: null });
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
triggerAISuggestion: () => ({ editor }) => {
|
|
66
|
+
const pluginState = AISuggestionPluginKey.getState(editor.state);
|
|
67
|
+
if (pluginState?.fetchSuggestion) {
|
|
68
|
+
pluginState.fetchSuggestion();
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
setAISuggestion: (text) => ({ tr, dispatch }) => {
|
|
73
|
+
if (dispatch) {
|
|
74
|
+
tr.setMeta(AISuggestionPluginKey, { suggestion: text });
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
addKeyboardShortcuts() {
|
|
81
|
+
return {
|
|
82
|
+
Tab: ({ editor }) => {
|
|
83
|
+
const pluginState = AISuggestionPluginKey.getState(editor.state);
|
|
84
|
+
if (pluginState?.suggestion) {
|
|
85
|
+
return editor.commands.acceptAISuggestion();
|
|
86
|
+
}
|
|
87
|
+
return false; // Let other Tab handlers run
|
|
88
|
+
},
|
|
89
|
+
Escape: ({ editor }) => {
|
|
90
|
+
const pluginState = AISuggestionPluginKey.getState(editor.state);
|
|
91
|
+
if (pluginState?.suggestion) {
|
|
92
|
+
return editor.commands.dismissAISuggestion();
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
addProseMirrorPlugins() {
|
|
99
|
+
const { options } = this;
|
|
100
|
+
return [
|
|
101
|
+
new Plugin({
|
|
102
|
+
key: AISuggestionPluginKey,
|
|
103
|
+
state: {
|
|
104
|
+
init() {
|
|
105
|
+
return {
|
|
106
|
+
suggestion: null,
|
|
107
|
+
isLoading: false,
|
|
108
|
+
fetchSuggestion: null,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
apply(tr, value) {
|
|
112
|
+
const meta = tr.getMeta(AISuggestionPluginKey);
|
|
113
|
+
if (meta !== undefined) {
|
|
114
|
+
return { ...value, ...meta };
|
|
115
|
+
}
|
|
116
|
+
// Clear suggestion if document changed (user is typing)
|
|
117
|
+
if (tr.docChanged && value.suggestion) {
|
|
118
|
+
return { ...value, suggestion: null };
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
view(editorView) {
|
|
124
|
+
let debounceTimer = null;
|
|
125
|
+
let lastContent = "";
|
|
126
|
+
const fetchSuggestion = async () => {
|
|
127
|
+
if (!options.enabled)
|
|
128
|
+
return;
|
|
129
|
+
const { state } = editorView;
|
|
130
|
+
const { from } = state.selection;
|
|
131
|
+
// Get text before cursor
|
|
132
|
+
const textBefore = state.doc.textBetween(Math.max(0, from - 500), from, "\n");
|
|
133
|
+
if (textBefore.trim().length < (options.minChars ?? 10)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Set loading state
|
|
137
|
+
editorView.dispatch(state.tr.setMeta(AISuggestionPluginKey, { isLoading: true }));
|
|
138
|
+
try {
|
|
139
|
+
const suggestion = await options.onFetchSuggestion(textBefore);
|
|
140
|
+
if (suggestion && suggestion.trim()) {
|
|
141
|
+
// Add leading space if needed
|
|
142
|
+
let finalSuggestion = suggestion.trim();
|
|
143
|
+
if (!finalSuggestion.startsWith(" ") &&
|
|
144
|
+
!textBefore.endsWith(" ")) {
|
|
145
|
+
finalSuggestion = " " + finalSuggestion;
|
|
146
|
+
}
|
|
147
|
+
editorView.dispatch(editorView.state.tr.setMeta(AISuggestionPluginKey, {
|
|
148
|
+
suggestion: finalSuggestion,
|
|
149
|
+
isLoading: false,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
editorView.dispatch(editorView.state.tr.setMeta(AISuggestionPluginKey, {
|
|
154
|
+
suggestion: null,
|
|
155
|
+
isLoading: false,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error("[AISuggestion] Error fetching suggestion:", error);
|
|
161
|
+
editorView.dispatch(editorView.state.tr.setMeta(AISuggestionPluginKey, {
|
|
162
|
+
suggestion: null,
|
|
163
|
+
isLoading: false,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Store fetchSuggestion reference in plugin state
|
|
168
|
+
editorView.dispatch(editorView.state.tr.setMeta(AISuggestionPluginKey, {
|
|
169
|
+
fetchSuggestion,
|
|
170
|
+
}));
|
|
171
|
+
return {
|
|
172
|
+
update(view, prevState) {
|
|
173
|
+
if (!options.enabled)
|
|
174
|
+
return;
|
|
175
|
+
const currentContent = view.state.doc.textContent;
|
|
176
|
+
// Only react to content changes
|
|
177
|
+
if (currentContent !== lastContent) {
|
|
178
|
+
lastContent = currentContent;
|
|
179
|
+
// Clear existing timer
|
|
180
|
+
if (debounceTimer) {
|
|
181
|
+
clearTimeout(debounceTimer);
|
|
182
|
+
}
|
|
183
|
+
// Set new timer
|
|
184
|
+
debounceTimer = setTimeout(() => {
|
|
185
|
+
fetchSuggestion();
|
|
186
|
+
}, options.debounceMs ?? 2000);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
destroy() {
|
|
190
|
+
if (debounceTimer) {
|
|
191
|
+
clearTimeout(debounceTimer);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
props: {
|
|
197
|
+
decorations(state) {
|
|
198
|
+
const pluginState = AISuggestionPluginKey.getState(state);
|
|
199
|
+
if (!pluginState)
|
|
200
|
+
return DecorationSet.empty;
|
|
201
|
+
const { suggestion, isLoading } = pluginState;
|
|
202
|
+
const { from } = state.selection;
|
|
203
|
+
const decorations = [];
|
|
204
|
+
if (isLoading) {
|
|
205
|
+
// Show loading indicator
|
|
206
|
+
const widget = document.createElement("span");
|
|
207
|
+
widget.className = `${options.suggestionClass}-loading`;
|
|
208
|
+
widget.innerHTML = `<span class="ai-suggestion-spinner"></span>`;
|
|
209
|
+
widget.style.cssText = "opacity: 0.4; pointer-events: none;";
|
|
210
|
+
decorations.push(Decoration.widget(from, widget, { side: 1 }));
|
|
211
|
+
}
|
|
212
|
+
else if (suggestion) {
|
|
213
|
+
// Show ghost text
|
|
214
|
+
const widget = document.createElement("span");
|
|
215
|
+
widget.className = options.suggestionClass ?? "ai-suggestion";
|
|
216
|
+
widget.textContent = suggestion;
|
|
217
|
+
widget.style.cssText = `
|
|
218
|
+
opacity: 0.4;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
color: var(--muted-foreground, #888);
|
|
221
|
+
`;
|
|
222
|
+
// Add Tab hint
|
|
223
|
+
const hint = document.createElement("kbd");
|
|
224
|
+
hint.textContent = "Tab";
|
|
225
|
+
hint.style.cssText = `
|
|
226
|
+
margin-left: 8px;
|
|
227
|
+
font-size: 10px;
|
|
228
|
+
opacity: 0.3;
|
|
229
|
+
background: var(--muted, #f1f1f1);
|
|
230
|
+
padding: 1px 4px;
|
|
231
|
+
border-radius: 3px;
|
|
232
|
+
font-family: system-ui, sans-serif;
|
|
233
|
+
`;
|
|
234
|
+
widget.appendChild(hint);
|
|
235
|
+
decorations.push(Decoration.widget(from, widget, { side: 1 }));
|
|
236
|
+
}
|
|
237
|
+
return DecorationSet.create(state.doc, decorations);
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
];
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
export default AISuggestion;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AISuggestion, AISuggestionPluginKey } from "./AISuggestion";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CodeBlock: import("@tiptap/core").Node<import("@tiptap/extension-code-block-lowlight").CodeBlockLowlightOptions, any>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CodeBlock";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CodeBlock";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Node } from "@tiptap/pm/model";
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
export interface DragHandleCallbacks {
|
|
4
|
+
onAddBlock?: (editor: Editor, node: Node | null) => void;
|
|
5
|
+
onGripClick?: (editor: Editor, node: Node | null, element: HTMLElement) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function setDragHandleCallbacks(callbacks: DragHandleCallbacks): void;
|
|
8
|
+
export declare const DragHandle: import("@tiptap/core").Extension<import("@tiptap/extension-drag-handle").DragHandleOptions, any>;
|
|
9
|
+
export default DragHandle;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import BaseDragHandle from "@tiptap/extension-drag-handle";
|
|
2
|
+
let currentCallbacks = {};
|
|
3
|
+
let currentNode = null;
|
|
4
|
+
let currentEditor = null;
|
|
5
|
+
export function setDragHandleCallbacks(callbacks) {
|
|
6
|
+
currentCallbacks = callbacks;
|
|
7
|
+
}
|
|
8
|
+
function createDragHandleElement() {
|
|
9
|
+
const container = document.createElement("div");
|
|
10
|
+
container.className = "nph-drag-handle";
|
|
11
|
+
const plusBtn = document.createElement("button");
|
|
12
|
+
plusBtn.className = "nph-drag-handle__btn";
|
|
13
|
+
plusBtn.type = "button";
|
|
14
|
+
plusBtn.setAttribute("aria-label", "Add block");
|
|
15
|
+
plusBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>`;
|
|
16
|
+
plusBtn.addEventListener("click", (e) => {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
if (currentEditor) {
|
|
20
|
+
currentCallbacks.onAddBlock?.(currentEditor, currentNode);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const gripBtn = document.createElement("button");
|
|
24
|
+
gripBtn.className = "nph-drag-handle__btn nph-drag-handle__grip";
|
|
25
|
+
gripBtn.type = "button";
|
|
26
|
+
gripBtn.setAttribute("aria-label", "Drag to reorder");
|
|
27
|
+
gripBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
|
28
|
+
gripBtn.addEventListener("click", (e) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
e.stopPropagation();
|
|
31
|
+
if (currentEditor) {
|
|
32
|
+
currentCallbacks.onGripClick?.(currentEditor, currentNode, container);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
container.appendChild(plusBtn);
|
|
36
|
+
container.appendChild(gripBtn);
|
|
37
|
+
return container;
|
|
38
|
+
}
|
|
39
|
+
export const DragHandle = BaseDragHandle.configure({
|
|
40
|
+
render: createDragHandleElement,
|
|
41
|
+
nested: true,
|
|
42
|
+
onNodeChange: ({ node, editor }) => {
|
|
43
|
+
currentNode = node;
|
|
44
|
+
currentEditor = editor;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
export default DragHandle;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DragHandle, setDragHandleCallbacks } from "./DragHandle";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Image as TiptapImage } from "@tiptap/extension-image";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
export const Image = TiptapImage.extend({
|
|
4
|
+
addOptions() {
|
|
5
|
+
return {
|
|
6
|
+
...this.parent?.(),
|
|
7
|
+
uploadImage: undefined,
|
|
8
|
+
allowBase64: false,
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
addAttributes() {
|
|
12
|
+
return {
|
|
13
|
+
...this.parent?.(),
|
|
14
|
+
src: {
|
|
15
|
+
default: null,
|
|
16
|
+
},
|
|
17
|
+
alt: {
|
|
18
|
+
default: null,
|
|
19
|
+
},
|
|
20
|
+
title: {
|
|
21
|
+
default: null,
|
|
22
|
+
},
|
|
23
|
+
width: {
|
|
24
|
+
default: null,
|
|
25
|
+
parseHTML: (element) => element.getAttribute("width"),
|
|
26
|
+
renderHTML: (attributes) => {
|
|
27
|
+
if (!attributes.width) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
width: attributes.width,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
height: {
|
|
36
|
+
default: null,
|
|
37
|
+
parseHTML: (element) => element.getAttribute("height"),
|
|
38
|
+
renderHTML: (attributes) => {
|
|
39
|
+
if (!attributes.height) {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
height: attributes.height,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
align: {
|
|
48
|
+
default: null,
|
|
49
|
+
parseHTML: (element) => element.getAttribute("data-align"),
|
|
50
|
+
renderHTML: (attributes) => {
|
|
51
|
+
if (!attributes.align) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
"data-align": attributes.align,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
addCommands() {
|
|
62
|
+
return {
|
|
63
|
+
...this.parent?.(),
|
|
64
|
+
setImage: (options) => ({ commands }) => {
|
|
65
|
+
return commands.insertContent({
|
|
66
|
+
type: this.name,
|
|
67
|
+
attrs: options,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
uploadImage: () => ({ editor }) => {
|
|
71
|
+
const input = document.createElement("input");
|
|
72
|
+
input.type = "file";
|
|
73
|
+
input.accept = "image/*";
|
|
74
|
+
input.onchange = async (e) => {
|
|
75
|
+
const file = e.target.files?.[0];
|
|
76
|
+
if (!file)
|
|
77
|
+
return;
|
|
78
|
+
// Check if uploadImage handler is provided
|
|
79
|
+
if (this.options.uploadImage) {
|
|
80
|
+
try {
|
|
81
|
+
const url = await this.options.uploadImage(file);
|
|
82
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error("Failed to upload image:", error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (this.options.allowBase64) {
|
|
89
|
+
// Fallback to base64 if no upload handler provided
|
|
90
|
+
const reader = new FileReader();
|
|
91
|
+
reader.onload = () => {
|
|
92
|
+
const url = reader.result;
|
|
93
|
+
editor.chain().focus().setImage({ src: url }).run();
|
|
94
|
+
};
|
|
95
|
+
reader.readAsDataURL(file);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.warn("No upload handler provided. Please provide uploadImage option or enable allowBase64.");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
input.click();
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
addProseMirrorPlugins() {
|
|
107
|
+
return [
|
|
108
|
+
new Plugin({
|
|
109
|
+
key: new PluginKey("imageDrop"),
|
|
110
|
+
props: {
|
|
111
|
+
handleDOMEvents: {
|
|
112
|
+
drop: (view, event) => {
|
|
113
|
+
const hasFiles = event.dataTransfer &&
|
|
114
|
+
event.dataTransfer.files &&
|
|
115
|
+
event.dataTransfer.files.length;
|
|
116
|
+
if (!hasFiles) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const images = Array.from(event.dataTransfer.files).filter((file) => /image/i.test(file.type));
|
|
120
|
+
if (images.length === 0) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
const { schema } = view.state;
|
|
125
|
+
const coordinates = view.posAtCoords({
|
|
126
|
+
left: event.clientX,
|
|
127
|
+
top: event.clientY,
|
|
128
|
+
});
|
|
129
|
+
if (!coordinates)
|
|
130
|
+
return false;
|
|
131
|
+
images.forEach(async (image) => {
|
|
132
|
+
if (this.options.uploadImage) {
|
|
133
|
+
try {
|
|
134
|
+
const url = await this.options.uploadImage(image);
|
|
135
|
+
const node = schema.nodes.image.create({ src: url });
|
|
136
|
+
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
137
|
+
view.dispatch(transaction);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error("Failed to upload image:", error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (this.options.allowBase64) {
|
|
144
|
+
const reader = new FileReader();
|
|
145
|
+
reader.onload = (readerEvent) => {
|
|
146
|
+
const node = schema.nodes.image.create({
|
|
147
|
+
src: readerEvent.target?.result,
|
|
148
|
+
});
|
|
149
|
+
const transaction = view.state.tr.insert(coordinates.pos, node);
|
|
150
|
+
view.dispatch(transaction);
|
|
151
|
+
};
|
|
152
|
+
reader.readAsDataURL(image);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
},
|
|
157
|
+
paste: (view, event) => {
|
|
158
|
+
const hasFiles = event.clipboardData &&
|
|
159
|
+
event.clipboardData.files &&
|
|
160
|
+
event.clipboardData.files.length;
|
|
161
|
+
if (!hasFiles) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const images = Array.from(event.clipboardData.files).filter((file) => /image/i.test(file.type));
|
|
165
|
+
if (images.length === 0) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
images.forEach(async (image) => {
|
|
170
|
+
if (this.options.uploadImage) {
|
|
171
|
+
try {
|
|
172
|
+
const url = await this.options.uploadImage(image);
|
|
173
|
+
view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.image.create({ src: url })));
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
console.error("Failed to upload image:", error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (this.options.allowBase64) {
|
|
180
|
+
const reader = new FileReader();
|
|
181
|
+
reader.onload = (readerEvent) => {
|
|
182
|
+
view.dispatch(view.state.tr.replaceSelectionWith(view.state.schema.nodes.image.create({
|
|
183
|
+
src: readerEvent.target?.result,
|
|
184
|
+
})));
|
|
185
|
+
};
|
|
186
|
+
reader.readAsDataURL(image);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return true;
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
];
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
export default Image;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Range } from "@tiptap/core";
|
|
2
|
+
export interface ImageBlockOptions {
|
|
3
|
+
uploadImage?: (file: File) => Promise<string>;
|
|
4
|
+
browseAssets?: (onSelect: (url: string) => void) => void;
|
|
5
|
+
nodeView?: any;
|
|
6
|
+
}
|
|
7
|
+
declare module "@tiptap/core" {
|
|
8
|
+
interface Commands<ReturnType> {
|
|
9
|
+
imageBlock: {
|
|
10
|
+
setImageBlock: (attributes: {
|
|
11
|
+
src: string;
|
|
12
|
+
}) => ReturnType;
|
|
13
|
+
setImageBlockAt: (attributes: {
|
|
14
|
+
src: string;
|
|
15
|
+
pos: number | Range;
|
|
16
|
+
}) => ReturnType;
|
|
17
|
+
setImageBlockAlign: (align: "left" | "center" | "right") => ReturnType;
|
|
18
|
+
setImageBlockWidth: (width: number) => ReturnType;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export declare const ImageBlock: import("@tiptap/core").Node<ImageBlockOptions, any>;
|
|
23
|
+
export default ImageBlock;
|