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