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";
7 type MarkerDomRecord = {x: HTMLElement, y: HTMLElement};
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;
19 constructor(editor: LexicalEditor, editArea: HTMLElement) {
21 this.editArea = editArea;
23 this.setupListeners();
27 this.editArea.removeEventListener('mousemove', this.onCellMouseMove);
28 if (this.mouseTracker) {
29 this.mouseTracker.teardown();
33 protected setupListeners() {
34 this.onCellMouseMove = this.onCellMouseMove.bind(this);
35 this.editArea.addEventListener('mousemove', this.onCellMouseMove);
38 protected onCellMouseMove(event: MouseEvent) {
39 const cell = (event.target as HTMLElement).closest('td,th') as HTMLElement;
40 if (!cell || this.dragging) {
44 const rect = cell.getBoundingClientRect();
45 const midX = rect.left + (rect.width / 2);
46 const midY = rect.top + (rect.height / 2);
48 this.targetCell = cell;
49 this.xMarkerAtStart = event.clientX <= midX;
50 this.yMarkerAtStart = event.clientY <= midY;
52 const xMarkerPos = this.xMarkerAtStart ? rect.left : rect.right;
53 const yMarkerPos = this.yMarkerAtStart ? rect.top : rect.bottom;
54 this.updateMarkersTo(cell, xMarkerPos, yMarkerPos);
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();
62 markers.x.style.left = xPos + 'px';
63 markers.x.style.height = tableRect.height + 'px';
64 markers.x.style.top = tableRect.top + 'px';
66 markers.y.style.top = yPos + 'px';
67 markers.y.style.left = tableRect.left + 'px';
68 markers.y.style.width = tableRect.width + 'px';
71 protected getMarkers(): MarkerDomRecord {
72 if (!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'}),
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);
84 return this.markerDom;
87 protected watchMarkerMouseDrags(wrapper: HTMLElement) {
89 let markerStart: number = 0;
90 let markerProp: 'left' | 'top' = 'left';
92 this.mouseTracker = new MouseDragTracker(wrapper, '.editor-table-marker', {
93 down(event: MouseEvent, marker: HTMLElement) {
94 marker.classList.add('active');
95 _this.dragging = true;
97 markerProp = marker.classList.contains('editor-table-marker-column') ? 'left' : 'top';
98 markerStart = Number(marker.style[markerProp].replace('px', ''));
100 move(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
101 marker.style[markerProp] = (markerStart + distance[markerProp === 'left' ? 'x' : 'y']) + 'px';
103 up(event: MouseEvent, marker: HTMLElement, distance: MouseDragTrackerDistance) {
104 marker.classList.remove('active');
105 marker.style.left = '0';
106 marker.style.top = '0';
108 _this.dragging = false;
109 const parentTable = _this.targetCell?.closest('table');
111 if (markerProp === 'left' && _this.targetCell && parentTable) {
112 let cellIndex = _this.getTargetCellColumnIndex();
113 let change = distance.x;
114 if (_this.xMarkerAtStart && cellIndex > 0) {
116 } else if (_this.xMarkerAtStart && cellIndex === 0) {
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);
130 if (markerProp === 'top' && _this.targetCell) {
131 const cellElement = _this.targetCell;
133 _this.editor.update(() => {
134 const cellNode = $getNearestNodeFromDOMNode(cellElement);
135 const rowNode = cellNode?.getParent();
136 let rowIndex = rowNode?.getIndexWithinParent() || 0;
138 let change = distance.y;
139 if (_this.yMarkerAtStart && rowIndex > 0) {
141 } else if (_this.yMarkerAtStart && rowIndex === 0) {
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);
157 protected getTargetCellColumnIndex(): number {
158 const cell = this.targetCell;
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) {
173 if (rowCell === cell) {
183 export function registerTableResizer(editor: LexicalEditor, editorArea: HTMLElement): (() => void) {
184 const resizer = new TableResizer(editor, editorArea);