neuphlo-editor 2.5.1 → 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.
Files changed (128) hide show
  1. package/dist/Markdown-BAW0cb-U.d.cts +153 -0
  2. package/dist/Markdown-BAW0cb-U.d.ts +153 -0
  3. package/dist/chunk-ABL4D65Z.js +1454 -0
  4. package/dist/chunk-ABL4D65Z.js.map +1 -0
  5. package/dist/chunk-Z2DR2FK3.js +1582 -0
  6. package/dist/chunk-Z2DR2FK3.js.map +1 -0
  7. package/dist/headless/components/editor-bubble-item.d.ts +9 -0
  8. package/dist/headless/components/editor-bubble-item.js +29 -0
  9. package/dist/headless/components/editor-bubble.d.ts +9 -0
  10. package/dist/headless/components/editor-bubble.js +9 -0
  11. package/dist/headless/components/editor-command-item.d.ts +3 -0
  12. package/dist/headless/components/editor-command-item.js +27 -0
  13. package/dist/headless/components/editor-command.d.ts +3 -0
  14. package/dist/headless/components/editor-command.js +136 -0
  15. package/dist/headless/components/editor.d.ts +24 -0
  16. package/dist/headless/components/editor.js +13 -0
  17. package/dist/headless/extensions/AISuggestion/AISuggestion.d.ts +82 -0
  18. package/dist/headless/extensions/AISuggestion/AISuggestion.js +244 -0
  19. package/dist/headless/extensions/AISuggestion/index.d.ts +2 -0
  20. package/dist/headless/extensions/AISuggestion/index.js +1 -0
  21. package/dist/headless/extensions/CodeBlock/CodeBlock.d.ts +1 -0
  22. package/dist/headless/extensions/CodeBlock/CodeBlock.js +6 -0
  23. package/dist/headless/extensions/CodeBlock/index.d.ts +1 -0
  24. package/dist/headless/extensions/CodeBlock/index.js +1 -0
  25. package/dist/headless/extensions/DragHandle/DragHandle.d.ts +9 -0
  26. package/dist/headless/extensions/DragHandle/DragHandle.js +47 -0
  27. package/dist/headless/extensions/DragHandle/index.d.ts +2 -0
  28. package/dist/headless/extensions/DragHandle/index.js +1 -0
  29. package/dist/headless/extensions/Image/Image.d.ts +6 -0
  30. package/dist/headless/extensions/Image/Image.js +197 -0
  31. package/dist/headless/extensions/ImageBlock/ImageBlock.d.ts +23 -0
  32. package/dist/headless/extensions/ImageBlock/ImageBlock.js +242 -0
  33. package/dist/headless/extensions/ImageBlock/index.d.ts +2 -0
  34. package/dist/headless/extensions/ImageBlock/index.js +1 -0
  35. package/dist/headless/extensions/Link/Link.d.ts +2 -0
  36. package/dist/headless/extensions/Link/Link.js +44 -0
  37. package/dist/headless/extensions/Link/index.d.ts +1 -0
  38. package/dist/headless/extensions/Link/index.js +1 -0
  39. package/dist/headless/extensions/Markdown.d.ts +9 -0
  40. package/dist/headless/extensions/Markdown.js +138 -0
  41. package/dist/headless/extensions/MarkdownPaste.d.ts +3 -0
  42. package/dist/headless/extensions/MarkdownPaste.js +168 -0
  43. package/dist/headless/extensions/Mention/index.d.ts +4 -0
  44. package/dist/headless/extensions/Mention/index.js +3 -0
  45. package/dist/headless/extensions/Mention/mention-command.d.ts +8 -0
  46. package/dist/headless/extensions/Mention/mention-command.js +165 -0
  47. package/dist/headless/extensions/Mention/mention-list.d.ts +8 -0
  48. package/dist/headless/extensions/Mention/mention-list.js +150 -0
  49. package/dist/headless/extensions/Mention/mention-node-view.d.ts +2 -0
  50. package/dist/headless/extensions/Mention/mention-node-view.js +92 -0
  51. package/dist/headless/extensions/Mention/mention.d.ts +46 -0
  52. package/dist/headless/extensions/Mention/mention.js +218 -0
  53. package/dist/headless/extensions/Table/index.d.ts +2 -0
  54. package/dist/headless/extensions/Table/index.js +2 -0
  55. package/dist/headless/extensions/VideoBlock/VideoBlock.d.ts +17 -0
  56. package/dist/headless/extensions/VideoBlock/VideoBlock.js +73 -0
  57. package/dist/headless/extensions/VideoBlock/index.d.ts +2 -0
  58. package/dist/headless/extensions/VideoBlock/index.js +1 -0
  59. package/dist/headless/extensions/extension-kit.d.ts +29 -0
  60. package/dist/headless/extensions/extension-kit.js +132 -0
  61. package/dist/headless/extensions/index.d.ts +16 -0
  62. package/dist/headless/extensions/index.js +12 -0
  63. package/dist/headless/extensions/slash-command.d.ts +24 -0
  64. package/dist/headless/extensions/slash-command.js +99 -0
  65. package/dist/headless/index.cjs +143 -2
  66. package/dist/headless/index.cjs.map +1 -1
  67. package/dist/headless/index.d.cts +3 -2
  68. package/dist/headless/index.d.ts +3 -2
  69. package/dist/headless/index.js +5 -1
  70. package/dist/headless/utils/atoms.d.ts +13 -0
  71. package/dist/headless/utils/atoms.js +5 -0
  72. package/dist/headless/utils/store.d.ts +2 -0
  73. package/dist/headless/utils/store.js +4 -0
  74. package/dist/highlight.css +0 -6
  75. package/dist/react/Editor.d.ts +43 -0
  76. package/dist/react/Editor.js +119 -0
  77. package/dist/react/TableOfContents.d.ts +7 -0
  78. package/dist/react/TableOfContents.js +96 -0
  79. package/dist/react/index.cjs +156 -9
  80. package/dist/react/index.cjs.map +1 -1
  81. package/dist/react/index.d.cts +5 -2
  82. package/dist/react/index.d.ts +5 -2
  83. package/dist/react/index.js +14 -4
  84. package/dist/react/index.js.map +1 -1
  85. package/dist/react/menus/DragHandle/BlockActionMenu.d.ts +7 -0
  86. package/dist/react/menus/DragHandle/BlockActionMenu.js +111 -0
  87. package/dist/react/menus/DragHandle/DragHandleMenu.d.ts +7 -0
  88. package/dist/react/menus/DragHandle/DragHandleMenu.js +97 -0
  89. package/dist/react/menus/ImageBlock/ImageBlockLoading.d.ts +2 -0
  90. package/dist/react/menus/ImageBlock/ImageBlockLoading.js +6 -0
  91. package/dist/react/menus/ImageBlock/ImageBlockMenu.d.ts +8 -0
  92. package/dist/react/menus/ImageBlock/ImageBlockMenu.js +75 -0
  93. package/dist/react/menus/ImageBlock/ImageBlockView.d.ts +10 -0
  94. package/dist/react/menus/ImageBlock/ImageBlockView.js +60 -0
  95. package/dist/react/menus/ImageBlock/ImageBlockWidth.d.ts +6 -0
  96. package/dist/react/menus/ImageBlock/ImageBlockWidth.js +29 -0
  97. package/dist/react/menus/ImageBlock/ImageResizeHandle.d.ts +8 -0
  98. package/dist/react/menus/ImageBlock/ImageResizeHandle.js +37 -0
  99. package/dist/react/menus/ImageBlock/ImageUploader.d.ts +7 -0
  100. package/dist/react/menus/ImageBlock/ImageUploader.js +73 -0
  101. package/dist/react/menus/ImageBlock/index.d.ts +4 -0
  102. package/dist/react/menus/ImageBlock/index.js +4 -0
  103. package/dist/react/menus/ImageMenu.d.ts +10 -0
  104. package/dist/react/menus/ImageMenu.js +77 -0
  105. package/dist/react/menus/ImagePopover.d.ts +8 -0
  106. package/dist/react/menus/ImagePopover.js +146 -0
  107. package/dist/react/menus/LinkMenu.d.ts +2 -0
  108. package/dist/react/menus/LinkMenu.js +69 -0
  109. package/dist/react/menus/MenuList.d.ts +10 -0
  110. package/dist/react/menus/MenuList.js +94 -0
  111. package/dist/react/menus/SlashMenu.d.ts +5 -0
  112. package/dist/react/menus/SlashMenu.js +156 -0
  113. package/dist/react/menus/TableMenu.d.ts +5 -0
  114. package/dist/react/menus/TableMenu.js +462 -0
  115. package/dist/react/menus/TaskItem/TaskItemView.d.ts +12 -0
  116. package/dist/react/menus/TaskItem/TaskItemView.js +11 -0
  117. package/dist/react/menus/TextMenu.d.ts +10 -0
  118. package/dist/react/menus/TextMenu.js +139 -0
  119. package/dist/react/menus/VideoBlock/VideoBlockMenu.d.ts +7 -0
  120. package/dist/react/menus/VideoBlock/VideoBlockMenu.js +72 -0
  121. package/dist/react/menus/VideoBlock/VideoBlockView.d.ts +10 -0
  122. package/dist/react/menus/VideoBlock/VideoBlockView.js +87 -0
  123. package/dist/react/menus/VideoBlock/index.d.ts +2 -0
  124. package/dist/react/menus/VideoBlock/index.js +2 -0
  125. package/dist/react/menus/index.d.ts +15 -0
  126. package/dist/react/menus/index.js +9 -0
  127. package/package.json +13 -9
  128. package/LICENSE +0 -21
@@ -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,2 @@
1
+ export { AISuggestion, AISuggestionPluginKey } from "./AISuggestion";
2
+ export type { AISuggestionOptions } from "./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,6 @@
1
+ import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
2
+ import { all, createLowlight } from "lowlight";
3
+ const lowlight = createLowlight(all);
4
+ export const CodeBlock = CodeBlockLowlight.configure({
5
+ lowlight,
6
+ });
@@ -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,2 @@
1
+ export { DragHandle, setDragHandleCallbacks } from "./DragHandle";
2
+ export type { DragHandleCallbacks } from "./DragHandle";
@@ -0,0 +1 @@
1
+ export { DragHandle, setDragHandleCallbacks } from "./DragHandle";
@@ -0,0 +1,6 @@
1
+ export interface ImageOptions {
2
+ uploadImage?: (file: File) => Promise<string>;
3
+ allowBase64?: boolean;
4
+ }
5
+ export declare const Image: import("@tiptap/core").Node<ImageOptions, any>;
6
+ export default Image;
@@ -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;