@firecms/core 3.1.0 → 3.2.0-canary.4c3b8f2

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 (191) hide show
  1. package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
  2. package/dist/components/ErrorBoundary.d.ts +3 -1
  3. package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
  4. package/dist/components/LanguageToggle.d.ts +1 -0
  5. package/dist/components/UnsavedChangesDialog.d.ts +1 -0
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  8. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  9. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  10. package/dist/editor/components/editor-bubble.d.ts +8 -0
  11. package/dist/editor/components/image-bubble.d.ts +5 -0
  12. package/dist/editor/components/index.d.ts +16 -0
  13. package/dist/editor/components/table-bubble.d.ts +5 -0
  14. package/dist/editor/editor.d.ts +30 -0
  15. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  16. package/dist/editor/extensions/Image/index.d.ts +6 -0
  17. package/dist/editor/extensions/Image.d.ts +6 -0
  18. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  19. package/dist/editor/extensions/clipboard.d.ts +7 -0
  20. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  21. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  22. package/dist/editor/hooks/useProseMirror.d.ts +13 -0
  23. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  24. package/dist/editor/index.d.ts +2 -0
  25. package/dist/editor/markdown.d.ts +5 -0
  26. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  27. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  28. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  29. package/dist/editor/nodeViews/index.d.ts +6 -0
  30. package/dist/editor/plugins/index.d.ts +2 -0
  31. package/dist/editor/plugins/inputrules.d.ts +6 -0
  32. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  33. package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
  34. package/dist/editor/schema.d.ts +2 -0
  35. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  36. package/dist/editor/selectors/color-selector.d.ts +10 -0
  37. package/dist/editor/selectors/link-selector.d.ts +8 -0
  38. package/dist/editor/selectors/node-selector.d.ts +15 -0
  39. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  40. package/dist/editor/types.d.ts +5 -0
  41. package/dist/editor/useProseMirror.d.ts +16 -0
  42. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  43. package/dist/editor/utils/remove_classes.d.ts +1 -0
  44. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  45. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  46. package/dist/hooks/index.d.ts +1 -0
  47. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  48. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  49. package/dist/hooks/useTranslation.d.ts +17 -0
  50. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.es.js +12898 -2265
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/index.umd.js +12877 -2264
  55. package/dist/index.umd.js.map +1 -1
  56. package/dist/locales/de.d.ts +2 -0
  57. package/dist/locales/en.d.ts +10 -0
  58. package/dist/locales/es.d.ts +10 -0
  59. package/dist/locales/fr.d.ts +2 -0
  60. package/dist/locales/hi.d.ts +2 -0
  61. package/dist/locales/it.d.ts +2 -0
  62. package/dist/locales/pt.d.ts +7 -0
  63. package/dist/types/customization_controller.d.ts +2 -1
  64. package/dist/types/firecms.d.ts +2 -1
  65. package/dist/types/index.d.ts +1 -0
  66. package/dist/types/navigation.d.ts +2 -2
  67. package/dist/types/plugins.d.ts +7 -0
  68. package/dist/types/storage.d.ts +1 -0
  69. package/dist/types/translations.d.ts +646 -0
  70. package/dist/util/useStorageUploadController.d.ts +10 -1
  71. package/package.json +45 -9
  72. package/src/app/Scaffold.tsx +7 -5
  73. package/src/components/AIIcon.tsx +3 -1
  74. package/src/components/ArrayContainer.tsx +6 -4
  75. package/src/components/ClearFilterSortButton.tsx +6 -3
  76. package/src/components/ConfirmationDialog.tsx +4 -2
  77. package/src/components/DeleteEntityDialog.tsx +10 -7
  78. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  79. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  81. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  82. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  83. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  84. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  85. package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
  86. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  87. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  88. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  89. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  90. package/src/components/EntityView.tsx +3 -2
  91. package/src/components/ErrorBoundary.tsx +27 -15
  92. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  93. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  94. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  95. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  96. package/src/components/LanguageToggle.tsx +66 -0
  97. package/src/components/NotFoundPage.tsx +5 -3
  98. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  99. package/src/components/ReferenceWidget.tsx +3 -2
  100. package/src/components/SearchIconsView.tsx +3 -1
  101. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  102. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  103. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  104. package/src/components/UnsavedChangesDialog.tsx +6 -4
  105. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  106. package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
  107. package/src/components/common/default_entity_actions.tsx +4 -0
  108. package/src/components/common/useDataSourceTableController.tsx +12 -4
  109. package/src/components/index.tsx +1 -0
  110. package/src/core/DefaultAppBar.tsx +14 -10
  111. package/src/core/DefaultDrawer.tsx +8 -2
  112. package/src/core/DrawerNavigationGroup.tsx +5 -3
  113. package/src/core/EntityEditView.tsx +4 -3
  114. package/src/core/EntityEditViewFormActions.tsx +24 -17
  115. package/src/core/EntitySidePanel.tsx +6 -5
  116. package/src/core/FireCMS.tsx +33 -6
  117. package/src/editor/components/SlashCommandMenu.tsx +516 -0
  118. package/src/editor/components/editor-bubble-item.tsx +32 -0
  119. package/src/editor/components/editor-bubble.tsx +118 -0
  120. package/src/editor/components/image-bubble.tsx +156 -0
  121. package/src/editor/components/index.ts +14 -0
  122. package/src/editor/components/table-bubble.tsx +165 -0
  123. package/src/editor/editor.tsx +455 -0
  124. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  125. package/src/editor/extensions/Image/index.ts +133 -0
  126. package/src/editor/extensions/Image.ts +159 -0
  127. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  128. package/src/editor/extensions/clipboard.ts +72 -0
  129. package/src/editor/extensions/custom-keymap.ts +24 -0
  130. package/src/editor/extensions/drag-and-drop.tsx +480 -0
  131. package/src/editor/hooks/useProseMirror.ts +124 -0
  132. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  133. package/src/editor/index.ts +2 -0
  134. package/src/editor/markdown.ts +172 -0
  135. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  136. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  137. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  138. package/src/editor/nodeViews/index.ts +35 -0
  139. package/src/editor/plugins/index.ts +58 -0
  140. package/src/editor/plugins/inputrules.ts +82 -0
  141. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  142. package/src/editor/plugins/slashCommandPlugin.ts +61 -0
  143. package/src/editor/schema.ts +240 -0
  144. package/src/editor/selectors/ai-selector.tsx +111 -0
  145. package/src/editor/selectors/color-selector.tsx +200 -0
  146. package/src/editor/selectors/link-selector.tsx +118 -0
  147. package/src/editor/selectors/node-selector.tsx +157 -0
  148. package/src/editor/selectors/text-buttons.tsx +86 -0
  149. package/src/editor/types.ts +6 -0
  150. package/src/editor/useProseMirror.ts +126 -0
  151. package/src/editor/utils/prosemirror-utils.ts +108 -0
  152. package/src/editor/utils/remove_classes.ts +17 -0
  153. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  154. package/src/form/EntityForm.tsx +16 -3
  155. package/src/form/EntityFormActions.tsx +19 -12
  156. package/src/form/PropertyFieldBinding.tsx +3 -2
  157. package/src/form/components/LocalChangesMenu.tsx +13 -13
  158. package/src/form/components/StorageItemPreview.tsx +3 -2
  159. package/src/form/components/StorageUploadProgress.tsx +18 -3
  160. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  161. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  162. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  163. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  164. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +33 -19
  165. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  166. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -3
  167. package/src/hooks/index.tsx +1 -0
  168. package/src/hooks/useBuildNavigationController.tsx +45 -18
  169. package/src/hooks/useCollapsedGroups.ts +7 -6
  170. package/src/hooks/useTranslation.ts +31 -0
  171. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  172. package/src/index.ts +4 -0
  173. package/src/internal/useBuildSideEntityController.tsx +22 -20
  174. package/src/locales/de.ts +691 -0
  175. package/src/locales/en.ts +703 -0
  176. package/src/locales/es.ts +703 -0
  177. package/src/locales/fr.ts +691 -0
  178. package/src/locales/hi.ts +691 -0
  179. package/src/locales/it.ts +691 -0
  180. package/src/locales/pt.ts +700 -0
  181. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  182. package/src/preview/components/UserPreview.tsx +3 -1
  183. package/src/types/customization_controller.tsx +2 -1
  184. package/src/types/firecms.tsx +2 -1
  185. package/src/types/index.ts +1 -0
  186. package/src/types/navigation.ts +2 -2
  187. package/src/types/plugins.tsx +8 -0
  188. package/src/types/properties.ts +1 -0
  189. package/src/types/storage.ts +2 -1
  190. package/src/types/translations.ts +725 -0
  191. package/src/util/useStorageUploadController.tsx +23 -29
