@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.
- 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/index.d.ts +14 -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 +14 -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 +11 -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 +11293 -2142
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +11274 -2142
- 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/translations.d.ts +646 -0
- package/package.json +43 -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 +5 -14
- 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 +3 -2
- package/src/core/EntityEditViewFormActions.tsx +24 -17
- package/src/core/EntitySidePanel.tsx +4 -3
- package/src/core/FireCMS.tsx +33 -6
- package/src/editor/components/SlashCommandMenu.tsx +348 -0
- package/src/editor/components/editor-bubble-item.tsx +32 -0
- package/src/editor/components/editor-bubble.tsx +118 -0
- package/src/editor/components/index.ts +12 -0
- package/src/editor/editor.tsx +307 -0
- package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
- package/src/editor/extensions/Image/index.ts +133 -0
- package/src/editor/extensions/Image.ts +144 -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 +472 -0
- package/src/editor/hooks/useProseMirror.ts +115 -0
- package/src/editor/hooks/useProseMirrorContext.ts +15 -0
- package/src/editor/index.ts +2 -0
- package/src/editor/markdown.ts +110 -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 +55 -0
- package/src/editor/plugins/inputrules.ts +82 -0
- package/src/editor/plugins/placeholderPlugin.ts +55 -0
- package/src/editor/plugins/slashCommandPlugin.ts +49 -0
- package/src/editor/schema.ts +228 -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 +78 -0
- package/src/editor/utils/remove_classes.ts +17 -0
- package/src/editor/utils/useDebouncedCallback.ts +25 -0
- package/src/form/EntityForm.tsx +7 -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/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 +3 -3
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -2
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +20 -13
- 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/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/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
|
+
|