@firecms/core 3.1.0 → 3.2.0-canary.44dc65b

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,455 @@
1
+ "use client";
2
+ import React, { useEffect, useState, useRef } from "react";
3
+ import { EditorState } from "prosemirror-state";
4
+ import { cls, defaultBorderMixin, Separator, useInjectStyles, TextareaAutosize } from "@firecms/ui";
5
+ import { useTranslation } from "../hooks/useTranslation";
6
+ import { EditorBubble, ImageBubble, SlashCommandMenu, TableBubble, type JSONContent } from "./components";
7
+ import { NodeSelector } from "./selectors/node-selector";
8
+ import { LinkSelector } from "./selectors/link-selector";
9
+ import { TextButtons } from "./selectors/text-buttons";
10
+ import { removeClassesFromJson } from "./utils/remove_classes";
11
+ import { parser, serializer } from "./markdown";
12
+ import { EditorAIController } from "./types";
13
+ import { useProseMirror } from "./hooks/useProseMirror";
14
+ import { ProseMirrorContext } from "./hooks/useProseMirrorContext";
15
+ import { highlightCommands } from "./extensions/HighlightDecorationExtension";
16
+ import { schema } from "./schema";
17
+
18
+ export type CustomEditorComponent = {
19
+ name: string,
20
+ component: React.FC
21
+ };
22
+
23
+ export interface MarkdownEditorConfig {
24
+ html?: boolean;
25
+ transformPastedText?: boolean;
26
+ }
27
+
28
+ export type FireCMSEditorTextSize = "sm" | "base" | "lg";
29
+
30
+ export type FireCMSEditorProps = {
31
+ content?: JSONContent | string,
32
+ onMarkdownContentChange?: (content: string) => void,
33
+ onJsonContentChange?: (content: JSONContent | null) => void,
34
+ onHtmlContentChange?: (content: string) => void,
35
+ handleImageUpload: (file: File) => Promise<string>,
36
+ version?: number,
37
+ textSize?: FireCMSEditorTextSize,
38
+ highlight?: { from: number, to: number },
39
+ aiController?: EditorAIController,
40
+ customComponents?: CustomEditorComponent[];
41
+ disabled?: boolean;
42
+ markdownConfig?: MarkdownEditorConfig;
43
+ };
44
+
45
+ const proseClasses = {
46
+ "sm": "prose-sm",
47
+ "base": "prose-base",
48
+ "lg": "prose-lg"
49
+ }
50
+
51
+ export const FireCMSEditor = ({
52
+ content,
53
+ onJsonContentChange,
54
+ onHtmlContentChange,
55
+ onMarkdownContentChange,
56
+ version,
57
+ textSize = "base",
58
+ highlight,
59
+ handleImageUpload,
60
+ aiController,
61
+ disabled,
62
+ markdownConfig
63
+ }: FireCMSEditorProps) => {
64
+ const { t } = useTranslation();
65
+
66
+ const [openNode, setOpenNode] = useState(false);
67
+ const [openLink, setOpenLink] = useState(false);
68
+
69
+ const [isMarkdownMode, setIsMarkdownMode] = useState(false);
70
+ const [internalMarkdown, setInternalMarkdown] = useState<string>("");
71
+
72
+ useEffect(() => {
73
+ if (!isMarkdownMode) return;
74
+ const timeout = setTimeout(() => {
75
+ if (callbacksRef.current.onMarkdownContentChange) {
76
+ callbacksRef.current.onMarkdownContentChange(internalMarkdown);
77
+ }
78
+ }, 250);
79
+ return () => clearTimeout(timeout);
80
+ }, [internalMarkdown, isMarkdownMode]);
81
+
82
+ const handleToggleMarkdown = () => {
83
+ if (!isMarkdownMode) {
84
+ if (view) {
85
+ setInternalMarkdown(serializer.serialize(view.state.doc));
86
+ }
87
+ setIsMarkdownMode(true);
88
+ } else {
89
+ if (view) {
90
+ const newDoc = parser.parse(internalMarkdown);
91
+ if (newDoc) {
92
+ const tr = view.state.tr.replaceWith(0, view.state.doc.content.size, newDoc.content);
93
+ view.dispatch(tr);
94
+ }
95
+ }
96
+ setIsMarkdownMode(false);
97
+ }
98
+ };
99
+
100
+ const handleMarkdownChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
101
+ setInternalMarkdown(e.target.value);
102
+ };
103
+
104
+ const handleMarkdownBlur = () => {
105
+ const { onMarkdownContentChange, onJsonContentChange } = callbacksRef.current;
106
+ if (onMarkdownContentChange) {
107
+ onMarkdownContentChange(internalMarkdown);
108
+ }
109
+ if (onJsonContentChange) {
110
+ try {
111
+ const newDoc = parser.parse(internalMarkdown);
112
+ if (newDoc) {
113
+ onJsonContentChange(removeClassesFromJson(newDoc.toJSON()) as JSONContent);
114
+ }
115
+ } catch (e) {
116
+ console.warn("Could not parse markdown to JSON", e);
117
+ }
118
+ }
119
+ };
120
+
121
+ useInjectStyles("Editor", cssStyles);
122
+
123
+ const callbacksRef = useRef({ onMarkdownContentChange, onJsonContentChange });
124
+ useEffect(() => {
125
+ callbacksRef.current = { onMarkdownContentChange, onJsonContentChange };
126
+ }, [onMarkdownContentChange, onJsonContentChange]);
127
+
128
+ const flushChanges = (currentState: EditorState) => {
129
+ const { onMarkdownContentChange, onJsonContentChange } = callbacksRef.current;
130
+ if (onMarkdownContentChange) {
131
+ try {
132
+ const markdown = serializer.serialize(currentState.doc);
133
+ onMarkdownContentChange(markdown);
134
+ } catch (e) {
135
+ console.warn("[FireCMSEditor] Could not serialize editor state to markdown:", e);
136
+ }
137
+ }
138
+ if (onJsonContentChange) {
139
+ const jsonContent = removeClassesFromJson(currentState.doc.toJSON()) as JSONContent;
140
+ onJsonContentChange(jsonContent);
141
+ }
142
+ };
143
+
144
+ const { state, view, editorRef } = useProseMirror({
145
+ initialContent: content,
146
+ editable: !disabled,
147
+ handleImageUpload,
148
+ });
149
+
150
+ const doc = state?.doc;
151
+ useEffect(() => {
152
+ if (!state) return;
153
+ const timeout = setTimeout(() => {
154
+ flushChanges(state);
155
+ }, 250);
156
+ return () => clearTimeout(timeout);
157
+ }, [doc]);
158
+
159
+ useEffect(() => {
160
+ if (!view) return;
161
+ const dom = view.dom;
162
+ const handleBlur = () => {
163
+ flushChanges(view.state);
164
+ };
165
+ dom.addEventListener("blur", handleBlur);
166
+ return () => dom.removeEventListener("blur", handleBlur);
167
+ }, [view]);
168
+
169
+
170
+
171
+ useEffect(() => {
172
+ if (view) {
173
+ if (highlight) {
174
+ highlightCommands.toggleAutocompleteHighlight(highlight)(view.state, view.dispatch);
175
+ } else {
176
+ highlightCommands.removeAutocompleteHighlight()(view.state, view.dispatch);
177
+ }
178
+ }
179
+ }, [highlight?.from, highlight?.to]);
180
+
181
+ const proseClass = proseClasses[textSize];
182
+
183
+
184
+
185
+ return (
186
+ <div className="relative min-h-[300px] w-full">
187
+ <button
188
+ type="button"
189
+ onClick={handleToggleMarkdown}
190
+ title={isMarkdownMode ? "Switch to Visual Editor" : "Switch to Markdown"}
191
+ className="absolute top-2 right-2 z-10 px-2 py-1 text-xs font-medium text-surface-400 hover:text-surface-700 dark:text-surface-600 dark:hover:text-surface-300 transition-colors opacity-50 hover:opacity-100 bg-transparent rounded"
192
+ >
193
+ {isMarkdownMode ? "Visual" : "Markdown"}
194
+ </button>
195
+
196
+ <ProseMirrorContext.Provider value={{ state, view }}>
197
+
198
+ <div style={{ display: isMarkdownMode ? "none" : "block" }}>
199
+ <div
200
+ ref={editorRef}
201
+ className={cls(proseClass, "prose-headings:font-title font-default focus:outline-none max-w-full p-12")}
202
+ />
203
+
204
+ {view && (
205
+ <>
206
+ <EditorBubble
207
+ options={{
208
+ placement: "top",
209
+ offset: 6,
210
+ }}
211
+ className={cls("flex w-fit max-w-[90vw] h-10 overflow-hidden rounded border bg-white dark:bg-surface-900 shadow", defaultBorderMixin)}
212
+ >
213
+ <NodeSelector portalContainer={editorRef.current} open={openNode} onOpenChange={setOpenNode} />
214
+ <Separator orientation="vertical" />
215
+ <LinkSelector open={openLink} onOpenChange={setOpenLink} />
216
+ <Separator orientation="vertical" />
217
+ <TextButtons />
218
+ </EditorBubble>
219
+
220
+ <ImageBubble
221
+ options={{
222
+ placement: "bottom",
223
+ offset: 6,
224
+ }}
225
+ />
226
+ <TableBubble />
227
+ </>
228
+ )}
229
+
230
+ <SlashCommandMenu upload={handleImageUpload} aiController={aiController} />
231
+ </div>
232
+
233
+ {isMarkdownMode && (
234
+ <TextareaAutosize
235
+ value={internalMarkdown}
236
+ onChange={handleMarkdownChange as any}
237
+ onBlur={handleMarkdownBlur as any}
238
+ className={cls(
239
+ "w-full h-full min-h-[300px] p-12 bg-transparent resize-none font-mono focus:ring-0",
240
+ proseClass
241
+ )}
242
+ style={{
243
+ tabSize: 4,
244
+ outline: "none",
245
+ border: "none",
246
+ boxShadow: "none"
247
+ }}
248
+ />
249
+ )}
250
+
251
+ </ProseMirrorContext.Provider>
252
+ </div>
253
+ );
254
+ };
255
+
256
+
257
+
258
+ const cssStyles = `
259
+ .ProseMirror {
260
+ box-shadow: none !important;
261
+ }
262
+ .ProseMirror .is-editor-empty:first-child::before {
263
+ content: attr(data-placeholder);
264
+ float: left;
265
+ color: rgb(100 116 139); //500
266
+ pointer-events: none;
267
+ height: 0;
268
+ }
269
+ .ProseMirror .is-empty::before {
270
+ content: attr(data-placeholder);
271
+ float: left;
272
+ color: rgb(100 116 139); //500
273
+ pointer-events: none;
274
+ height: 0;
275
+ }
276
+
277
+ [data-theme="dark"] {
278
+ .ProseMirror .is-empty::before {
279
+ color: rgb(100 116 139); //500
280
+ }
281
+ }
282
+
283
+ .is-empty {
284
+ cursor: text;
285
+ color: rgb(100 116 139); //500
286
+ }
287
+
288
+ .ProseMirror img {
289
+ transition: filter 0.1s ease-in-out;
290
+ &:hover {
291
+ cursor: pointer;
292
+ filter: brightness(90%);
293
+ }
294
+ &.ProseMirror-selectednode {
295
+ filter: brightness(90%);
296
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000) !important;
297
+ }
298
+ }
299
+
300
+ ul[data-type="taskList"] li > label {
301
+ margin-right: 0.2rem;
302
+ user-select: none;
303
+ }
304
+ @media screen and (max-width: 768px) {
305
+ ul[data-type="taskList"] li > label {
306
+ margin-right: 0.5rem;
307
+ }
308
+ }
309
+ [data-theme="dark"] {
310
+ ul[data-type="taskList"] li > label input[type="checkbox"] {
311
+ background-color: rgb(30 41 59);
312
+ border: 2px solid #666;
313
+ &:hover { background-color: rgb(51 65 85); }
314
+ &:active { background-color: rgb(71 85 105); }
315
+ }
316
+ }
317
+ ul[data-type="taskList"] li > label input[type="checkbox"] {
318
+ -webkit-appearance: none;
319
+ appearance: none;
320
+ background-color: white;
321
+ margin: 0;
322
+ cursor: pointer;
323
+ width: 1.2em;
324
+ height: 1.2em;
325
+ position: relative;
326
+ top: 5px;
327
+ border: 2px solid #777;
328
+ border-radius: 0.25em;
329
+ margin-right: 0.3rem;
330
+ display: grid;
331
+ place-content: center;
332
+ &:hover { background-color: rgb(241 245 249); }
333
+ &:active { background-color: rgb(226 232 240); }
334
+ &::before {
335
+ content: "";
336
+ width: 0.65em;
337
+ height: 0.65em;
338
+ transform: scale(0);
339
+ transition: 120ms transform ease-in-out;
340
+ box-shadow: inset 1em 1em;
341
+ transform-origin: center;
342
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
343
+ }
344
+ &:checked::before { transform: scale(1); }
345
+ }
346
+
347
+ [data-theme="dark"] {
348
+ ul[data-type="taskList"] li[data-checked="true"] > div > p {
349
+ color: rgb(226 232 240);
350
+ text-decoration: line-through;
351
+ text-decoration-thickness: 2px;
352
+ }
353
+ }
354
+ ul[data-type="taskList"] li[data-checked="true"] > div > p {
355
+ color: rgb(51 65 85);
356
+ text-decoration: line-through;
357
+ text-decoration-thickness: 2px;
358
+ }
359
+ .tippy-box { max-width: 400px !important; }
360
+
361
+ .ProseMirror:not(.dragging) .ProseMirror-selectednode {
362
+ background-color: rgb(219 234 254);
363
+ transition: background-color 0.2s;
364
+ box-shadow: none;
365
+ }
366
+ [data-theme="dark"] .ProseMirror:not(.dragging) .ProseMirror-selectednode {
367
+ background-color: rgb(51 65 85);
368
+ }
369
+ .prose-base table p { margin: 0; }
370
+
371
+ .drag-handle {
372
+ position: absolute;
373
+ opacity: 1;
374
+ transition: opacity ease-in 0.2s;
375
+ border-radius: 0.25rem;
376
+ 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");
377
+ background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem);
378
+ background-repeat: no-repeat;
379
+ background-position: center;
380
+ width: 1.2rem;
381
+ height: 1.5rem;
382
+ z-index: 100;
383
+ cursor: grab;
384
+
385
+ /* Create a hover area around the handle itself that doesn't overlap text */
386
+ &::before {
387
+ content: '';
388
+ position: absolute;
389
+ top: -10px;
390
+ bottom: -10px;
391
+ left: -20px;
392
+ right: 0px;
393
+ z-index: -1;
394
+ }
395
+
396
+ &:hover { background-color: rgb(241 245 249); transition: background-color 0.2s; }
397
+ &:active { background-color: rgb(226 232 240); transition: background-color 0.2s; }
398
+ &.hide { opacity: 0; pointer-events: none; }
399
+ @media screen and (max-width: 600px) { display: none; pointer-events: none; }
400
+ }
401
+ [data-theme="dark"] .drag-handle {
402
+ &:hover { background-color: rgb(51 65 85); }
403
+ &:active { background-color: rgb(51 65 85); }
404
+ }
405
+ .prosemirror-dropcursor-block {
406
+ background-color: var(--color-surface-accent-600);
407
+ }
408
+ [data-theme="dark"] .prosemirror-dropcursor-block {
409
+ background-color: var(--color-surface-accent-300);
410
+ }
411
+
412
+ .ProseMirror table {
413
+ border-collapse: separate;
414
+ border-spacing: 0;
415
+ table-layout: fixed;
416
+ width: 100%;
417
+ margin: 1em 0;
418
+ overflow: hidden;
419
+ border-radius: 0.375rem;
420
+ border: 1px solid #e5e7eb;
421
+ }
422
+ [data-theme="dark"] .ProseMirror table {
423
+ border-color: #374151;
424
+ }
425
+
426
+ .ProseMirror td, .ProseMirror th {
427
+ min-width: 1em;
428
+ padding: 8px 10px;
429
+ vertical-align: top;
430
+ box-sizing: border-box;
431
+ position: relative;
432
+ border-right: 1px solid #e5e7eb;
433
+ border-bottom: 1px solid #e5e7eb;
434
+ }
435
+ [data-theme="dark"] .ProseMirror td, [data-theme="dark"] .ProseMirror th {
436
+ border-right-color: #374151;
437
+ border-bottom-color: #374151;
438
+ }
439
+
440
+ .ProseMirror tr:last-child td, .ProseMirror tr:last-child th {
441
+ border-bottom: none;
442
+ }
443
+ .ProseMirror th:last-child, .ProseMirror td:last-child {
444
+ border-right: none;
445
+ }
446
+
447
+ .ProseMirror th {
448
+ font-weight: 600;
449
+ text-align: left;
450
+ background-color: #f9fafb;
451
+ }
452
+ [data-theme="dark"] .ProseMirror th {
453
+ background-color: #1f2937;
454
+ }
455
+ `;
@@ -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
+ };