@firecms/core 3.1.0-canary.24c8270 → 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 (180) 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/index.d.ts +14 -0
  12. package/dist/editor/editor.d.ts +30 -0
  13. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  14. package/dist/editor/extensions/Image/index.d.ts +6 -0
  15. package/dist/editor/extensions/Image.d.ts +6 -0
  16. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  17. package/dist/editor/extensions/clipboard.d.ts +7 -0
  18. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  19. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  20. package/dist/editor/hooks/useProseMirror.d.ts +14 -0
  21. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  22. package/dist/editor/index.d.ts +2 -0
  23. package/dist/editor/markdown.d.ts +5 -0
  24. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  25. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  26. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  27. package/dist/editor/nodeViews/index.d.ts +6 -0
  28. package/dist/editor/plugins/index.d.ts +2 -0
  29. package/dist/editor/plugins/inputrules.d.ts +6 -0
  30. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  31. package/dist/editor/plugins/slashCommandPlugin.d.ts +11 -0
  32. package/dist/editor/schema.d.ts +2 -0
  33. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  34. package/dist/editor/selectors/color-selector.d.ts +10 -0
  35. package/dist/editor/selectors/link-selector.d.ts +8 -0
  36. package/dist/editor/selectors/node-selector.d.ts +15 -0
  37. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  38. package/dist/editor/types.d.ts +5 -0
  39. package/dist/editor/useProseMirror.d.ts +16 -0
  40. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  41. package/dist/editor/utils/remove_classes.d.ts +1 -0
  42. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  43. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  44. package/dist/hooks/index.d.ts +1 -0
  45. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  46. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  47. package/dist/hooks/useTranslation.d.ts +17 -0
  48. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.es.js +11293 -2142
  51. package/dist/index.es.js.map +1 -1
  52. package/dist/index.umd.js +11274 -2142
  53. package/dist/index.umd.js.map +1 -1
  54. package/dist/locales/de.d.ts +2 -0
  55. package/dist/locales/en.d.ts +10 -0
  56. package/dist/locales/es.d.ts +10 -0
  57. package/dist/locales/fr.d.ts +2 -0
  58. package/dist/locales/hi.d.ts +2 -0
  59. package/dist/locales/it.d.ts +2 -0
  60. package/dist/locales/pt.d.ts +7 -0
  61. package/dist/types/customization_controller.d.ts +2 -1
  62. package/dist/types/firecms.d.ts +2 -1
  63. package/dist/types/index.d.ts +1 -0
  64. package/dist/types/navigation.d.ts +2 -2
  65. package/dist/types/plugins.d.ts +7 -0
  66. package/dist/types/translations.d.ts +646 -0
  67. package/package.json +43 -9
  68. package/src/app/Scaffold.tsx +7 -5
  69. package/src/components/AIIcon.tsx +3 -1
  70. package/src/components/ArrayContainer.tsx +6 -4
  71. package/src/components/ClearFilterSortButton.tsx +6 -3
  72. package/src/components/ConfirmationDialog.tsx +4 -2
  73. package/src/components/DeleteEntityDialog.tsx +10 -7
  74. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  75. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  76. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  77. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  78. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  79. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  80. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  81. package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
  82. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  83. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  84. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  85. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  86. package/src/components/EntityView.tsx +3 -2
  87. package/src/components/ErrorBoundary.tsx +27 -15
  88. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  89. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  90. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  91. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  92. package/src/components/LanguageToggle.tsx +66 -0
  93. package/src/components/NotFoundPage.tsx +5 -3
  94. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  95. package/src/components/ReferenceWidget.tsx +3 -2
  96. package/src/components/SearchIconsView.tsx +3 -1
  97. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  98. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  99. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  100. package/src/components/UnsavedChangesDialog.tsx +6 -4
  101. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  102. package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
  103. package/src/components/common/default_entity_actions.tsx +4 -0
  104. package/src/components/common/useDataSourceTableController.tsx +5 -14
  105. package/src/components/index.tsx +1 -0
  106. package/src/core/DefaultAppBar.tsx +14 -10
  107. package/src/core/DefaultDrawer.tsx +8 -2
  108. package/src/core/DrawerNavigationGroup.tsx +5 -3
  109. package/src/core/EntityEditView.tsx +3 -2
  110. package/src/core/EntityEditViewFormActions.tsx +24 -17
  111. package/src/core/EntitySidePanel.tsx +4 -3
  112. package/src/core/FireCMS.tsx +33 -6
  113. package/src/editor/components/SlashCommandMenu.tsx +348 -0
  114. package/src/editor/components/editor-bubble-item.tsx +32 -0
  115. package/src/editor/components/editor-bubble.tsx +118 -0
  116. package/src/editor/components/index.ts +12 -0
  117. package/src/editor/editor.tsx +307 -0
  118. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  119. package/src/editor/extensions/Image/index.ts +133 -0
  120. package/src/editor/extensions/Image.ts +144 -0
  121. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  122. package/src/editor/extensions/clipboard.ts +72 -0
  123. package/src/editor/extensions/custom-keymap.ts +24 -0
  124. package/src/editor/extensions/drag-and-drop.tsx +472 -0
  125. package/src/editor/hooks/useProseMirror.ts +115 -0
  126. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  127. package/src/editor/index.ts +2 -0
  128. package/src/editor/markdown.ts +110 -0
  129. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  130. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  131. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  132. package/src/editor/nodeViews/index.ts +35 -0
  133. package/src/editor/plugins/index.ts +55 -0
  134. package/src/editor/plugins/inputrules.ts +82 -0
  135. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  136. package/src/editor/plugins/slashCommandPlugin.ts +49 -0
  137. package/src/editor/schema.ts +228 -0
  138. package/src/editor/selectors/ai-selector.tsx +111 -0
  139. package/src/editor/selectors/color-selector.tsx +200 -0
  140. package/src/editor/selectors/link-selector.tsx +118 -0
  141. package/src/editor/selectors/node-selector.tsx +157 -0
  142. package/src/editor/selectors/text-buttons.tsx +86 -0
  143. package/src/editor/types.ts +6 -0
  144. package/src/editor/useProseMirror.ts +126 -0
  145. package/src/editor/utils/prosemirror-utils.ts +78 -0
  146. package/src/editor/utils/remove_classes.ts +17 -0
  147. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  148. package/src/form/EntityForm.tsx +7 -3
  149. package/src/form/EntityFormActions.tsx +19 -12
  150. package/src/form/PropertyFieldBinding.tsx +3 -2
  151. package/src/form/components/LocalChangesMenu.tsx +13 -13
  152. package/src/form/components/StorageItemPreview.tsx +3 -2
  153. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  154. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  155. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  156. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  157. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -3
  158. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  159. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -2
  160. package/src/hooks/index.tsx +1 -0
  161. package/src/hooks/useBuildNavigationController.tsx +20 -13
  162. package/src/hooks/useCollapsedGroups.ts +7 -6
  163. package/src/hooks/useTranslation.ts +31 -0
  164. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  165. package/src/index.ts +4 -0
  166. package/src/locales/de.ts +691 -0
  167. package/src/locales/en.ts +703 -0
  168. package/src/locales/es.ts +703 -0
  169. package/src/locales/fr.ts +691 -0
  170. package/src/locales/hi.ts +691 -0
  171. package/src/locales/it.ts +691 -0
  172. package/src/locales/pt.ts +700 -0
  173. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  174. package/src/preview/components/UserPreview.tsx +3 -1
  175. package/src/types/customization_controller.tsx +2 -1
  176. package/src/types/firecms.tsx +2 -1
  177. package/src/types/index.ts +1 -0
  178. package/src/types/navigation.ts +2 -2
  179. package/src/types/plugins.tsx +8 -0
  180. package/src/types/translations.ts +725 -0
