import type { Delta as TypeDelta } from 'quill'; import type { TableMainFormat, TableRowFormat } from '../../formats'; import type { TableUp } from '../../table-up'; import type { DragPosition } from '../../utils'; import type { TableSelection } from '../table-selection'; import type { TableResizeCommonHelper } from './table-resize-common'; import Quill from 'quill'; import { getTableMainRect } from '../../formats'; import { AutoScroller, isUndefined, tableUpInternal } from '../../utils'; import { isCellsSpan } from './utils'; const Delta = Quill.import('delta'); export class TableAutoScroller extends AutoScroller { minusY = 0; minusX = 0; checkMinY(containerRect: DOMRect) { return this.mouseY + this.minusY < containerRect.top + this.scrollThresholdY; } checkMinX(containerRect: DOMRect) { return this.mouseX + this.minusX < containerRect.left + this.scrollThresholdX; } } interface DragHelperOptions { isDragX: boolean; allowMoveToIndex?: (index: number) => boolean; } export class DragTableHelper { startPosition: ({ position: number; size: number; index: number })[] = []; selectedIndex = new Set(); moveToIndex = -1; tableModule: TableUp; tableBlot: TableMainFormat; dragCommon: TableResizeCommonHelper; options: DragHelperOptions; get isDragX() { return this.options.isDragX; } constructor(tableModule: TableUp, tableBlot: TableMainFormat, dragCommon: TableResizeCommonHelper, options: DragHelperOptions) { this.tableModule = tableModule; this.tableBlot = tableBlot; this.dragCommon = dragCommon; this.options = options; } onStart(positionInfo: DragPosition, e: PointerEvent, callback?: (context: this) => void) { const tableSelection = this.tableModule.getModule(tableUpInternal.tableSelectionName); if (!tableSelection?.boundary || !this.tableBlot) return false; const { isSpan, cellIndex } = isCellsSpan(this.isDragX, this.tableBlot, tableSelection.selectedTds); if (!isSpan) return false; const { rect: tableRect } = getTableMainRect(this.tableBlot); if (!tableRect) return false; e.preventDefault(); this.dragCommon.startValue = this.isDragX ? tableRect.x : tableRect.y; this.selectedIndex = cellIndex; callback?.(this); this.recalculateStartPosition(); } onMove(positionInfo: DragPosition, e: PointerEvent, callback?: (context: this) => void) { this.moveToIndex = this.findTheMovedToIndex(e); callback?.(this); } onEnd(positionInfo: DragPosition, e: PointerEvent, callback?: (context: this) => void) { callback?.(this); this.moveToIndex = -1; this.selectedIndex = new Set(); } recalculateStartPosition() { if (this.isDragX) { this.startPosition = []; if (!this.tableBlot) return; // calculate column position const cols = this.tableBlot.getCols(); let left = cols[0].domNode.getBoundingClientRect().left; for (let index = 0; index < cols.length; index++) { const colRect = cols[index].domNode.getBoundingClientRect(); this.startPosition.push({ size: colRect.width, position: left, index }); left += colRect.width; } } else { this.startPosition = []; if (!this.tableBlot) return; // calculate row position const rows = this.tableBlot.getRows(); this.startPosition = rows.map((row, index) => { const rowRect = row.domNode.getBoundingClientRect(); return { size: rowRect.height, position: rowRect.top, index, }; }); } } findTheMovedToIndex(e: PointerEvent) { // find the index moved to const offset = this.dragCommon.getOffsetFromStart(this.tableBlot); const positionInfo = this.startPosition.find(({ position, size }) => { return (this.isDragX ? e.clientX : e.clientY) < position + size / 2 + offset; }); let index = positionInfo?.index; if (isUndefined(index) || index < 0) index = this.startPosition.length; index = Math.max(0, Math.min(index, this.startPosition.length)); // if index in selectedIndex, not allow to move if (this.selectedIndex.has(index)) return -1; if (this.options.allowMoveToIndex && !this.options.allowMoveToIndex(index)) return -1; return index; } updateTableStructure(content: TypeDelta, isMoveMinus: boolean) { let changeDelta = new Delta(); if (!this.tableBlot || this.moveToIndex < 0) return changeDelta; const tableSelection = this.tableModule.getModule(tableUpInternal.tableSelectionName); if (!tableSelection) return changeDelta; if (this.isDragX) { const cols = this.tableBlot.getCols(); const maxColIndex = cols.length - 1; const isMoveLast = this.moveToIndex > maxColIndex; const selectColId = new Set(tableSelection.selectedTds.map(td => td.colId)); const moveCols = cols.filter(col => selectColId.has(col.colId)); const moveColsInfo = moveCols.map((col) => { const length = col.length(); const offset = col.offset(col.scroll); const delta = content.slice(offset, offset + length); return { offset, delta, length }; }); const moveToIndex = Math.min(maxColIndex, this.moveToIndex); const baseCol = cols[moveToIndex]; const colInsertIndex = baseCol.offset(baseCol.scroll) + (isMoveLast ? baseCol.length() : 0); const colInsertDelta = moveColsInfo.reduce((delta, cur) => { delta = delta.concat(cur.delta); return delta; }, new Delta().retain(colInsertIndex)); const colDeletaDelta = moveColsInfo.reduce((delta, cur, index) => { const prevColInfo = moveColsInfo[index - 1]; const offset = prevColInfo ? prevColInfo.offset + prevColInfo.length : 0; delta = delta.retain(cur.offset - offset).delete(cur.length); return delta; }, new Delta()); const colChangeDelta = isMoveMinus ? colDeletaDelta.compose(colInsertDelta) : colInsertDelta.compose(colDeletaDelta); const tdInfo = tableSelection.selectedTds.map((td) => { const length = td.length(); const offset = td.offset(td.scroll); const delta = content.slice(offset, offset + length); return { offset, delta, length, rowId: td.rowId }; }); const { delta: tdDeleteDelta, insertDeltaInfo: tdInsertDeltaInfo, } = tdInfo.reduce(({ delta, insertDeltaInfo }, cur, index) => { const prev = tdInfo[index - 1]; const offset = prev ? prev.offset + prev.length : 0; let prevLength = 0; if (index !== 0 && !isMoveMinus) { // move to right. delete before insert. retain need count in delete // if prev cell not in the same row, retain need add the `insert` delta length prevLength = prev.rowId !== cur.rowId ? insertDeltaInfo[prev.rowId]?.length() : 0; } delta = delta.retain(cur.offset - offset + prevLength).delete(cur.length); if (!insertDeltaInfo[cur.rowId]) insertDeltaInfo[cur.rowId] = new Delta(); insertDeltaInfo[cur.rowId] = insertDeltaInfo[cur.rowId].concat(cur.delta); return { delta, insertDeltaInfo }; }, { delta: new Delta(), insertDeltaInfo: {} } as { delta: TypeDelta; insertDeltaInfo: Record }); const rows = this.tableBlot.getRows(); const { delta: tdInsertDelta } = rows.reduce(({ delta, offset }, row, index) => { const info = row.getCellByColumIndex(Math.min(maxColIndex, this.moveToIndex)); const td = info[0]; if (!td) return { delta, offset }; const tdOffset = td.offset(td.scroll) + (isMoveLast ? td.length() : 0); let retain = tdOffset - offset; if (index !== 0 && isMoveMinus) { // move to left. insert before delete. retain need count in insert // minus each previous line `insert` delta length const prevRow = tdInsertDeltaInfo[rows[index - 1].rowId]; if (prevRow) { retain -= prevRow.length(); } } delta.retain(retain); if (tdInsertDeltaInfo[td.rowId]) { delta = delta.concat(tdInsertDeltaInfo[td.rowId]); } return { delta, offset: tdOffset }; }, { delta: new Delta(), offset: 0 }); const cellChangeDelta = isMoveMinus ? tdDeleteDelta.compose(tdInsertDelta) : tdInsertDelta.compose(tdDeleteDelta); // Delta.compose doesn't calculate retain with `delete` or `insert`, but `updateContent` will changeDelta = colChangeDelta.compose(cellChangeDelta); } else { const rows = this.tableBlot.getRows(); const maxRowIndex = rows.length - 1; const isMoveLast = this.moveToIndex > maxRowIndex; const moveToIndex = Math.min(maxRowIndex, this.moveToIndex); const baseRow = rows[moveToIndex]; const selectedRows = Array.from( tableSelection.selectedTds.reduce( (rowSet, td) => rowSet.add(td.getTableRow()!), new Set(), ), ).filter(Boolean); let lastOffset = 0; const { delta: rowDeleteDelta, start, end } = selectedRows.reduce(({ delta, start, end }, row) => { const offset = row.offset(row.scroll); const length = row.length(); delta.retain(offset - lastOffset).delete(length); lastOffset = offset + length; return { delta, start: Math.min(start, offset), end: Math.max(end, offset + length), }; }, { delta: new Delta(), start: Number.POSITIVE_INFINITY, end: 0 }); const rowInsertIndex = baseRow.offset(baseRow.scroll) + (isMoveLast ? baseRow.length() : 0); const rowInsertDelta = new Delta().retain(rowInsertIndex).concat(content.slice(start, end)); changeDelta = isMoveMinus ? rowDeleteDelta.compose(rowInsertDelta) : rowInsertDelta.compose(rowDeleteDelta); } return changeDelta; } }