]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts
8f1e978e9dc5119a1a15ea7503b16322c48165bd
[bookstack] / resources / js / wysiwyg / ui / framework / helpers / table-resizer.ts
1 import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
2 import {el} from "../../../helpers";
3 import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
4 import {$getTableColumnWidth, $setTableColumnWidth, CustomTableNode} from "../../../nodes/custom-table";
5 import {TableRowNode} from "@lexical/table";
6
7 type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
8
9 class TableResizer {
10     protected editor: LexicalEditor;
11     protected editArea: HTMLElement;
12     protected markerDom: MarkerDomRecord|null = null;
13     protected mouseTracker: MouseDragTracker|null = null;
14     protected dragging: boolean = false;
15     protected targetCell: HTMLElement|null = null;
16     protected xMarkerAtStart : boolean = false;
17     protected yMarkerAtStart : boolean = false;
18
19     constructor(editor: LexicalEditor, editArea: HTMLElement) {
20         this.editor = editor;
21         this.editArea = editArea;
22
23         this.setupListeners();
24     }
25
26     teardown() {
27         this.editArea.removeEventListener('mousemove', this.onCellMouseMove);
28         if (this.mouseTracker) {
29             this.mouseTracker.teardown();
30         }
31     }
32
33     protected setupListeners() {
34         this.onCellMouseMove = this.onCellMouseMove.bind(this);
35         this.editArea.addEventListener('mousemove', this.onCellMouseMove);
36     }
37
38     protected onCellMouseMove(event: MouseEvent) {
39         const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement;
40         if (!cell || this.dragging) {
41             return;
42         }
43
44         const rect = cell.getBoundingClientRect();
45         const midX = rect.left + (rect.width / 2);
46         const midY = rect.top + (rect.height / 2);
47
48         this.targetCell = cell;
49         this.xMarkerAtStart = event.clientX <= midX;
50         this.yMarkerAtStart = event.clientY <= midY;
51
52         const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
53         const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
54         this.updateMarkersTo(cell, xMarkerPos, yMarkerPos);
55     }
56
57     protected updateMarkersTo(cell: HTMLElement, xPos: number, yPos: number) {
58         const markers: MarkerDomRecord = this.getMarkers();
59         const table = cell.closest('table') as HTMLElement;
60         const tableRect = table.getBoundingClientRect();
61
62         markers.x.style.left = xPos + 'px';
63         markers.x.style.height = tableRect.height + 'px';
64         markers.x.style.top = tableRect.top + 'px';
65
66         markers.y.style.top = yPos + 'px';
67         markers.y.style.left = tableRect.left + 'px';
68         markers.y.style.width = tableRect.width + 'px';
69     }
70
71     protected getMarkers(): MarkerDomRecord {
72         if (!this.markerDom) {
73             this.markerDom = {
74                 x: el('div', {class: 'editor-table-marker editor-table-marker-column'}),
75                 y: el('div', {class: 'editor-table-marker editor-table-marker-row'}),
76             }
77             const wrapper = el('div', {
78                 class: 'editor-table-marker-wrap',
79             }, [this.markerDom.x, this.markerDom.y]);
80             this.editArea.after(wrapper);
81             this.watchMarkerMouseDrags(wrapper);
82         }
83
84         return this.markerDom;
85     }
86
87     protected watchMarkerMouseDrags(wrapper: HTMLElement) {
88         const _this = this;
89         let markerStart: number = 0;
90         let markerProp: 'left' | 'top' = 'left';
91
92         this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {
93             down(event: MouseEvent, marker: HTMLElement) {
94                 marker.classList.add('active');
95                 _this.dragging = true;
96
97                 markerProp = marker.classList.contains('editor-table-marker-column') ? 'left' : 'top';
98                 markerStart = Number(marker.style[markerProp].replace('px', ''));
99             },
100             move(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
101                   marker.style[markerProp] = (markerStart + distance[markerProp === 'left' ? 'x' : 'y']) + 'px';
102             },
103             up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
104                 marker.classList.remove('active');
105                 marker.style.left = '0';
106                 marker.style.top = '0';
107
108                 _this.dragging = false;
109                 const parentTable = _this.targetCell?.closest('table');
110
111                 if (markerProp === 'left' && _this.targetCell && parentTable) {
112                     let cellIndex = _this.getTargetCellColumnIndex();
113                     let change = distance.x;
114                     if (_this.xMarkerAtStart && cellIndex > 0) {
115                         cellIndex -= 1;
116                     } else if  (_this.xMarkerAtStart && cellIndex === 0) {
117                         change = -change;
118                     }
119
120                     _this.editor.update(() => {
121                         const table = $getNearestNodeFromDOMNode(parentTable);
122                         if (table instanceof CustomTableNode) {
123                             const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
124                             const newWidth = Math.max(originalWidth + change, 10);
125                             $setTableColumnWidth(table, cellIndex, newWidth);
126                         }
127                     });
128                 }
129
130                 if (markerProp === 'top' && _this.targetCell) {
131                     const cellElement = _this.targetCell;
132
133                     _this.editor.update(() => {
134                         const cellNode = $getNearestNodeFromDOMNode(cellElement);
135                         const rowNode = cellNode?.getParent();
136                         let rowIndex = rowNode?.getIndexWithinParent() || 0;
137
138                         let change = distance.y;
139                         if (_this.yMarkerAtStart && rowIndex > 0) {
140                             rowIndex -= 1;
141                         } else if  (_this.yMarkerAtStart && rowIndex === 0) {
142                             change = -change;
143                         }
144
145                         const targetRow = rowNode?.getParent()?.getChildren()[rowIndex];
146                         if (targetRow instanceof TableRowNode) {
147                             const height  = targetRow.getHeight() || 0;
148                             const newHeight = Math.max(height + change, 10);
149                             targetRow.setHeight(newHeight);
150                         }
151                     });
152                 }
153             }
154         });
155     }
156
157     protected getTargetCellColumnIndex(): number {
158         const cell = this.targetCell;
159         if (cell === null) {
160             return -1;
161         }
162
163         let index = 0;
164         const row = cell.parentElement;
165         for (const rowCell of row?.children || []) {
166             let size = Number(rowCell.getAttribute('colspan'));
167             if (Number.isNaN(size) || size < 1) {
168                 size = 1;
169             }
170
171             index += size;
172
173             if (rowCell === cell) {
174                 return index - 1;
175             }
176         }
177
178         return -1;
179     }
180 }
181
182
183 export function registerTableResizer(editor: LexicalEditor, editorArea: HTMLElement): (() => void) {
184     const resizer = new TableResizer(editor, editorArea);
185
186     return () => {
187         resizer.teardown();
188     };
189 }