@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.
- package/README.md +458 -0
- package/package.json +100 -0
- package/src/editor/constants.tsx +66 -0
- package/src/editor/container.css +46 -0
- package/src/editor/control/character-count/index.tsx +39 -0
- package/src/editor/control/drag-handle/index.tsx +85 -0
- package/src/editor/control/drag-handle/use.content.actions.ts +71 -0
- package/src/editor/control/drag-handle/use.data.ts +29 -0
- package/src/editor/control/drag-handle/use.handle.id.ts +6 -0
- package/src/editor/control/index.tsx +35 -0
- package/src/editor/editor.css +626 -0
- package/src/editor/extension/block-quote-figure/BlockquoteFigure.ts +73 -0
- package/src/editor/extension/block-quote-figure/Quote/Quote.ts +31 -0
- package/src/editor/extension/block-quote-figure/Quote/index.ts +1 -0
- package/src/editor/extension/block-quote-figure/QuoteCaption/QuoteCaption.ts +54 -0
- package/src/editor/extension/block-quote-figure/QuoteCaption/index.ts +1 -0
- package/src/editor/extension/block-quote-figure/index.ts +1 -0
- package/src/editor/extension/document/index.ts +5 -0
- package/src/editor/extension/figcaption/Figcaption.ts +90 -0
- package/src/editor/extension/figcaption/index.ts +1 -0
- package/src/editor/extension/figure/Figure.ts +62 -0
- package/src/editor/extension/figure/index.ts +1 -0
- package/src/editor/extension/font-size/FontSize.ts +64 -0
- package/src/editor/extension/font-size/index.ts +1 -0
- package/src/editor/extension/global-drag-handle/clipboard-serializer.ts +28 -0
- package/src/editor/extension/global-drag-handle/index.ts +377 -0
- package/src/editor/extension/heading/index.ts +13 -0
- package/src/editor/extension/horizontal-rule/HorizontalRule.ts +10 -0
- package/src/editor/extension/horizontal-rule/index.ts +1 -0
- package/src/editor/extension/image/index.ts +5 -0
- package/src/editor/extension/image-block/ImageBlock.ts +103 -0
- package/src/editor/extension/image-block/components/ImageBlockMenu.tsx +100 -0
- package/src/editor/extension/image-block/components/ImageBlockView.tsx +47 -0
- package/src/editor/extension/image-block/components/ImageBlockWidth.tsx +40 -0
- package/src/editor/extension/image-block/index.ts +1 -0
- package/src/editor/extension/image-upload/ImageUpload.ts +58 -0
- package/src/editor/extension/image-upload/index.ts +1 -0
- package/src/editor/extension/image-upload/view/ImageUpload.tsx +27 -0
- package/src/editor/extension/image-upload/view/ImageUploader.tsx +64 -0
- package/src/editor/extension/image-upload/view/hooks.ts +109 -0
- package/src/editor/extension/image-upload/view/index.tsx +1 -0
- package/src/editor/extension/index.ts +30 -0
- package/src/editor/extension/link/Link.ts +39 -0
- package/src/editor/extension/link/index.ts +1 -0
- package/src/editor/extension/multi-column/Column.ts +33 -0
- package/src/editor/extension/multi-column/Columns.ts +65 -0
- package/src/editor/extension/multi-column/index.ts +2 -0
- package/src/editor/extension/multi-column/menus/ColumnsMenu.tsx +82 -0
- package/src/editor/extension/multi-column/menus/index.ts +1 -0
- package/src/editor/extension/selection/Selection.ts +36 -0
- package/src/editor/extension/selection/index.ts +1 -0
- package/src/editor/extension/slash-command/MenuList.tsx +145 -0
- package/src/editor/extension/slash-command/groups.ts +153 -0
- package/src/editor/extension/slash-command/index.ts +277 -0
- package/src/editor/extension/slash-command/types.ts +25 -0
- package/src/editor/extension/table/Cell.ts +126 -0
- package/src/editor/extension/table/Header.ts +89 -0
- package/src/editor/extension/table/Row.ts +8 -0
- package/src/editor/extension/table/Table.ts +9 -0
- package/src/editor/extension/table/index.ts +4 -0
- package/src/editor/extension/table/menus/TableColumn/index.tsx +73 -0
- package/src/editor/extension/table/menus/TableColumn/utils.ts +38 -0
- package/src/editor/extension/table/menus/TableRow/index.tsx +74 -0
- package/src/editor/extension/table/menus/TableRow/utils.ts +38 -0
- package/src/editor/extension/table/menus/index.tsx +2 -0
- package/src/editor/extension/table/utils.ts +258 -0
- package/src/editor/extension/task-item/index.ts +1 -0
- package/src/editor/extension/task-item/task-item.ts +225 -0
- package/src/editor/extension/task-list/index.ts +1 -0
- package/src/editor/extension/task-list/task-list.ts +81 -0
- package/src/editor/extension/trailing-node/index.ts +1 -0
- package/src/editor/extension/trailing-node/trailing-node.ts +70 -0
- package/src/editor/extension/unique-id/index.ts +1 -0
- package/src/editor/extension/unique-id/uniqueId.ts +123 -0
- package/src/editor/hooks.ts +264 -0
- package/src/editor/index.tsx +53 -0
- package/src/editor/menus/LinkMenu/LinkMenu.tsx +75 -0
- package/src/editor/menus/LinkMenu/index.tsx +1 -0
- package/src/editor/menus/TextMenu/TextMenu.tsx +193 -0
- package/src/editor/menus/TextMenu/components/AIDropdown.tsx +140 -0
- package/src/editor/menus/TextMenu/components/ContentTypePicker.tsx +76 -0
- package/src/editor/menus/TextMenu/components/EditLinkPopover.tsx +25 -0
- package/src/editor/menus/TextMenu/components/FontFamilyPicker.tsx +84 -0
- package/src/editor/menus/TextMenu/components/FontSizePicker.tsx +56 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuCommands.ts +96 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuContentTypes.ts +86 -0
- package/src/editor/menus/TextMenu/hooks/useTextmenuStates.ts +50 -0
- package/src/editor/menus/TextMenu/index.tsx +2 -0
- package/src/editor/menus/types.ts +21 -0
- package/src/editor/panels/Colorpicker/ColorButton.tsx +35 -0
- package/src/editor/panels/Colorpicker/Colorpicker.tsx +67 -0
- package/src/editor/panels/Colorpicker/index.tsx +2 -0
- package/src/editor/panels/LinkEditorPanel/LinkEditorPanel.tsx +76 -0
- package/src/editor/panels/LinkEditorPanel/index.tsx +1 -0
- package/src/editor/panels/LinkPreviewPanel/LinkPreviewPanel.tsx +32 -0
- package/src/editor/panels/LinkPreviewPanel/index.tsx +1 -0
- package/src/editor/panels/index.tsx +3 -0
- package/src/editor/types.tsx +38 -0
- package/src/editor/ui/Button/Button.tsx +70 -0
- package/src/editor/ui/Button/index.tsx +2 -0
- package/src/editor/ui/Dropdown/Dropdown.tsx +39 -0
- package/src/editor/ui/Dropdown/index.tsx +1 -0
- package/src/editor/ui/Icon.tsx +21 -0
- package/src/editor/ui/Loader/Loader.tsx +39 -0
- package/src/editor/ui/Loader/index.ts +1 -0
- package/src/editor/ui/Loader/types.ts +7 -0
- package/src/editor/ui/Panel/index.tsx +109 -0
- package/src/editor/ui/PopoverMenu.tsx +127 -0
- package/src/editor/ui/Spinner/Spinner.tsx +10 -0
- package/src/editor/ui/Spinner/index.tsx +1 -0
- package/src/editor/ui/Surface.tsx +27 -0
- package/src/editor/ui/Textarea/Textarea.tsx +20 -0
- package/src/editor/ui/Textarea/index.tsx +1 -0
- package/src/editor/ui/Toggle/Toggle.tsx +39 -0
- package/src/editor/ui/Toggle/index.tsx +1 -0
- package/src/editor/ui/Toolbar.tsx +107 -0
- package/src/editor/ui/Tooltip/index.tsx +77 -0
- package/src/editor/ui/Tooltip/types.ts +17 -0
- package/src/editor/utils/cssVar.ts +14 -0
- package/src/editor/utils/getRenderContainer.ts +39 -0
- package/src/editor/utils/index.ts +16 -0
- package/src/editor/utils/isCustomNodeSelected.ts +47 -0
- package/src/editor/utils/isTextSelected.ts +25 -0
- package/src/editor/utils/locale.ts +5 -0
- package/src/editor/viewer/index.tsx +26 -0
- package/src/globals.css +1 -0
- package/src/index.ts +7 -0
- package/src/locales/en-us.ts +133 -0
- package/src/locales/zh-cn.ts +133 -0
- package/src/locales/zh-tw.ts +133 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { type Editor, Extension } from "@tiptap/core";
|
|
2
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { ReactRenderer } from "@tiptap/react";
|
|
4
|
+
import Suggestion, { type SuggestionKeyDownProps, type SuggestionProps } from "@tiptap/suggestion";
|
|
5
|
+
import tippy from "tippy.js";
|
|
6
|
+
|
|
7
|
+
import { getGroups } from "./groups";
|
|
8
|
+
import { MenuList } from "./MenuList";
|
|
9
|
+
import type { Group } from "./types";
|
|
10
|
+
|
|
11
|
+
const extensionName = "slashCommand";
|
|
12
|
+
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: <popup>
|
|
14
|
+
let popup: any;
|
|
15
|
+
|
|
16
|
+
export type SlashCommandProps = {
|
|
17
|
+
groups?: Group[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const SlashCommand = Extension.create({
|
|
21
|
+
name: extensionName,
|
|
22
|
+
|
|
23
|
+
priority: 200,
|
|
24
|
+
|
|
25
|
+
addOptions(): SlashCommandProps {
|
|
26
|
+
return {
|
|
27
|
+
groups: [],
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
onCreate() {
|
|
32
|
+
popup = tippy("body", {
|
|
33
|
+
interactive: true,
|
|
34
|
+
trigger: "manual",
|
|
35
|
+
placement: "bottom-start",
|
|
36
|
+
theme: "slash-command",
|
|
37
|
+
maxWidth: "16rem",
|
|
38
|
+
offset: [16, 8],
|
|
39
|
+
popperOptions: {
|
|
40
|
+
strategy: "fixed",
|
|
41
|
+
modifiers: [
|
|
42
|
+
{
|
|
43
|
+
name: "flip",
|
|
44
|
+
enabled: false,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
addProseMirrorPlugins() {
|
|
52
|
+
const { groups: extendGroups } = this.options as SlashCommandProps;
|
|
53
|
+
return [
|
|
54
|
+
Suggestion({
|
|
55
|
+
editor: this.editor,
|
|
56
|
+
char: "/",
|
|
57
|
+
allowSpaces: true,
|
|
58
|
+
startOfLine: true,
|
|
59
|
+
pluginKey: new PluginKey(extensionName),
|
|
60
|
+
allow: ({ state, range }) => {
|
|
61
|
+
const $from = state.doc.resolve(range.from);
|
|
62
|
+
const isRootDepth = $from.depth === 1;
|
|
63
|
+
const isParagraph = $from.parent.type.name === "paragraph";
|
|
64
|
+
const isStartOfNode = $from.parent.textContent?.charAt(0) === "/";
|
|
65
|
+
// TODO
|
|
66
|
+
const isInColumn = this.editor.isActive("column");
|
|
67
|
+
|
|
68
|
+
const afterContent = $from.parent.textContent?.substring($from.parent.textContent?.indexOf("/"));
|
|
69
|
+
const isValidAfterContent = !afterContent?.endsWith(" ");
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
((isRootDepth && isParagraph && isStartOfNode) || (isInColumn && isParagraph && isStartOfNode)) &&
|
|
73
|
+
isValidAfterContent
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
// biome-ignore lint/suspicious/noExplicitAny: <props>
|
|
77
|
+
command: ({ editor, props }: { editor: Editor; props: any }) => {
|
|
78
|
+
const { view, state } = editor;
|
|
79
|
+
const { $head, $from } = view.state.selection;
|
|
80
|
+
|
|
81
|
+
const end = $from.pos;
|
|
82
|
+
const from = $head?.nodeBefore
|
|
83
|
+
? end - ($head.nodeBefore.text?.substring($head.nodeBefore.text?.indexOf("/")).length ?? 0)
|
|
84
|
+
: $from.start();
|
|
85
|
+
|
|
86
|
+
const tr = state.tr.deleteRange(from, end);
|
|
87
|
+
view.dispatch(tr);
|
|
88
|
+
|
|
89
|
+
props.action(editor);
|
|
90
|
+
view.focus();
|
|
91
|
+
},
|
|
92
|
+
items: ({ query }: { query: string }) => {
|
|
93
|
+
const presetsGroups = getGroups();
|
|
94
|
+
const startGroups = extendGroups?.filter((group) => group.position === "start") || [];
|
|
95
|
+
const endGroups = extendGroups?.filter((group) => group.position === "end") || [];
|
|
96
|
+
const all = [...startGroups, ...presetsGroups, ...endGroups];
|
|
97
|
+
const withFilteredCommands = all.map((group) => ({
|
|
98
|
+
...group,
|
|
99
|
+
commands: group.commands
|
|
100
|
+
.filter((item) => {
|
|
101
|
+
const labelNormalized = item.label?.toLowerCase().trim();
|
|
102
|
+
const queryNormalized = query.toLowerCase().trim();
|
|
103
|
+
|
|
104
|
+
if (item.aliases) {
|
|
105
|
+
const aliases = item.aliases.map((alias) => alias.toLowerCase().trim());
|
|
106
|
+
|
|
107
|
+
return labelNormalized?.includes(queryNormalized) || aliases.includes(queryNormalized);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return labelNormalized?.includes(queryNormalized);
|
|
111
|
+
})
|
|
112
|
+
.filter((command) => (command.shouldBeHidden ? !command.shouldBeHidden(this.editor) : true)),
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const withoutEmptyGroups = withFilteredCommands.filter((group) => {
|
|
116
|
+
if (group.commands.length > 0) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const withEnabledSettings = withoutEmptyGroups.map((group) => ({
|
|
124
|
+
...group,
|
|
125
|
+
commands: group.commands.map((command) => ({
|
|
126
|
+
...command,
|
|
127
|
+
isEnabled: true,
|
|
128
|
+
})),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
return withEnabledSettings;
|
|
132
|
+
},
|
|
133
|
+
render: () => {
|
|
134
|
+
// biome-ignore lint/suspicious/noExplicitAny: <component>
|
|
135
|
+
let component: any;
|
|
136
|
+
|
|
137
|
+
let scrollHandler: (() => void) | null = null;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
onStart: (props: SuggestionProps) => {
|
|
141
|
+
component = new ReactRenderer(MenuList, {
|
|
142
|
+
props,
|
|
143
|
+
editor: props.editor,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const { view } = props.editor;
|
|
147
|
+
|
|
148
|
+
const _editorNode = view.dom as HTMLElement;
|
|
149
|
+
|
|
150
|
+
const getReferenceClientRect = () => {
|
|
151
|
+
if (!props.clientRect) {
|
|
152
|
+
return props.editor.storage[extensionName].rect;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rect = props.clientRect();
|
|
156
|
+
|
|
157
|
+
if (!rect) {
|
|
158
|
+
return props.editor.storage[extensionName].rect;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let yPos = rect.y;
|
|
162
|
+
|
|
163
|
+
if (rect.top + component.element.offsetHeight + 40 > window.innerHeight) {
|
|
164
|
+
const diff = rect.top + component.element.offsetHeight - window.innerHeight + 40;
|
|
165
|
+
yPos = rect.y - diff;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen
|
|
169
|
+
// const editorXOffset = editorNode.getBoundingClientRect().x
|
|
170
|
+
return new DOMRect(rect.x, yPos, rect.width, rect.height);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
scrollHandler = () => {
|
|
174
|
+
popup?.[0].setProps({
|
|
175
|
+
getReferenceClientRect,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
view.dom.parentElement?.addEventListener("scroll", scrollHandler);
|
|
180
|
+
|
|
181
|
+
popup?.[0].setProps({
|
|
182
|
+
getReferenceClientRect,
|
|
183
|
+
appendTo: () => document.body,
|
|
184
|
+
content: component.element,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
popup?.[0].show();
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
onUpdate(props: SuggestionProps) {
|
|
191
|
+
component.updateProps(props);
|
|
192
|
+
|
|
193
|
+
const { view } = props.editor;
|
|
194
|
+
|
|
195
|
+
//const editorNode = view.dom as HTMLElement
|
|
196
|
+
|
|
197
|
+
const getReferenceClientRect = () => {
|
|
198
|
+
if (!props.clientRect) {
|
|
199
|
+
return props.editor.storage[extensionName].rect;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rect = props.clientRect();
|
|
203
|
+
|
|
204
|
+
if (!rect) {
|
|
205
|
+
return props.editor.storage[extensionName].rect;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen
|
|
209
|
+
return new DOMRect(rect.x, rect.y, rect.width, rect.height);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const scrollHandler = () => {
|
|
213
|
+
popup?.[0].setProps({
|
|
214
|
+
getReferenceClientRect,
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
view.dom.parentElement?.addEventListener("scroll", scrollHandler);
|
|
219
|
+
|
|
220
|
+
props.editor.storage[extensionName].rect = props.clientRect
|
|
221
|
+
? getReferenceClientRect()
|
|
222
|
+
: {
|
|
223
|
+
width: 0,
|
|
224
|
+
height: 0,
|
|
225
|
+
left: 0,
|
|
226
|
+
top: 0,
|
|
227
|
+
right: 0,
|
|
228
|
+
bottom: 0,
|
|
229
|
+
};
|
|
230
|
+
popup?.[0].setProps({
|
|
231
|
+
getReferenceClientRect,
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
onKeyDown(props: SuggestionKeyDownProps) {
|
|
236
|
+
if (props.event.key === "Escape") {
|
|
237
|
+
popup?.[0].hide();
|
|
238
|
+
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!popup?.[0].state.isShown) {
|
|
243
|
+
popup?.[0].show();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return component.ref?.onKeyDown(props);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
onExit(props) {
|
|
250
|
+
popup?.[0].hide();
|
|
251
|
+
if (scrollHandler) {
|
|
252
|
+
const { view } = props.editor;
|
|
253
|
+
view.dom.parentElement?.removeEventListener("scroll", scrollHandler);
|
|
254
|
+
}
|
|
255
|
+
component.destroy();
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
];
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
addStorage() {
|
|
264
|
+
return {
|
|
265
|
+
rect: {
|
|
266
|
+
width: 0,
|
|
267
|
+
height: 0,
|
|
268
|
+
left: 0,
|
|
269
|
+
top: 0,
|
|
270
|
+
right: 0,
|
|
271
|
+
bottom: 0,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
export default SlashCommand;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { icons } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export interface Group {
|
|
5
|
+
name: string;
|
|
6
|
+
title: string;
|
|
7
|
+
commands: Command[];
|
|
8
|
+
position?: "start" | "end";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Command {
|
|
12
|
+
name: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
aliases?: string[];
|
|
16
|
+
iconName: keyof typeof icons;
|
|
17
|
+
action: (editor: Editor) => void;
|
|
18
|
+
shouldBeHidden?: (editor: Editor) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MenuListProps {
|
|
22
|
+
editor: Editor;
|
|
23
|
+
items: Group[];
|
|
24
|
+
command: (command: Command) => void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mergeAttributes, Node } from "@tiptap/core";
|
|
2
|
+
import { Plugin } from "@tiptap/pm/state";
|
|
3
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
|
|
5
|
+
import { getCellsInColumn, isRowSelected, selectRow } from "./utils";
|
|
6
|
+
|
|
7
|
+
export interface TableCellOptions {
|
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: <HTMLAttributes>
|
|
9
|
+
HTMLAttributes: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TableCell = Node.create<TableCellOptions>({
|
|
13
|
+
name: "tableCell",
|
|
14
|
+
|
|
15
|
+
content: "block+", // TODO: Do not allow table in table
|
|
16
|
+
|
|
17
|
+
tableRole: "cell",
|
|
18
|
+
|
|
19
|
+
isolating: true,
|
|
20
|
+
|
|
21
|
+
addOptions() {
|
|
22
|
+
return {
|
|
23
|
+
HTMLAttributes: {},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
parseHTML() {
|
|
28
|
+
return [{ tag: "td" }];
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
renderHTML({ HTMLAttributes }) {
|
|
32
|
+
return ["td", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
addAttributes() {
|
|
36
|
+
return {
|
|
37
|
+
colspan: {
|
|
38
|
+
default: 1,
|
|
39
|
+
parseHTML: (element) => {
|
|
40
|
+
const colspan = element.getAttribute("colspan");
|
|
41
|
+
const value = colspan ? Number.parseInt(colspan, 10) : 1;
|
|
42
|
+
|
|
43
|
+
return value;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
rowspan: {
|
|
47
|
+
default: 1,
|
|
48
|
+
parseHTML: (element) => {
|
|
49
|
+
const rowspan = element.getAttribute("rowspan");
|
|
50
|
+
const value = rowspan ? Number.parseInt(rowspan, 10) : 1;
|
|
51
|
+
|
|
52
|
+
return value;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
colwidth: {
|
|
56
|
+
default: null,
|
|
57
|
+
parseHTML: (element) => {
|
|
58
|
+
const colwidth = element.getAttribute("colwidth");
|
|
59
|
+
const value = colwidth ? [Number.parseInt(colwidth, 10)] : null;
|
|
60
|
+
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
style: {
|
|
65
|
+
default: null,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
addProseMirrorPlugins() {
|
|
71
|
+
const { isEditable } = this.editor;
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
new Plugin({
|
|
75
|
+
props: {
|
|
76
|
+
decorations: (state) => {
|
|
77
|
+
if (!isEditable) {
|
|
78
|
+
return DecorationSet.empty;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { doc, selection } = state;
|
|
82
|
+
const decorations: Decoration[] = [];
|
|
83
|
+
const cells = getCellsInColumn(0)(selection);
|
|
84
|
+
|
|
85
|
+
if (cells) {
|
|
86
|
+
cells.forEach(({ pos }: { pos: number }, index: number) => {
|
|
87
|
+
decorations.push(
|
|
88
|
+
Decoration.widget(pos + 1, () => {
|
|
89
|
+
const rowSelected = isRowSelected(index)(selection);
|
|
90
|
+
let className = "grip-row";
|
|
91
|
+
|
|
92
|
+
if (rowSelected) {
|
|
93
|
+
className += " selected";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (index === 0) {
|
|
97
|
+
className += " first";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (index === cells.length - 1) {
|
|
101
|
+
className += " last";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const grip = document.createElement("a");
|
|
105
|
+
|
|
106
|
+
grip.className = className;
|
|
107
|
+
grip.addEventListener("mousedown", (event) => {
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
event.stopImmediatePropagation();
|
|
110
|
+
|
|
111
|
+
this.editor.view.dispatch(selectRow(index)(this.editor.state.tr));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return grip;
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return DecorationSet.create(doc, decorations);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
},
|
|
126
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import TiptapTableHeader from "@tiptap/extension-table-header";
|
|
2
|
+
import { Plugin } from "@tiptap/pm/state";
|
|
3
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
|
|
5
|
+
import { getCellsInRow, isColumnSelected, selectColumn } from "./utils";
|
|
6
|
+
|
|
7
|
+
export const TableHeader = TiptapTableHeader.extend({
|
|
8
|
+
addAttributes() {
|
|
9
|
+
return {
|
|
10
|
+
colspan: {
|
|
11
|
+
default: 1,
|
|
12
|
+
},
|
|
13
|
+
rowspan: {
|
|
14
|
+
default: 1,
|
|
15
|
+
},
|
|
16
|
+
colwidth: {
|
|
17
|
+
default: null,
|
|
18
|
+
parseHTML: (element) => {
|
|
19
|
+
const colwidth = element.getAttribute("colwidth");
|
|
20
|
+
const value = colwidth ? colwidth.split(",").map((item) => Number.parseInt(item, 10)) : null;
|
|
21
|
+
|
|
22
|
+
return value;
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
style: {
|
|
26
|
+
default: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
addProseMirrorPlugins() {
|
|
32
|
+
const { isEditable } = this.editor;
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
new Plugin({
|
|
36
|
+
props: {
|
|
37
|
+
decorations: (state) => {
|
|
38
|
+
if (!isEditable) {
|
|
39
|
+
return DecorationSet.empty;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { doc, selection } = state;
|
|
43
|
+
const decorations: Decoration[] = [];
|
|
44
|
+
const cells = getCellsInRow(0)(selection);
|
|
45
|
+
|
|
46
|
+
if (cells) {
|
|
47
|
+
cells.forEach(({ pos }: { pos: number }, index: number) => {
|
|
48
|
+
decorations.push(
|
|
49
|
+
Decoration.widget(pos + 1, () => {
|
|
50
|
+
const colSelected = isColumnSelected(index)(selection);
|
|
51
|
+
let className = "grip-column";
|
|
52
|
+
|
|
53
|
+
if (colSelected) {
|
|
54
|
+
className += " selected";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (index === 0) {
|
|
58
|
+
className += " first";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (index === cells.length - 1) {
|
|
62
|
+
className += " last";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const grip = document.createElement("a");
|
|
66
|
+
|
|
67
|
+
grip.className = className;
|
|
68
|
+
grip.addEventListener("mousedown", (event) => {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
event.stopImmediatePropagation();
|
|
71
|
+
|
|
72
|
+
this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return grip;
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return DecorationSet.create(doc, decorations);
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
];
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export default TableHeader;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { type ReactElement, useCallback } from "react";
|
|
2
|
+
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react";
|
|
3
|
+
|
|
4
|
+
import type { MenuProps, ShouldShowProps } from "../../../../menus/types";
|
|
5
|
+
import { Icon } from "../../../../ui/Icon";
|
|
6
|
+
import { Item } from "../../../../ui/PopoverMenu";
|
|
7
|
+
import { Toolbar } from "../../../../ui/Toolbar";
|
|
8
|
+
import { i18n } from "../../../../utils/locale";
|
|
9
|
+
import { isColumnGripSelected } from "./utils";
|
|
10
|
+
|
|
11
|
+
export const TableColumnMenu = React.memo(({ editor, appendTo }: MenuProps): ReactElement => {
|
|
12
|
+
const shouldShow = useCallback(
|
|
13
|
+
({ view, state, from }: ShouldShowProps) => {
|
|
14
|
+
if (!state) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return isColumnGripSelected({ editor, view, state, from: from || 0 });
|
|
19
|
+
},
|
|
20
|
+
[editor],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const onAddColumnBefore = useCallback(() => {
|
|
24
|
+
editor.chain().focus().addColumnBefore().run();
|
|
25
|
+
}, [editor]);
|
|
26
|
+
|
|
27
|
+
const onAddColumnAfter = useCallback(() => {
|
|
28
|
+
editor.chain().focus().addColumnAfter().run();
|
|
29
|
+
}, [editor]);
|
|
30
|
+
|
|
31
|
+
const onDeleteColumn = useCallback(() => {
|
|
32
|
+
editor.chain().focus().deleteColumn().run();
|
|
33
|
+
}, [editor]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<BaseBubbleMenu
|
|
37
|
+
editor={editor}
|
|
38
|
+
pluginKey="tableColumnMenu"
|
|
39
|
+
shouldShow={shouldShow}
|
|
40
|
+
tippyOptions={{
|
|
41
|
+
zIndex: 40,
|
|
42
|
+
appendTo: () => {
|
|
43
|
+
return appendTo?.current;
|
|
44
|
+
},
|
|
45
|
+
offset: [0, 15],
|
|
46
|
+
popperOptions: {
|
|
47
|
+
modifiers: [{ name: "flip", enabled: false }],
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
updateDelay={0}
|
|
51
|
+
>
|
|
52
|
+
<Toolbar.Wrapper isVertical>
|
|
53
|
+
<Item
|
|
54
|
+
close={false}
|
|
55
|
+
iconComponent={<Icon name="ArrowLeftToLine" />}
|
|
56
|
+
label={i18n("tableColumnMenu.onAddColumnBefore")}
|
|
57
|
+
onClick={onAddColumnBefore}
|
|
58
|
+
/>
|
|
59
|
+
<Item
|
|
60
|
+
close={false}
|
|
61
|
+
iconComponent={<Icon name="ArrowRightToLine" />}
|
|
62
|
+
label={i18n("tableColumnMenu.onAddColumnAfter")}
|
|
63
|
+
onClick={onAddColumnAfter}
|
|
64
|
+
/>
|
|
65
|
+
<Item close={false} icon="Trash" label={i18n("tableColumnMenu.onDeleteColumn")} onClick={onDeleteColumn} />
|
|
66
|
+
</Toolbar.Wrapper>
|
|
67
|
+
</BaseBubbleMenu>
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
TableColumnMenu.displayName = "TableColumnMenu";
|
|
72
|
+
|
|
73
|
+
export default TableColumnMenu;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { EditorState } from "@tiptap/pm/state";
|
|
2
|
+
import type { EditorView } from "@tiptap/pm/view";
|
|
3
|
+
import type { Editor } from "@tiptap/react";
|
|
4
|
+
|
|
5
|
+
import { Table } from "../..";
|
|
6
|
+
import { isTableSelected } from "../../utils";
|
|
7
|
+
|
|
8
|
+
export const isColumnGripSelected = ({
|
|
9
|
+
editor,
|
|
10
|
+
view,
|
|
11
|
+
state,
|
|
12
|
+
from,
|
|
13
|
+
}: {
|
|
14
|
+
editor: Editor;
|
|
15
|
+
view: EditorView;
|
|
16
|
+
state: EditorState;
|
|
17
|
+
from: number;
|
|
18
|
+
}) => {
|
|
19
|
+
const domAtPos = view.domAtPos(from).node as HTMLElement;
|
|
20
|
+
const nodeDOM = view.nodeDOM(from) as HTMLElement;
|
|
21
|
+
const node = nodeDOM || domAtPos;
|
|
22
|
+
|
|
23
|
+
if (!(editor.isActive(Table.name) && node) || isTableSelected(state.selection)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let container = node;
|
|
28
|
+
|
|
29
|
+
while (container && !["TD", "TH"].includes(container.tagName)) {
|
|
30
|
+
container = container.parentElement!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const gripColumn = container?.querySelector?.("a.grip-column.selected");
|
|
34
|
+
|
|
35
|
+
return !!gripColumn;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default isColumnGripSelected;
|