@firecms/core 3.1.0 → 3.2.0-canary.44dc65b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
- package/dist/components/ErrorBoundary.d.ts +3 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
- package/dist/components/LanguageToggle.d.ts +1 -0
- package/dist/components/UnsavedChangesDialog.d.ts +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/core/DrawerNavigationGroup.d.ts +2 -2
- package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
- package/dist/editor/components/editor-bubble-item.d.ts +8 -0
- package/dist/editor/components/editor-bubble.d.ts +8 -0
- package/dist/editor/components/image-bubble.d.ts +5 -0
- package/dist/editor/components/index.d.ts +16 -0
- package/dist/editor/components/table-bubble.d.ts +5 -0
- package/dist/editor/editor.d.ts +30 -0
- package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
- package/dist/editor/extensions/Image/index.d.ts +6 -0
- package/dist/editor/extensions/Image.d.ts +6 -0
- package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
- package/dist/editor/extensions/clipboard.d.ts +7 -0
- package/dist/editor/extensions/custom-keymap.d.ts +1 -0
- package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
- package/dist/editor/hooks/useProseMirror.d.ts +13 -0
- package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/markdown.d.ts +5 -0
- package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
- package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
- package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
- package/dist/editor/nodeViews/index.d.ts +6 -0
- package/dist/editor/plugins/index.d.ts +2 -0
- package/dist/editor/plugins/inputrules.d.ts +6 -0
- package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
- package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
- package/dist/editor/schema.d.ts +2 -0
- package/dist/editor/selectors/ai-selector.d.ts +0 -0
- package/dist/editor/selectors/color-selector.d.ts +10 -0
- package/dist/editor/selectors/link-selector.d.ts +8 -0
- package/dist/editor/selectors/node-selector.d.ts +15 -0
- package/dist/editor/selectors/text-buttons.d.ts +1 -0
- package/dist/editor/types.d.ts +5 -0
- package/dist/editor/useProseMirror.d.ts +16 -0
- package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
- package/dist/editor/utils/remove_classes.d.ts +1 -0
- package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +0 -1
- package/dist/hooks/useCollapsedGroups.d.ts +3 -3
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +12898 -2265
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +12877 -2264
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/types/customization_controller.d.ts +2 -1
- package/dist/types/firecms.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/navigation.d.ts +2 -2
- package/dist/types/plugins.d.ts +7 -0
- package/dist/types/storage.d.ts +1 -0
- package/dist/types/translations.d.ts +646 -0
- package/dist/util/useStorageUploadController.d.ts +10 -1
- package/package.json +45 -9
- package/src/app/Scaffold.tsx +7 -5
- package/src/components/AIIcon.tsx +3 -1
- package/src/components/ArrayContainer.tsx +6 -4
- package/src/components/ClearFilterSortButton.tsx +6 -3
- package/src/components/ConfirmationDialog.tsx +4 -2
- package/src/components/DeleteEntityDialog.tsx +10 -7
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
- package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
- package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
- package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
- package/src/components/EntityView.tsx +3 -2
- package/src/components/ErrorBoundary.tsx +27 -15
- package/src/components/HomePage/DefaultHomePage.tsx +19 -13
- package/src/components/HomePage/HomePageDnD.tsx +3 -1
- package/src/components/HomePage/NavigationGroup.tsx +3 -1
- package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
- package/src/components/LanguageToggle.tsx +66 -0
- package/src/components/NotFoundPage.tsx +5 -3
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
- package/src/components/ReferenceWidget.tsx +3 -2
- package/src/components/SearchIconsView.tsx +3 -1
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
- package/src/components/UnsavedChangesDialog.tsx +6 -4
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
- package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
- package/src/components/common/default_entity_actions.tsx +4 -0
- package/src/components/common/useDataSourceTableController.tsx +12 -4
- package/src/components/index.tsx +1 -0
- package/src/core/DefaultAppBar.tsx +14 -10
- package/src/core/DefaultDrawer.tsx +8 -2
- package/src/core/DrawerNavigationGroup.tsx +5 -3
- package/src/core/EntityEditView.tsx +4 -3
- package/src/core/EntityEditViewFormActions.tsx +24 -17
- package/src/core/EntitySidePanel.tsx +6 -5
- package/src/core/FireCMS.tsx +33 -6
- package/src/editor/components/SlashCommandMenu.tsx +516 -0
- package/src/editor/components/editor-bubble-item.tsx +32 -0
- package/src/editor/components/editor-bubble.tsx +118 -0
- package/src/editor/components/image-bubble.tsx +156 -0
- package/src/editor/components/index.ts +14 -0
- package/src/editor/components/table-bubble.tsx +165 -0
- package/src/editor/editor.tsx +455 -0
- package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
- package/src/editor/extensions/Image/index.ts +133 -0
- package/src/editor/extensions/Image.ts +159 -0
- package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
- package/src/editor/extensions/clipboard.ts +72 -0
- package/src/editor/extensions/custom-keymap.ts +24 -0
- package/src/editor/extensions/drag-and-drop.tsx +480 -0
- package/src/editor/hooks/useProseMirror.ts +124 -0
- package/src/editor/hooks/useProseMirrorContext.ts +15 -0
- package/src/editor/index.ts +2 -0
- package/src/editor/markdown.ts +172 -0
- package/src/editor/nodeViews/ImageComponent.tsx +20 -0
- package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
- package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
- package/src/editor/nodeViews/index.ts +35 -0
- package/src/editor/plugins/index.ts +58 -0
- package/src/editor/plugins/inputrules.ts +82 -0
- package/src/editor/plugins/placeholderPlugin.ts +55 -0
- package/src/editor/plugins/slashCommandPlugin.ts +61 -0
- package/src/editor/schema.ts +240 -0
- package/src/editor/selectors/ai-selector.tsx +111 -0
- package/src/editor/selectors/color-selector.tsx +200 -0
- package/src/editor/selectors/link-selector.tsx +118 -0
- package/src/editor/selectors/node-selector.tsx +157 -0
- package/src/editor/selectors/text-buttons.tsx +86 -0
- package/src/editor/types.ts +6 -0
- package/src/editor/useProseMirror.ts +126 -0
- package/src/editor/utils/prosemirror-utils.ts +108 -0
- package/src/editor/utils/remove_classes.ts +17 -0
- package/src/editor/utils/useDebouncedCallback.ts +25 -0
- package/src/form/EntityForm.tsx +16 -3
- package/src/form/EntityFormActions.tsx +19 -12
- package/src/form/PropertyFieldBinding.tsx +3 -2
- package/src/form/components/LocalChangesMenu.tsx +13 -13
- package/src/form/components/StorageItemPreview.tsx +3 -2
- package/src/form/components/StorageUploadProgress.tsx +18 -3
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
- package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
- package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +33 -19
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -3
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +45 -18
- package/src/hooks/useCollapsedGroups.ts +7 -6
- package/src/hooks/useTranslation.ts +31 -0
- package/src/i18n/FireCMSi18nProvider.tsx +160 -0
- package/src/index.ts +4 -0
- package/src/internal/useBuildSideEntityController.tsx +22 -20
- package/src/locales/de.ts +691 -0
- package/src/locales/en.ts +703 -0
- package/src/locales/es.ts +703 -0
- package/src/locales/fr.ts +691 -0
- package/src/locales/hi.ts +691 -0
- package/src/locales/it.ts +691 -0
- package/src/locales/pt.ts +700 -0
- package/src/preview/components/UrlComponentPreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +3 -1
- package/src/types/customization_controller.tsx +2 -1
- package/src/types/firecms.tsx +2 -1
- package/src/types/index.ts +1 -0
- package/src/types/navigation.ts +2 -2
- package/src/types/plugins.tsx +8 -0
- package/src/types/properties.ts +1 -0
- package/src/types/storage.ts +2 -1
- package/src/types/translations.ts +725 -0
- 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);
|