@firecms/core 3.1.0-canary.1df3b2c → 3.1.0-canary.75005e4

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 (209) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  2. package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
  3. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +5 -10
  4. package/dist/components/ErrorBoundary.d.ts +4 -2
  5. package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
  6. package/dist/components/LanguageToggle.d.ts +1 -0
  7. package/dist/components/UnsavedChangesDialog.d.ts +1 -0
  8. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -1
  9. package/dist/components/index.d.ts +1 -0
  10. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  11. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  12. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  13. package/dist/editor/components/editor-bubble.d.ts +8 -0
  14. package/dist/editor/components/index.d.ts +14 -0
  15. package/dist/editor/editor.d.ts +30 -0
  16. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  17. package/dist/editor/extensions/Image/index.d.ts +6 -0
  18. package/dist/editor/extensions/Image.d.ts +6 -0
  19. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  20. package/dist/editor/extensions/clipboard.d.ts +7 -0
  21. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  22. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  23. package/dist/editor/hooks/useProseMirror.d.ts +14 -0
  24. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  25. package/dist/editor/index.d.ts +2 -0
  26. package/dist/editor/markdown.d.ts +5 -0
  27. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  28. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  29. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  30. package/dist/editor/nodeViews/index.d.ts +6 -0
  31. package/dist/editor/plugins/index.d.ts +2 -0
  32. package/dist/editor/plugins/inputrules.d.ts +6 -0
  33. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  34. package/dist/editor/plugins/slashCommandPlugin.d.ts +11 -0
  35. package/dist/editor/schema.d.ts +2 -0
  36. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  37. package/dist/editor/selectors/color-selector.d.ts +10 -0
  38. package/dist/editor/selectors/link-selector.d.ts +8 -0
  39. package/dist/editor/selectors/node-selector.d.ts +15 -0
  40. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  41. package/dist/editor/types.d.ts +5 -0
  42. package/dist/editor/useProseMirror.d.ts +16 -0
  43. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  44. package/dist/editor/utils/remove_classes.d.ts +1 -0
  45. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  46. package/dist/form/components/ErrorFocus.d.ts +1 -1
  47. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  48. package/dist/hooks/index.d.ts +1 -0
  49. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  50. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  51. package/dist/hooks/useTranslation.d.ts +17 -0
  52. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.es.js +11441 -2215
  55. package/dist/index.es.js.map +1 -1
  56. package/dist/index.umd.js +11423 -2216
  57. package/dist/index.umd.js.map +1 -1
  58. package/dist/internal/useRestoreScroll.d.ts +1 -1
  59. package/dist/locales/de.d.ts +2 -0
  60. package/dist/locales/en.d.ts +10 -0
  61. package/dist/locales/es.d.ts +10 -0
  62. package/dist/locales/fr.d.ts +2 -0
  63. package/dist/locales/hi.d.ts +2 -0
  64. package/dist/locales/it.d.ts +2 -0
  65. package/dist/locales/pt.d.ts +7 -0
  66. package/dist/types/analytics.d.ts +1 -1
  67. package/dist/types/collections.d.ts +8 -0
  68. package/dist/types/customization_controller.d.ts +2 -1
  69. package/dist/types/firecms.d.ts +2 -1
  70. package/dist/types/index.d.ts +1 -0
  71. package/dist/types/navigation.d.ts +2 -2
  72. package/dist/types/plugins.d.ts +23 -0
  73. package/dist/types/translations.d.ts +646 -0
  74. package/dist/util/entities.d.ts +1 -1
  75. package/dist/util/resolutions.d.ts +2 -2
  76. package/package.json +47 -13
  77. package/src/app/Scaffold.tsx +7 -5
  78. package/src/components/AIIcon.tsx +3 -1
  79. package/src/components/ArrayContainer.tsx +6 -4
  80. package/src/components/ClearFilterSortButton.tsx +6 -3
  81. package/src/components/ConfirmationDialog.tsx +4 -2
  82. package/src/components/DeleteEntityDialog.tsx +10 -7
  83. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  84. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  85. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  86. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  87. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  88. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  89. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  90. package/src/components/EntityCollectionView/EntityBoardCard.tsx +1 -1
  91. package/src/components/EntityCollectionView/EntityCard.tsx +4 -0
  92. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +39 -46
  93. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  94. package/src/components/EntityCollectionView/EntityCollectionView.tsx +73 -31
  95. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  96. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  97. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  98. package/src/components/EntityCollectionView/ViewModeToggle.tsx +37 -37
  99. package/src/components/EntityView.tsx +3 -2
  100. package/src/components/ErrorBoundary.tsx +27 -15
  101. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  102. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  103. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  104. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  105. package/src/components/LanguageToggle.tsx +66 -0
  106. package/src/components/NotFoundPage.tsx +5 -3
  107. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  108. package/src/components/ReferenceWidget.tsx +3 -2
  109. package/src/components/SearchIconsView.tsx +3 -1
  110. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  111. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  112. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  113. package/src/components/UnsavedChangesDialog.tsx +6 -4
  114. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  115. package/src/components/VirtualTable/VirtualTable.tsx +116 -113
  116. package/src/components/VirtualTable/VirtualTableHeader.tsx +54 -52
  117. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +1 -1
  118. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +3 -3
  119. package/src/components/common/default_entity_actions.tsx +4 -0
  120. package/src/components/common/useDataSourceTableController.tsx +12 -4
  121. package/src/components/index.tsx +1 -0
  122. package/src/core/DefaultAppBar.tsx +15 -11
  123. package/src/core/DefaultDrawer.tsx +8 -2
  124. package/src/core/DrawerNavigationGroup.tsx +5 -3
  125. package/src/core/EntityEditView.tsx +4 -3
  126. package/src/core/EntityEditViewFormActions.tsx +24 -17
  127. package/src/core/EntitySidePanel.tsx +32 -29
  128. package/src/core/FireCMS.tsx +33 -6
  129. package/src/core/field_configs.tsx +14 -9
  130. package/src/editor/components/SlashCommandMenu.tsx +348 -0
  131. package/src/editor/components/editor-bubble-item.tsx +32 -0
  132. package/src/editor/components/editor-bubble.tsx +118 -0
  133. package/src/editor/components/index.ts +12 -0
  134. package/src/editor/editor.tsx +307 -0
  135. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  136. package/src/editor/extensions/Image/index.ts +133 -0
  137. package/src/editor/extensions/Image.ts +144 -0
  138. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  139. package/src/editor/extensions/clipboard.ts +72 -0
  140. package/src/editor/extensions/custom-keymap.ts +24 -0
  141. package/src/editor/extensions/drag-and-drop.tsx +472 -0
  142. package/src/editor/hooks/useProseMirror.ts +115 -0
  143. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  144. package/src/editor/index.ts +2 -0
  145. package/src/editor/markdown.ts +110 -0
  146. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  147. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  148. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  149. package/src/editor/nodeViews/index.ts +35 -0
  150. package/src/editor/plugins/index.ts +55 -0
  151. package/src/editor/plugins/inputrules.ts +82 -0
  152. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  153. package/src/editor/plugins/slashCommandPlugin.ts +49 -0
  154. package/src/editor/schema.ts +228 -0
  155. package/src/editor/selectors/ai-selector.tsx +111 -0
  156. package/src/editor/selectors/color-selector.tsx +200 -0
  157. package/src/editor/selectors/link-selector.tsx +118 -0
  158. package/src/editor/selectors/node-selector.tsx +157 -0
  159. package/src/editor/selectors/text-buttons.tsx +86 -0
  160. package/src/editor/types.ts +6 -0
  161. package/src/editor/useProseMirror.ts +126 -0
  162. package/src/editor/utils/prosemirror-utils.ts +78 -0
  163. package/src/editor/utils/remove_classes.ts +17 -0
  164. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  165. package/src/form/EntityForm.tsx +76 -63
  166. package/src/form/EntityFormActions.tsx +19 -12
  167. package/src/form/PropertyFieldBinding.tsx +6 -5
  168. package/src/form/components/ErrorFocus.tsx +3 -3
  169. package/src/form/components/LocalChangesMenu.tsx +13 -13
  170. package/src/form/components/StorageItemPreview.tsx +3 -2
  171. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  172. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  173. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  174. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  175. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -4
  176. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  177. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +87 -85
  178. package/src/hooks/index.tsx +1 -0
  179. package/src/hooks/useBuildNavigationController.tsx +49 -22
  180. package/src/hooks/useCollapsedGroups.ts +7 -6
  181. package/src/hooks/useTranslation.ts +31 -0
  182. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  183. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  184. package/src/index.ts +4 -0
  185. package/src/internal/useBuildDataSource.ts +1 -2
  186. package/src/internal/useBuildSideEntityController.tsx +22 -20
  187. package/src/locales/de.ts +691 -0
  188. package/src/locales/en.ts +703 -0
  189. package/src/locales/es.ts +703 -0
  190. package/src/locales/fr.ts +691 -0
  191. package/src/locales/hi.ts +691 -0
  192. package/src/locales/it.ts +691 -0
  193. package/src/locales/pt.ts +700 -0
  194. package/src/preview/PropertyPreview.tsx +1 -0
  195. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  196. package/src/preview/components/UserPreview.tsx +3 -1
  197. package/src/types/analytics.ts +10 -0
  198. package/src/types/collections.ts +9 -0
  199. package/src/types/customization_controller.tsx +2 -1
  200. package/src/types/firecms.tsx +2 -1
  201. package/src/types/index.ts +1 -0
  202. package/src/types/navigation.ts +2 -2
  203. package/src/types/plugins.tsx +26 -0
  204. package/src/types/translations.ts +725 -0
  205. package/src/util/entities.ts +1 -1
  206. package/src/util/join_collections.ts +10 -8
  207. package/src/util/previews.ts +2 -2
  208. package/src/util/property_utils.tsx +1 -1
  209. package/src/util/resolutions.ts +5 -3
