@meta-1/editor 0.0.27

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 (130) hide show
  1. package/README.md +458 -0
  2. package/package.json +100 -0
  3. package/src/editor/constants.tsx +66 -0
  4. package/src/editor/container.css +46 -0
  5. package/src/editor/control/character-count/index.tsx +39 -0
  6. package/src/editor/control/drag-handle/index.tsx +85 -0
  7. package/src/editor/control/drag-handle/use.content.actions.ts +71 -0
  8. package/src/editor/control/drag-handle/use.data.ts +29 -0
  9. package/src/editor/control/drag-handle/use.handle.id.ts +6 -0
  10. package/src/editor/control/index.tsx +35 -0
  11. package/src/editor/editor.css +626 -0
  12. package/src/editor/extension/block-quote-figure/BlockquoteFigure.ts +73 -0
  13. package/src/editor/extension/block-quote-figure/Quote/Quote.ts +31 -0
  14. package/src/editor/extension/block-quote-figure/Quote/index.ts +1 -0
  15. package/src/editor/extension/block-quote-figure/QuoteCaption/QuoteCaption.ts +54 -0
  16. package/src/editor/extension/block-quote-figure/QuoteCaption/index.ts +1 -0
  17. package/src/editor/extension/block-quote-figure/index.ts +1 -0
  18. package/src/editor/extension/document/index.ts +5 -0
  19. package/src/editor/extension/figcaption/Figcaption.ts +90 -0
  20. package/src/editor/extension/figcaption/index.ts +1 -0
  21. package/src/editor/extension/figure/Figure.ts +62 -0
  22. package/src/editor/extension/figure/index.ts +1 -0
  23. package/src/editor/extension/font-size/FontSize.ts +64 -0
  24. package/src/editor/extension/font-size/index.ts +1 -0
  25. package/src/editor/extension/global-drag-handle/clipboard-serializer.ts +28 -0
  26. package/src/editor/extension/global-drag-handle/index.ts +377 -0
  27. package/src/editor/extension/heading/index.ts +13 -0
  28. package/src/editor/extension/horizontal-rule/HorizontalRule.ts +10 -0
  29. package/src/editor/extension/horizontal-rule/index.ts +1 -0
  30. package/src/editor/extension/image/index.ts +5 -0
  31. package/src/editor/extension/image-block/ImageBlock.ts +103 -0
  32. package/src/editor/extension/image-block/components/ImageBlockMenu.tsx +100 -0
  33. package/src/editor/extension/image-block/components/ImageBlockView.tsx +47 -0
  34. package/src/editor/extension/image-block/components/ImageBlockWidth.tsx +40 -0
  35. package/src/editor/extension/image-block/index.ts +1 -0
  36. package/src/editor/extension/image-upload/ImageUpload.ts +58 -0
  37. package/src/editor/extension/image-upload/index.ts +1 -0
  38. package/src/editor/extension/image-upload/view/ImageUpload.tsx +27 -0
  39. package/src/editor/extension/image-upload/view/ImageUploader.tsx +64 -0
  40. package/src/editor/extension/image-upload/view/hooks.ts +109 -0
  41. package/src/editor/extension/image-upload/view/index.tsx +1 -0
  42. package/src/editor/extension/index.ts +30 -0
  43. package/src/editor/extension/link/Link.ts +39 -0
  44. package/src/editor/extension/link/index.ts +1 -0
  45. package/src/editor/extension/multi-column/Column.ts +33 -0
  46. package/src/editor/extension/multi-column/Columns.ts +65 -0
  47. package/src/editor/extension/multi-column/index.ts +2 -0
  48. package/src/editor/extension/multi-column/menus/ColumnsMenu.tsx +82 -0
  49. package/src/editor/extension/multi-column/menus/index.ts +1 -0
  50. package/src/editor/extension/selection/Selection.ts +36 -0
  51. package/src/editor/extension/selection/index.ts +1 -0
  52. package/src/editor/extension/slash-command/MenuList.tsx +145 -0
  53. package/src/editor/extension/slash-command/groups.ts +153 -0
  54. package/src/editor/extension/slash-command/index.ts +277 -0
  55. package/src/editor/extension/slash-command/types.ts +25 -0
  56. package/src/editor/extension/table/Cell.ts +126 -0
  57. package/src/editor/extension/table/Header.ts +89 -0
  58. package/src/editor/extension/table/Row.ts +8 -0
  59. package/src/editor/extension/table/Table.ts +9 -0
  60. package/src/editor/extension/table/index.ts +4 -0
  61. package/src/editor/extension/table/menus/TableColumn/index.tsx +73 -0
  62. package/src/editor/extension/table/menus/TableColumn/utils.ts +38 -0
  63. package/src/editor/extension/table/menus/TableRow/index.tsx +74 -0
  64. package/src/editor/extension/table/menus/TableRow/utils.ts +38 -0
  65. package/src/editor/extension/table/menus/index.tsx +2 -0
  66. package/src/editor/extension/table/utils.ts +258 -0
  67. package/src/editor/extension/task-item/index.ts +1 -0
  68. package/src/editor/extension/task-item/task-item.ts +225 -0
  69. package/src/editor/extension/task-list/index.ts +1 -0
  70. package/src/editor/extension/task-list/task-list.ts +81 -0
  71. package/src/editor/extension/trailing-node/index.ts +1 -0
  72. package/src/editor/extension/trailing-node/trailing-node.ts +70 -0
  73. package/src/editor/extension/unique-id/index.ts +1 -0
  74. package/src/editor/extension/unique-id/uniqueId.ts +123 -0
  75. package/src/editor/hooks.ts +264 -0
  76. package/src/editor/index.tsx +53 -0
  77. package/src/editor/menus/LinkMenu/LinkMenu.tsx +75 -0
  78. package/src/editor/menus/LinkMenu/index.tsx +1 -0
  79. package/src/editor/menus/TextMenu/TextMenu.tsx +193 -0
  80. package/src/editor/menus/TextMenu/components/AIDropdown.tsx +140 -0
  81. package/src/editor/menus/TextMenu/components/ContentTypePicker.tsx +76 -0
  82. package/src/editor/menus/TextMenu/components/EditLinkPopover.tsx +25 -0
  83. package/src/editor/menus/TextMenu/components/FontFamilyPicker.tsx +84 -0
  84. package/src/editor/menus/TextMenu/components/FontSizePicker.tsx +56 -0
  85. package/src/editor/menus/TextMenu/hooks/useTextmenuCommands.ts +96 -0
  86. package/src/editor/menus/TextMenu/hooks/useTextmenuContentTypes.ts +86 -0
  87. package/src/editor/menus/TextMenu/hooks/useTextmenuStates.ts +50 -0
  88. package/src/editor/menus/TextMenu/index.tsx +2 -0
  89. package/src/editor/menus/types.ts +21 -0
  90. package/src/editor/panels/Colorpicker/ColorButton.tsx +35 -0
  91. package/src/editor/panels/Colorpicker/Colorpicker.tsx +67 -0
  92. package/src/editor/panels/Colorpicker/index.tsx +2 -0
  93. package/src/editor/panels/LinkEditorPanel/LinkEditorPanel.tsx +76 -0
  94. package/src/editor/panels/LinkEditorPanel/index.tsx +1 -0
  95. package/src/editor/panels/LinkPreviewPanel/LinkPreviewPanel.tsx +32 -0
  96. package/src/editor/panels/LinkPreviewPanel/index.tsx +1 -0
  97. package/src/editor/panels/index.tsx +3 -0
  98. package/src/editor/types.tsx +38 -0
  99. package/src/editor/ui/Button/Button.tsx +70 -0
  100. package/src/editor/ui/Button/index.tsx +2 -0
  101. package/src/editor/ui/Dropdown/Dropdown.tsx +39 -0
  102. package/src/editor/ui/Dropdown/index.tsx +1 -0
  103. package/src/editor/ui/Icon.tsx +21 -0
  104. package/src/editor/ui/Loader/Loader.tsx +39 -0
  105. package/src/editor/ui/Loader/index.ts +1 -0
  106. package/src/editor/ui/Loader/types.ts +7 -0
  107. package/src/editor/ui/Panel/index.tsx +109 -0
  108. package/src/editor/ui/PopoverMenu.tsx +127 -0
  109. package/src/editor/ui/Spinner/Spinner.tsx +10 -0
  110. package/src/editor/ui/Spinner/index.tsx +1 -0
  111. package/src/editor/ui/Surface.tsx +27 -0
  112. package/src/editor/ui/Textarea/Textarea.tsx +20 -0
  113. package/src/editor/ui/Textarea/index.tsx +1 -0
  114. package/src/editor/ui/Toggle/Toggle.tsx +39 -0
  115. package/src/editor/ui/Toggle/index.tsx +1 -0
  116. package/src/editor/ui/Toolbar.tsx +107 -0
  117. package/src/editor/ui/Tooltip/index.tsx +77 -0
  118. package/src/editor/ui/Tooltip/types.ts +17 -0
  119. package/src/editor/utils/cssVar.ts +14 -0
  120. package/src/editor/utils/getRenderContainer.ts +39 -0
  121. package/src/editor/utils/index.ts +16 -0
  122. package/src/editor/utils/isCustomNodeSelected.ts +47 -0
  123. package/src/editor/utils/isTextSelected.ts +25 -0
  124. package/src/editor/utils/locale.ts +5 -0
  125. package/src/editor/viewer/index.tsx +26 -0
  126. package/src/globals.css +1 -0
  127. package/src/index.ts +7 -0
  128. package/src/locales/en-us.ts +133 -0
  129. package/src/locales/zh-cn.ts +133 -0
  130. package/src/locales/zh-tw.ts +133 -0
