1 import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
2 import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
3 import {CustomTableNode} from "../../../nodes/custom-table";
4 import {TableRowNode} from "@lexical/table";
5 import {el} from "../../../utils/dom";
6 import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
8 type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
11 protected editor: LexicalEditor;
12 protected editScrollContainer: HTMLElement;
13 protected markerDom: MarkerDomRecord|null = null;
14 protected mouseTracker: MouseDragTracker|null = null;
15 protected dragging: boolean = false;
16 protected targetCell: HTMLElement|null = null;
17 protected xMarkerAtStart : boolean = false;
18 protected yMarkerAtStart : boolean = false;
20 constructor(editor: LexicalEditor, editScrollContainer: HTMLElement) {
22 this.editScrollContainer = editScrollContainer;
24 this.setupListeners();
28 this.editScrollContainer.removeEventListener('mousemove', this.onCellMouseMove);
29 window.removeEventListener('scroll', this.onScrollOrResize, {capture: true});
30 window.removeEventListener('resize', this.onScrollOrResize);
31 if (this.mouseTracker) {
32 this.mouseTracker.teardown();
36 protected setupListeners() {
37 this.onCellMouseMove = this.onCellMouseMove.bind(this);
38 this.onScrollOrResize = this.onScrollOrResize.bind(this);
39 this.editScrollContainer.addEventListener('mousemove', this.onCellMouseMove);
40 window.addEventListener('scroll', this.onScrollOrResize, {capture: true, passive: true});
41 window.addEventListener('resize', this.onScrollOrResize, {passive: true});
44 protected onScrollOrResize(): void {
45 this.updateCurrentMarkerTargetPosition();
48 protected onCellMouseMove(event: MouseEvent) {
49 const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement;
50 if (!cell || this.dragging) {
54 const rect = cell.getBoundingClientRect();
55 const midX = rect.left + (rect.width / 2);
56 const midY = rect.top + (rect.height / 2);
58 this.targetCell = cell;
59 this.xMarkerAtStart = event.clientX <= midX;
60 this.yMarkerAtStart = event.clientY <= midY;
62 const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
63 const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
64 this.updateMarkersTo(cell, xMarkerPos, yMarkerPos);
67 protected updateMarkersTo(cell: HTMLElement, xPos: number, yPos: number) {
68 const markers: MarkerDomRecord = this.getMarkers();
69 const table = cell.closest('table') as HTMLElement;
70 const tableRect = table.getBoundingClientRect();
71 const editBounds = this.editScrollContainer.getBoundingClientRect();
73 const maxTop = Math.max(tableRect.top, editBounds.top);
74 const maxBottom = Math.min(tableRect.bottom, editBounds.bottom);
75 const maxHeight = maxBottom - maxTop;
76 markers.x.style.left = xPos + 'px';
77 markers.x.style.top = maxTop + 'px';
78 markers.x.style.height = maxHeight + 'px';
80 markers.y.style.top = yPos + 'px';
81 markers.y.style.left = tableRect.left + 'px';
82 markers.y.style.width = tableRect.width + 'px';
84 // Hide markers when out of bounds
85 markers.y.hidden = yPos < editBounds.top || yPos > editBounds.bottom;
86 markers.x.hidden = tableRect.top > editBounds.bottom || tableRect.bottom < editBounds.top;
89 protected updateCurrentMarkerTargetPosition(): void {
90 if (!this.targetCell) {
94 const rect = this.targetCell.getBoundingClientRect();
95 const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
96 const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
97 this.updateMarkersTo(this.targetCell, xMarkerPos, yMarkerPos);
100 protected getMarkers(): MarkerDomRecord {
101 if (!this.markerDom) {
103 x: el('div', {class: 'editor-table-marker editor-table-marker-column'}),
104 y: el('div', {class: 'editor-table-marker editor-table-marker-row'}),
106 const wrapper = el('div', {
107 class: 'editor-table-marker-wrap',
108 }, [this.markerDom.x, this.markerDom.y]);
109 this.editScrollContainer.after(wrapper);
110 this.watchMarkerMouseDrags(wrapper);
113 return this.markerDom;
116 protected watchMarkerMouseDrags(wrapper: HTMLElement) {
118 let markerStart: number = 0;
119 let markerProp: 'left' | 'top' = 'left';
121 this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {
122 down(event: MouseEvent, marker: HTMLElement) {
123 marker.classList.add('active');
124 _this.dragging = true;
126 markerProp = marker.classList.contains('editor-table-marker-column') ? 'left' : 'top';
127 markerStart = Number(marker.style[markerProp].replace('px', ''));
129 move(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
130 marker.style[markerProp] = (markerStart + distance[markerProp === 'left' ? 'x' : 'y']) + 'px';
132 up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
133 marker.classList.remove('active');
134 marker.style.left = '0';
135 marker.style.top = '0';
137 _this.dragging = false;
138 const parentTable = _this.targetCell?.closest('table');
140 if (markerProp === 'left' && _this.targetCell && parentTable) {
141 let cellIndex = _this.getTargetCellColumnIndex();
142 let change = distance.x;
143 if (_this.xMarkerAtStart && cellIndex > 0) {
145 } else if (_this.xMarkerAtStart && cellIndex === 0) {
149 _this.editor.update(() => {
150 const table = $getNearestNodeFromDOMNode(parentTable);
151 if (table instanceof CustomTableNode) {
152 const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
153 const newWidth = Math.max(originalWidth + change, 10);
154 $setTableColumnWidth(table, cellIndex, newWidth);
159 if (markerProp === 'top' && _this.targetCell) {
160 const cellElement = _this.targetCell;
162 _this.editor.update(() => {
163 const cellNode = $getNearestNodeFromDOMNode(cellElement);
164 const rowNode = cellNode?.getParent();
165 let rowIndex = rowNode?.getIndexWithinParent() || 0;
167 let change = distance.y;
168 if (_this.yMarkerAtStart && rowIndex > 0) {
170 } else if (_this.yMarkerAtStart && rowIndex === 0) {
174 const targetRow = rowNode?.getParent()?.getChildren()[rowIndex];
175 if (targetRow instanceof TableRowNode) {
176 const height = targetRow.getHeight() || 0;
177 const newHeight = Math.max(height + change, 10);
178 targetRow.setHeight(newHeight);
186 protected getTargetCellColumnIndex(): number {
187 const cell = this.targetCell;
193 const row = cell.parentElement;
194 for (const rowCell of row?.children || []) {
195 let size = Number(rowCell.getAttribute('colspan'));
196 if (Number.isNaN(size) || size < 1) {
202 if (rowCell === cell) {
212 export function registerTableResizer(editor: LexicalEditor, editScrollContainer: HTMLElement): (() => void) {
213 const resizer = new TableResizer(editor, editScrollContainer);