@firecms/core 3.1.0 → 3.2.0-canary.9c3d298

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 (191) 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/image-bubble.d.ts +5 -0
  12. package/dist/editor/components/index.d.ts +16 -0
  13. package/dist/editor/components/table-bubble.d.ts +5 -0
  14. package/dist/editor/editor.d.ts +30 -0
  15. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  16. package/dist/editor/extensions/Image/index.d.ts +6 -0
  17. package/dist/editor/extensions/Image.d.ts +6 -0
  18. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  19. package/dist/editor/extensions/clipboard.d.ts +7 -0
  20. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  21. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  22. package/dist/editor/hooks/useProseMirror.d.ts +13 -0
  23. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  24. package/dist/editor/index.d.ts +2 -0
  25. package/dist/editor/markdown.d.ts +5 -0
  26. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  27. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  28. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  29. package/dist/editor/nodeViews/index.d.ts +6 -0
  30. package/dist/editor/plugins/index.d.ts +2 -0
  31. package/dist/editor/plugins/inputrules.d.ts +6 -0
  32. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  33. package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
  34. package/dist/editor/schema.d.ts +2 -0
  35. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  36. package/dist/editor/selectors/color-selector.d.ts +10 -0
  37. package/dist/editor/selectors/link-selector.d.ts +8 -0
  38. package/dist/editor/selectors/node-selector.d.ts +15 -0
  39. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  40. package/dist/editor/types.d.ts +5 -0
  41. package/dist/editor/useProseMirror.d.ts +16 -0
  42. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  43. package/dist/editor/utils/remove_classes.d.ts +1 -0
  44. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  45. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  46. package/dist/hooks/index.d.ts +1 -0
  47. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  48. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  49. package/dist/hooks/useTranslation.d.ts +17 -0
  50. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.es.js +12898 -2265
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/index.umd.js +12877 -2264
  55. package/dist/index.umd.js.map +1 -1
  56. package/dist/locales/de.d.ts +2 -0
  57. package/dist/locales/en.d.ts +10 -0
  58. package/dist/locales/es.d.ts +10 -0
  59. package/dist/locales/fr.d.ts +2 -0
  60. package/dist/locales/hi.d.ts +2 -0
  61. package/dist/locales/it.d.ts +2 -0
  62. package/dist/locales/pt.d.ts +7 -0
  63. package/dist/types/customization_controller.d.ts +2 -1
  64. package/dist/types/firecms.d.ts +2 -1
  65. package/dist/types/index.d.ts +1 -0
  66. package/dist/types/navigation.d.ts +2 -2
  67. package/dist/types/plugins.d.ts +7 -0
  68. package/dist/types/storage.d.ts +1 -0
  69. package/dist/types/translations.d.ts +646 -0
  70. package/dist/util/useStorageUploadController.d.ts +10 -1
  71. package/package.json +45 -9
  72. package/src/app/Scaffold.tsx +7 -5
  73. package/src/components/AIIcon.tsx +3 -1
  74. package/src/components/ArrayContainer.tsx +6 -4
  75. package/src/components/ClearFilterSortButton.tsx +6 -3
  76. package/src/components/ConfirmationDialog.tsx +4 -2
  77. package/src/components/DeleteEntityDialog.tsx +10 -7
  78. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  79. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  81. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  82. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  83. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  84. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  85. package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
  86. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  87. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  88. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  89. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  90. package/src/components/EntityView.tsx +3 -2
  91. package/src/components/ErrorBoundary.tsx +27 -15
  92. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  93. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  94. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  95. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  96. package/src/components/LanguageToggle.tsx +66 -0
  97. package/src/components/NotFoundPage.tsx +5 -3
  98. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  99. package/src/components/ReferenceWidget.tsx +3 -2
  100. package/src/components/SearchIconsView.tsx +3 -1
  101. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  102. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  103. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  104. package/src/components/UnsavedChangesDialog.tsx +6 -4
  105. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  106. package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
  107. package/src/components/common/default_entity_actions.tsx +4 -0
  108. package/src/components/common/useDataSourceTableController.tsx +12 -4
  109. package/src/components/index.tsx +1 -0
  110. package/src/core/DefaultAppBar.tsx +14 -10
  111. package/src/core/DefaultDrawer.tsx +8 -2
  112. package/src/core/DrawerNavigationGroup.tsx +5 -3
  113. package/src/core/EntityEditView.tsx +4 -3
  114. package/src/core/EntityEditViewFormActions.tsx +24 -17
  115. package/src/core/EntitySidePanel.tsx +6 -5
  116. package/src/core/FireCMS.tsx +33 -6
  117. package/src/editor/components/SlashCommandMenu.tsx +516 -0
  118. package/src/editor/components/editor-bubble-item.tsx +32 -0
  119. package/src/editor/components/editor-bubble.tsx +118 -0
  120. package/src/editor/components/image-bubble.tsx +156 -0
  121. package/src/editor/components/index.ts +14 -0
  122. package/src/editor/components/table-bubble.tsx +165 -0
  123. package/src/editor/editor.tsx +455 -0
  124. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  125. package/src/editor/extensions/Image/index.ts +133 -0
  126. package/src/editor/extensions/Image.ts +159 -0
  127. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  128. package/src/editor/extensions/clipboard.ts +72 -0
  129. package/src/editor/extensions/custom-keymap.ts +24 -0
  130. package/src/editor/extensions/drag-and-drop.tsx +480 -0
  131. package/src/editor/hooks/useProseMirror.ts +124 -0
  132. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  133. package/src/editor/index.ts +2 -0
  134. package/src/editor/markdown.ts +172 -0
  135. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  136. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  137. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  138. package/src/editor/nodeViews/index.ts +35 -0
  139. package/src/editor/plugins/index.ts +58 -0
  140. package/src/editor/plugins/inputrules.ts +82 -0
  141. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  142. package/src/editor/plugins/slashCommandPlugin.ts +61 -0
  143. package/src/editor/schema.ts +240 -0
  144. package/src/editor/selectors/ai-selector.tsx +111 -0
  145. package/src/editor/selectors/color-selector.tsx +200 -0
  146. package/src/editor/selectors/link-selector.tsx +118 -0
  147. package/src/editor/selectors/node-selector.tsx +157 -0
  148. package/src/editor/selectors/text-buttons.tsx +86 -0
  149. package/src/editor/types.ts +6 -0
  150. package/src/editor/useProseMirror.ts +126 -0
  151. package/src/editor/utils/prosemirror-utils.ts +108 -0
  152. package/src/editor/utils/remove_classes.ts +17 -0
  153. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  154. package/src/form/EntityForm.tsx +16 -3
  155. package/src/form/EntityFormActions.tsx +19 -12
  156. package/src/form/PropertyFieldBinding.tsx +3 -2
  157. package/src/form/components/LocalChangesMenu.tsx +13 -13
  158. package/src/form/components/StorageItemPreview.tsx +3 -2
  159. package/src/form/components/StorageUploadProgress.tsx +18 -3
  160. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  161. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  162. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  163. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  164. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +33 -19
  165. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  166. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -3
  167. package/src/hooks/index.tsx +1 -0
  168. package/src/hooks/useBuildNavigationController.tsx +45 -18
  169. package/src/hooks/useCollapsedGroups.ts +7 -6
  170. package/src/hooks/useTranslation.ts +31 -0
  171. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  172. package/src/index.ts +4 -0
  173. package/src/internal/useBuildSideEntityController.tsx +22 -20
  174. package/src/locales/de.ts +691 -0
  175. package/src/locales/en.ts +703 -0
  176. package/src/locales/es.ts +703 -0
  177. package/src/locales/fr.ts +691 -0
  178. package/src/locales/hi.ts +691 -0
  179. package/src/locales/it.ts +691 -0
  180. package/src/locales/pt.ts +700 -0
  181. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  182. package/src/preview/components/UserPreview.tsx +3 -1
  183. package/src/types/customization_controller.tsx +2 -1
  184. package/src/types/firecms.tsx +2 -1
  185. package/src/types/index.ts +1 -0
  186. package/src/types/navigation.ts +2 -2
  187. package/src/types/plugins.tsx +8 -0
  188. package/src/types/properties.ts +1 -0
  189. package/src/types/storage.ts +2 -1
  190. package/src/types/translations.ts +725 -0
  191. package/src/util/useStorageUploadController.tsx +23 -29