@@ -0,0 +1,70 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
3
+
4
+ // @ts-expect-error
5
+ function nodeEqualsType({ types, node }) {
6
+ return (Array.isArray(types) && types.includes(node.type)) || node.type === types;
7
+ }
8
+
9
+ /**
10
+ * Extension based on:
11
+ * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
12
+ * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
13
+ */
14
+
15
+ export interface TrailingNodeOptions {
16
+ node: string;
17
+ notAfter: string[];
18
+ }
19
+
20
+ export const TrailingNode = Extension.create<TrailingNodeOptions>({
21
+ name: "trailingNode",
22
+
23
+ addOptions() {
24
+ return {
25
+ node: "paragraph",
26
+ notAfter: ["paragraph"],
27
+ };
28
+ },
29
+
30
+ addProseMirrorPlugins() {
31
+ const plugin = new PluginKey(this.name);
32
+ const disabledNodes = Object.entries(this.editor.schema.nodes)
33
+ .map(([, value]) => value)
34
+ .filter((node) => this.options.notAfter.includes(node.name));
35
+
36
+ return [
37
+ new Plugin({
38
+ key: plugin,
39
+ appendTransaction: (_, __, state) => {
40
+ const { doc, tr, schema } = state;
41
+ const shouldInsertNodeAtEnd = plugin.getState(state);
42
+ const endPosition = doc.content.size;
43
+ const type = schema.nodes[this.options.node];
44
+
45
+ if (!shouldInsertNodeAtEnd) {
46
+ return;
47
+ }
48
+
49
+ return tr.insert(endPosition, type.create());
50
+ },
51
+ state: {
52
+ init: (_, state) => {
53
+ const lastNode = state.tr.doc.lastChild;
54
+
55
+ return !nodeEqualsType({ node: lastNode, types: disabledNodes });
56
+ },
57
+ apply: (tr, value) => {
58
+ if (!tr.docChanged) {
59
+ return value;
60
+ }
61
+
62
+ const lastNode = tr.doc.lastChild;
63
+
64
+ return !nodeEqualsType({ node: lastNode, types: disabledNodes });
65
+ },
66
+ },
67
+ }),
68
+ ];
69
+ },
70
+ });
@@ -0,0 +1 @@
1
+ export { UniqueId } from "./uniqueId";
@@ -0,0 +1,123 @@
1
+ import {
2
+ combineTransactionSteps,
3
+ Extension,
4
+ findChildrenInRange,
5
+ findDuplicates,
6
+ getChangedRanges,
7
+ } from "@tiptap/core";
8
+ import type { Transaction } from "@tiptap/pm/state";
9
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
10
+ import { nanoid } from "nanoid";
11
+
12
+ export interface UniqueIdOptions {
13
+ attributeName: string;
14
+ types: string[];
15
+ generateID: () => string;
16
+ injectNodeName: boolean;
17
+ }
18
+
19
+ const pluginKey = new PluginKey("uniqueId");
20
+
21
+ export const UniqueId = Extension.create<UniqueIdOptions>({
22
+ name: "uniqueId",
23
+ addOptions() {
24
+ return {
25
+ attributeName: "id",
26
+ types: ["paragraph"],
27
+ generateID: nanoid,
28
+ injectNodeName: true,
29
+ };
30
+ },
31
+ addGlobalAttributes() {
32
+ const { attributeName, types } = this.options;
33
+
34
+ return [
35
+ {
36
+ types,
37
+ attributes: {
38
+ [attributeName!]: {
39
+ default: null,
40
+ rendered: true,
41
+ isRequired: true,
42
+ keepOnSplit: false,
43
+ parseHTML: (element) => element.getAttribute(`data-${attributeName}`),
44
+ renderHTML: (attributes) => {
45
+ return {
46
+ [`data-${attributeName}`]: attributes[attributeName!],
47
+ };
48
+ },
49
+ },
50
+ },
51
+ },
52
+ {
53
+ types: this.options.injectNodeName ? types : [],
54
+ attributes: {
55
+ "data-node-name": {
56
+ default: null,
57
+ rendered: true,
58
+ isRequired: true,
59
+ keepOnSplit: false,
60
+ parseHTML: (element) => element.getAttribute("data-node-name"),
61
+ renderHTML: (attributes) => {
62
+ return {
63
+ "data-node-name": attributes["data-node-name"],
64
+ };
65
+ },
66
+ },
67
+ },
68
+ },
69
+ ];
70
+ },
71
+ onCreate() {
72
+ const { tr, doc } = this.editor.state;
73
+ const { attributeName, types, generateID, injectNodeName } = this.options;
74
+
75
+ doc.descendants((node, pos) => {
76
+ if (node.type.name === "text" || !types?.includes(node.type.name)) return;
77
+
78
+ if (injectNodeName) tr.setNodeAttribute(pos, "data-node-name", node.type.name);
79
+
80
+ if (!node.attrs[attributeName!]) tr.setNodeAttribute(pos, attributeName!, generateID?.());
81
+ });
82
+
83
+ this.editor.view.dispatch(tr);
84
+ },
85
+ addProseMirrorPlugins() {
86
+ const { attributeName, types, generateID, injectNodeName } = this.options;
87
+
88
+ return [
89
+ new Plugin({
90
+ key: pluginKey,
91
+ appendTransaction(trs, { doc: oldDoc }, { doc: newDoc, tr }) {
92
+ if (!trs.some((tr) => tr.docChanged) || oldDoc.eq(newDoc)) return;
93
+
94
+ const transform = combineTransactionSteps(oldDoc, trs as Transaction[]);
95
+
96
+ for (const { newRange } of getChangedRanges(transform)) {
97
+ const newNodes = findChildrenInRange(newDoc, newRange, (node) => types?.includes(node.type.name));
98
+
99
+ const newIds = newNodes.map(({ node }) => node.attrs[attributeName!]).filter((item) => !!item);
100
+ for (const { node, pos } of newNodes) {
101
+ if (injectNodeName && !node.attrs["data-node-name"])
102
+ tr.setNodeAttribute(pos, "data-node-name", node.type.name);
103
+
104
+ const uniqueId = node.attrs[attributeName!];
105
+
106
+ if (!uniqueId) {
107
+ tr.setNodeAttribute(pos, attributeName!, generateID?.());
108
+ continue;
109
+ }
110
+
111
+ if (tr.mapping.invert().mapResult(pos) && findDuplicates(newIds).includes(uniqueId))
112
+ tr.setNodeAttribute(pos, attributeName!, generateID?.());
113
+ }
114
+ }
115
+
116
+ if (!transform.steps.length) return null;
117
+
118
+ return tr;
119
+ },
120
+ }),
121
+ ];
122
+ },
123
+ });
@@ -0,0 +1,264 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+ import type { EditorEvents, Editor as EditorInstance, Extensions, JSONContent } from "@tiptap/core";
3
+ import CharacterCount from "@tiptap/extension-character-count";
4
+ import Placeholder from "@tiptap/extension-placeholder";
5
+ import Typography from "@tiptap/extension-typography";
6
+ import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
7
+ import { useEditor as useBaseEditor } from "@tiptap/react";
8
+ import StarterKit from "@tiptap/starter-kit";
9
+ import { common, createLowlight } from "lowlight";
10
+
11
+ import { type NodeData, useData } from "./control/drag-handle/use.data";
12
+ import { useHandleId } from "./control/drag-handle/use.handle.id";
13
+ import {
14
+ BlockquoteFigure,
15
+ CodeBlockLowlight,
16
+ Color,
17
+ Column,
18
+ Columns,
19
+ Document,
20
+ Dropcursor,
21
+ Figcaption,
22
+ Focus,
23
+ FontFamily,
24
+ FontSize,
25
+ Heading,
26
+ Highlight,
27
+ History,
28
+ HorizontalRule,
29
+ ImageBlock,
30
+ ImageUpload,
31
+ Link,
32
+ Selection,
33
+ Subscript,
34
+ Superscript,
35
+ Table,
36
+ TableCell,
37
+ TableHeader,
38
+ TableRow,
39
+ TaskItem,
40
+ TaskList,
41
+ TextAlign,
42
+ TextStyle,
43
+ Underline,
44
+ UniqueId,
45
+ } from "./extension";
46
+ import GlobalDragHandle from "./extension/global-drag-handle";
47
+ import type { UploadImageFunction } from "./extension/image-upload/ImageUpload";
48
+ import SlashCommand, { type SlashCommandProps } from "./extension/slash-command";
49
+ import { i18n } from "./utils/locale";
50
+
51
+ export type UseEditorProps = {
52
+ limit?: number;
53
+ value?: string;
54
+ onChange?: (value: string) => void;
55
+ onReadOnlyChange?: (value: string) => void;
56
+ editable?: boolean;
57
+ onCreate?: (props: EditorEvents["create"]) => void;
58
+ extensions?: Extensions;
59
+ slashCommandProps?: SlashCommandProps;
60
+ uploadImage?: UploadImageFunction;
61
+ };
62
+
63
+ // biome-ignore lint/suspicious/noExplicitAny: <updateAttrById>
64
+ const updateAttrById = (json: JSONContent, id: string, attr: string, value: any) => {
65
+ if (!json.content) return;
66
+ for (const node of json.content) {
67
+ if (node.attrs?.id === id) {
68
+ node.attrs[attr] = value;
69
+ }
70
+ if (node.content) {
71
+ updateAttrById(node, id, attr, value);
72
+ }
73
+ }
74
+ };
75
+
76
+ const JSON_EXCLUDE = ["imageUpload"];
77
+
78
+ export const getJSONString = (editor: EditorInstance): string => {
79
+ const json = editor.getJSON();
80
+ // biome-ignore lint/suspicious/noExplicitAny: <node>
81
+ json.content = json.content?.filter((node: any) => !JSON_EXCLUDE.includes(node.type));
82
+ return JSON.stringify(json);
83
+ };
84
+
85
+ const UNIQUE_ATTRIBUTE_NAME = "id";
86
+
87
+ export const useEditor = (props: UseEditorProps): [EditorInstance, NodeData, string] => {
88
+ const {
89
+ onReadOnlyChange,
90
+ value,
91
+ onCreate,
92
+ onChange,
93
+ limit,
94
+ editable = true,
95
+ extensions = [],
96
+ slashCommandProps,
97
+ uploadImage,
98
+ } = props;
99
+ const handleId = useHandleId();
100
+ const data = useData();
101
+
102
+ const onReadOnlyChecked = useCallback(
103
+ (node: ProseMirrorNode, checked: boolean) => {
104
+ const jsonValue = editor?.getJSON();
105
+ updateAttrById(jsonValue!, node.attrs.id, "checked", checked);
106
+ editor?.commands.setContent(jsonValue!, false);
107
+ onReadOnlyChange?.(JSON.stringify(jsonValue));
108
+ return true;
109
+ },
110
+ [onReadOnlyChange],
111
+ );
112
+
113
+ const editor = useBaseEditor(
114
+ {
115
+ editable,
116
+ extensions: [
117
+ Document,
118
+ Columns,
119
+ TaskList,
120
+ TaskItem.configure({
121
+ nested: true,
122
+ onReadOnlyChecked,
123
+ }),
124
+ Column,
125
+ Selection,
126
+ Heading.configure({
127
+ levels: [1, 2, 3, 4, 5, 6],
128
+ }),
129
+ HorizontalRule,
130
+ StarterKit.configure({
131
+ document: false,
132
+ dropcursor: false,
133
+ heading: false,
134
+ horizontalRule: false,
135
+ blockquote: false,
136
+ history: false,
137
+ codeBlock: false,
138
+ }),
139
+ CharacterCount.configure({
140
+ limit,
141
+ }),
142
+ GlobalDragHandle.configure({
143
+ dragHandleSelector: `#${handleId}`,
144
+ onNodeChange: data.handleNodeChange,
145
+ onShow: () => data.setHidden(false),
146
+ onHide: () => data.setHidden(true),
147
+ uniqueId: UNIQUE_ATTRIBUTE_NAME,
148
+ }),
149
+ CodeBlockLowlight.configure({
150
+ lowlight: createLowlight(common),
151
+ defaultLanguage: null,
152
+ }),
153
+ TextStyle,
154
+ FontSize,
155
+ FontFamily,
156
+ Color,
157
+ // TrailingNode,
158
+ Link.configure({
159
+ openOnClick: false,
160
+ }),
161
+ Highlight.configure({ multicolor: true }),
162
+ Underline,
163
+ TextAlign.extend({
164
+ addKeyboardShortcuts() {
165
+ return {};
166
+ },
167
+ }).configure({
168
+ types: ["heading", "paragraph"],
169
+ }),
170
+ Subscript,
171
+ Superscript,
172
+ Table,
173
+ TableCell,
174
+ TableHeader,
175
+ TableRow,
176
+ Typography,
177
+ Placeholder.configure({
178
+ includeChildren: true,
179
+ showOnlyCurrent: false,
180
+ placeholder: ({ node }) => {
181
+ if (node.type.name === "blockquoteFigure") {
182
+ return "";
183
+ }
184
+ if (node.type.name === "quoteCaption") {
185
+ return i18n("placeholder.quoteCaption");
186
+ }
187
+ if (node.type.name === "quote") {
188
+ return i18n("placeholder.quote");
189
+ }
190
+ return i18n("placeholder.default");
191
+ },
192
+ }),
193
+ SlashCommand.configure(slashCommandProps),
194
+ Focus,
195
+ Figcaption,
196
+ BlockquoteFigure,
197
+ Dropcursor.configure({
198
+ width: 2,
199
+ class: "!bg-primary",
200
+ }),
201
+ ImageBlock,
202
+ ImageUpload.configure({
203
+ uploadImage,
204
+ }),
205
+ UniqueId.configure({
206
+ attributeName: UNIQUE_ATTRIBUTE_NAME,
207
+ types: [
208
+ "paragraph",
209
+ "heading",
210
+ "blockquoteFigure",
211
+ "codeBlock",
212
+ "bulletList",
213
+ "listItem",
214
+ "orderedList",
215
+ "taskList",
216
+ "taskItem",
217
+ "imageBlock",
218
+ "table",
219
+ "horizontalRule",
220
+ "imageUpload",
221
+ ],
222
+ injectNodeName: false,
223
+ }),
224
+ History,
225
+ ...extensions,
226
+ ],
227
+ immediatelyRender: false,
228
+ content: value ? JSON.parse(value) : value,
229
+ onUpdate: ({ editor }) => {
230
+ if (editor.isInitialized) {
231
+ onChange?.(getJSONString(editor));
232
+ }
233
+ },
234
+ onCreate,
235
+ },
236
+ [onChange, onReadOnlyChecked, onCreate],
237
+ );
238
+
239
+ const isUpdate = useRef(false);
240
+
241
+ // 监听 value 变化,必要时 setContent
242
+ useEffect(() => {
243
+ if (!(editor && value)) return;
244
+ const current = JSON.stringify(editor.getJSON());
245
+ if (current !== value && !isUpdate.current) {
246
+ editor.commands.setContent(JSON.parse(value), false);
247
+ }
248
+ }, [value, editor]);
249
+
250
+ // onUpdate 里判断是否是外部 setContent
251
+ useEffect(() => {
252
+ if (!editor) return;
253
+ const handler = () => {
254
+ isUpdate.current = true;
255
+ onChange?.(getJSONString(editor));
256
+ };
257
+ editor.on("update", handler);
258
+ return () => {
259
+ editor.off("update", handler);
260
+ };
261
+ }, [editor, onChange]);
262
+
263
+ return [editor!, data, handleId];
264
+ };
@@ -0,0 +1,53 @@
1
+ import "./container.css";
2
+ import "./editor.css";
3
+ import { forwardRef, useImperativeHandle, useRef } from "react";
4
+ import type { EditorEvents, Editor as EditorInstance, Extensions } from "@tiptap/core";
5
+ import { EditorContent } from "@tiptap/react";
6
+
7
+ import { EditorController } from "./control";
8
+ import type { UploadImageFunction } from "./extension/image-upload/ImageUpload";
9
+ import type { SlashCommandProps } from "./extension/slash-command";
10
+ import { useEditor } from "./hooks";
11
+
12
+ export type EditorProps = {
13
+ limit?: number;
14
+ value?: string;
15
+ onChange?: (value: string) => void;
16
+ onCreate?: (props: EditorEvents["create"]) => void;
17
+ extensions?: Extensions;
18
+ slashCommandProps?: SlashCommandProps;
19
+ uploadImage?: UploadImageFunction;
20
+ };
21
+
22
+ export type EditorRef = {
23
+ editor: EditorInstance | null;
24
+ };
25
+
26
+ export const Editor = forwardRef<EditorRef, EditorProps>((props, ref) => {
27
+ const { limit, value, onChange, onCreate, extensions, slashCommandProps, uploadImage } = props;
28
+ const menuContainerRef = useRef<HTMLDivElement>(null);
29
+ const [editor, data, handleId] = useEditor({
30
+ limit,
31
+ value,
32
+ onChange,
33
+ onCreate,
34
+ extensions,
35
+ slashCommandProps,
36
+ uploadImage,
37
+ });
38
+
39
+ useImperativeHandle(
40
+ ref,
41
+ () => ({
42
+ editor,
43
+ }),
44
+ [editor],
45
+ );
46
+
47
+ return (
48
+ <div className="editor-container" ref={menuContainerRef}>
49
+ <EditorContent className="editor" editor={editor} />
50
+ <EditorController appendTo={menuContainerRef} data={data} editor={editor} handleId={handleId} limit={limit} />
51
+ </div>
52
+ );
53
+ });
@@ -0,0 +1,75 @@
1
+ import { type ReactElement, useCallback, useState } from "react";
2
+ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react";
3
+
4
+ import { LinkEditorPanel, LinkPreviewPanel } from "../../panels";
5
+ import type { MenuProps } from "../types";
6
+
7
+ export const LinkMenu = ({ editor, appendTo }: MenuProps): ReactElement => {
8
+ const [showEdit, setShowEdit] = useState(false);
9
+
10
+ const shouldShow = useCallback(() => {
11
+ return editor.isActive("link");
12
+ }, [editor]);
13
+
14
+ const { href: link, target } = editor.getAttributes("link");
15
+
16
+ const handleEdit = useCallback(() => {
17
+ setShowEdit(true);
18
+ }, []);
19
+
20
+ const onSetLink = useCallback(
21
+ (url: string, openInNewTab?: boolean) => {
22
+ editor
23
+ .chain()
24
+ .focus()
25
+ .extendMarkRange("link")
26
+ .setLink({ href: url, target: openInNewTab ? "_blank" : "" })
27
+ .run();
28
+ setShowEdit(false);
29
+ },
30
+ [editor],
31
+ );
32
+
33
+ const onUnsetLink = useCallback(() => {
34
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
35
+ setShowEdit(false);
36
+ return null;
37
+ }, [editor]);
38
+
39
+ // const onShowEdit = useCallback(() => {
40
+ // setShowEdit(true)
41
+ // }, [])
42
+
43
+ // const onHideEdit = useCallback(() => {
44
+ // setShowEdit(false)
45
+ // }, [])
46
+
47
+ return (
48
+ <BaseBubbleMenu
49
+ editor={editor}
50
+ pluginKey="textMenu"
51
+ shouldShow={shouldShow}
52
+ tippyOptions={{
53
+ zIndex: 40,
54
+ popperOptions: {
55
+ modifiers: [{ name: "flip", enabled: false }],
56
+ },
57
+ appendTo: () => {
58
+ return appendTo?.current;
59
+ },
60
+ onHidden: () => {
61
+ setShowEdit(false);
62
+ },
63
+ }}
64
+ updateDelay={0}
65
+ >
66
+ {showEdit ? (
67
+ <LinkEditorPanel initialOpenInNewTab={target === "_blank"} initialUrl={link} onSetLink={onSetLink} />
68
+ ) : (
69
+ <LinkPreviewPanel onClear={onUnsetLink} onEdit={handleEdit} url={link} />
70
+ )}
71
+ </BaseBubbleMenu>
72
+ );
73
+ };
74
+
75
+ export default LinkMenu;
@@ -0,0 +1 @@
1
+ export { LinkMenu } from "./LinkMenu";