@firecms/core 3.1.0-canary.24c8270 → 3.1.0-canary.75005e4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
  2. package/dist/components/ErrorBoundary.d.ts +3 -1
  3. package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
  4. package/dist/components/LanguageToggle.d.ts +1 -0
  5. package/dist/components/UnsavedChangesDialog.d.ts +1 -0
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  8. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  9. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  10. package/dist/editor/components/editor-bubble.d.ts +8 -0
  11. package/dist/editor/components/index.d.ts +14 -0
  12. package/dist/editor/editor.d.ts +30 -0
  13. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  14. package/dist/editor/extensions/Image/index.d.ts +6 -0
  15. package/dist/editor/extensions/Image.d.ts +6 -0
  16. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  17. package/dist/editor/extensions/clipboard.d.ts +7 -0
  18. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  19. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  20. package/dist/editor/hooks/useProseMirror.d.ts +14 -0
  21. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  22. package/dist/editor/index.d.ts +2 -0
  23. package/dist/editor/markdown.d.ts +5 -0
  24. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  25. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  26. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  27. package/dist/editor/nodeViews/index.d.ts +6 -0
  28. package/dist/editor/plugins/index.d.ts +2 -0
  29. package/dist/editor/plugins/inputrules.d.ts +6 -0
  30. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  31. package/dist/editor/plugins/slashCommandPlugin.d.ts +11 -0
  32. package/dist/editor/schema.d.ts +2 -0
  33. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  34. package/dist/editor/selectors/color-selector.d.ts +10 -0
  35. package/dist/editor/selectors/link-selector.d.ts +8 -0
  36. package/dist/editor/selectors/node-selector.d.ts +15 -0
  37. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  38. package/dist/editor/types.d.ts +5 -0
  39. package/dist/editor/useProseMirror.d.ts +16 -0
  40. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  41. package/dist/editor/utils/remove_classes.d.ts +1 -0
  42. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  43. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  44. package/dist/hooks/index.d.ts +1 -0
  45. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  46. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  47. package/dist/hooks/useTranslation.d.ts +17 -0
  48. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.es.js +11293 -2142
  51. package/dist/index.es.js.map +1 -1
  52. package/dist/index.umd.js +11274 -2142
  53. package/dist/index.umd.js.map +1 -1
  54. package/dist/locales/de.d.ts +2 -0
  55. package/dist/locales/en.d.ts +10 -0
  56. package/dist/locales/es.d.ts +10 -0
  57. package/dist/locales/fr.d.ts +2 -0
  58. package/dist/locales/hi.d.ts +2 -0
  59. package/dist/locales/it.d.ts +2 -0
  60. package/dist/locales/pt.d.ts +7 -0
  61. package/dist/types/customization_controller.d.ts +2 -1
  62. package/dist/types/firecms.d.ts +2 -1
  63. package/dist/types/index.d.ts +1 -0
  64. package/dist/types/navigation.d.ts +2 -2
  65. package/dist/types/plugins.d.ts +7 -0
  66. package/dist/types/translations.d.ts +646 -0
  67. package/package.json +43 -9
  68. package/src/app/Scaffold.tsx +7 -5
  69. package/src/components/AIIcon.tsx +3 -1
  70. package/src/components/ArrayContainer.tsx +6 -4
  71. package/src/components/ClearFilterSortButton.tsx +6 -3
  72. package/src/components/ConfirmationDialog.tsx +4 -2
  73. package/src/components/DeleteEntityDialog.tsx +10 -7
  74. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  75. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  76. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  77. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  78. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  79. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  80. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  81. package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
  82. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  83. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  84. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  85. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  86. package/src/components/EntityView.tsx +3 -2
  87. package/src/components/ErrorBoundary.tsx +27 -15
  88. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  89. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  90. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  91. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  92. package/src/components/LanguageToggle.tsx +66 -0
  93. package/src/components/NotFoundPage.tsx +5 -3
  94. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  95. package/src/components/ReferenceWidget.tsx +3 -2
  96. package/src/components/SearchIconsView.tsx +3 -1
  97. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  98. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  99. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  100. package/src/components/UnsavedChangesDialog.tsx +6 -4
  101. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  102. package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
  103. package/src/components/common/default_entity_actions.tsx +4 -0
  104. package/src/components/common/useDataSourceTableController.tsx +5 -14
  105. package/src/components/index.tsx +1 -0
  106. package/src/core/DefaultAppBar.tsx +14 -10
  107. package/src/core/DefaultDrawer.tsx +8 -2
  108. package/src/core/DrawerNavigationGroup.tsx +5 -3
  109. package/src/core/EntityEditView.tsx +3 -2
  110. package/src/core/EntityEditViewFormActions.tsx +24 -17
  111. package/src/core/EntitySidePanel.tsx +4 -3
  112. package/src/core/FireCMS.tsx +33 -6
  113. package/src/editor/components/SlashCommandMenu.tsx +348 -0
  114. package/src/editor/components/editor-bubble-item.tsx +32 -0
  115. package/src/editor/components/editor-bubble.tsx +118 -0
  116. package/src/editor/components/index.ts +12 -0
  117. package/src/editor/editor.tsx +307 -0
  118. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  119. package/src/editor/extensions/Image/index.ts +133 -0
  120. package/src/editor/extensions/Image.ts +144 -0
  121. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  122. package/src/editor/extensions/clipboard.ts +72 -0
  123. package/src/editor/extensions/custom-keymap.ts +24 -0
  124. package/src/editor/extensions/drag-and-drop.tsx +472 -0
  125. package/src/editor/hooks/useProseMirror.ts +115 -0
  126. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  127. package/src/editor/index.ts +2 -0
  128. package/src/editor/markdown.ts +110 -0
  129. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  130. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  131. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  132. package/src/editor/nodeViews/index.ts +35 -0
  133. package/src/editor/plugins/index.ts +55 -0
  134. package/src/editor/plugins/inputrules.ts +82 -0
  135. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  136. package/src/editor/plugins/slashCommandPlugin.ts +49 -0
  137. package/src/editor/schema.ts +228 -0
  138. package/src/editor/selectors/ai-selector.tsx +111 -0
  139. package/src/editor/selectors/color-selector.tsx +200 -0
  140. package/src/editor/selectors/link-selector.tsx +118 -0
  141. package/src/editor/selectors/node-selector.tsx +157 -0
  142. package/src/editor/selectors/text-buttons.tsx +86 -0
  143. package/src/editor/types.ts +6 -0
  144. package/src/editor/useProseMirror.ts +126 -0
  145. package/src/editor/utils/prosemirror-utils.ts +78 -0
  146. package/src/editor/utils/remove_classes.ts +17 -0
  147. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  148. package/src/form/EntityForm.tsx +7 -3
  149. package/src/form/EntityFormActions.tsx +19 -12
  150. package/src/form/PropertyFieldBinding.tsx +3 -2
  151. package/src/form/components/LocalChangesMenu.tsx +13 -13
  152. package/src/form/components/StorageItemPreview.tsx +3 -2
  153. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  154. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  155. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  156. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  157. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -3
  158. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  159. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -2
  160. package/src/hooks/index.tsx +1 -0
  161. package/src/hooks/useBuildNavigationController.tsx +20 -13
  162. package/src/hooks/useCollapsedGroups.ts +7 -6
  163. package/src/hooks/useTranslation.ts +31 -0
  164. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  165. package/src/index.ts +4 -0
  166. package/src/locales/de.ts +691 -0
  167. package/src/locales/en.ts +703 -0
  168. package/src/locales/es.ts +703 -0
  169. package/src/locales/fr.ts +691 -0
  170. package/src/locales/hi.ts +691 -0
  171. package/src/locales/it.ts +691 -0
  172. package/src/locales/pt.ts +700 -0
  173. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  174. package/src/preview/components/UserPreview.tsx +3 -1
  175. package/src/types/customization_controller.tsx +2 -1
  176. package/src/types/firecms.tsx +2 -1
  177. package/src/types/index.ts +1 -0
  178. package/src/types/navigation.ts +2 -2
  179. package/src/types/plugins.tsx +8 -0
  180. package/src/types/translations.ts +725 -0
