@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,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,8 @@
1
+ import TiptapTableRow from "@tiptap/extension-table-row";
2
+
3
+ export const TableRow = TiptapTableRow.extend({
4
+ allowGapCursor: false,
5
+ content: "tableCell*",
6
+ });
7
+
8
+ export default TableRow;
@@ -0,0 +1,9 @@
1
+ import TiptapTable from "@tiptap/extension-table";
2
+
3
+ export const Table = TiptapTable.configure({
4
+ allowTableNodeSelection: true,
5
+ resizable: true,
6
+ lastColumnResizable: false,
7
+ });
8
+
9
+ export default Table;
@@ -0,0 +1,4 @@
1
+ export { TableCell } from "./Cell";
2
+ export { TableHeader } from "./Header";
3
+ export { TableRow } from "./Row";
4
+ export { Table } from "./Table";
@@ -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;