@@ -0,0 +1,172 @@
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
+ table: { block: "table" },
40
+ thead: { ignore: true },
41
+ tbody: { ignore: true },
42
+ tr: { block: "table_row" },
43
+ th: { block: "table_header" },
44
+ td: { block: "table_cell" }
45
+ };
46
+
47
+ const md = markdownIt({ html: false })
48
+ .use(markdownItTaskLists)
49
+ .use(markdownItMark)
50
+ .use(markdownItIns);
51
+
52
+ // Unwrap images from paragraphs so they can be parsed as block nodes by ProseMirror
53
+ md.core.ruler.after("inline", "image-to-block", (state: any) => {
54
+ const tokens = state.tokens;
55
+ for (let i = tokens.length - 2; i >= 1; i--) {
56
+ if (
57
+ tokens[i - 1] && tokens[i - 1].type === "paragraph_open" &&
58
+ tokens[i] && tokens[i].type === "inline" &&
59
+ tokens[i + 1] && tokens[i + 1].type === "paragraph_close"
60
+ ) {
61
+ const inlineTokens = tokens[i].children || [];
62
+ if (inlineTokens.length === 1 && inlineTokens[0].type === "image") {
63
+ state.tokens.splice(i - 1, 3, inlineTokens[0]);
64
+ // No need to adjust index when looping backward!
65
+ }
66
+ }
67
+ }
68
+ });
69
+
70
+ // Wrap inline tokens inside table cells into paragraphs to satisfy ProseMirror table cell schema (block+)
71
+ md.core.ruler.after("inline", "tables-wrap-paragraphs", (state: any) => {
72
+ const tokens = state.tokens;
73
+ for (let i = tokens.length - 1; i >= 0; i--) {
74
+ if (tokens[i].type === "td_open" || tokens[i].type === "th_open") {
75
+ let closeIndex = i + 1;
76
+ while (closeIndex < tokens.length && tokens[closeIndex].type !== "td_close" && tokens[closeIndex].type !== "th_close") {
77
+ closeIndex++;
78
+ }
79
+ if (closeIndex < tokens.length) {
80
+ const pOpen = new state.Token("paragraph_open", "p", 1);
81
+ pOpen.block = true;
82
+ const pClose = new state.Token("paragraph_close", "p", -1);
83
+ pClose.block = true;
84
+
85
+ state.tokens.splice(closeIndex, 0, pClose);
86
+ state.tokens.splice(i + 1, 0, pOpen);
87
+ }
88
+ }
89
+ }
90
+ });
91
+
92
+ export const markdownParser = new MarkdownParser(schema, md, parserTokens);
93
+
94
+
95
+ export const markdownSerializer = new MarkdownSerializer(
96
+ {
97
+ ...defaultMarkdownSerializer.nodes,
98
+ // Add custom serialization for task lists
99
+ task_list(state, node) {
100
+ state.renderList(node, " ", () => "- ");
101
+ },
102
+ task_item(state, node) {
103
+ state.write(`[${node.attrs.checked ? "x" : " "}] `);
104
+ state.renderContent(node);
105
+ },
106
+ horizontal_rule(state, node) {
107
+ state.write(node.attrs.markup || "---");
108
+ state.closeBlock(node);
109
+ },
110
+ image(state, node) {
111
+ const rawSrc = node.attrs.src || "";
112
+ const src = rawSrc.replace(/ /g, "%20");
113
+ state.write("![" + state.esc(node.attrs.alt || "") + "](" + src.replace(/[\(\)]/g, "\\$&") +
114
+ (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")");
115
+ state.closeBlock(node);
116
+ },
117
+ table(state, node) {
118
+ node.forEach((row, _, i) => {
119
+ row.forEach((cell, _, j) => {
120
+ state.write(j === 0 ? "| " : " ");
121
+ let cellContent = "";
122
+ const oldWrite = state.write.bind(state);
123
+ state.write = (s: string) => { cellContent += s; };
124
+
125
+ let first = true;
126
+ cell.forEach((block: any) => {
127
+ if (!first) cellContent += "<br>";
128
+ state.renderInline(block);
129
+ first = false;
130
+ });
131
+
132
+ state.write = oldWrite;
133
+ state.write(cellContent.replace(/\|/g, "\\|"));
134
+ state.write(" |");
135
+ });
136
+ state.write("\n");
137
+ if (i === 0) {
138
+ row.forEach((cell, _, j) => {
139
+ state.write(j === 0 ? "|---|" : "---|");
140
+ });
141
+ state.write("\n");
142
+ }
143
+ });
144
+ state.closeBlock(node);
145
+ },
146
+ table_row() {},
147
+ table_cell() {},
148
+ table_header() {}
149
+ },
150
+ {
151
+ ...defaultMarkdownSerializer.marks,
152
+ bold: defaultMarkdownSerializer.marks.strong,
153
+ italic: defaultMarkdownSerializer.marks.em,
154
+ strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true },
155
+ highlight: { open: "==", close: "==", mixable: true, expelEnclosingWhitespace: true },
156
+ underline: { open: "++", close: "++", mixable: true, expelEnclosingWhitespace: true },
157
+ link: {
158
+ ...defaultMarkdownSerializer.marks.link,
159
+ close(state: any, mark, parent, index) {
160
+ const inAutolink = state.inAutolink;
161
+ state.inAutolink = undefined;
162
+ const href = mark.attrs.href.replace(/ /g, "%20");
163
+ return inAutolink ? ">"
164
+ : "](" + href.replace(/[\(\)"]/g, "\\$&") + (mark.attrs.title ? ` "${mark.attrs.title.replace(/"/g, '\\"')}"` : "") + ")";
165
+ }
166
+ },
167
+ // textStyle (colored text from HTML) has no markdown equivalent — emit content as-is
168
+ textStyle: { open: "", close: "", mixable: true, expelEnclosingWhitespace: true },
169
+ }
170
+ );
171
+ export const parser = markdownParser;
172
+ 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,58 @@
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
+ import { goToNextCell } from "prosemirror-tables";
16
+
17
+ const customKeymap = {
18
+ "Tab": goToNextCell(1),
19
+ "Shift-Tab": goToNextCell(-1),
20
+ "Mod-z": undo,
21
+ "Mod-y": redo,
22
+ "Shift-Mod-z": redo,
23
+ "Mod-b": toggleMark(schema.marks.bold),
24
+ "Mod-i": toggleMark(schema.marks.italic),
25
+ "Mod-u": toggleMark(schema.marks.underline),
26
+ "Mod-Shift-s": toggleMark(schema.marks.strike),
27
+ "Mod-e": toggleMark(schema.marks.code),
28
+ "Mod-Shift-h": toggleMark(schema.marks.highlight),
29
+
30
+ "Enter": splitListItem(schema.nodes.list_item),
31
+ "Shift-Enter": splitListItem(schema.nodes.task_item),
32
+
33
+ "Mod-[": liftListItem(schema.nodes.list_item),
34
+ "Mod-]": sinkListItem(schema.nodes.list_item),
35
+
36
+ "Shift-Mod-8": setBlockType(schema.nodes.bullet_list),
37
+ "Shift-Mod-9": setBlockType(schema.nodes.ordered_list),
38
+
39
+ "Mod-Alt-1": setBlockType(schema.nodes.heading, { level: 1 }),
40
+ "Mod-Alt-2": setBlockType(schema.nodes.heading, { level: 2 }),
41
+ "Mod-Alt-3": setBlockType(schema.nodes.heading, { level: 3 }),
42
+
43
+ "Mod-Alt-0": setBlockType(schema.nodes.paragraph),
44
+ };
45
+
46
+ export const corePlugins: Plugin[] = [
47
+ history(),
48
+ keymap(customKeymap),
49
+ keymap(baseKeymap),
50
+ globalDragDropPlugin(),
51
+ gapCursor(),
52
+ slashCommandPlugin(),
53
+ dragHandlePlugin(),
54
+ highlightDecorationPlugin(),
55
+ textLoadingDecorationPlugin(),
56
+ markdownInputRules,
57
+ placeholderPlugin("Press '/' for commands")
58
+ ];
@@ -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,61 @@
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
+ dismissed?: boolean;
10
+ }
11
+
12
+ export function slashCommandPlugin() {
13
+ return new Plugin({
14
+ key: SlashCommandPluginKey,
15
+ state: {
16
+ init(): SlashCommandState {
17
+ return { active: false };
18
+ },
19
+ apply(tr, value, oldState, newState): SlashCommandState {
20
+ const meta = tr.getMeta(SlashCommandPluginKey);
21
+ if (meta !== undefined) {
22
+ return meta;
23
+ }
24
+
25
+ const { selection } = newState;
26
+ if (!(selection instanceof TextSelection) || !selection.empty) {
27
+ return { active: false };
28
+ }
29
+
30
+ // Make sure we are in a paragraph or heading block, not a code_block for example
31
+ const $anchor = selection.$anchor;
32
+ if ($anchor.parent.type.name === "code_block") {
33
+ return { active: false };
34
+ }
35
+
36
+ const textBefore = $anchor.parent.textBetween(
37
+ Math.max(0, $anchor.parentOffset - 20),
38
+ $anchor.parentOffset,
39
+ undefined,
40
+ "\ufffc"
41
+ );
42
+ const match = textBefore.match(/(?:\s|^)(\/)([a-zA-Z0-9]*)$/);
43
+
44
+ if (!match) {
45
+ return { active: false };
46
+ }
47
+
48
+ // If the user previously dismissed this slash command, keep it dismissed
49
+ if (value.dismissed) {
50
+ return { active: false, dismissed: true };
51
+ }
52
+
53
+ // match[1] is the slash, match[2] is the query
54
+ const query = match[2];
55
+ const from = $anchor.pos - query.length - 1;
56
+ const to = $anchor.pos;
57
+ return { active: true, range: { from, to }, query };
58
+ },
59
+ }
60
+ });
61
+ }