@firecms/core 3.1.0-canary.24c8270 → 3.1.0-canary.75005e4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
- package/dist/components/ErrorBoundary.d.ts +3 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
- package/dist/components/LanguageToggle.d.ts +1 -0
- package/dist/components/UnsavedChangesDialog.d.ts +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/core/DrawerNavigationGroup.d.ts +2 -2
- package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
- package/dist/editor/components/editor-bubble-item.d.ts +8 -0
- package/dist/editor/components/editor-bubble.d.ts +8 -0
- package/dist/editor/components/index.d.ts +14 -0
- package/dist/editor/editor.d.ts +30 -0
- package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
- package/dist/editor/extensions/Image/index.d.ts +6 -0
- package/dist/editor/extensions/Image.d.ts +6 -0
- package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
- package/dist/editor/extensions/clipboard.d.ts +7 -0
- package/dist/editor/extensions/custom-keymap.d.ts +1 -0
- package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
- package/dist/editor/hooks/useProseMirror.d.ts +14 -0
- package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/markdown.d.ts +5 -0
- package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
- package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
- package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
- package/dist/editor/nodeViews/index.d.ts +6 -0
- package/dist/editor/plugins/index.d.ts +2 -0
- package/dist/editor/plugins/inputrules.d.ts +6 -0
- package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
- package/dist/editor/plugins/slashCommandPlugin.d.ts +11 -0
- package/dist/editor/schema.d.ts +2 -0
- package/dist/editor/selectors/ai-selector.d.ts +0 -0
- package/dist/editor/selectors/color-selector.d.ts +10 -0
- package/dist/editor/selectors/link-selector.d.ts +8 -0
- package/dist/editor/selectors/node-selector.d.ts +15 -0
- package/dist/editor/selectors/text-buttons.d.ts +1 -0
- package/dist/editor/types.d.ts +5 -0
- package/dist/editor/useProseMirror.d.ts +16 -0
- package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
- package/dist/editor/utils/remove_classes.d.ts +1 -0
- package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +0 -1
- package/dist/hooks/useCollapsedGroups.d.ts +3 -3
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +11293 -2142
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +11274 -2142
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/types/customization_controller.d.ts +2 -1
- package/dist/types/firecms.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/navigation.d.ts +2 -2
- package/dist/types/plugins.d.ts +7 -0
- package/dist/types/translations.d.ts +646 -0
- package/package.json +43 -9
- package/src/app/Scaffold.tsx +7 -5
- package/src/components/AIIcon.tsx +3 -1
- package/src/components/ArrayContainer.tsx +6 -4
- package/src/components/ClearFilterSortButton.tsx +6 -3
- package/src/components/ConfirmationDialog.tsx +4 -2
- package/src/components/DeleteEntityDialog.tsx +10 -7
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
- package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
- package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
- package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
- package/src/components/EntityView.tsx +3 -2
- package/src/components/ErrorBoundary.tsx +27 -15
- package/src/components/HomePage/DefaultHomePage.tsx +19 -13
- package/src/components/HomePage/HomePageDnD.tsx +3 -1
- package/src/components/HomePage/NavigationGroup.tsx +3 -1
- package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
- package/src/components/LanguageToggle.tsx +66 -0
- package/src/components/NotFoundPage.tsx +5 -3
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
- package/src/components/ReferenceWidget.tsx +3 -2
- package/src/components/SearchIconsView.tsx +3 -1
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
- package/src/components/UnsavedChangesDialog.tsx +6 -4
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
- package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
- package/src/components/common/default_entity_actions.tsx +4 -0
- package/src/components/common/useDataSourceTableController.tsx +5 -14
- package/src/components/index.tsx +1 -0
- package/src/core/DefaultAppBar.tsx +14 -10
- package/src/core/DefaultDrawer.tsx +8 -2
- package/src/core/DrawerNavigationGroup.tsx +5 -3
- package/src/core/EntityEditView.tsx +3 -2
- package/src/core/EntityEditViewFormActions.tsx +24 -17
- package/src/core/EntitySidePanel.tsx +4 -3
- package/src/core/FireCMS.tsx +33 -6
- package/src/editor/components/SlashCommandMenu.tsx +348 -0
- package/src/editor/components/editor-bubble-item.tsx +32 -0
- package/src/editor/components/editor-bubble.tsx +118 -0
- package/src/editor/components/index.ts +12 -0
- package/src/editor/editor.tsx +307 -0
- package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
- package/src/editor/extensions/Image/index.ts +133 -0
- package/src/editor/extensions/Image.ts +144 -0
- package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
- package/src/editor/extensions/clipboard.ts +72 -0
- package/src/editor/extensions/custom-keymap.ts +24 -0
- package/src/editor/extensions/drag-and-drop.tsx +472 -0
- package/src/editor/hooks/useProseMirror.ts +115 -0
- package/src/editor/hooks/useProseMirrorContext.ts +15 -0
- package/src/editor/index.ts +2 -0
- package/src/editor/markdown.ts +110 -0
- package/src/editor/nodeViews/ImageComponent.tsx +20 -0
- package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
- package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
- package/src/editor/nodeViews/index.ts +35 -0
- package/src/editor/plugins/index.ts +55 -0
- package/src/editor/plugins/inputrules.ts +82 -0
- package/src/editor/plugins/placeholderPlugin.ts +55 -0
- package/src/editor/plugins/slashCommandPlugin.ts +49 -0
- package/src/editor/schema.ts +228 -0
- package/src/editor/selectors/ai-selector.tsx +111 -0
- package/src/editor/selectors/color-selector.tsx +200 -0
- package/src/editor/selectors/link-selector.tsx +118 -0
- package/src/editor/selectors/node-selector.tsx +157 -0
- package/src/editor/selectors/text-buttons.tsx +86 -0
- package/src/editor/types.ts +6 -0
- package/src/editor/useProseMirror.ts +126 -0
- package/src/editor/utils/prosemirror-utils.ts +78 -0
- package/src/editor/utils/remove_classes.ts +17 -0
- package/src/editor/utils/useDebouncedCallback.ts +25 -0
- package/src/form/EntityForm.tsx +7 -3
- package/src/form/EntityFormActions.tsx +19 -12
- package/src/form/PropertyFieldBinding.tsx +3 -2
- package/src/form/components/LocalChangesMenu.tsx +13 -13
- package/src/form/components/StorageItemPreview.tsx +3 -2
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
- package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
- package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -3
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -2
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +20 -13
- package/src/hooks/useCollapsedGroups.ts +7 -6
- package/src/hooks/useTranslation.ts +31 -0
- package/src/i18n/FireCMSi18nProvider.tsx +160 -0
- package/src/index.ts +4 -0
- package/src/locales/de.ts +691 -0
- package/src/locales/en.ts +703 -0
- package/src/locales/es.ts +703 -0
- package/src/locales/fr.ts +691 -0
- package/src/locales/hi.ts +691 -0
- package/src/locales/it.ts +691 -0
- package/src/locales/pt.ts +700 -0
- package/src/preview/components/UrlComponentPreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +3 -1
- package/src/types/customization_controller.tsx +2 -1
- package/src/types/firecms.tsx +2 -1
- package/src/types/index.ts +1 -0
- package/src/types/navigation.ts +2 -2
- package/src/types/plugins.tsx +8 -0
- package/src/types/translations.ts +725 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import { cls, defaultBorderMixin, Separator, useInjectStyles } from "@firecms/ui";
|
|
4
|
+
import { useTranslation } from "../hooks/useTranslation";
|
|
5
|
+
import { EditorBubble, SlashCommandMenu, type JSONContent } from "./components";
|
|
6
|
+
import { NodeSelector } from "./selectors/node-selector";
|
|
7
|
+
import { LinkSelector } from "./selectors/link-selector";
|
|
8
|
+
import { TextButtons } from "./selectors/text-buttons";
|
|
9
|
+
import { removeClassesFromJson } from "./utils/remove_classes";
|
|
10
|
+
import { parser, serializer } from "./markdown";
|
|
11
|
+
import { EditorAIController } from "./types";
|
|
12
|
+
import { useProseMirror } from "./hooks/useProseMirror";
|
|
13
|
+
import { ProseMirrorContext } from "./hooks/useProseMirrorContext";
|
|
14
|
+
import { highlightCommands } from "./extensions/HighlightDecorationExtension";
|
|
15
|
+
import { schema } from "./schema";
|
|
16
|
+
|
|
17
|
+
export type CustomEditorComponent = {
|
|
18
|
+
name: string,
|
|
19
|
+
component: React.FC
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface MarkdownEditorConfig {
|
|
23
|
+
html?: boolean;
|
|
24
|
+
transformPastedText?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FireCMSEditorTextSize = "sm" | "base" | "lg";
|
|
28
|
+
|
|
29
|
+
export type FireCMSEditorProps = {
|
|
30
|
+
content?: JSONContent | string,
|
|
31
|
+
onMarkdownContentChange?: (content: string) => void,
|
|
32
|
+
onJsonContentChange?: (content: JSONContent | null) => void,
|
|
33
|
+
onHtmlContentChange?: (content: string) => void,
|
|
34
|
+
handleImageUpload: (file: File) => Promise<string>,
|
|
35
|
+
version?: number,
|
|
36
|
+
textSize?: FireCMSEditorTextSize,
|
|
37
|
+
highlight?: { from: number, to: number },
|
|
38
|
+
aiController?: EditorAIController,
|
|
39
|
+
customComponents?: CustomEditorComponent[];
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
markdownConfig?: MarkdownEditorConfig;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const proseClasses = {
|
|
45
|
+
"sm": "prose-sm",
|
|
46
|
+
"base": "prose-base",
|
|
47
|
+
"lg": "prose-lg"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const FireCMSEditor = ({
|
|
51
|
+
content,
|
|
52
|
+
onJsonContentChange,
|
|
53
|
+
onHtmlContentChange,
|
|
54
|
+
onMarkdownContentChange,
|
|
55
|
+
version,
|
|
56
|
+
textSize = "base",
|
|
57
|
+
highlight,
|
|
58
|
+
handleImageUpload,
|
|
59
|
+
aiController,
|
|
60
|
+
disabled,
|
|
61
|
+
markdownConfig
|
|
62
|
+
}: FireCMSEditorProps) => {
|
|
63
|
+
const { t } = useTranslation();
|
|
64
|
+
|
|
65
|
+
const [openNode, setOpenNode] = useState(false);
|
|
66
|
+
const [openLink, setOpenLink] = useState(false);
|
|
67
|
+
|
|
68
|
+
useInjectStyles("Editor", cssStyles);
|
|
69
|
+
|
|
70
|
+
const { state, view, editorRef } = useProseMirror({
|
|
71
|
+
initialContent: content,
|
|
72
|
+
editable: !disabled,
|
|
73
|
+
handleImageUpload,
|
|
74
|
+
onChange: (newState, editorView) => {
|
|
75
|
+
if (onMarkdownContentChange) {
|
|
76
|
+
try {
|
|
77
|
+
const markdown = addLineBreakAfterImages(serializer.serialize(newState.doc));
|
|
78
|
+
onMarkdownContentChange(markdown);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn("[FireCMSEditor] Could not serialize editor state to markdown:", e);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (onJsonContentChange) {
|
|
84
|
+
const jsonContent = removeClassesFromJson(newState.doc.toJSON()) as JSONContent;
|
|
85
|
+
onJsonContentChange(jsonContent);
|
|
86
|
+
}
|
|
87
|
+
if (onHtmlContentChange) {
|
|
88
|
+
// Not strictly required for FireCMS initially, DOMParser/Serializer can be added if needed
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (version !== undefined && version > -1 && view) {
|
|
95
|
+
if (!content) return;
|
|
96
|
+
const newDoc = typeof content === "string" ? parser.parse(content) : schema.nodeFromJSON(content);
|
|
97
|
+
if (newDoc) {
|
|
98
|
+
view.dispatch(view.state.tr.replaceWith(0, view.state.doc.content.size, newDoc.content));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [version]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (view) {
|
|
105
|
+
if (highlight) {
|
|
106
|
+
highlightCommands.toggleAutocompleteHighlight(highlight)(view.state, view.dispatch);
|
|
107
|
+
} else {
|
|
108
|
+
highlightCommands.removeAutocompleteHighlight()(view.state, view.dispatch);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, [highlight?.from, highlight?.to]);
|
|
112
|
+
|
|
113
|
+
const proseClass = proseClasses[textSize];
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="relative min-h-[300px] w-full">
|
|
119
|
+
<ProseMirrorContext.Provider value={{ state, view }}>
|
|
120
|
+
|
|
121
|
+
<div
|
|
122
|
+
ref={editorRef}
|
|
123
|
+
className={cls(proseClass, "prose-headings:font-title font-default focus:outline-none max-w-full p-12")}
|
|
124
|
+
/>
|
|
125
|
+
|
|
126
|
+
{view && (
|
|
127
|
+
<EditorBubble
|
|
128
|
+
options={{
|
|
129
|
+
placement: "top",
|
|
130
|
+
offset: 6,
|
|
131
|
+
}}
|
|
132
|
+
className={cls("flex w-fit max-w-[90vw] h-10 overflow-hidden rounded border bg-white dark:bg-surface-900 shadow", defaultBorderMixin)}
|
|
133
|
+
>
|
|
134
|
+
<NodeSelector portalContainer={editorRef.current} open={openNode} onOpenChange={setOpenNode} />
|
|
135
|
+
<Separator orientation="vertical" />
|
|
136
|
+
<LinkSelector open={openLink} onOpenChange={setOpenLink} />
|
|
137
|
+
<Separator orientation="vertical" />
|
|
138
|
+
<TextButtons />
|
|
139
|
+
</EditorBubble>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
<SlashCommandMenu upload={handleImageUpload} aiController={aiController} />
|
|
143
|
+
|
|
144
|
+
</ProseMirrorContext.Provider>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function addLineBreakAfterImages(markdown: string): string {
|
|
150
|
+
const imageRegex = /!\[.*?\]\((?:[^)\\]|\\.)*\)/g;
|
|
151
|
+
return markdown.replace(imageRegex, (match) => `${match}\n`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const cssStyles = `
|
|
155
|
+
.ProseMirror {
|
|
156
|
+
box-shadow: none !important;
|
|
157
|
+
}
|
|
158
|
+
.ProseMirror .is-editor-empty:first-child::before {
|
|
159
|
+
content: attr(data-placeholder);
|
|
160
|
+
float: left;
|
|
161
|
+
color: rgb(100 116 139); //500
|
|
162
|
+
pointer-events: none;
|
|
163
|
+
height: 0;
|
|
164
|
+
}
|
|
165
|
+
.ProseMirror .is-empty::before {
|
|
166
|
+
content: attr(data-placeholder);
|
|
167
|
+
float: left;
|
|
168
|
+
color: rgb(100 116 139); //500
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
height: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[data-theme="dark"] {
|
|
174
|
+
.ProseMirror .is-empty::before {
|
|
175
|
+
color: rgb(100 116 139); //500
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.is-empty {
|
|
180
|
+
cursor: text;
|
|
181
|
+
color: rgb(100 116 139); //500
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.ProseMirror img {
|
|
185
|
+
transition: filter 0.1s ease-in-out;
|
|
186
|
+
&:hover {
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
filter: brightness(90%);
|
|
189
|
+
}
|
|
190
|
+
&.ProseMirror-selectednode {
|
|
191
|
+
filter: brightness(90%);
|
|
192
|
+
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000) !important;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
ul[data-type="taskList"] li > label {
|
|
197
|
+
margin-right: 0.2rem;
|
|
198
|
+
user-select: none;
|
|
199
|
+
}
|
|
200
|
+
@media screen and (max-width: 768px) {
|
|
201
|
+
ul[data-type="taskList"] li > label {
|
|
202
|
+
margin-right: 0.5rem;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
[data-theme="dark"] {
|
|
206
|
+
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
|
207
|
+
background-color: rgb(30 41 59);
|
|
208
|
+
border: 2px solid #666;
|
|
209
|
+
&:hover { background-color: rgb(51 65 85); }
|
|
210
|
+
&:active { background-color: rgb(71 85 105); }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
|
214
|
+
-webkit-appearance: none;
|
|
215
|
+
appearance: none;
|
|
216
|
+
background-color: white;
|
|
217
|
+
margin: 0;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
width: 1.2em;
|
|
220
|
+
height: 1.2em;
|
|
221
|
+
position: relative;
|
|
222
|
+
top: 5px;
|
|
223
|
+
border: 2px solid #777;
|
|
224
|
+
border-radius: 0.25em;
|
|
225
|
+
margin-right: 0.3rem;
|
|
226
|
+
display: grid;
|
|
227
|
+
place-content: center;
|
|
228
|
+
&:hover { background-color: rgb(241 245 249); }
|
|
229
|
+
&:active { background-color: rgb(226 232 240); }
|
|
230
|
+
&::before {
|
|
231
|
+
content: "";
|
|
232
|
+
width: 0.65em;
|
|
233
|
+
height: 0.65em;
|
|
234
|
+
transform: scale(0);
|
|
235
|
+
transition: 120ms transform ease-in-out;
|
|
236
|
+
box-shadow: inset 1em 1em;
|
|
237
|
+
transform-origin: center;
|
|
238
|
+
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
|
239
|
+
}
|
|
240
|
+
&:checked::before { transform: scale(1); }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
[data-theme="dark"] {
|
|
244
|
+
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|
245
|
+
color: rgb(226 232 240);
|
|
246
|
+
text-decoration: line-through;
|
|
247
|
+
text-decoration-thickness: 2px;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|
251
|
+
color: rgb(51 65 85);
|
|
252
|
+
text-decoration: line-through;
|
|
253
|
+
text-decoration-thickness: 2px;
|
|
254
|
+
}
|
|
255
|
+
.tippy-box { max-width: 400px !important; }
|
|
256
|
+
|
|
257
|
+
.ProseMirror:not(.dragging) .ProseMirror-selectednode {
|
|
258
|
+
background-color: rgb(219 234 254);
|
|
259
|
+
transition: background-color 0.2s;
|
|
260
|
+
box-shadow: none;
|
|
261
|
+
}
|
|
262
|
+
[data-theme="dark"] .ProseMirror:not(.dragging) .ProseMirror-selectednode {
|
|
263
|
+
background-color: rgb(51 65 85);
|
|
264
|
+
}
|
|
265
|
+
.prose-base table p { margin: 0; }
|
|
266
|
+
|
|
267
|
+
.drag-handle {
|
|
268
|
+
position: absolute;
|
|
269
|
+
opacity: 1;
|
|
270
|
+
transition: opacity ease-in 0.2s;
|
|
271
|
+
border-radius: 0.25rem;
|
|
272
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(128, 128, 128, 0.9)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7,8.44771525 7,9 7,9 C7,9.55228475 7,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E");
|
|
273
|
+
background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem);
|
|
274
|
+
background-repeat: no-repeat;
|
|
275
|
+
background-position: center;
|
|
276
|
+
width: 1.2rem;
|
|
277
|
+
height: 1.5rem;
|
|
278
|
+
z-index: 100;
|
|
279
|
+
cursor: grab;
|
|
280
|
+
|
|
281
|
+
/* Create a hover area around the handle itself that doesn't overlap text */
|
|
282
|
+
&::before {
|
|
283
|
+
content: '';
|
|
284
|
+
position: absolute;
|
|
285
|
+
top: -10px;
|
|
286
|
+
bottom: -10px;
|
|
287
|
+
left: -20px;
|
|
288
|
+
right: 0px;
|
|
289
|
+
z-index: -1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
&:hover { background-color: rgb(241 245 249); transition: background-color 0.2s; }
|
|
293
|
+
&:active { background-color: rgb(226 232 240); transition: background-color 0.2s; }
|
|
294
|
+
&.hide { opacity: 0; pointer-events: none; }
|
|
295
|
+
@media screen and (max-width: 600px) { display: none; pointer-events: none; }
|
|
296
|
+
}
|
|
297
|
+
[data-theme="dark"] .drag-handle {
|
|
298
|
+
&:hover { background-color: rgb(51 65 85); }
|
|
299
|
+
&:active { background-color: rgb(51 65 85); }
|
|
300
|
+
}
|
|
301
|
+
.prosemirror-dropcursor-block {
|
|
302
|
+
background-color: var(--color-surface-accent-600);
|
|
303
|
+
}
|
|
304
|
+
[data-theme="dark"] .prosemirror-dropcursor-block {
|
|
305
|
+
background-color: var(--color-surface-accent-300);
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Plugin, PluginKey, Transaction, EditorState } from "prosemirror-state";
|
|
2
|
+
import { Decoration, DecorationSet } from "prosemirror-view";
|
|
3
|
+
|
|
4
|
+
export interface HighlightRange {
|
|
5
|
+
from: number
|
|
6
|
+
to: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AutocompleteHighlightState {
|
|
10
|
+
highlight?: HighlightRange
|
|
11
|
+
decorationSet?: DecorationSet
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const highlightDecorationKey = new PluginKey<AutocompleteHighlightState>("highlightDecoration");
|
|
15
|
+
|
|
16
|
+
function buildDecorationSet(highlight: any, doc: any) {
|
|
17
|
+
const decorations: [any?] = [];
|
|
18
|
+
|
|
19
|
+
if (highlight) {
|
|
20
|
+
decorations.push(
|
|
21
|
+
Decoration.inline(highlight.from, highlight.to, {
|
|
22
|
+
class: "dark:bg-surface-accent-700 bg-surface-accent-300"
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const decorationSet = DecorationSet.create(doc, decorations);
|
|
27
|
+
return decorationSet;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Commands to toggle the highlight
|
|
32
|
+
*/
|
|
33
|
+
export const highlightCommands = {
|
|
34
|
+
toggleAutocompleteHighlight: (range?: HighlightRange) => (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
|
|
35
|
+
const { selection } = state;
|
|
36
|
+
const pos = selection.from;
|
|
37
|
+
|
|
38
|
+
if (!dispatch) return false;
|
|
39
|
+
|
|
40
|
+
const tr = state.tr.setMeta(highlightDecorationKey, {
|
|
41
|
+
pos,
|
|
42
|
+
type: "highlightDecoration",
|
|
43
|
+
remove: false,
|
|
44
|
+
range
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
dispatch(tr);
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
removeAutocompleteHighlight: () => (state: EditorState, dispatch?: (tr: Transaction) => void): boolean => {
|
|
52
|
+
if (!dispatch) return false;
|
|
53
|
+
|
|
54
|
+
const tr = state.tr.setMeta(highlightDecorationKey, {
|
|
55
|
+
pos: 0,
|
|
56
|
+
type: "highlightDecoration",
|
|
57
|
+
remove: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
dispatch(tr);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This plugin is used to highlight the current autocomplete suggestion.
|
|
67
|
+
* It allows to set a range and remove it.
|
|
68
|
+
*/
|
|
69
|
+
export const highlightDecorationPlugin = (initialHighlight?: HighlightRange) => {
|
|
70
|
+
return new Plugin<AutocompleteHighlightState>({
|
|
71
|
+
key: highlightDecorationKey,
|
|
72
|
+
state: {
|
|
73
|
+
init: (_, { doc }) => {
|
|
74
|
+
const decorationSet = initialHighlight && doc ? buildDecorationSet(initialHighlight, doc) : DecorationSet.empty;
|
|
75
|
+
return {
|
|
76
|
+
decorationSet,
|
|
77
|
+
highlight: initialHighlight
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
apply(transaction, oldState) {
|
|
81
|
+
const action = transaction.getMeta(highlightDecorationKey);
|
|
82
|
+
const highlight = action?.range;
|
|
83
|
+
if (action?.type === "highlightDecoration") {
|
|
84
|
+
|
|
85
|
+
const doc = transaction.doc;
|
|
86
|
+
const { remove } = action;
|
|
87
|
+
|
|
88
|
+
if (remove) {
|
|
89
|
+
return {
|
|
90
|
+
decorationSet: DecorationSet.empty
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const decorationSet = buildDecorationSet(highlight, doc);
|
|
94
|
+
return {
|
|
95
|
+
decorationSet: decorationSet,
|
|
96
|
+
highlight
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
return oldState
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
props: {
|
|
104
|
+
decorations(state) {
|
|
105
|
+
const autocompleteState = this.getState(state);
|
|
106
|
+
if (autocompleteState?.decorationSet) {
|
|
107
|
+
return autocompleteState.decorationSet
|
|
108
|
+
} else {
|
|
109
|
+
return DecorationSet.empty
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
|
2
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
3
|
+
import { schema } from "../../schema";
|
|
4
|
+
|
|
5
|
+
export type UploadFn = (image: File) => Promise<string>;
|
|
6
|
+
|
|
7
|
+
export async function onFileRead(view: EditorView, readerEvent: ProgressEvent<FileReader>, pos: number, upload: UploadFn, image: File) {
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
const plugin = view.state.plugins.find((p: Plugin) => p.key === ImagePluginKey.key);
|
|
10
|
+
if (!plugin) {
|
|
11
|
+
console.error("Image plugin not found");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let decorationSet = plugin.getState(view.state);
|
|
15
|
+
|
|
16
|
+
const placeholder = document.createElement("div");
|
|
17
|
+
const imageElement = document.createElement("img");
|
|
18
|
+
// basic styling for loading state
|
|
19
|
+
imageElement.setAttribute("class", "opacity-40 rounded-lg border");
|
|
20
|
+
imageElement.src = readerEvent.target?.result as string;
|
|
21
|
+
placeholder.appendChild(imageElement);
|
|
22
|
+
|
|
23
|
+
const deco = Decoration.widget(pos, placeholder);
|
|
24
|
+
decorationSet = decorationSet?.add(view.state.doc, [deco]);
|
|
25
|
+
view.dispatch(view.state.tr.setMeta(plugin, { decorationSet }));
|
|
26
|
+
|
|
27
|
+
// Image Upload Logic
|
|
28
|
+
const src = await upload(image);
|
|
29
|
+
console.debug("Uploaded image", src);
|
|
30
|
+
|
|
31
|
+
// Replace placeholder with actual image
|
|
32
|
+
const imageNode = schema.nodes.image.create({ src });
|
|
33
|
+
const tr = view.state.tr.replaceWith(pos, pos, imageNode);
|
|
34
|
+
|
|
35
|
+
// Remove placeholder decoration
|
|
36
|
+
decorationSet = decorationSet?.remove([deco]);
|
|
37
|
+
tr.setMeta(plugin, { decorationSet });
|
|
38
|
+
view.dispatch(tr);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ImagePluginKey = new PluginKey("imagePlugin");
|
|
42
|
+
|
|
43
|
+
export const createDropImagePlugin = (upload: UploadFn): Plugin => {
|
|
44
|
+
const plugin: Plugin = new Plugin({
|
|
45
|
+
key: ImagePluginKey,
|
|
46
|
+
state: {
|
|
47
|
+
// Initialize the plugin state with an empty DecorationSet
|
|
48
|
+
init: () => DecorationSet.empty,
|
|
49
|
+
// Apply transactions to update the state
|
|
50
|
+
apply: (tr, old) => {
|
|
51
|
+
// Handle custom transaction steps that update decorations
|
|
52
|
+
const meta = tr.getMeta(plugin);
|
|
53
|
+
if (meta && meta.decorationSet) {
|
|
54
|
+
return meta.decorationSet;
|
|
55
|
+
}
|
|
56
|
+
// Map decorations to the new document structure
|
|
57
|
+
return old.map(tr.mapping, tr.doc);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
props: {
|
|
61
|
+
handleDOMEvents: {
|
|
62
|
+
drop: (view, event) => {
|
|
63
|
+
if (!event.dataTransfer?.files || event.dataTransfer?.files.length === 0) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
|
|
68
|
+
const files = Array.from(event.dataTransfer.files);
|
|
69
|
+
const images = files.filter(file => /image/i.test(file.type));
|
|
70
|
+
|
|
71
|
+
if (images.length === 0) {
|
|
72
|
+
console.log("No images found in dropped files");
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
images.forEach(image => {
|
|
77
|
+
const position = view.posAtCoords({
|
|
78
|
+
left: event.clientX,
|
|
79
|
+
top: event.clientY
|
|
80
|
+
});
|
|
81
|
+
if (!position) return;
|
|
82
|
+
|
|
83
|
+
const reader = new FileReader();
|
|
84
|
+
reader.onload = async (readerEvent) => {
|
|
85
|
+
await onFileRead(view, readerEvent, position.pos, upload, image);
|
|
86
|
+
};
|
|
87
|
+
reader.readAsDataURL(image);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
handlePaste(view, event, slice) {
|
|
94
|
+
const items = Array.from(event.clipboardData?.items || []);
|
|
95
|
+
const pos = view.state.selection.from;
|
|
96
|
+
let anyImageFound = false;
|
|
97
|
+
|
|
98
|
+
items.forEach((item) => {
|
|
99
|
+
const image = item.getAsFile();
|
|
100
|
+
if (image && /image/i.test(item.type)) {
|
|
101
|
+
anyImageFound = true;
|
|
102
|
+
const reader = new FileReader();
|
|
103
|
+
|
|
104
|
+
reader.onload = async (readerEvent) => {
|
|
105
|
+
await onFileRead(view, readerEvent, pos, upload, image);
|
|
106
|
+
};
|
|
107
|
+
reader.readAsDataURL(image);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return anyImageFound;
|
|
112
|
+
},
|
|
113
|
+
decorations(state) {
|
|
114
|
+
return plugin.getState(state);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
view(editorView) {
|
|
118
|
+
// This is needed to immediately apply the decoration updates
|
|
119
|
+
return {
|
|
120
|
+
update(view, prevState) {
|
|
121
|
+
const prevDecos = plugin.getState(prevState);
|
|
122
|
+
const newDecos = plugin.getState(view.state);
|
|
123
|
+
|
|
124
|
+
if (prevDecos !== newDecos) {
|
|
125
|
+
view.updateState(view.state);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return plugin;
|
|
133
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { cls, defaultBorderMixin } from "@firecms/ui";
|
|
2
|
+
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
|
|
3
|
+
import { Plugin, PluginKey } from "prosemirror-state";
|
|
4
|
+
|
|
5
|
+
export type UploadFn = (image: File) => Promise<string>;
|
|
6
|
+
|
|
7
|
+
export async function onFileRead(view: EditorView, readerEvent: ProgressEvent<FileReader>, pos: number, upload: UploadFn, image: File) {
|
|
8
|
+
|
|
9
|
+
const { schema } = view.state;
|
|
10
|
+
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
const plugin = view.state.plugins.find((p: Plugin) => p.key === ImagePluginKey.key);
|
|
13
|
+
if (!plugin) {
|
|
14
|
+
console.error("Image plugin not found");
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let decorationSet = plugin.getState(view.state);
|
|
18
|
+
|
|
19
|
+
const placeholder = document.createElement("div");
|
|
20
|
+
const imageElement = document.createElement("img");
|
|
21
|
+
imageElement.setAttribute("class", "opacity-40 rounded-lg border " + defaultBorderMixin);
|
|
22
|
+
imageElement.src = readerEvent.target?.result as string;
|
|
23
|
+
placeholder.appendChild(imageElement);
|
|
24
|
+
|
|
25
|
+
const deco = Decoration.widget(pos, placeholder);
|
|
26
|
+
decorationSet = decorationSet?.add(view.state.doc, [deco]);
|
|
27
|
+
view.dispatch(view.state.tr.setMeta(plugin, { decorationSet }));
|
|
28
|
+
|
|
29
|
+
// Image Upload Logic
|
|
30
|
+
const src = await upload(image);
|
|
31
|
+
console.debug("Uploaded image", src);
|
|
32
|
+
|
|
33
|
+
// Replace placeholder with actual image
|
|
34
|
+
const imageNode = schema.nodes.image.create({ src });
|
|
35
|
+
const tr = view.state.tr.replaceWith(pos, pos, imageNode);
|
|
36
|
+
|
|
37
|
+
// Remove placeholder decoration
|
|
38
|
+
decorationSet = decorationSet?.remove([deco]);
|
|
39
|
+
tr.setMeta(plugin, { decorationSet });
|
|
40
|
+
view.dispatch(tr);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const ImagePluginKey = new PluginKey("imagePlugin");
|
|
44
|
+
|
|
45
|
+
export const createDropImagePlugin = (upload: UploadFn): Plugin => {
|
|
46
|
+
const plugin: Plugin = new Plugin({
|
|
47
|
+
key: ImagePluginKey,
|
|
48
|
+
state: {
|
|
49
|
+
// Initialize the plugin state with an empty DecorationSet
|
|
50
|
+
init: () => DecorationSet.empty,
|
|
51
|
+
// Apply transactions to update the state
|
|
52
|
+
apply: (tr, old) => {
|
|
53
|
+
// Handle custom transaction steps that update decorations
|
|
54
|
+
const meta = tr.getMeta(plugin);
|
|
55
|
+
if (meta && meta.decorationSet) {
|
|
56
|
+
return meta.decorationSet;
|
|
57
|
+
}
|
|
58
|
+
// Map decorations to the new document structure
|
|
59
|
+
return old.map(tr.mapping, tr.doc);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
props: {
|
|
63
|
+
handleDOMEvents: {
|
|
64
|
+
dragover: (view: EditorView, event: DragEvent) => {
|
|
65
|
+
if (event.dataTransfer?.types?.includes("Files")) {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
},
|
|
71
|
+
drop: (view: EditorView, event: DragEvent) => {
|
|
72
|
+
if (!event.dataTransfer?.files || event.dataTransfer?.files.length === 0) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
|
|
77
|
+
const files = Array.from(event.dataTransfer.files);
|
|
78
|
+
const images = files.filter(file => /image/i.test(file.type));
|
|
79
|
+
|
|
80
|
+
if (images.length === 0) {
|
|
81
|
+
console.log("No images found in dropped files");
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
images.forEach(image => {
|
|
86
|
+
const position = view.posAtCoords({
|
|
87
|
+
left: event.clientX,
|
|
88
|
+
top: event.clientY
|
|
89
|
+
});
|
|
90
|
+
if (!position) return;
|
|
91
|
+
|
|
92
|
+
const reader = new FileReader();
|
|
93
|
+
reader.onload = async (readerEvent) => {
|
|
94
|
+
await onFileRead(view as any, readerEvent, position.pos, upload, image);
|
|
95
|
+
};
|
|
96
|
+
reader.readAsDataURL(image);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
handlePaste(view: EditorView, event: ClipboardEvent, slice: any) {
|
|
103
|
+
const items = Array.from(event.clipboardData?.items || []);
|
|
104
|
+
const pos = view.state.selection.from;
|
|
105
|
+
let anyImageFound = false;
|
|
106
|
+
|
|
107
|
+
items
|
|
108
|
+
.filter((item) => item.type.startsWith("image/"))
|
|
109
|
+
.forEach((item) => {
|
|
110
|
+
const image = item.getAsFile();
|
|
111
|
+
if (image) {
|
|
112
|
+
anyImageFound = true;
|
|
113
|
+
const reader = new FileReader();
|
|
114
|
+
reader.onload = async (readerEvent) => {
|
|
115
|
+
await onFileRead(view as any, readerEvent, pos, upload, image);
|
|
116
|
+
};
|
|
117
|
+
reader.readAsDataURL(image);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return anyImageFound;
|
|
122
|
+
},
|
|
123
|
+
decorations(state) {
|
|
124
|
+
return plugin.getState(state);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
view(editorView) {
|
|
128
|
+
return {
|
|
129
|
+
update(view, prevState) {
|
|
130
|
+
const prevDecos = plugin.getState(prevState);
|
|
131
|
+
const newDecos = plugin.getState(view.state);
|
|
132
|
+
|
|
133
|
+
if (prevDecos !== newDecos) {
|
|
134
|
+
view.updateState(view.state);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return plugin;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
|