1 import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical";
2 import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker";
3 import {TableNode, TableRowNode} from "@lexical/table";
4 import {el} from "../../../utils/dom";
5 import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables";
7 type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
10 protected editor: LexicalEditor;
11 protected editScrollContainer: 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;
19 constructor(editor: LexicalEditor, editScrollContainer: HTMLElement) {
21 this.editScrollContainer = editScrollContainer;
23 this.setupListeners();
27 this.editScrollContainer.removeEventListener('mousemove', this.onCellMouseMove);
28 window.removeEventListener('scroll', this.onScrollOrResize, {capture: true});
29 window.removeEventListener('resize', this.onScrollOrResize);
30 if (this.mouseTracker) {
31 this.mouseTracker.teardown();
35 protected setupListeners() {
36 this.onCellMouseMove = this.onCellMouseMove.bind(this);
37 this.onScrollOrResize = this.onScrollOrResize.bind(this);
38 this.editScrollContainer.addEventListener('mousemove', this.onCellMouseMove);
39 window.addEventListener('scroll', this.onScrollOrResize, {capture: true, passive: true});
40 window.addEventListener('resize', this.onScrollOrResize, {passive: true});
43 protected onScrollOrResize(): void {
44 this.updateCurrentMarkerTargetPosition();
47 protected onCellMouseMove(event: MouseEvent) {
48 const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement;
49 if (!cell || this.dragging) {
53 const rect = cell.getBoundingClientRect();
54 const midX = rect.left + (rect.width / 2);
55 const midY = rect.top + (rect.height / 2);
57 this.targetCell = cell;
58 this.xMarkerAtStart = event.clientX <= midX;
59 this.yMarkerAtStart = event.clientY <= midY;
61 const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
62 const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
63 this.updateMarkersTo(cell, xMarkerPos, yMarkerPos);
66 protected updateMarkersTo(cell: HTMLElement, xPos: number, yPos: number) {
67 const markers: MarkerDomRecord = this.getMarkers();
68 const table = cell.closest('table') as HTMLElement;
69 const tableRect = table.getBoundingClientRect();
70 const editBounds = this.editScrollContainer.getBoundingClientRect();
72 const maxTop = Math.max(tableRect.top, editBounds.top);
73 const maxBottom = Math.min(tableRect.bottom, editBounds.bottom);
74 const maxHeight = maxBottom - maxTop;
75 markers.x.style.left = xPos + 'px';
76 markers.x.style.top = maxTop + 'px';
77 markers.x.style.height = maxHeight + 'px';
79 markers.y.style.top = yPos + 'px';
80 markers.y.style.left = tableRect.left + 'px';
81 markers.y.style.width = tableRect.width + 'px';
83 // Hide markers when out of bounds
84 markers.y.hidden = yPos < editBounds.top || yPos > editBounds.bottom;
85 markers.x.hidden = tableRect.top > editBounds.bottom || tableRect.bottom < editBounds.top;
88 protected updateCurrentMarkerTargetPosition(): void {
89 if (!this.targetCell) {
93 const rect = this.targetCell.getBoundingClientRect();
94 const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
95 const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
96 this.updateMarkersTo(this.targetCell, xMarkerPos, yMarkerPos);
99 protected getMarkers(): MarkerDomRecord {
100 if (!this.markerDom) {
102 x: el('div', {class: 'editor-table-marker editor-table-marker-column'}),
103 y: el('div', {class: 'editor-table-marker editor-table-marker-row'}),
105 const wrapper = el('div', {
106 class: 'editor-table-marker-wrap',
107 }, [this.markerDom.x, this.markerDom.y]);
108 this.editScrollContainer.after(wrapper);
109 this.watchMarkerMouseDrags(wrapper);
112 return this.markerDom;
115 protected watchMarkerMouseDrags(wrapper: HTMLElement) {
117 let markerStart: number = 0;
118 let markerProp: 'left' | 'top' = 'left';
120 this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {
121 down(event: MouseEvent, marker: HTMLElement) {
122 marker.classList.add('active');
123 _this.dragging = true;
125 markerProp = marker.classList.contains('editor-table-marker-column') ? 'left' : 'top';
126 markerStart = Number(marker.style[markerProp].replace('px', ''));
128 move(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
129 marker.style[markerProp] = (markerStart + distance[markerProp === 'left' ? 'x' : 'y']) + 'px';
131 up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
132 marker.classList.remove('active');
133 marker.style.left = '0';
134 marker.style.top = '0';
136 _this.dragging = false;
137 const parentTable = _this.targetCell?.closest('table');
139 if (markerProp === 'left' && _this.targetCell && parentTable) {
140 let cellIndex = _this.getTargetCellColumnIndex();
141 let change = distance.x;
142 if (_this.xMarkerAtStart && cellIndex > 0) {
144 } else if (_this.xMarkerAtStart && cellIndex === 0) {
148 _this.editor.update(() => {
149 const table = $getNearestNodeFromDOMNode(parentTable);
150 if (table instanceof TableNode) {
151 const originalWidth = $getTableColumnWidth(_this.editor, table, cellIndex);
152 const newWidth = Math.max(originalWidth + change, 10);
153 $setTableColumnWidth(table, cellIndex, newWidth);
158 if (markerProp === 'top' && _this.targetCell) {
159 const cellElement = _this.targetCell;
161 _this.editor.update(() => {
162 const cellNode = $getNearestNodeFromDOMNode(cellElement);
163 const rowNode = cellNode?.getParent();
164 let rowIndex = rowNode?.getIndexWithinParent() || 0;
166 let change = distance.y;
167 if (_this.yMarkerAtStart && rowIndex > 0) {
169 } else if (_this.yMarkerAtStart && rowIndex === 0) {
173 const targetRow = rowNode?.getParent()?.getChildren()[rowIndex];
174 if (targetRow instanceof TableRowNode) {
175 const height = targetRow.getHeight() || 0;
176 const newHeight = Math.max(height + change, 10);
177 targetRow.setHeight(newHeight);
185 protected getTargetCellColumnIndex(): number {
186 const cell = this.targetCell;
192 const row = cell.parentElement;
193 for (const rowCell of row?.children || []) {
194 let size = Number(rowCell.getAttribute('colspan'));
195 if (Number.isNaN(size) || size < 1) {
201 if (rowCell === cell) {
211 export function registerTableResizer(editor: LexicalEditor, editScrollContainer: HTMLElement): (() => void) {
212 const resizer = new TableResizer(editor, editScrollContainer);