@@ -0,0 +1,480 @@
1
+ import { NodeSelection, Plugin } from "prosemirror-state";
2
+ import { EditorView } from "prosemirror-view";
3
+ import { Slice } from "prosemirror-model";
4
+ import { serializeForClipboard } from "./clipboard";
5
+
6
+ export interface DragHandleOptions {
7
+ /**
8
+ * The width of the drag handle
9
+ */
10
+ dragHandleWidth: number;
11
+ }
12
+
13
+ function absoluteRect(element: Element) {
14
+ const data = element.getBoundingClientRect();
15
+
16
+ let ancestor = element.parentElement;
17
+ while (ancestor && window.getComputedStyle(ancestor).position === "static") {
18
+ ancestor = ancestor.parentElement;
19
+ }
20
+ const ancestorRect = ancestor?.getBoundingClientRect();
21
+
22
+ return {
23
+ top: data.top - (ancestorRect?.top ?? 0),
24
+ left: data.left - (ancestorRect?.left ?? 0),
25
+ width: data.width
26
+ };
27
+ }
28
+
29
+ function nodeDOMAtCoords(coords: { x: number; y: number }, view: EditorView) {
30
+ const editorRect = view.dom.getBoundingClientRect();
31
+
32
+ // 0. Give up if outside vertical bounds or too far horizontally
33
+ if (coords.y < editorRect.top || coords.y > editorRect.bottom) return undefined;
34
+ if (coords.x < editorRect.left - 100 || coords.x > editorRect.right + 50) return undefined;
35
+
36
+ // 1. First probe exactly at the mouse coordinates
37
+ let elem = document.elementFromPoint(coords.x, coords.y);
38
+ let block = elem?.closest('li, p:not(:first-child), pre, blockquote, h1, h2, h3, h4, h5, h6, img, [data-type="taskList"]');
39
+ if (block && view.dom.contains(block)) {
40
+ return block.closest('li') || block;
41
+ }
42
+
43
+ // 2. If mouse is in the left gutter, probe horizontally into the editor
44
+ const probeX = editorRect.left + Math.min(60, editorRect.width / 4);
45
+ if (coords.x > probeX) return undefined;
46
+
47
+ let probeElem = document.elementFromPoint(probeX, coords.y);
48
+ let probeBlock = probeElem?.closest('li, p:not(:first-child), pre, blockquote, h1, h2, h3, h4, h5, h6, img, [data-type="taskList"]');
49
+ if (probeBlock) {
50
+ // Ensure the found block is actually inside our editor
51
+ if (view.dom.contains(probeBlock)) {
52
+ return probeBlock.closest('li') || probeBlock;
53
+ }
54
+ }
55
+
56
+ return undefined;
57
+ }
58
+
59
+ function nodePosAtDOM(node: Element, view: EditorView, options: DragHandleOptions) {
60
+ try {
61
+ if (!view.dom.contains(node)) return null;
62
+ const pos = view.posAtDOM(node, 0);
63
+ const $pos = view.state.doc.resolve(pos);
64
+ // posAtDOM(node, 0) generally returns the position inside the node.
65
+ // We want the position right before the node to create a NodeSelection.
66
+ if ($pos.depth > 0) {
67
+ return $pos.before();
68
+ }
69
+ return pos;
70
+ } catch (e) {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function dragHandlePlugin(options: DragHandleOptions = { dragHandleWidth: 24 }) {
76
+ function handleDragStart(event: DragEvent, view: EditorView) {
77
+ view.focus();
78
+
79
+ if (!event.dataTransfer) return;
80
+
81
+ const node = nodeDOMAtCoords({
82
+ x: event.clientX,
83
+ y: event.clientY
84
+ }, view);
85
+
86
+ if (!(node instanceof Element)) return;
87
+
88
+ const nodePos = nodePosAtDOM(node, view, options);
89
+ if (nodePos == null || nodePos < 0) return;
90
+
91
+ const draggedNodeSelection = NodeSelection.create(view.state.doc, nodePos);
92
+ view.dispatch(view.state.tr.setSelection(draggedNodeSelection));
93
+
94
+ const slice = view.state.selection.content();
95
+ const {
96
+ dom,
97
+ text
98
+ } = serializeForClipboard(view as any, slice);
99
+
100
+ event.dataTransfer.clearData();
101
+ event.dataTransfer.setData("text/html", dom.innerHTML);
102
+ event.dataTransfer.setData("text/plain", text);
103
+ event.dataTransfer.effectAllowed = "copyMove";
104
+
105
+ event.dataTransfer.setDragImage(node, 0, 0);
106
+
107
+ (view as any).dragging = {
108
+ slice,
109
+ move: true,
110
+ node: draggedNodeSelection
111
+ } as { slice: Slice, move: boolean, node: NodeSelection };
112
+ }
113
+
114
+ function handleClick(event: MouseEvent, view: EditorView) {
115
+ view.focus();
116
+
117
+ view.dom.classList.remove("dragging");
118
+
119
+ const node = nodeDOMAtCoords({
120
+ x: event.clientX,
121
+ y: event.clientY
122
+ }, view);
123
+
124
+ if (!(node instanceof Element)) return;
125
+
126
+ const nodePos = nodePosAtDOM(node, view, options);
127
+ if (!nodePos) return;
128
+
129
+ view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
130
+ }
131
+
132
+ let dragHandleElement: HTMLElement | null = null;
133
+
134
+ function hideDragHandle() {
135
+ if (dragHandleElement) {
136
+ dragHandleElement.classList.add("hide");
137
+ }
138
+ }
139
+
140
+ function showDragHandle() {
141
+ if (dragHandleElement) {
142
+ dragHandleElement.classList.remove("hide");
143
+ }
144
+ }
145
+
146
+ return new Plugin({
147
+ view: (view) => {
148
+ dragHandleElement = document.createElement("div");
149
+ dragHandleElement.draggable = true;
150
+ dragHandleElement.dataset.dragHandle = "";
151
+ dragHandleElement.classList.add("drag-handle");
152
+ dragHandleElement.addEventListener("dragstart", (e) => {
153
+ handleDragStart(e, view as any);
154
+ });
155
+ dragHandleElement.addEventListener("click", (e) => {
156
+ handleClick(e, view as any);
157
+ });
158
+ const onMouseMove = (event: MouseEvent) => {
159
+ if (!view.editable) {
160
+ return;
161
+ }
162
+
163
+ const node = nodeDOMAtCoords({
164
+ x: event.clientX,
165
+ y: event.clientY
166
+ }, view);
167
+
168
+ if (!(node instanceof Element)) {
169
+ hideDragHandle();
170
+ return;
171
+ }
172
+
173
+ const compStyle = window.getComputedStyle(node);
174
+ const lineHeight = parseInt(compStyle.lineHeight, 10);
175
+ const paddingTop = parseInt(compStyle.paddingTop, 10);
176
+
177
+ const rect = absoluteRect(node);
178
+ if (!rect) {
179
+ hideDragHandle();
180
+ return;
181
+ }
182
+
183
+ rect.top += (lineHeight - 24) / 2;
184
+ rect.top += paddingTop;
185
+ // Li markers
186
+ if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
187
+ rect.left -= options.dragHandleWidth;
188
+ }
189
+ rect.width = options.dragHandleWidth;
190
+
191
+ if (!dragHandleElement) return;
192
+
193
+ dragHandleElement.style.left = `${rect.left - rect.width}px`;
194
+ dragHandleElement.style.top = `${rect.top}px`;
195
+ showDragHandle();
196
+ };
197
+
198
+ window.addEventListener("mousemove", onMouseMove);
199
+
200
+ hideDragHandle();
201
+
202
+ view?.dom?.parentElement?.appendChild(dragHandleElement);
203
+
204
+ return {
205
+ destroy: () => {
206
+ window.removeEventListener("mousemove", onMouseMove);
207
+ dragHandleElement?.remove?.();
208
+ dragHandleElement = null;
209
+ }
210
+ };
211
+ },
212
+ props: {
213
+ handleDOMEvents: {
214
+ keydown: () => {
215
+ hideDragHandle();
216
+ },
217
+ mousewheel: () => {
218
+ hideDragHandle();
219
+ },
220
+ // dragging class is used for CSS
221
+ dragstart: (view) => {
222
+ view.dom.classList.add("dragging");
223
+ },
224
+ drop: (view) => {
225
+ view.dom.classList.remove("dragging");
226
+ },
227
+ dragend: (view) => {
228
+ view.dom.classList.remove("dragging");
229
+ }
230
+ }
231
+ }
232
+ });
233
+ }
234
+
235
+ import { dropPoint } from "prosemirror-transform";
236
+
237
+ export function globalDragDropPlugin() {
238
+ let dropCursorElement: HTMLElement | null = null;
239
+ let cleanup: (() => void) | null = null;
240
+
241
+ function updateDropCursorColor(el: HTMLElement) {
242
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
243
+ const varName = isDark ? "--color-surface-accent-300" : "--color-surface-accent-600";
244
+ const color = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
245
+ el.style.backgroundColor = color || (isDark ? "#cbd5e1" : "#475569");
246
+ }
247
+
248
+ return new Plugin({
249
+ view(editorView) {
250
+ dropCursorElement = document.createElement("div");
251
+ dropCursorElement.className = "prosemirror-dropcursor-block";
252
+ dropCursorElement.style.cssText = "position: absolute; z-index: 50; pointer-events: none; height: 2px;";
253
+ updateDropCursorColor(dropCursorElement);
254
+
255
+ // Watch for theme changes
256
+ const observer = new MutationObserver(() => {
257
+ if (dropCursorElement) updateDropCursorColor(dropCursorElement);
258
+ });
259
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
260
+
261
+ const removeCursor = () => {
262
+ if (dropCursorElement && dropCursorElement.parentNode) {
263
+ dropCursorElement.parentNode.removeChild(dropCursorElement);
264
+ }
265
+ };
266
+
267
+ const getBlockInsertionPoint = (event: DragEvent, clampedX: number) => {
268
+ const pos = editorView.posAtCoords({ left: clampedX, top: event.clientY });
269
+ if (!pos) return null;
270
+
271
+ let $pos = editorView.state.doc.resolve(pos.pos);
272
+
273
+ let blockDepth = 0;
274
+ for (let i = $pos.depth; i > 0; i--) {
275
+ const name = $pos.node(i).type.name;
276
+ if (name === "list_item" || name === "taskItem") {
277
+ blockDepth = i;
278
+ break;
279
+ }
280
+ if ($pos.node(i).isBlock && name !== "bullet_list" && name !== "ordered_list" && name !== "taskList") {
281
+ blockDepth = i;
282
+ }
283
+ }
284
+
285
+ if (blockDepth === 0) return pos.pos;
286
+
287
+ const nodeBeforePos = $pos.before(blockDepth);
288
+ const nodeAfterPos = $pos.after(blockDepth);
289
+
290
+ const domNode = editorView.nodeDOM(nodeBeforePos);
291
+ if (domNode instanceof HTMLElement) {
292
+ const rect = domNode.getBoundingClientRect();
293
+ const isTopHalf = event.clientY < rect.top + rect.height / 2;
294
+ return isTopHalf ? nodeBeforePos : nodeAfterPos;
295
+ }
296
+ return pos.pos;
297
+ };
298
+
299
+ const handleDragOver = (event: DragEvent) => {
300
+ if (!editorView.editable || !editorView.dragging) return;
301
+
302
+ // If it's a native slice drag (no explicitly captured node from our drag handle)
303
+ // then we bail out here and let ProseMirror native drop logic and native dropcursor handle it
304
+ if (!(editorView.dragging as any).node) {
305
+ removeCursor();
306
+ return;
307
+ }
308
+
309
+ event.preventDefault(); // browser requires this to allow drop
310
+
311
+ const editorRect = editorView.dom.getBoundingClientRect();
312
+ const clampedX = Math.max(editorRect.left + 10, Math.min(event.clientX, editorRect.right - 10));
313
+
314
+ let target = getBlockInsertionPoint(event, clampedX);
315
+ if (target === null) {
316
+ removeCursor();
317
+ return;
318
+ }
319
+
320
+ const $pos = editorView.state.doc.resolve(target);
321
+
322
+ let rect;
323
+ const before = $pos.nodeBefore;
324
+ const after = $pos.nodeAfter;
325
+
326
+ if (before || after) {
327
+ const nodeDOM = editorView.nodeDOM(target - (before ? before.nodeSize : 0));
328
+ if (nodeDOM && nodeDOM instanceof HTMLElement) {
329
+ const nodeRect = nodeDOM.getBoundingClientRect();
330
+ let top = before ? nodeRect.bottom : nodeRect.top;
331
+ if (before && after) {
332
+ const afterDOM = editorView.nodeDOM(target);
333
+ if (afterDOM && afterDOM instanceof HTMLElement) {
334
+ top = (top + afterDOM.getBoundingClientRect().top) / 2;
335
+ }
336
+ }
337
+ rect = { left: nodeRect.left, right: nodeRect.right, top: top - 1, bottom: top + 1 };
338
+ }
339
+ }
340
+
341
+ if (!rect) {
342
+ const coords = editorView.coordsAtPos(target);
343
+ rect = { left: editorRect.left + 50, right: editorRect.right - 50, top: coords.top - 1, bottom: coords.top + 1 };
344
+ }
345
+
346
+ const parent = editorView.dom.offsetParent as HTMLElement;
347
+ let parentLeft = 0;
348
+ let parentTop = 0;
349
+ if (parent && parent !== document.body && getComputedStyle(parent).position !== "static") {
350
+ const parentRect = parent.getBoundingClientRect();
351
+ parentLeft = parentRect.left - parent.scrollLeft;
352
+ parentTop = parentRect.top - parent.scrollTop;
353
+ }
354
+
355
+ if (!dropCursorElement!.parentNode) {
356
+ parent.appendChild(dropCursorElement!);
357
+ }
358
+
359
+ dropCursorElement!.style.left = `${rect.left - parentLeft}px`;
360
+ dropCursorElement!.style.top = `${rect.top - parentTop}px`;
361
+ dropCursorElement!.style.width = `${rect.right - rect.left}px`;
362
+ };
363
+
364
+ const handleDrop = (event: DragEvent) => {
365
+ if (!editorView.editable || !editorView.dragging) return;
366
+
367
+ if (!(editorView.dragging as any).node) {
368
+ removeCursor();
369
+ return;
370
+ }
371
+
372
+ event.preventDefault();
373
+ removeCursor();
374
+ editorView.dom.classList.remove("dragging");
375
+
376
+ const editorRect = editorView.dom.getBoundingClientRect();
377
+ const clampedX = Math.max(editorRect.left + 10, Math.min(event.clientX, editorRect.right - 10));
378
+
379
+ let targetPos = getBlockInsertionPoint(event, clampedX);
380
+ if (targetPos === null) return;
381
+
382
+ const dragging = (editorView as any).dragging as { slice: Slice, move: boolean, node?: NodeSelection };
383
+ if (dragging && dragging.slice) {
384
+ let tr = editorView.state.tr;
385
+ if (dragging.move) {
386
+ const { node } = dragging;
387
+ if (node) {
388
+ node.replace(tr); // exact native ProseMirror delete
389
+ } else {
390
+ tr = tr.deleteSelection();
391
+ }
392
+ }
393
+
394
+ const mappedTarget = tr.mapping.map(targetPos);
395
+ const beforeInsert = tr.doc;
396
+
397
+ let { node, slice } = dragging;
398
+
399
+ if (node && node.node) {
400
+ let nodeToInsert: any = node.node;
401
+ const $mapped = tr.doc.resolve(mappedTarget);
402
+ const parentName = $mapped.parent.type.name;
403
+
404
+ const isTargetList = parentName === "bullet_list" || parentName === "ordered_list";
405
+ const isTargetTaskList = parentName === "taskList";
406
+
407
+ // 1. Unwrap incoming lists if they don't match the destination perfectly
408
+ if (nodeToInsert.type.name === "list_item" && !isTargetList) {
409
+ nodeToInsert = nodeToInsert.content;
410
+ } else if (nodeToInsert.type.name === "taskItem" && !isTargetTaskList) {
411
+ nodeToInsert = nodeToInsert.content;
412
+ }
413
+
414
+ // 2. Wrap incoming blocks/fragments into exactly the target list type
415
+ const isFragment = !nodeToInsert.type;
416
+ const needsWrap = isFragment || (nodeToInsert.type.name !== "list_item" && nodeToInsert.type.name !== "taskItem");
417
+
418
+ if (needsWrap) {
419
+ if (isTargetList) {
420
+ const listItemType = editorView.state.schema.nodes.list_item;
421
+ if (listItemType) nodeToInsert = listItemType.create(null, nodeToInsert);
422
+ } else if (isTargetTaskList) {
423
+ const taskItemType = editorView.state.schema.nodes.taskItem;
424
+ if (taskItemType) nodeToInsert = taskItemType.create({ checked: false }, nodeToInsert);
425
+ }
426
+ }
427
+
428
+ // 3. Force insertion. tr.replace slices and splits lists. tr.insert preserves the explicit boundary.
429
+ try {
430
+ tr = tr.insert(mappedTarget, nodeToInsert);
431
+ } catch (e) {
432
+ console.warn("Could not insert dragged node exactly at target. Attempting fallback.", e);
433
+ const point = dropPoint(tr.doc, mappedTarget, slice);
434
+ if (point !== null) {
435
+ tr = tr.replace(point, point, slice);
436
+ }
437
+ }
438
+ } else if (slice) {
439
+ // For generic slices (e.g native image dragging), we MUST use dropPoint
440
+ // so ProseMirror finds a schema-valid depth to insert the node structure natively.
441
+ const point = dropPoint(tr.doc, mappedTarget, slice);
442
+ const finalTarget = point !== null ? point : mappedTarget;
443
+ tr = tr.replace(finalTarget, finalTarget, slice);
444
+ }
445
+
446
+ if (!tr.doc.eq(beforeInsert)) {
447
+ editorView.dispatch(tr.setMeta("uiEvent", "drop"));
448
+ editorView.focus();
449
+ }
450
+ }
451
+
452
+ (editorView as any).dragging = null;
453
+ };
454
+
455
+ const handleDragEnd = () => {
456
+ removeCursor();
457
+ };
458
+
459
+ window.addEventListener("dragover", handleDragOver, { capture: true });
460
+ window.addEventListener("drop", handleDrop, { capture: true });
461
+ window.addEventListener("dragend", handleDragEnd, { capture: true });
462
+
463
+ cleanup = () => {
464
+ removeCursor();
465
+ observer.disconnect();
466
+ window.removeEventListener("dragover", handleDragOver, { capture: true });
467
+ window.removeEventListener("drop", handleDrop, { capture: true });
468
+ window.removeEventListener("dragend", handleDragEnd, { capture: true });
469
+ };
470
+
471
+ return {
472
+ destroy() {
473
+ if (cleanup) cleanup();
474
+ }
475
+ };
476
+ }
477
+ });
478
+ }
479
+
480
+
@@ -0,0 +1,124 @@
1
+ import { useState, useEffect, useRef, useLayoutEffect } from "react";
2
+ import { EditorState, Transaction, Plugin } from "prosemirror-state";
3
+ import { EditorView } from "prosemirror-view";
4
+ import { schema } from "../schema";
5
+ import { corePlugins } from "../plugins";
6
+ import { parser } from "../markdown";
7
+ import { nodeViews } from "../nodeViews";
8
+ import { createDropImagePlugin } from "../extensions/Image";
9
+ import { columnResizing, tableEditing } from "prosemirror-tables";
10
+
11
+ const trailingNodePlugin = new Plugin({
12
+ appendTransaction: (_, oldState, newState) => {
13
+ const doc = newState.doc;
14
+ if (doc.lastChild && doc.lastChild.type.name !== "paragraph") {
15
+ return newState.tr.insert(doc.content.size, newState.schema.nodes.paragraph.create());
16
+ }
17
+ return null;
18
+ }
19
+ });
20
+
21
+ interface UseProseMirrorProps {
22
+ initialContent?: string | any;
23
+ editable?: boolean;
24
+ handleImageUpload?: (file: File) => Promise<string>;
25
+ }
26
+
27
+ export function useProseMirror({ initialContent, editable = true, handleImageUpload }: UseProseMirrorProps) {
28
+ const plugins = [
29
+ ...corePlugins,
30
+ columnResizing(),
31
+ tableEditing(),
32
+ trailingNodePlugin
33
+ ];
34
+ if (handleImageUpload) {
35
+ plugins.push(createDropImagePlugin(handleImageUpload));
36
+ }
37
+
38
+ const defaultState = EditorState.create({
39
+ doc: typeof initialContent === "string"
40
+ ? parser.parse(initialContent)
41
+ : initialContent
42
+ ? schema.nodeFromJSON(initialContent)
43
+ : schema.node("doc", null, [schema.node("paragraph")]),
44
+ schema,
45
+ plugins
46
+ });
47
+
48
+ const [state, setState] = useState<EditorState>(defaultState);
49
+ const [view, setView] = useState<EditorView | null>(null);
50
+
51
+ const editorRef = useRef<HTMLDivElement>(null);
52
+ const viewRef = useRef<EditorView | null>(null);
53
+
54
+ useLayoutEffect(() => {
55
+ if (!editorRef.current) return;
56
+
57
+ const editorView = new EditorView(editorRef.current, {
58
+ state: defaultState,
59
+ editable: () => editable,
60
+ dispatchTransaction: (tr: Transaction) => {
61
+ const newState = editorView.state.apply(tr);
62
+ editorView.updateState(newState);
63
+ setState(newState);
64
+ },
65
+ nodeViews: nodeViews,
66
+ transformPastedHTML(html: string) {
67
+ // Strip inline styles and classes from pasted HTML so we don't
68
+ // get textStyle marks (color, font-size, etc.) that have no
69
+ // markdown representation. This makes paste look consistent.
70
+ const div = document.createElement("div");
71
+ div.innerHTML = html;
72
+ div.querySelectorAll("*").forEach((el) => {
73
+ el.removeAttribute("style");
74
+ el.removeAttribute("class");
75
+ el.removeAttribute("color");
76
+ el.removeAttribute("bgcolor");
77
+ el.removeAttribute("face");
78
+ });
79
+ return div.innerHTML;
80
+ },
81
+ });
82
+
83
+ // Patch posAtCoords to allow dropping/interacting anywhere horizontally natively
84
+ const originalPosAtCoords = editorView.posAtCoords.bind(editorView);
85
+ editorView.posAtCoords = (coords: { left: number, top: number }) => {
86
+ let res = originalPosAtCoords(coords);
87
+ if (!res) {
88
+ const editorRect = editorView.dom.getBoundingClientRect();
89
+ // If it's literally anywhere to the left of the actual ProseMirror content block
90
+ if (coords.left <= editorRect.left) {
91
+ const probeX = editorRect.left + Math.min(60, editorRect.width / 4);
92
+ return originalPosAtCoords({ left: probeX, top: coords.top });
93
+ }
94
+ // Or if it's anywhere to the right
95
+ if (coords.left >= editorRect.right) {
96
+ const probeX = editorRect.right - Math.min(60, editorRect.width / 4);
97
+ return originalPosAtCoords({ left: probeX, top: coords.top });
98
+ }
99
+ }
100
+ return res;
101
+ };
102
+
103
+ viewRef.current = editorView;
104
+ setView(editorView);
105
+
106
+ return () => {
107
+ editorView.destroy();
108
+ viewRef.current = null;
109
+ };
110
+ }, []);
111
+
112
+ // Effect to update editable status without re-mounting
113
+ useEffect(() => {
114
+ if (viewRef.current) {
115
+ viewRef.current.setProps({ editable: () => editable });
116
+ }
117
+ }, [editable]);
118
+
119
+ return {
120
+ state,
121
+ view,
122
+ editorRef
123
+ };
124
+ }
@@ -0,0 +1,15 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { EditorState } from "prosemirror-state";
3
+ import { EditorView } from "prosemirror-view";
4
+
5
+ export interface ProseMirrorContextType {
6
+ state: EditorState | null;
7
+ view: EditorView | null;
8
+ }
9
+
10
+ export const ProseMirrorContext = createContext<ProseMirrorContextType>({
11
+ state: null,
12
+ view: null,
13
+ });
14
+
15
+ export const useProseMirrorContext = () => useContext(ProseMirrorContext);
@@ -0,0 +1,2 @@
1
+ export * from "./editor";
2
+ export * from "./types";