]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/defaults/buttons/tables.ts
Lexical: Kinda made row copy/paste work
[bookstack] / resources / js / wysiwyg / ui / defaults / buttons / tables.ts
1 import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons";
2 import tableIcon from "@icons/editor/table.svg";
3 import deleteIcon from "@icons/editor/table-delete.svg";
4 import deleteColumnIcon from "@icons/editor/table-delete-column.svg";
5 import deleteRowIcon from "@icons/editor/table-delete-row.svg";
6 import insertColumnAfterIcon from "@icons/editor/table-insert-column-after.svg";
7 import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg";
8 import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
9 import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
10 import {EditorUiContext} from "../../framework/core";
11 import {$createNodeSelection, $createRangeSelection, $getSelection, BaseSelection} from "lexical";
12 import {$isCustomTableNode} from "../../../nodes/custom-table";
13 import {
14     $deleteTableColumn__EXPERIMENTAL,
15     $deleteTableRow__EXPERIMENTAL,
16     $insertTableColumn__EXPERIMENTAL,
17     $insertTableRow__EXPERIMENTAL,
18     $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
19 } from "@lexical/table";
20 import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
21 import {$getParentOfType} from "../../../utils/nodes";
22 import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell";
23 import {$showCellPropertiesForm, $showRowPropertiesForm} from "../forms/tables";
24 import {$getTableRowsFromSelection, $mergeTableCellsInSelection} from "../../../utils/tables";
25 import {$isCustomTableRowNode, CustomTableRowNode} from "../../../nodes/custom-table-row";
26 import {NodeClipboard} from "../../../services/node-clipboard";
27 import {r} from "@codemirror/legacy-modes/mode/r";
28 import {$generateHtmlFromNodes} from "@lexical/html";
29
30 const neverActive = (): boolean => false;
31 const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
32
33 export const table: EditorBasicButtonDefinition = {
34     label: 'Table',
35     icon: tableIcon,
36 };
37
38 export const tableProperties: EditorButtonDefinition = {
39     label: 'Table properties',
40     icon: tableIcon,
41     action(context: EditorUiContext) {
42         context.editor.getEditorState().read(() => {
43             const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
44             if (!$isCustomTableCellNode(cell)) {
45                 return;
46             }
47
48             const table = $getParentOfType(cell, $isTableNode);
49             const modalForm = context.manager.createModal('table_properties');
50             modalForm.show({});
51             // TODO
52         });
53     },
54     isActive: neverActive,
55     isDisabled: cellNotSelected,
56 };
57
58 export const clearTableFormatting: EditorButtonDefinition = {
59     label: 'Clear table formatting',
60     format: 'long',
61     action(context: EditorUiContext) {
62         context.editor.getEditorState().read(() => {
63             const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
64             if (!$isCustomTableCellNode(cell)) {
65                 return;
66             }
67
68             const table = $getParentOfType(cell, $isTableNode);
69             // TODO
70         });
71     },
72     isActive: neverActive,
73     isDisabled: cellNotSelected,
74 };
75
76 export const resizeTableToContents: EditorButtonDefinition = {
77     label: 'Resize to contents',
78     format: 'long',
79     action(context: EditorUiContext) {
80         context.editor.getEditorState().read(() => {
81             const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
82             if (!$isCustomTableCellNode(cell)) {
83                 return;
84             }
85
86             const table = $getParentOfType(cell, $isCustomTableNode);
87             if (!$isCustomTableNode(table)) {
88                 return;
89             }
90
91             for (const row of table.getChildren()) {
92                 if ($isTableRowNode(row)) {
93                     // TODO - Come back later as this may depend on if we
94                     //   are using a custom table row
95                 }
96             }
97         });
98     },
99     isActive: neverActive,
100     isDisabled: cellNotSelected,
101 };
102
103 export const deleteTable: EditorButtonDefinition = {
104     label: 'Delete table',
105     icon: deleteIcon,
106     action(context: EditorUiContext) {
107         context.editor.update(() => {
108             const table = $getNodeFromSelection($getSelection(), $isCustomTableNode);
109             if (table) {
110                 table.remove();
111             }
112         });
113     },
114     isActive() {
115         return false;
116     }
117 };
118
119 export const deleteTableMenuAction: EditorButtonDefinition = {
120     ...deleteTable,
121     format: 'long',
122     isDisabled(selection) {
123         return !$selectionContainsNodeType(selection, $isTableNode);
124     },
125 };
126
127 export const insertRowAbove: EditorButtonDefinition = {
128     label: 'Insert row before',
129     icon: insertRowAboveIcon,
130     action(context: EditorUiContext) {
131         context.editor.update(() => {
132             $insertTableRow__EXPERIMENTAL(false);
133         });
134     },
135     isActive: neverActive,
136     isDisabled: cellNotSelected,
137 };
138
139 export const insertRowBelow: EditorButtonDefinition = {
140     label: 'Insert row after',
141     icon: insertRowBelowIcon,
142     action(context: EditorUiContext) {
143         context.editor.update(() => {
144             $insertTableRow__EXPERIMENTAL(true);
145         });
146     },
147     isActive: neverActive,
148     isDisabled: cellNotSelected,
149 };
150
151 export const deleteRow: EditorButtonDefinition = {
152     label: 'Delete row',
153     icon: deleteRowIcon,
154     action(context: EditorUiContext) {
155         context.editor.update(() => {
156             $deleteTableRow__EXPERIMENTAL();
157         });
158     },
159     isActive: neverActive,
160     isDisabled: cellNotSelected,
161 };
162
163 export const rowProperties: EditorButtonDefinition = {
164     label: 'Row properties',
165     format: 'long',
166     action(context: EditorUiContext) {
167         context.editor.getEditorState().read(() => {
168             const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
169             if (!$isCustomTableCellNode(cell)) {
170                 return;
171             }
172
173             const row = $getParentOfType(cell, $isCustomTableRowNode);
174             if ($isCustomTableRowNode(row)) {
175                 $showRowPropertiesForm(row, context);
176             }
177         });
178     },
179     isActive: neverActive,
180     isDisabled: cellNotSelected,
181 };
182
183 const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
184
185 export const cutRow: EditorButtonDefinition = {
186     label: 'Cut row',
187     format: 'long',
188     action(context: EditorUiContext) {
189         context.editor.update(() => {
190             const rows = $getTableRowsFromSelection($getSelection());
191             rowClipboard.set(...rows);
192             for (const row of rows) {
193                 row.remove();
194             }
195         });
196     },
197     isActive: neverActive,
198     isDisabled: cellNotSelected,
199 };
200
201 export const copyRow: EditorButtonDefinition = {
202     label: 'Copy row',
203     format: 'long',
204     action(context: EditorUiContext) {
205         context.editor.getEditorState().read(() => {
206             const rows = $getTableRowsFromSelection($getSelection());
207             rowClipboard.set(...rows);
208         });
209     },
210     isActive: neverActive,
211     isDisabled: cellNotSelected,
212 };
213
214 export const pasteRowBefore: EditorButtonDefinition = {
215     label: 'Paste row before',
216     format: 'long',
217     action(context: EditorUiContext) {
218         context.editor.update(() => {
219             const rows = $getTableRowsFromSelection($getSelection());
220             const lastRow = rows[rows.length - 1];
221             if (lastRow) {
222                 for (const row of rowClipboard.get(context.editor)) {
223                     lastRow.insertBefore(row);
224                 }
225             }
226         });
227     },
228     isActive: neverActive,
229     isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0,
230 };
231
232 export const pasteRowAfter: EditorButtonDefinition = {
233     label: 'Paste row after',
234     format: 'long',
235     action(context: EditorUiContext) {
236         context.editor.update(() => {
237             const rows = $getTableRowsFromSelection($getSelection());
238             const lastRow = rows[rows.length - 1];
239             if (lastRow) {
240                 for (const row of rowClipboard.get(context.editor).reverse()) {
241                     lastRow.insertAfter(row);
242                 }
243             }
244         });
245     },
246     isActive: neverActive,
247     isDisabled: (selection) => cellNotSelected(selection) || rowClipboard.size() === 0,
248 };
249
250 export const cutColumn: EditorButtonDefinition = {
251     label: 'Cut column',
252     format: 'long',
253     action(context: EditorUiContext) {
254         context.editor.getEditorState().read(() => {
255             // TODO
256         });
257     },
258     isActive: neverActive,
259     isDisabled: cellNotSelected,
260 };
261
262 export const copyColumn: EditorButtonDefinition = {
263     label: 'Copy column',
264     format: 'long',
265     action(context: EditorUiContext) {
266         context.editor.getEditorState().read(() => {
267             // TODO
268         });
269     },
270     isActive: neverActive,
271     isDisabled: cellNotSelected,
272 };
273
274 export const pasteColumnBefore: EditorButtonDefinition = {
275     label: 'Paste column before',
276     format: 'long',
277     action(context: EditorUiContext) {
278         context.editor.getEditorState().read(() => {
279             // TODO
280         });
281     },
282     isActive: neverActive,
283     isDisabled: cellNotSelected,
284 };
285
286 export const pasteColumnAfter: EditorButtonDefinition = {
287     label: 'Paste column after',
288     format: 'long',
289     action(context: EditorUiContext) {
290         context.editor.getEditorState().read(() => {
291             // TODO
292         });
293     },
294     isActive: neverActive,
295     isDisabled: cellNotSelected,
296 };
297
298 export const insertColumnBefore: EditorButtonDefinition = {
299     label: 'Insert column before',
300     icon: insertColumnBeforeIcon,
301     action(context: EditorUiContext) {
302         context.editor.update(() => {
303             $insertTableColumn__EXPERIMENTAL(false);
304         });
305     },
306     isActive() {
307         return false;
308     }
309 };
310
311 export const insertColumnAfter: EditorButtonDefinition = {
312     label: 'Insert column after',
313     icon: insertColumnAfterIcon,
314     action(context: EditorUiContext) {
315         context.editor.update(() => {
316             $insertTableColumn__EXPERIMENTAL(true);
317         });
318     },
319     isActive() {
320         return false;
321     }
322 };
323
324 export const deleteColumn: EditorButtonDefinition = {
325     label: 'Delete column',
326     icon: deleteColumnIcon,
327     action(context: EditorUiContext) {
328         context.editor.update(() => {
329             $deleteTableColumn__EXPERIMENTAL();
330         });
331     },
332     isActive() {
333         return false;
334     }
335 };
336
337 export const cellProperties: EditorButtonDefinition = {
338     label: 'Cell properties',
339     action(context: EditorUiContext) {
340         context.editor.getEditorState().read(() => {
341             const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
342             if ($isCustomTableCellNode(cell)) {
343                 $showCellPropertiesForm(cell, context);
344             }
345         });
346     },
347     isActive: neverActive,
348     isDisabled: cellNotSelected,
349 };
350
351 export const mergeCells: EditorButtonDefinition = {
352     label: 'Merge cells',
353     action(context: EditorUiContext) {
354         context.editor.update(() => {
355             const selection = $getSelection();
356             if ($isTableSelection(selection)) {
357                 $mergeTableCellsInSelection(selection);
358             }
359         });
360     },
361     isActive: neverActive,
362     isDisabled(selection) {
363         return !$isTableSelection(selection);
364     }
365 };
366
367 export const splitCell: EditorButtonDefinition = {
368     label: 'Split cell',
369     action(context: EditorUiContext) {
370         context.editor.update(() => {
371             $unmergeCell();
372         });
373     },
374     isActive: neverActive,
375     isDisabled(selection) {
376         const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as TableCellNode|null;
377         if (cell) {
378             const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
379             return !merged;
380         }
381
382         return true;
383     }
384 };