@@ -0,0 +1,472 @@
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
+ tr = tr.insert(mappedTarget, nodeToInsert);
430
+ } else if (slice) {
431
+ // For generic slices (e.g native image dragging), we MUST use dropPoint
432
+ // so ProseMirror finds a schema-valid depth to insert the node structure natively.
433
+ const point = dropPoint(tr.doc, mappedTarget, slice);
434
+ const finalTarget = point !== null ? point : mappedTarget;
435
+ tr = tr.replace(finalTarget, finalTarget, slice);
436
+ }
437
+
438
+ if (!tr.doc.eq(beforeInsert)) {
439
+ editorView.dispatch(tr.setMeta("uiEvent", "drop"));
440
+ editorView.focus();
441
+ }
442
+ }
443
+
444
+ (editorView as any).dragging = null;
445
+ };
446
+
447
+ const handleDragEnd = () => {
448
+ removeCursor();
449
+ };
450
+
451
+ window.addEventListener("dragover", handleDragOver, { capture: true });
452
+ window.addEventListener("drop", handleDrop, { capture: true });
453
+ window.addEventListener("dragend", handleDragEnd, { capture: true });
454
+
455
+ cleanup = () => {
456
+ removeCursor();
457
+ observer.disconnect();
458
+ window.removeEventListener("dragover", handleDragOver, { capture: true });
459
+ window.removeEventListener("drop", handleDrop, { capture: true });
460
+ window.removeEventListener("dragend", handleDragEnd, { capture: true });
461
+ };
462
+
463
+ return {
464
+ destroy() {
465
+ if (cleanup) cleanup();
466
+ }
467
+ };
468
+ }
469
+ });
470
+ }
471
+
472
+
@@ -0,0 +1,115 @@
1
+ import { useState, useEffect, useRef, useLayoutEffect } from "react";
2
+ import { EditorState, Transaction } 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
+
10
+ interface UseProseMirrorProps {
11
+ initialContent?: string | any;
12
+ onChange?: (state: EditorState, view: EditorView) => void;
13
+ editable?: boolean;
14
+ handleImageUpload?: (file: File) => Promise<string>;
15
+ }
16
+
17
+ export function useProseMirror({ initialContent, onChange, editable = true, handleImageUpload }: UseProseMirrorProps) {
18
+ // Store onChange in a ref so that the latest version is always called inside
19
+ // the dispatchTransaction closure (which only runs once at mount with [] deps).
20
+ const onChangeRef = useRef(onChange);
21
+ onChangeRef.current = onChange;
22
+
23
+ const plugins = [...corePlugins];
24
+ if (handleImageUpload) {
25
+ plugins.push(createDropImagePlugin(handleImageUpload));
26
+ }
27
+
28
+ const defaultState = EditorState.create({
29
+ doc: typeof initialContent === "string"
30
+ ? parser.parse(initialContent)
31
+ : initialContent
32
+ ? schema.nodeFromJSON(initialContent)
33
+ : schema.node("doc", null, [schema.node("paragraph")]),
34
+ schema,
35
+ plugins
36
+ });
37
+
38
+ const [state, setState] = useState<EditorState>(defaultState);
39
+ const [view, setView] = useState<EditorView | null>(null);
40
+
41
+ const editorRef = useRef<HTMLDivElement>(null);
42
+ const viewRef = useRef<EditorView | null>(null);
43
+
44
+ useLayoutEffect(() => {
45
+ if (!editorRef.current) return;
46
+
47
+ const editorView = new EditorView(editorRef.current, {
48
+ state: defaultState,
49
+ editable: () => editable,
50
+ dispatchTransaction: (tr: Transaction) => {
51
+ const newState = editorView.state.apply(tr);
52
+ editorView.updateState(newState);
53
+ setState(newState);
54
+ onChangeRef.current?.(newState, editorView);
55
+ },
56
+ nodeViews: nodeViews,
57
+ transformPastedHTML(html: string) {
58
+ // Strip inline styles and classes from pasted HTML so we don't
59
+ // get textStyle marks (color, font-size, etc.) that have no
60
+ // markdown representation. This makes paste look consistent.
61
+ const div = document.createElement("div");
62
+ div.innerHTML = html;
63
+ div.querySelectorAll("*").forEach((el) => {
64
+ el.removeAttribute("style");
65
+ el.removeAttribute("class");
66
+ el.removeAttribute("color");
67
+ el.removeAttribute("bgcolor");
68
+ el.removeAttribute("face");
69
+ });
70
+ return div.innerHTML;
71
+ },
72
+ });
73
+
74
+ // Patch posAtCoords to allow dropping/interacting anywhere horizontally natively
75
+ const originalPosAtCoords = editorView.posAtCoords.bind(editorView);
76
+ editorView.posAtCoords = (coords: { left: number, top: number }) => {
77
+ let res = originalPosAtCoords(coords);
78
+ if (!res) {
79
+ const editorRect = editorView.dom.getBoundingClientRect();
80
+ // If it's literally anywhere to the left of the actual ProseMirror content block
81
+ if (coords.left <= editorRect.left) {
82
+ const probeX = editorRect.left + Math.min(60, editorRect.width / 4);
83
+ return originalPosAtCoords({ left: probeX, top: coords.top });
84
+ }
85
+ // Or if it's anywhere to the right
86
+ if (coords.left >= editorRect.right) {
87
+ const probeX = editorRect.right - Math.min(60, editorRect.width / 4);
88
+ return originalPosAtCoords({ left: probeX, top: coords.top });
89
+ }
90
+ }
91
+ return res;
92
+ };
93
+
94
+ viewRef.current = editorView;
95
+ setView(editorView);
96
+
97
+ return () => {
98
+ editorView.destroy();
99
+ viewRef.current = null;
100
+ };
101
+ }, []);
102
+
103
+ // Effect to update editable status without re-mounting
104
+ useEffect(() => {
105
+ if (viewRef.current) {
106
+ viewRef.current.setProps({ editable: () => editable });
107
+ }
108
+ }, [editable]);
109
+
110
+ return {
111
+ state,
112
+ view,
113
+ editorRef
114
+ };
115
+ }
@@ -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";