@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,115 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useLayoutEffect } from "react";
|
|
2
|
+
import { EditorState, Transaction } from "prosemirror-state";
|
|
3
|
+
import { EditorView } from "prosemirror-view";
|
|
4
|
+
import { schema } from "../schema";
|
|
5
|
+
import { corePlugins } from "../plugins";
|
|
6
|
+
import { parser } from "../markdown";
|
|
7
|
+
import { nodeViews } from "../nodeViews";
|
|
8
|
+
import { createDropImagePlugin } from "../extensions/Image";
|
|
9
|
+
|
|
10
|
+
interface UseProseMirrorProps {
|
|
11
|
+
initialContent?: string | any;
|
|
12
|
+
onChange?: (state: EditorState, view: EditorView) => void;
|
|
13
|
+
editable?: boolean;
|
|
14
|
+
handleImageUpload?: (file: File) => Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useProseMirror({ initialContent, onChange, editable = true, handleImageUpload }: UseProseMirrorProps) {
|
|
18
|
+
// Store onChange in a ref so that the latest version is always called inside
|
|
19
|
+
// the dispatchTransaction closure (which only runs once at mount with [] deps).
|
|
20
|
+
const onChangeRef = useRef(onChange);
|
|
21
|
+
onChangeRef.current = onChange;
|
|
22
|
+
|
|
23
|
+
const plugins = [...corePlugins];
|
|
24
|
+
if (handleImageUpload) {
|
|
25
|
+
plugins.push(createDropImagePlugin(handleImageUpload));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultState = EditorState.create({
|
|
29
|
+
doc: typeof initialContent === "string"
|
|
30
|
+
? parser.parse(initialContent)
|
|
31
|
+
: initialContent
|
|
32
|
+
? schema.nodeFromJSON(initialContent)
|
|
33
|
+
: schema.node("doc", null, [schema.node("paragraph")]),
|
|
34
|
+
schema,
|
|
35
|
+
plugins
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const [state, setState] = useState<EditorState>(defaultState);
|
|
39
|
+
const [view, setView] = useState<EditorView | null>(null);
|
|
40
|
+
|
|
41
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
43
|
+
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
if (!editorRef.current) return;
|
|
46
|
+
|
|
47
|
+
const editorView = new EditorView(editorRef.current, {
|
|
48
|
+
state: defaultState,
|
|
49
|
+
editable: () => editable,
|
|
50
|
+
dispatchTransaction: (tr: Transaction) => {
|
|
51
|
+
const newState = editorView.state.apply(tr);
|
|
52
|
+
editorView.updateState(newState);
|
|
53
|
+
setState(newState);
|
|
54
|
+
onChangeRef.current?.(newState, editorView);
|
|
55
|
+
},
|
|
56
|
+
nodeViews: nodeViews,
|
|
57
|
+
transformPastedHTML(html: string) {
|
|
58
|
+
// Strip inline styles and classes from pasted HTML so we don't
|
|
59
|
+
// get textStyle marks (color, font-size, etc.) that have no
|
|
60
|
+
// markdown representation. This makes paste look consistent.
|
|
61
|
+
const div = document.createElement("div");
|
|
62
|
+
div.innerHTML = html;
|
|
63
|
+
div.querySelectorAll("*").forEach((el) => {
|
|
64
|
+
el.removeAttribute("style");
|
|
65
|
+
el.removeAttribute("class");
|
|
66
|
+
el.removeAttribute("color");
|
|
67
|
+
el.removeAttribute("bgcolor");
|
|
68
|
+
el.removeAttribute("face");
|
|
69
|
+
});
|
|
70
|
+
return div.innerHTML;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Patch posAtCoords to allow dropping/interacting anywhere horizontally natively
|
|
75
|
+
const originalPosAtCoords = editorView.posAtCoords.bind(editorView);
|
|
76
|
+
editorView.posAtCoords = (coords: { left: number, top: number }) => {
|
|
77
|
+
let res = originalPosAtCoords(coords);
|
|
78
|
+
if (!res) {
|
|
79
|
+
const editorRect = editorView.dom.getBoundingClientRect();
|
|
80
|
+
// If it's literally anywhere to the left of the actual ProseMirror content block
|
|
81
|
+
if (coords.left <= editorRect.left) {
|
|
82
|
+
const probeX = editorRect.left + Math.min(60, editorRect.width / 4);
|
|
83
|
+
return originalPosAtCoords({ left: probeX, top: coords.top });
|
|
84
|
+
}
|
|
85
|
+
// Or if it's anywhere to the right
|
|
86
|
+
if (coords.left >= editorRect.right) {
|
|
87
|
+
const probeX = editorRect.right - Math.min(60, editorRect.width / 4);
|
|
88
|
+
return originalPosAtCoords({ left: probeX, top: coords.top });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return res;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
viewRef.current = editorView;
|
|
95
|
+
setView(editorView);
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
editorView.destroy();
|
|
99
|
+
viewRef.current = null;
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
// Effect to update editable status without re-mounting
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (viewRef.current) {
|
|
106
|
+
viewRef.current.setProps({ editable: () => editable });
|
|
107
|
+
}
|
|
108
|
+
}, [editable]);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
state,
|
|
112
|
+
view,
|
|
113
|
+
editorRef
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import { EditorState } from "prosemirror-state";
|
|
3
|
+
import { EditorView } from "prosemirror-view";
|
|
4
|
+
|
|
5
|
+
export interface ProseMirrorContextType {
|
|
6
|
+
state: EditorState | null;
|
|
7
|
+
view: EditorView | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ProseMirrorContext = createContext<ProseMirrorContextType>({
|
|
11
|
+
state: null,
|
|
12
|
+
view: null,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const useProseMirrorContext = () => useContext(ProseMirrorContext);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MarkdownParser,
|
|
3
|
+
MarkdownSerializer,
|
|
4
|
+
defaultMarkdownParser,
|
|
5
|
+
defaultMarkdownSerializer
|
|
6
|
+
} from "prosemirror-markdown";
|
|
7
|
+
import markdownIt from "markdown-it";
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import markdownItTaskLists from "markdown-it-task-lists";
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
import markdownItMark from "markdown-it-mark";
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
import markdownItIns from "markdown-it-ins";
|
|
14
|
+
|
|
15
|
+
import { schema } from "./schema";
|
|
16
|
+
|
|
17
|
+
const parserTokens: any = {
|
|
18
|
+
...defaultMarkdownParser.tokens,
|
|
19
|
+
em: { mark: "italic" },
|
|
20
|
+
strong: { mark: "bold" },
|
|
21
|
+
html_inline: { ignore: true, noCloseToken: true },
|
|
22
|
+
html_block: { ignore: true, noCloseToken: true },
|
|
23
|
+
s: {
|
|
24
|
+
mark: "strike",
|
|
25
|
+
},
|
|
26
|
+
task_list: {
|
|
27
|
+
block: "task_list",
|
|
28
|
+
},
|
|
29
|
+
task_item: {
|
|
30
|
+
block: "task_item",
|
|
31
|
+
getAttrs: (tok: any) => ({ checked: tok.attrGet("checked") === "true" }),
|
|
32
|
+
},
|
|
33
|
+
mark: {
|
|
34
|
+
mark: "highlight"
|
|
35
|
+
},
|
|
36
|
+
ins: {
|
|
37
|
+
mark: "underline"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const md = markdownIt({ html: false })
|
|
42
|
+
.use(markdownItTaskLists)
|
|
43
|
+
.use(markdownItMark)
|
|
44
|
+
.use(markdownItIns);
|
|
45
|
+
|
|
46
|
+
// Unwrap images from paragraphs so they can be parsed as block nodes by ProseMirror
|
|
47
|
+
md.core.ruler.after("inline", "image-to-block", (state: any) => {
|
|
48
|
+
const tokens = state.tokens;
|
|
49
|
+
for (let i = tokens.length - 2; i >= 1; i--) {
|
|
50
|
+
if (
|
|
51
|
+
tokens[i - 1] && tokens[i - 1].type === "paragraph_open" &&
|
|
52
|
+
tokens[i] && tokens[i].type === "inline" &&
|
|
53
|
+
tokens[i + 1] && tokens[i + 1].type === "paragraph_close"
|
|
54
|
+
) {
|
|
55
|
+
const inlineTokens = tokens[i].children || [];
|
|
56
|
+
if (inlineTokens.length === 1 && inlineTokens[0].type === "image") {
|
|
57
|
+
state.tokens.splice(i - 1, 3, inlineTokens[0]);
|
|
58
|
+
// No need to adjust index when looping backward!
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const markdownParser = new MarkdownParser(schema, md, parserTokens);
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export const markdownSerializer = new MarkdownSerializer(
|
|
68
|
+
{
|
|
69
|
+
...defaultMarkdownSerializer.nodes,
|
|
70
|
+
// Add custom serialization for task lists
|
|
71
|
+
task_list(state, node) {
|
|
72
|
+
state.renderList(node, " ", () => "- ");
|
|
73
|
+
},
|
|
74
|
+
task_item(state, node) {
|
|
75
|
+
state.write(`[${node.attrs.checked ? "x" : " "}] `);
|
|
76
|
+
state.renderContent(node);
|
|
77
|
+
},
|
|
78
|
+
horizontal_rule(state, node) {
|
|
79
|
+
state.write(node.attrs.markup || "---");
|
|
80
|
+
state.closeBlock(node);
|
|
81
|
+
},
|
|
82
|
+
image(state, node) {
|
|
83
|
+
const src = node.attrs.src.replace(/ /g, "%20");
|
|
84
|
+
state.write("]/g, "\\$&") +
|
|
85
|
+
(node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")");
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
...defaultMarkdownSerializer.marks,
|
|
90
|
+
bold: defaultMarkdownSerializer.marks.strong,
|
|
91
|
+
italic: defaultMarkdownSerializer.marks.em,
|
|
92
|
+
strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
|
|
93
|
+
highlight: { open: "==", close: "==", mixable: true, expelEnclosingWhitespace: true },
|
|
94
|
+
underline: { open: "++", close: "++", mixable: true, expelEnclosingWhitespace: true },
|
|
95
|
+
link: {
|
|
96
|
+
...defaultMarkdownSerializer.marks.link,
|
|
97
|
+
close(state: any, mark, parent, index) {
|
|
98
|
+
const inAutolink = state.inAutolink;
|
|
99
|
+
state.inAutolink = undefined;
|
|
100
|
+
const href = mark.attrs.href.replace(/ /g, "%20");
|
|
101
|
+
return inAutolink ? ">"
|
|
102
|
+
: "](" + href.replace(/[\(\)"]/g, "\\$&") + (mark.attrs.title ? ` "${mark.attrs.title.replace(/"/g, '\\"')}"` : "") + ")";
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
// textStyle (colored text from HTML) has no markdown equivalent — emit content as-is
|
|
106
|
+
textStyle: { open: "", close: "", mixable: true, expelEnclosingWhitespace: true },
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
export const parser = markdownParser;
|
|
110
|
+
export const serializer = markdownSerializer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ReactNodeViewProps } from "./ReactNodeView";
|
|
3
|
+
import { cls, defaultBorderMixin } from "@firecms/ui";
|
|
4
|
+
|
|
5
|
+
export const ImageComponent: React.FC<ReactNodeViewProps> = ({ node, view, getPos }) => {
|
|
6
|
+
// If the node is selected
|
|
7
|
+
const selected = view.state.selection.from === getPos();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<img
|
|
11
|
+
src={node.attrs.src}
|
|
12
|
+
alt={node.attrs.alt || ""}
|
|
13
|
+
title={node.attrs.title || ""}
|
|
14
|
+
className={cls(
|
|
15
|
+
"rounded-lg max-w-full !m-0",
|
|
16
|
+
selected ? "" : ""
|
|
17
|
+
)}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import { createRoot, Root } from "react-dom/client";
|
|
3
|
+
import { Node as ProseMirrorNode } from "prosemirror-model";
|
|
4
|
+
import { EditorView, NodeView } from "prosemirror-view";
|
|
5
|
+
|
|
6
|
+
export interface ReactNodeViewProps {
|
|
7
|
+
node: ProseMirrorNode;
|
|
8
|
+
view: EditorView;
|
|
9
|
+
getPos: () => number | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ReactNodeViewComponent = React.FC<ReactNodeViewProps>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A utility class that implements the ProseMirror NodeView interface but delegates rendering
|
|
16
|
+
* to a React component.
|
|
17
|
+
* Note: This uses createRoot, so it does not automatically inherit React Contexts.
|
|
18
|
+
* If contexts are needed, wrap them manually or use a portal-based approach instead.
|
|
19
|
+
*/
|
|
20
|
+
export class ReactNodeView implements NodeView {
|
|
21
|
+
public node: ProseMirrorNode;
|
|
22
|
+
public view: EditorView;
|
|
23
|
+
public getPos: () => number | undefined;
|
|
24
|
+
public dom: HTMLElement;
|
|
25
|
+
public contentDOM?: HTMLElement;
|
|
26
|
+
private root: Root;
|
|
27
|
+
private Component: ReactNodeViewComponent;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
node: ProseMirrorNode,
|
|
31
|
+
view: EditorView,
|
|
32
|
+
getPos: () => number | undefined,
|
|
33
|
+
Component: ReactNodeViewComponent,
|
|
34
|
+
as: string = "div",
|
|
35
|
+
className?: string,
|
|
36
|
+
contentDOMElement?: HTMLElement
|
|
37
|
+
) {
|
|
38
|
+
this.node = node;
|
|
39
|
+
this.view = view;
|
|
40
|
+
this.getPos = getPos;
|
|
41
|
+
this.Component = Component;
|
|
42
|
+
|
|
43
|
+
this.dom = document.createElement(as);
|
|
44
|
+
if (className) this.dom.className = className;
|
|
45
|
+
if (contentDOMElement) {
|
|
46
|
+
this.contentDOM = contentDOMElement;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const container = document.createElement("div");
|
|
50
|
+
// We render React next to contentDOM
|
|
51
|
+
this.dom.appendChild(container);
|
|
52
|
+
if (this.contentDOM) {
|
|
53
|
+
this.dom.appendChild(this.contentDOM);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.root = createRoot(container);
|
|
57
|
+
this.render();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private render() {
|
|
61
|
+
this.root.render(
|
|
62
|
+
<this.Component
|
|
63
|
+
node={this.node}
|
|
64
|
+
view={this.view}
|
|
65
|
+
getPos={this.getPos}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
update(node: ProseMirrorNode): boolean {
|
|
71
|
+
if (node.type !== this.node.type) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
this.node = node;
|
|
75
|
+
this.render();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
destroy() {
|
|
80
|
+
this.root.unmount();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ignoreMutation(mutation: any) {
|
|
84
|
+
if (!this.contentDOM) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return !this.contentDOM.contains(mutation.target);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ReactNodeViewProps } from "./ReactNodeView";
|
|
3
|
+
|
|
4
|
+
export const TaskItemComponent: React.FC<ReactNodeViewProps> = ({ node, view, getPos }) => {
|
|
5
|
+
const checked = node.attrs.checked;
|
|
6
|
+
|
|
7
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
8
|
+
const pos = getPos();
|
|
9
|
+
if (typeof pos !== "number") return;
|
|
10
|
+
|
|
11
|
+
view.dispatch(
|
|
12
|
+
view.state.tr.setNodeMarkup(pos, undefined, {
|
|
13
|
+
...node.attrs,
|
|
14
|
+
checked: e.target.checked
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<label contentEditable={false} className="flex items-start select-none px-1">
|
|
21
|
+
<input
|
|
22
|
+
type="checkbox"
|
|
23
|
+
checked={checked}
|
|
24
|
+
onChange={handleChange}
|
|
25
|
+
className="mt-1 flex-shrink-0 cursor-pointer"
|
|
26
|
+
/>
|
|
27
|
+
</label>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Node as ProseMirrorNode } from "prosemirror-model";
|
|
2
|
+
import { EditorView, NodeView } from "prosemirror-view";
|
|
3
|
+
import { ReactNodeView, ReactNodeViewComponent } from "./ReactNodeView";
|
|
4
|
+
import { TaskItemComponent } from "./TaskItemComponent";
|
|
5
|
+
import { ImageComponent } from "./ImageComponent";
|
|
6
|
+
|
|
7
|
+
function createReactNodeView(
|
|
8
|
+
Component: ReactNodeViewComponent,
|
|
9
|
+
as: string = "div",
|
|
10
|
+
className?: string,
|
|
11
|
+
createContentDOM?: () => HTMLElement
|
|
12
|
+
) {
|
|
13
|
+
return (node: ProseMirrorNode, view: EditorView, getPos: () => number | undefined): NodeView => {
|
|
14
|
+
const contentDOM = createContentDOM ? createContentDOM() : undefined;
|
|
15
|
+
return new ReactNodeView(node, view, getPos, Component, as, className, contentDOM);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const nodeViews = {
|
|
20
|
+
task_item: createReactNodeView(
|
|
21
|
+
TaskItemComponent,
|
|
22
|
+
"li",
|
|
23
|
+
"flex items-start",
|
|
24
|
+
() => {
|
|
25
|
+
const dom = document.createElement("div");
|
|
26
|
+
dom.className = "flex-grow min-w-0";
|
|
27
|
+
return dom;
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
image: createReactNodeView(
|
|
31
|
+
ImageComponent,
|
|
32
|
+
"span",
|
|
33
|
+
"inline-block w-full"
|
|
34
|
+
)
|
|
35
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { keymap } from "prosemirror-keymap";
|
|
2
|
+
import { history, undo, redo } from "prosemirror-history";
|
|
3
|
+
import { slashCommandPlugin } from "./slashCommandPlugin";
|
|
4
|
+
import { dragHandlePlugin, globalDragDropPlugin } from "../extensions/drag-and-drop";
|
|
5
|
+
import { baseKeymap, setBlockType, toggleMark, chainCommands, exitCode, joinUp, joinDown, lift, selectParentNode } from "prosemirror-commands";
|
|
6
|
+
import { highlightDecorationPlugin } from "../extensions/HighlightDecorationExtension";
|
|
7
|
+
import { textLoadingDecorationPlugin } from "../extensions/TextLoadingDecorationExtension";
|
|
8
|
+
import { splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list";
|
|
9
|
+
import { schema } from "../schema";
|
|
10
|
+
import { Plugin } from "prosemirror-state";
|
|
11
|
+
import { gapCursor } from "prosemirror-gapcursor";
|
|
12
|
+
import { dropCursor } from "prosemirror-dropcursor";
|
|
13
|
+
import { markdownInputRules } from "./inputrules";
|
|
14
|
+
import { placeholderPlugin } from "./placeholderPlugin";
|
|
15
|
+
|
|
16
|
+
const customKeymap = {
|
|
17
|
+
"Mod-z": undo,
|
|
18
|
+
"Mod-y": redo,
|
|
19
|
+
"Shift-Mod-z": redo,
|
|
20
|
+
"Mod-b": toggleMark(schema.marks.bold),
|
|
21
|
+
"Mod-i": toggleMark(schema.marks.italic),
|
|
22
|
+
"Mod-u": toggleMark(schema.marks.underline),
|
|
23
|
+
"Mod-Shift-s": toggleMark(schema.marks.strike),
|
|
24
|
+
"Mod-e": toggleMark(schema.marks.code),
|
|
25
|
+
"Mod-Shift-h": toggleMark(schema.marks.highlight),
|
|
26
|
+
|
|
27
|
+
"Enter": splitListItem(schema.nodes.list_item),
|
|
28
|
+
"Shift-Enter": splitListItem(schema.nodes.task_item),
|
|
29
|
+
|
|
30
|
+
"Mod-[": liftListItem(schema.nodes.list_item),
|
|
31
|
+
"Mod-]": sinkListItem(schema.nodes.list_item),
|
|
32
|
+
|
|
33
|
+
"Shift-Mod-8": setBlockType(schema.nodes.bullet_list),
|
|
34
|
+
"Shift-Mod-9": setBlockType(schema.nodes.ordered_list),
|
|
35
|
+
|
|
36
|
+
"Mod-Alt-1": setBlockType(schema.nodes.heading, { level: 1 }),
|
|
37
|
+
"Mod-Alt-2": setBlockType(schema.nodes.heading, { level: 2 }),
|
|
38
|
+
"Mod-Alt-3": setBlockType(schema.nodes.heading, { level: 3 }),
|
|
39
|
+
|
|
40
|
+
"Mod-Alt-0": setBlockType(schema.nodes.paragraph),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const corePlugins: Plugin[] = [
|
|
44
|
+
history(),
|
|
45
|
+
keymap(customKeymap),
|
|
46
|
+
keymap(baseKeymap),
|
|
47
|
+
globalDragDropPlugin(),
|
|
48
|
+
gapCursor(),
|
|
49
|
+
slashCommandPlugin(),
|
|
50
|
+
dragHandlePlugin(),
|
|
51
|
+
highlightDecorationPlugin(),
|
|
52
|
+
textLoadingDecorationPlugin(),
|
|
53
|
+
markdownInputRules,
|
|
54
|
+
placeholderPlugin("Press '/' for commands")
|
|
55
|
+
];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inputRules,
|
|
3
|
+
wrappingInputRule,
|
|
4
|
+
textblockTypeInputRule,
|
|
5
|
+
smartQuotes,
|
|
6
|
+
emDash,
|
|
7
|
+
ellipsis,
|
|
8
|
+
InputRule,
|
|
9
|
+
} from "prosemirror-inputrules";
|
|
10
|
+
import { schema } from "../schema";
|
|
11
|
+
import { MarkType } from "prosemirror-model";
|
|
12
|
+
|
|
13
|
+
const blockQuoteRule = wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote);
|
|
14
|
+
|
|
15
|
+
const orderedListRule = wrappingInputRule(
|
|
16
|
+
/^(\d+)\.\s$/,
|
|
17
|
+
schema.nodes.ordered_list,
|
|
18
|
+
(match) => ({ order: +match[1] }),
|
|
19
|
+
(match, node) => node.childCount + node.attrs.order === +match[1]
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const bulletListRule = wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list);
|
|
23
|
+
|
|
24
|
+
const taskListRule = wrappingInputRule(/^\s*(\[ \])\s$/, schema.nodes.task_list);
|
|
25
|
+
|
|
26
|
+
const codeBlockRule = textblockTypeInputRule(/^```$/, schema.nodes.code_block);
|
|
27
|
+
|
|
28
|
+
const headingRule = textblockTypeInputRule(
|
|
29
|
+
new RegExp("^(#{1,6})\\s$"),
|
|
30
|
+
schema.nodes.heading,
|
|
31
|
+
(match) => ({ level: match[1].length })
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const horizontalRuleInputRule = new InputRule(
|
|
35
|
+
/^(?:---|—-|___\s|\*\*\*\s)$/,
|
|
36
|
+
(state, match, start, end) => {
|
|
37
|
+
const tr = state.tr;
|
|
38
|
+
tr.replaceWith(start - 1, end, schema.nodes.horizontal_rule.create());
|
|
39
|
+
return tr;
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
function markInputRule(regexp: RegExp, markType: MarkType, getAttrs?: (match: RegExpMatchArray) => Record<string, any>) {
|
|
44
|
+
return new InputRule(regexp, (state, match, start, end) => {
|
|
45
|
+
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
|
|
46
|
+
const tr = state.tr;
|
|
47
|
+
if (match[1]) {
|
|
48
|
+
const textStart = start + match[0].indexOf(match[1]);
|
|
49
|
+
const textEnd = textStart + match[1].length;
|
|
50
|
+
if (textEnd < end) tr.delete(textEnd, end);
|
|
51
|
+
if (textStart > start) tr.delete(start, textStart);
|
|
52
|
+
end = start + match[1].length;
|
|
53
|
+
}
|
|
54
|
+
tr.addMark(start, end, markType.create(attrs));
|
|
55
|
+
tr.removeStoredMark(markType);
|
|
56
|
+
return tr;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const strongRule = markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, schema.marks.bold);
|
|
61
|
+
const emRule = markInputRule(/(?:^|[^*_])(?:\*|_)([^*_]+)(?:\*|_)$/, schema.marks.italic);
|
|
62
|
+
const codeRule = markInputRule(/(?:`)([^`]+)(?:`)$/, schema.marks.code);
|
|
63
|
+
const strikeRule = markInputRule(/(?:~~)([^~]+)(?:~~)$/, schema.marks.strike);
|
|
64
|
+
|
|
65
|
+
export const markdownInputRules = inputRules({
|
|
66
|
+
rules: [
|
|
67
|
+
...smartQuotes,
|
|
68
|
+
ellipsis,
|
|
69
|
+
emDash,
|
|
70
|
+
blockQuoteRule,
|
|
71
|
+
orderedListRule,
|
|
72
|
+
bulletListRule,
|
|
73
|
+
codeBlockRule,
|
|
74
|
+
headingRule,
|
|
75
|
+
taskListRule,
|
|
76
|
+
horizontalRuleInputRule,
|
|
77
|
+
strongRule,
|
|
78
|
+
emRule,
|
|
79
|
+
codeRule,
|
|
80
|
+
strikeRule
|
|
81
|
+
],
|
|
82
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
2
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
3
|
+
import { Node } from "prosemirror-model";
|
|
4
|
+
|
|
5
|
+
export const placeholderPluginKey = new PluginKey("placeholderPlugin");
|
|
6
|
+
|
|
7
|
+
function isNodeEmpty(node: Node) {
|
|
8
|
+
const defaultContent = node.type.createAndFill()
|
|
9
|
+
if (!defaultContent) return true
|
|
10
|
+
return node.content.eq(defaultContent.content)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function placeholderPlugin(text: string) {
|
|
14
|
+
return new Plugin({
|
|
15
|
+
key: placeholderPluginKey,
|
|
16
|
+
props: {
|
|
17
|
+
decorations: (state) => {
|
|
18
|
+
const doc = state.doc;
|
|
19
|
+
const decorations: Decoration[] = [];
|
|
20
|
+
const isEmptyDoc = doc.childCount === 1 && doc.firstChild?.isTextblock && doc.firstChild.content.size === 0;
|
|
21
|
+
const { anchor } = state.selection;
|
|
22
|
+
|
|
23
|
+
doc.descendants((node, pos) => {
|
|
24
|
+
const isEmpty = !node.isLeaf && isNodeEmpty(node);
|
|
25
|
+
|
|
26
|
+
if (isEmpty) {
|
|
27
|
+
// Only show placeholder on the node that contains the cursor.
|
|
28
|
+
// For a single-node empty doc, always show it (editor-empty state).
|
|
29
|
+
const nodeEnd = pos + node.nodeSize;
|
|
30
|
+
const hasCursor = anchor >= pos && anchor <= nodeEnd;
|
|
31
|
+
|
|
32
|
+
if (!hasCursor && !isEmptyDoc) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const classes = ["is-empty"];
|
|
37
|
+
if (isEmptyDoc) {
|
|
38
|
+
classes.push("is-editor-empty");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
decorations.push(
|
|
42
|
+
Decoration.node(pos, pos + node.nodeSize, {
|
|
43
|
+
class: classes.join(" "),
|
|
44
|
+
"data-placeholder": text
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return false; // Stop descending
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return DecorationSet.create(doc, decorations);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
|
2
|
+
|
|
3
|
+
export const SlashCommandPluginKey = new PluginKey("slash-command");
|
|
4
|
+
|
|
5
|
+
export interface SlashCommandState {
|
|
6
|
+
active: boolean;
|
|
7
|
+
range?: { from: number; to: number };
|
|
8
|
+
query?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function slashCommandPlugin() {
|
|
12
|
+
return new Plugin({
|
|
13
|
+
key: SlashCommandPluginKey,
|
|
14
|
+
state: {
|
|
15
|
+
init(): SlashCommandState {
|
|
16
|
+
return { active: false };
|
|
17
|
+
},
|
|
18
|
+
apply(tr, value, oldState, newState): SlashCommandState {
|
|
19
|
+
const { selection } = newState;
|
|
20
|
+
if (!(selection instanceof TextSelection) || !selection.empty) {
|
|
21
|
+
return { active: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Make sure we are in a paragraph or heading block, not a code_block for example
|
|
25
|
+
const $anchor = selection.$anchor;
|
|
26
|
+
if ($anchor.parent.type.name === "code_block") {
|
|
27
|
+
return { active: false };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const textBefore = $anchor.parent.textBetween(
|
|
31
|
+
Math.max(0, $anchor.parentOffset - 20),
|
|
32
|
+
$anchor.parentOffset,
|
|
33
|
+
undefined,
|
|
34
|
+
"\ufffc"
|
|
35
|
+
);
|
|
36
|
+
const match = textBefore.match(/(?:\s|^)(\/)([a-zA-Z0-9]*)$/);
|
|
37
|
+
|
|
38
|
+
if (match) {
|
|
39
|
+
// match[1] is the slash, match[2] is the query
|
|
40
|
+
const query = match[2];
|
|
41
|
+
const from = $anchor.pos - query.length - 1;
|
|
42
|
+
const to = $anchor.pos;
|
|
43
|
+
return { active: true, range: { from, to }, query };
|
|
44
|
+
}
|
|
45
|
+
return { active: false };
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|