@@ -0,0 +1,107 @@
1
+ import { Plugin, PluginKey, Transaction, EditorState } from "prosemirror-state";
2
+ import { Decoration, DecorationSet } from "prosemirror-view";
3
+
4
+ // Define and export the plugin key
5
+ export const loadingDecorationKey = new PluginKey<LoadingDecorationState>("loadingDecoration");
6
+
7
+ interface LoadingDecorationState {
8
+ decorationSet: DecorationSet;
9
+ hasDecoration: boolean;
10
+ }
11
+
12
+ export const textLoadingCommands = {
13
+ toggleLoadingDecoration: (state: EditorState, dispatch: ((tr: Transaction) => void) | undefined, loadingHtml?: string): boolean => {
14
+ const { selection } = state;
15
+ const pos = selection.from;
16
+
17
+ if (!dispatch) return false;
18
+
19
+ const tr = state.tr.setMeta(loadingDecorationKey, {
20
+ pos,
21
+ type: "loadingDecoration",
22
+ remove: false,
23
+ loadingHtml
24
+ });
25
+
26
+ dispatch(tr);
27
+ return true;
28
+ },
29
+
30
+ removeLoadingDecoration: (state: EditorState, dispatch: ((tr: Transaction) => void) | undefined): boolean => {
31
+ if (!dispatch) return false;
32
+
33
+ const tr = state.tr.setMeta(loadingDecorationKey, {
34
+ pos: 0, // We can pass any position as it will remove the entire decoration set
35
+ type: "loadingDecoration",
36
+ remove: true
37
+ });
38
+
39
+ dispatch(tr);
40
+ return true;
41
+ }
42
+ };
43
+
44
+ /**
45
+ * This plugin is used to display streaming content from an LLM.
46
+ */
47
+ export const textLoadingDecorationPlugin = () => {
48
+ return new Plugin<LoadingDecorationState>({
49
+ key: loadingDecorationKey,
50
+
51
+ state: {
52
+ init() {
53
+ return {
54
+ decorationSet: DecorationSet.empty,
55
+ hasDecoration: false
56
+ };
57
+ },
58
+
59
+ apply(tr, oldState) {
60
+ const action = tr.getMeta(loadingDecorationKey);
61
+
62
+ if (action?.type === "loadingDecoration") {
63
+ const { pos, remove, loadingHtml } = action;
64
+
65
+ if (remove) {
66
+ return {
67
+ decorationSet: DecorationSet.empty,
68
+ hasDecoration: false
69
+ };
70
+ }
71
+
72
+ const decoration = Decoration.widget(pos, () => {
73
+ const container = document.createElement("span");
74
+ container.className = "loading-decoration";
75
+
76
+ // Sanitize and append HTML
77
+ if (loadingHtml) {
78
+ container.innerHTML = loadingHtml;
79
+ } else {
80
+ const span = document.createElement("span");
81
+ span.innerText = "loading...";
82
+ container.appendChild(span);
83
+ }
84
+
85
+ return container;
86
+ });
87
+
88
+ return {
89
+ decorationSet: DecorationSet.empty.add(tr.doc, [decoration]),
90
+ hasDecoration: true
91
+ };
92
+ }
93
+
94
+ return {
95
+ decorationSet: oldState.decorationSet.map(tr.mapping, tr.doc),
96
+ hasDecoration: oldState.hasDecoration
97
+ };
98
+ }
99
+ },
100
+
101
+ props: {
102
+ decorations(state) {
103
+ return this.getState(state)?.decorationSet || DecorationSet.empty;
104
+ }
105
+ }
106
+ });
107
+ };
@@ -0,0 +1,72 @@
1
+ import { DOMSerializer, Slice } from "prosemirror-model"
2
+ import { EditorView } from "prosemirror-view";
3
+
4
+ export function serializeForClipboard(view: EditorView, slice: Slice) {
5
+ view.someProp("transformCopied", f => {
6
+ slice = f(slice!, view)
7
+ })
8
+
9
+ const context = [];
10
+ let {
11
+ content,
12
+ openStart,
13
+ openEnd
14
+ } = slice
15
+ while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild!.childCount == 1) {
16
+ openStart--
17
+ openEnd--
18
+ const node = content.firstChild!
19
+ // @ts-ignore
20
+ context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null)
21
+ content = node.content
22
+ }
23
+
24
+ const serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema)
25
+ const doc = detachedDoc(), wrap = doc.createElement("div")
26
+ wrap.appendChild(serializer.serializeFragment(content, { document: doc }))
27
+
28
+ let firstChild = wrap.firstChild, needsWrap, wrappers = 0
29
+ while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) {
30
+ for (let i = needsWrap.length - 1; i >= 0; i--) {
31
+ const wrapper = doc.createElement(needsWrap[i])
32
+ while (wrap.firstChild) wrapper.appendChild(wrap.firstChild)
33
+ wrap.appendChild(wrapper)
34
+ wrappers++
35
+ }
36
+ firstChild = wrap.firstChild
37
+ }
38
+
39
+ if (firstChild && firstChild.nodeType == 1)
40
+ (firstChild as HTMLElement).setAttribute(
41
+ "data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`)
42
+
43
+ const text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
44
+ slice.content.textBetween(0, slice.content.size, "\n\n")
45
+
46
+ return {
47
+ dom: wrap,
48
+ text,
49
+ slice
50
+ }
51
+ }
52
+
53
+ // Trick from jQuery -- some elements must be wrapped in other
54
+ // elements for innerHTML to work. I.e. if you do `div.innerHTML =
55
+ // "<td>..</td>"` the table cells are ignored.
56
+ const wrapMap: { [node: string]: string[] } = {
57
+ thead: ["table"],
58
+ tbody: ["table"],
59
+ tfoot: ["table"],
60
+ caption: ["table"],
61
+ colgroup: ["table"],
62
+ col: ["table", "colgroup"],
63
+ tr: ["table", "tbody"],
64
+ td: ["table", "tbody", "tr"],
65
+ th: ["table", "tbody", "tr"]
66
+ }
67
+
68
+ let _detachedDoc: Document | null = null
69
+
70
+ function detachedDoc() {
71
+ return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"))
72
+ }
@@ -0,0 +1,24 @@
1
+ import { keymap } from "prosemirror-keymap";
2
+
3
+ export const customKeymapPlugin = () => {
4
+ return keymap({
5
+ "Mod-a": (state, dispatch) => {
6
+ const { tr } = state;
7
+ const startSelectionPos = tr.selection.from;
8
+ const endSelectionPos = tr.selection.to;
9
+ const startNodePos = tr.selection.$from.start();
10
+ const endNodePos = tr.selection.$to.end();
11
+
12
+ const isCurrentTextSelectionNotExtendedToNodeBoundaries =
13
+ startSelectionPos > startNodePos || endSelectionPos < endNodePos;
14
+
15
+ if (isCurrentTextSelectionNotExtendedToNodeBoundaries) {
16
+ if (dispatch) {
17
+ dispatch(tr.setSelection((state.selection.constructor as any).create(state.doc, startNodePos, endNodePos)));
18
+ }
19
+ return true;
20
+ }
21
+ return false;
22
+ },
23
+ });
24
+ };
@@ -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
+