@@ -0,0 +1,115 @@
1
+ import { useState, useEffect, useRef, useLayoutEffect } from "react";
2
+ import { EditorState, Transaction } from "prosemirror-state";
3
+ import { EditorView } from "prosemirror-view";
4
+ import { schema } from "../schema";
5
+ import { corePlugins } from "../plugins";
6
+ import { parser } from "../markdown";
7
+ import { nodeViews } from "../nodeViews";
8
+ import { createDropImagePlugin } from "../extensions/Image";
9
+
10
+ interface UseProseMirrorProps {
11
+ initialContent?: string | any;
12
+ onChange?: (state: EditorState, view: EditorView) => void;
13
+ editable?: boolean;
14
+ handleImageUpload?: (file: File) => Promise<string>;
15
+ }
16
+
17
+ export function useProseMirror({ initialContent, onChange, editable = true, handleImageUpload }: UseProseMirrorProps) {
18
+ // Store onChange in a ref so that the latest version is always called inside
19
+ // the dispatchTransaction closure (which only runs once at mount with [] deps).
20
+ const onChangeRef = useRef(onChange);
21
+ onChangeRef.current = onChange;
22
+
23
+ const plugins = [...corePlugins];
24
+ if (handleImageUpload) {
25
+ plugins.push(createDropImagePlugin(handleImageUpload));
26
+ }
27
+
28
+ const defaultState = EditorState.create({
29
+ doc: typeof initialContent === "string"
30
+ ? parser.parse(initialContent)
31
+ : initialContent
32
+ ? schema.nodeFromJSON(initialContent)
33
+ : schema.node("doc", null, [schema.node("paragraph")]),
34
+ schema,
35
+ plugins
36
+ });
37
+
38
+ const [state, setState] = useState<EditorState>(defaultState);
39
+ const [view, setView] = useState<EditorView | null>(null);
40
+
41
+ const editorRef = useRef<HTMLDivElement>(null);
42
+ const viewRef = useRef<EditorView | null>(null);
43
+
44
+ useLayoutEffect(() => {
45
+ if (!editorRef.current) return;
46
+
47
+ const editorView = new EditorView(editorRef.current, {
48
+ state: defaultState,
49
+ editable: () => editable,
50
+ dispatchTransaction: (tr: Transaction) => {
51
+ const newState = editorView.state.apply(tr);
52
+ editorView.updateState(newState);
53
+ setState(newState);
54
+ onChangeRef.current?.(newState, editorView);
55
+ },
56
+ nodeViews: nodeViews,
57
+ transformPastedHTML(html: string) {
58
+ // Strip inline styles and classes from pasted HTML so we don't
59
+ // get textStyle marks (color, font-size, etc.) that have no
60
+ // markdown representation. This makes paste look consistent.
61
+ const div = document.createElement("div");
62
+ div.innerHTML = html;
63
+ div.querySelectorAll("*").forEach((el) => {
64
+ el.removeAttribute("style");
65
+ el.removeAttribute("class");
66
+ el.removeAttribute("color");
67
+ el.removeAttribute("bgcolor");
68
+ el.removeAttribute("face");
69
+ });
70
+ return div.innerHTML;
71
+ },
72
+ });
73
+
74
+ // Patch posAtCoords to allow dropping/interacting anywhere horizontally natively
75
+ const originalPosAtCoords = editorView.posAtCoords.bind(editorView);
76
+ editorView.posAtCoords = (coords: { left: number, top: number }) => {
77
+ let res = originalPosAtCoords(coords);
78
+ if (!res) {
79
+ const editorRect = editorView.dom.getBoundingClientRect();
80
+ // If it's literally anywhere to the left of the actual ProseMirror content block
81
+ if (coords.left <= editorRect.left) {
82
+ const probeX = editorRect.left + Math.min(60, editorRect.width / 4);
83
+ return originalPosAtCoords({ left: probeX, top: coords.top });
84
+ }
85
+ // Or if it's anywhere to the right
86
+ if (coords.left >= editorRect.right) {
87
+ const probeX = editorRect.right - Math.min(60, editorRect.width / 4);
88
+ return originalPosAtCoords({ left: probeX, top: coords.top });
89
+ }
90
+ }
91
+ return res;
92
+ };
93
+
94
+ viewRef.current = editorView;
95
+ setView(editorView);
96
+
97
+ return () => {
98
+ editorView.destroy();
99
+ viewRef.current = null;
100
+ };
101
+ }, []);
102
+
103
+ // Effect to update editable status without re-mounting
104
+ useEffect(() => {
105
+ if (viewRef.current) {
106
+ viewRef.current.setProps({ editable: () => editable });
107
+ }
108
+ }, [editable]);
109
+
110
+ return {
111
+ state,
112
+ view,
113
+ editorRef
114
+ };
115
+ }
@@ -0,0 +1,15 @@
1
+ import React, { createContext, useContext } from "react";
2
+ import { EditorState } from "prosemirror-state";
3
+ import { EditorView } from "prosemirror-view";
4
+
5
+ export interface ProseMirrorContextType {
6
+ state: EditorState | null;
7
+ view: EditorView | null;
8
+ }
9
+
10
+ export const ProseMirrorContext = createContext<ProseMirrorContextType>({
11
+ state: null,
12
+ view: null,
13
+ });
14
+
15
+ export const useProseMirrorContext = () => useContext(ProseMirrorContext);
@@ -0,0 +1,2 @@
1
+ export * from "./editor";
2
+ export * from "./types";
@@ -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("![" + state.esc(node.attrs.alt || "") + "](" + src.replace(/[\(\)]/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
+ }