]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts
869de8460d2fc37812cc9af2a99305bb61fbea16
[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
6 type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
7
8 class TableResizer {
9     protected editor: LexicalEditor;
10     protected editArea: HTMLElement;
11     protected markerDom: MarkerDomRecord|null = null;
12     protected mouseTracker: MouseDragTracker|null = null;
13     protected dragging: boolean = false;
14     protected targetCell: HTMLElement|null = null;
15     protected xMarkerAtStart : boolean = false;
16     protected yMarkerAtStart : boolean = false;
17
18     constructor(editor: LexicalEditor, editArea: HTMLElement) {
19         this.editor = editor;
20         this.editArea = editArea;
21         this.setupListeners();
22     }
23
24     setupListeners() {
25         this.editArea.addEventListener('mousemove', event => {
26             const cell = (event.target as HTMLElement).closest('td,th');
27             if (cell && !this.dragging) {
28                 this.onCellMouseMove(cell as HTMLElement, event);
29             }
30         });
31     }
32
33     onCellMouseMove(cell: HTMLElement, event: MouseEvent) {
34         const rect = cell.getBoundingClientRect();
35         const midX = rect.left + (rect.width / 2);
36         const midY = rect.top + (rect.height / 2);
37
38         this.targetCell = cell;
39         this.xMarkerAtStart = event.clientX <= midX;
40         this.yMarkerAtStart = event.clientY <= midY;
41
42         const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
43         const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
44         this.updateMarkersTo(cell, xMarkerPos, yMarkerPos);
45     }
46
47     updateMarkersTo(cell: HTMLElement, xPos: number, yPos: number) {
48         const markers: MarkerDomRecord = this.getMarkers();
49         const table = cell.closest('table') as HTMLElement;
50         const tableRect = table.getBoundingClientRect();
51
52         markers.x.style.left = xPos + 'px';
53         markers.x.style.height = tableRect.height + 'px';
54         markers.x.style.top = tableRect.top + 'px';
55
56         markers.y.style.top = yPos + 'px';
57         markers.y.style.left = tableRect.left + 'px';
58         markers.y.style.width = tableRect.width + 'px';
59     }
60
61     getMarkers(): MarkerDomRecord {
62         if (!this.markerDom) {
63             this.markerDom = {
64                 x: el('div', {class: 'editor-table-marker editor-table-marker-column'}),
65                 y: el('div', {class: 'editor-table-marker editor-table-marker-row'}),
66             }
67             const wrapper = el('div', {
68                 class: 'editor-table-marker-wrap',
69             }, [this.markerDom.x, this.markerDom.y]);
70             this.editArea.after(wrapper);
71             this.watchMarkerMouseDrags(wrapper);
72         }
73
74         return this.markerDom;
75     }
76
77     watchMarkerMouseDrags(wrapper: HTMLElement) {
78         const _this = this;
79         let markerStart: number = 0;
80         let markerProp: 'left' | 'top' = 'left';
81
82         this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {
83             down(event: MouseEvent, marker: HTMLElement) {
84                 marker.classList.add('active');
85                 _this.dragging = true;
86
87                 markerProp = marker.classList.contains('editor-table-marker-column') ? 'left' : 'top';
88                 markerStart = Number(marker.style[markerProp].replace('px', ''));
89             },
90             move(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
91                   marker.style[markerProp] = (markerStart + distance[markerProp === 'left' ? 'x' : 'y']) + 'px';
92             },
93             up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
94                 marker.classList.remove('active');
95                 marker.style.left = '0';
96                 marker.style.top = '0';
97
98                 _this.dragging = false;
99                 console.log('up', distance, marker, markerProp, _this.targetCell);
100                 const parentTable = _this.targetCell?.closest('table');
101
102                 if (markerProp === 'left' && _this.targetCell && parentTable) {
103                     const cellIndex = _this.getTargetCellColumnIndex();
104                     _this.editor.update(() => {
105                         const table = $getNearestNodeFromDOMNode(parentTable);
106                         if (table instanceof CustomTableNode) {
107                             const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
108                             const newWidth = Math.max(originalWidth + distance.x, 10);
109                             $setTableColumnWidth(table, cellIndex, newWidth);
110                         }
111                     });
112                 }
113             }
114         });
115     }
116
117     getTargetCellColumnIndex(): number {
118         const cell = this.targetCell;
119         if (cell === null) {
120             return -1;
121         }
122
123         let index = 0;
124         const row = cell.parentElement;
125         for (const rowCell of row?.children || []) {
126             let size = Number(rowCell.getAttribute('colspan'));
127             if (Number.isNaN(size) || size < 1) {
128                 size = 1;
129             }
130
131             index += size;
132
133             if (rowCell === cell) {
134                 return index - 1;
135             }
136         }
137
138         return -1;
139     }
140 }
141
142
143 export function registerTableResizer(editor: LexicalEditor, editorArea: HTMLElement): (() => void) {
144     const resizer = new TableResizer(editor, editorArea);
145
146     // TODO - Strip/close down resizer
147     return () => {};
148 }