From 908159564af358836e02863a55a1205396beacfd Mon Sep 17 00:00:00 2001 From: chris <kokosias@yahoo.gr> Date: Mon, 28 Aug 2023 17:23:19 +0300 Subject: [PATCH] removes ts files --- .../TablesService/tableSrc/cellselection.ts | 464 ---------- .../TablesService/tableSrc/columnresizing.ts | 383 -------- .../src/TablesService/tableSrc/commands.ts | 852 ------------------ .../src/TablesService/tableSrc/copypaste.ts | 381 -------- .../src/TablesService/tableSrc/fixtables.ts | 150 --- .../src/TablesService/tableSrc/index.ts | 136 --- .../src/TablesService/tableSrc/input.ts | 310 ------- .../src/TablesService/tableSrc/schema.ts | 197 ---- .../src/TablesService/tableSrc/tablemap.ts | 377 -------- .../src/TablesService/tableSrc/tableview.ts | 86 -- .../src/TablesService/tableSrc/util.ts | 195 ---- 11 files changed, 3531 deletions(-) delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/cellselection.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/columnresizing.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/commands.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/copypaste.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/fixtables.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/index.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/input.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/schema.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/tablemap.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/tableview.ts delete mode 100644 wax-prosemirror-services/src/TablesService/tableSrc/util.ts diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/cellselection.ts b/wax-prosemirror-services/src/TablesService/tableSrc/cellselection.ts deleted file mode 100644 index e092dfe8e..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/cellselection.ts +++ /dev/null @@ -1,464 +0,0 @@ -// This file defines a ProseMirror selection subclass that models -// table cell selections. The table plugin needs to be active to wire -// in the user interaction part of table selections (so that you -// actually get such selections when you select across cells). - -import { Fragment, Node, ResolvedPos, Slice } from 'prosemirror-model'; -import { - EditorState, - NodeSelection, - Selection, - SelectionRange, - TextSelection, - Transaction, -} from 'prosemirror-state'; -import { Decoration, DecorationSet, DecorationSource } from 'prosemirror-view'; - -import { Mappable } from 'prosemirror-transform'; -import { TableMap } from './tablemap'; -import { CellAttrs, inSameTable, pointsAtCell, removeColSpan } from './util'; - -/** - * @public - */ -export interface CellSelectionJSON { - type: string; - anchor: number; - head: number; -} - -/** - * A [`Selection`](http://prosemirror.net/docs/ref/#state.Selection) - * subclass that represents a cell selection spanning part of a table. - * With the plugin enabled, these will be created when the user - * selects across cells, and will be drawn by giving selected cells a - * `selectedCell` CSS class. - * - * @public - */ -export class CellSelection extends Selection { - // A resolved position pointing _in front of_ the anchor cell (the one - // that doesn't move when extending the selection). - public $anchorCell: ResolvedPos; - - // A resolved position pointing in front of the head cell (the one - // moves when extending the selection). - public $headCell: ResolvedPos; - - // A table selection is identified by its anchor and head cells. The - // positions given to this constructor should point _before_ two - // cells in the same table. They may be the same, to select a single - // cell. - constructor($anchorCell: ResolvedPos, $headCell: ResolvedPos = $anchorCell) { - const table = $anchorCell.node(-1); - const map = TableMap.get(table); - const tableStart = $anchorCell.start(-1); - const rect = map.rectBetween( - $anchorCell.pos - tableStart, - $headCell.pos - tableStart, - ); - - const doc = $anchorCell.node(0); - const cells = map - .cellsInRect(rect) - .filter((p) => p != $headCell.pos - tableStart); - // Make the head cell the first range, so that it counts as the - // primary part of the selection - cells.unshift($headCell.pos - tableStart); - const ranges = cells.map((pos) => { - const cell = table.nodeAt(pos); - if (!cell) { - throw RangeError(`No cell with offset ${pos} found`); - } - const from = tableStart + pos + 1; - return new SelectionRange( - doc.resolve(from), - doc.resolve(from + cell.content.size), - ); - }); - super(ranges[0].$from, ranges[0].$to, ranges); - this.$anchorCell = $anchorCell; - this.$headCell = $headCell; - } - - public map(doc: Node, mapping: Mappable): CellSelection | Selection { - const $anchorCell = doc.resolve(mapping.map(this.$anchorCell.pos)); - const $headCell = doc.resolve(mapping.map(this.$headCell.pos)); - if ( - pointsAtCell($anchorCell) && - pointsAtCell($headCell) && - inSameTable($anchorCell, $headCell) - ) { - const tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1); - if (tableChanged && this.isRowSelection()) - return CellSelection.rowSelection($anchorCell, $headCell); - else if (tableChanged && this.isColSelection()) - return CellSelection.colSelection($anchorCell, $headCell); - else return new CellSelection($anchorCell, $headCell); - } - return TextSelection.between($anchorCell, $headCell); - } - - // Returns a rectangular slice of table rows containing the selected - // cells. - public content(): Slice { - const table = this.$anchorCell.node(-1); - const map = TableMap.get(table); - const tableStart = this.$anchorCell.start(-1); - - const rect = map.rectBetween( - this.$anchorCell.pos - tableStart, - this.$headCell.pos - tableStart, - ); - const seen: Record<number, boolean> = {}; - const rows = []; - for (let row = rect.top; row < rect.bottom; row++) { - const rowContent = []; - for ( - let index = row * map.width + rect.left, col = rect.left; - col < rect.right; - col++, index++ - ) { - const pos = map.map[index]; - if (seen[pos]) continue; - seen[pos] = true; - - const cellRect = map.findCell(pos); - let cell = table.nodeAt(pos); - if (!cell) { - throw RangeError(`No cell with offset ${pos} found`); - } - - const extraLeft = rect.left - cellRect.left; - const extraRight = cellRect.right - rect.right; - - if (extraLeft > 0 || extraRight > 0) { - let attrs = cell.attrs as CellAttrs; - if (extraLeft > 0) { - attrs = removeColSpan(attrs, 0, extraLeft); - } - if (extraRight > 0) { - attrs = removeColSpan( - attrs, - attrs.colspan - extraRight, - extraRight, - ); - } - if (cellRect.left < rect.left) { - cell = cell.type.createAndFill(attrs); - if (!cell) { - throw RangeError( - `Could not create cell with attrs ${JSON.stringify(attrs)}`, - ); - } - } else { - cell = cell.type.create(attrs, cell.content); - } - } - if (cellRect.top < rect.top || cellRect.bottom > rect.bottom) { - const attrs = { - ...cell.attrs, - rowspan: - Math.min(cellRect.bottom, rect.bottom) - - Math.max(cellRect.top, rect.top), - }; - if (cellRect.top < rect.top) { - cell = cell.type.createAndFill(attrs)!; - } else { - cell = cell.type.create(attrs, cell.content); - } - } - rowContent.push(cell); - } - rows.push(table.child(row).copy(Fragment.from(rowContent))); - } - - const fragment = - this.isColSelection() && this.isRowSelection() ? table : rows; - return new Slice(Fragment.from(fragment), 1, 1); - } - - public replace(tr: Transaction, content: Slice = Slice.empty): void { - const mapFrom = tr.steps.length, - ranges = this.ranges; - for (let i = 0; i < ranges.length; i++) { - const { $from, $to } = ranges[i], - mapping = tr.mapping.slice(mapFrom); - tr.replace( - mapping.map($from.pos), - mapping.map($to.pos), - i ? Slice.empty : content, - ); - } - const sel = Selection.findFrom( - tr.doc.resolve(tr.mapping.slice(mapFrom).map(this.to)), - -1, - ); - if (sel) tr.setSelection(sel); - } - - public replaceWith(tr: Transaction, node: Node): void { - this.replace(tr, new Slice(Fragment.from(node), 0, 0)); - } - - public forEachCell(f: (node: Node, pos: number) => void): void { - const table = this.$anchorCell.node(-1); - const map = TableMap.get(table); - const tableStart = this.$anchorCell.start(-1); - - const cells = map.cellsInRect( - map.rectBetween( - this.$anchorCell.pos - tableStart, - this.$headCell.pos - tableStart, - ), - ); - for (let i = 0; i < cells.length; i++) { - f(table.nodeAt(cells[i])!, tableStart + cells[i]); - } - } - - // True if this selection goes all the way from the top to the - // bottom of the table. - public isColSelection(): boolean { - const anchorTop = this.$anchorCell.index(-1); - const headTop = this.$headCell.index(-1); - if (Math.min(anchorTop, headTop) > 0) return false; - - const anchorBottom = anchorTop + this.$anchorCell.nodeAfter!.attrs.rowspan; - const headBottom = headTop + this.$headCell.nodeAfter!.attrs.rowspan; - - return ( - Math.max(anchorBottom, headBottom) == this.$headCell.node(-1).childCount - ); - } - - // Returns the smallest column selection that covers the given anchor - // and head cell. - public static colSelection( - $anchorCell: ResolvedPos, - $headCell: ResolvedPos = $anchorCell, - ): CellSelection { - const table = $anchorCell.node(-1); - const map = TableMap.get(table); - const tableStart = $anchorCell.start(-1); - - const anchorRect = map.findCell($anchorCell.pos - tableStart); - const headRect = map.findCell($headCell.pos - tableStart); - const doc = $anchorCell.node(0); - - if (anchorRect.top <= headRect.top) { - if (anchorRect.top > 0) - $anchorCell = doc.resolve(tableStart + map.map[anchorRect.left]); - if (headRect.bottom < map.height) - $headCell = doc.resolve( - tableStart + - map.map[map.width * (map.height - 1) + headRect.right - 1], - ); - } else { - if (headRect.top > 0) - $headCell = doc.resolve(tableStart + map.map[headRect.left]); - if (anchorRect.bottom < map.height) - $anchorCell = doc.resolve( - tableStart + - map.map[map.width * (map.height - 1) + anchorRect.right - 1], - ); - } - return new CellSelection($anchorCell, $headCell); - } - - // True if this selection goes all the way from the left to the - // right of the table. - public isRowSelection(): boolean { - const table = this.$anchorCell.node(-1); - const map = TableMap.get(table); - const tableStart = this.$anchorCell.start(-1); - - const anchorLeft = map.colCount(this.$anchorCell.pos - tableStart); - const headLeft = map.colCount(this.$headCell.pos - tableStart); - if (Math.min(anchorLeft, headLeft) > 0) return false; - - const anchorRight = anchorLeft + this.$anchorCell.nodeAfter!.attrs.colspan; - const headRight = headLeft + this.$headCell.nodeAfter!.attrs.colspan; - return Math.max(anchorRight, headRight) == map.width; - } - - public eq(other: unknown): boolean { - return ( - other instanceof CellSelection && - other.$anchorCell.pos == this.$anchorCell.pos && - other.$headCell.pos == this.$headCell.pos - ); - } - - // Returns the smallest row selection that covers the given anchor - // and head cell. - public static rowSelection( - $anchorCell: ResolvedPos, - $headCell: ResolvedPos = $anchorCell, - ): CellSelection { - const table = $anchorCell.node(-1); - const map = TableMap.get(table); - const tableStart = $anchorCell.start(-1); - - const anchorRect = map.findCell($anchorCell.pos - tableStart); - const headRect = map.findCell($headCell.pos - tableStart); - const doc = $anchorCell.node(0); - - if (anchorRect.left <= headRect.left) { - if (anchorRect.left > 0) - $anchorCell = doc.resolve( - tableStart + map.map[anchorRect.top * map.width], - ); - if (headRect.right < map.width) - $headCell = doc.resolve( - tableStart + map.map[map.width * (headRect.top + 1) - 1], - ); - } else { - if (headRect.left > 0) - $headCell = doc.resolve(tableStart + map.map[headRect.top * map.width]); - if (anchorRect.right < map.width) - $anchorCell = doc.resolve( - tableStart + map.map[map.width * (anchorRect.top + 1) - 1], - ); - } - return new CellSelection($anchorCell, $headCell); - } - - public toJSON(): CellSelectionJSON { - return { - type: 'cell', - anchor: this.$anchorCell.pos, - head: this.$headCell.pos, - }; - } - - static fromJSON(doc: Node, json: CellSelectionJSON): CellSelection { - return new CellSelection(doc.resolve(json.anchor), doc.resolve(json.head)); - } - - static create( - doc: Node, - anchorCell: number, - headCell: number = anchorCell, - ): CellSelection { - return new CellSelection(doc.resolve(anchorCell), doc.resolve(headCell)); - } - - getBookmark(): CellBookmark { - return new CellBookmark(this.$anchorCell.pos, this.$headCell.pos); - } -} - -CellSelection.prototype.visible = false; - -Selection.jsonID('cell', CellSelection); - -/** - * @public - */ -export class CellBookmark { - constructor(public anchor: number, public head: number) {} - - map(mapping: Mappable): CellBookmark { - return new CellBookmark(mapping.map(this.anchor), mapping.map(this.head)); - } - - resolve(doc: Node): CellSelection | Selection { - const $anchorCell = doc.resolve(this.anchor), - $headCell = doc.resolve(this.head); - if ( - $anchorCell.parent.type.spec.tableRole == 'row' && - $headCell.parent.type.spec.tableRole == 'row' && - $anchorCell.index() < $anchorCell.parent.childCount && - $headCell.index() < $headCell.parent.childCount && - inSameTable($anchorCell, $headCell) - ) - return new CellSelection($anchorCell, $headCell); - else return Selection.near($headCell, 1); - } -} - -export function drawCellSelection(state: EditorState): DecorationSource | null { - if (!(state.selection instanceof CellSelection)) return null; - const cells: Decoration[] = []; - state.selection.forEachCell((node, pos) => { - cells.push( - Decoration.node(pos, pos + node.nodeSize, { class: 'selectedCell' }), - ); - }); - return DecorationSet.create(state.doc, cells); -} - -function isCellBoundarySelection({ $from, $to }: TextSelection) { - if ($from.pos == $to.pos || $from.pos < $from.pos - 6) return false; // Cheap elimination - let afterFrom = $from.pos; - let beforeTo = $to.pos; - let depth = $from.depth; - for (; depth >= 0; depth--, afterFrom++) - if ($from.after(depth + 1) < $from.end(depth)) break; - for (let d = $to.depth; d >= 0; d--, beforeTo--) - if ($to.before(d + 1) > $to.start(d)) break; - return ( - afterFrom == beforeTo && - /row|table/.test($from.node(depth).type.spec.tableRole) - ); -} - -function isTextSelectionAcrossCells({ $from, $to }: TextSelection) { - let fromCellBoundaryNode: Node | undefined; - let toCellBoundaryNode: Node | undefined; - - for (let i = $from.depth; i > 0; i--) { - const node = $from.node(i); - if ( - node.type.spec.tableRole === 'cell' || - node.type.spec.tableRole === 'header_cell' - ) { - fromCellBoundaryNode = node; - break; - } - } - - for (let i = $to.depth; i > 0; i--) { - const node = $to.node(i); - if ( - node.type.spec.tableRole === 'cell' || - node.type.spec.tableRole === 'header_cell' - ) { - toCellBoundaryNode = node; - break; - } - } - - return fromCellBoundaryNode !== toCellBoundaryNode && $to.parentOffset === 0; -} - -export function normalizeSelection( - state: EditorState, - tr: Transaction | undefined, - allowTableNodeSelection: boolean, -): Transaction | undefined { - const sel = (tr || state).selection; - const doc = (tr || state).doc; - let normalize: Selection | undefined; - let role: string | undefined; - if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) { - if (role == 'cell' || role == 'header_cell') { - normalize = CellSelection.create(doc, sel.from); - } else if (role == 'row') { - const $cell = doc.resolve(sel.from + 1); - normalize = CellSelection.rowSelection($cell, $cell); - } else if (!allowTableNodeSelection) { - const map = TableMap.get(sel.node); - const start = sel.from + 1; - const lastCell = start + map.map[map.width * map.height - 1]; - normalize = CellSelection.create(doc, start + 1, lastCell); - } - } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) { - normalize = TextSelection.create(doc, sel.from); - } else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) { - normalize = TextSelection.create(doc, sel.$from.start(), sel.$from.end()); - } - if (normalize) (tr || (tr = state.tr)).setSelection(normalize); - return tr; -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/columnresizing.ts b/wax-prosemirror-services/src/TablesService/tableSrc/columnresizing.ts deleted file mode 100644 index ed2019206..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/columnresizing.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { Attrs, Node as ProsemirrorNode } from 'prosemirror-model'; -import { EditorState, Plugin, PluginKey, Transaction } from 'prosemirror-state'; -import { - Decoration, - DecorationSet, - EditorView, - NodeView, -} from 'prosemirror-view'; -import { tableNodeTypes } from './schema'; -import { TableMap } from './tablemap'; -import { TableView, updateColumnsOnResize } from './tableview'; -import { cellAround, CellAttrs, pointsAtCell } from './util'; - -/** - * @public - */ -export const columnResizingPluginKey = new PluginKey<ResizeState>( - 'tableColumnResizing', -); - -/** - * @public - */ -export type ColumnResizingOptions = { - handleWidth?: number; - cellMinWidth?: number; - lastColumnResizable?: boolean; - View?: new ( - node: ProsemirrorNode, - cellMinWidth: number, - view: EditorView, - ) => NodeView; -}; - -/** - * @public - */ -export type Dragging = { startX: number; startWidth: number }; - -/** - * @public - */ -export function columnResizing({ - handleWidth = 5, - cellMinWidth = 25, - View = TableView, - lastColumnResizable = true, -}: ColumnResizingOptions = {}): Plugin { - const plugin = new Plugin<ResizeState>({ - key: columnResizingPluginKey, - state: { - init(_, state) { - plugin.spec!.props!.nodeViews![ - tableNodeTypes(state.schema).table.name - ] = (node, view) => new View(node, cellMinWidth, view); - return new ResizeState(-1, false); - }, - apply(tr, prev) { - return prev.apply(tr); - }, - }, - props: { - attributes: (state): Record<string, string> => { - const pluginState = columnResizingPluginKey.getState(state); - return pluginState && pluginState.activeHandle > -1 - ? { class: 'resize-cursor' } - : {}; - }, - - handleDOMEvents: { - mousemove: (view, event) => { - handleMouseMove( - view, - event, - handleWidth, - cellMinWidth, - lastColumnResizable, - ); - }, - mouseleave: (view) => { - handleMouseLeave(view); - }, - mousedown: (view, event) => { - handleMouseDown(view, event, cellMinWidth); - }, - }, - - decorations: (state) => { - const pluginState = columnResizingPluginKey.getState(state); - if (pluginState && pluginState.activeHandle > -1) { - return handleDecorations(state, pluginState.activeHandle); - } - }, - - nodeViews: {}, - }, - }); - return plugin; -} - -/** - * @public - */ -export class ResizeState { - constructor(public activeHandle: number, public dragging: Dragging | false) {} - - apply(tr: Transaction): ResizeState { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const state = this; - const action = tr.getMeta(columnResizingPluginKey); - if (action && action.setHandle != null) - return new ResizeState(action.setHandle, false); - if (action && action.setDragging !== undefined) - return new ResizeState(state.activeHandle, action.setDragging); - if (state.activeHandle > -1 && tr.docChanged) { - let handle = tr.mapping.map(state.activeHandle, -1); - if (!pointsAtCell(tr.doc.resolve(handle))) { - handle = -1; - } - return new ResizeState(handle, state.dragging); - } - return state; - } -} - -function handleMouseMove( - view: EditorView, - event: MouseEvent, - handleWidth: number, - cellMinWidth: number, - lastColumnResizable: boolean, -): void { - const pluginState = columnResizingPluginKey.getState(view.state); - if (!pluginState) return; - - if (!pluginState.dragging) { - const target = domCellAround(event.target as HTMLElement); - let cell = -1; - if (target) { - const { left, right } = target.getBoundingClientRect(); - if (event.clientX - left <= handleWidth) - cell = edgeCell(view, event, 'left', handleWidth); - else if (right - event.clientX <= handleWidth) - cell = edgeCell(view, event, 'right', handleWidth); - } - - if (cell != pluginState.activeHandle) { - if (!lastColumnResizable && cell !== -1) { - const $cell = view.state.doc.resolve(cell); - const table = $cell.node(-1); - const map = TableMap.get(table); - const tableStart = $cell.start(-1); - const col = - map.colCount($cell.pos - tableStart) + - $cell.nodeAfter!.attrs.colspan - - 1; - - if (col == map.width - 1) { - return; - } - } - - updateHandle(view, cell); - } - } -} - -function handleMouseLeave(view: EditorView): void { - const pluginState = columnResizingPluginKey.getState(view.state); - if (pluginState && pluginState.activeHandle > -1 && !pluginState.dragging) - updateHandle(view, -1); -} - -function handleMouseDown( - view: EditorView, - event: MouseEvent, - cellMinWidth: number, -): boolean { - const pluginState = columnResizingPluginKey.getState(view.state); - if (!pluginState || pluginState.activeHandle == -1 || pluginState.dragging) - return false; - - const cell = view.state.doc.nodeAt(pluginState.activeHandle)!; - const width = currentColWidth(view, pluginState.activeHandle, cell.attrs); - view.dispatch( - view.state.tr.setMeta(columnResizingPluginKey, { - setDragging: { startX: event.clientX, startWidth: width }, - }), - ); - - function finish(event: MouseEvent) { - window.removeEventListener('mouseup', finish); - window.removeEventListener('mousemove', move); - const pluginState = columnResizingPluginKey.getState(view.state); - if (pluginState?.dragging) { - updateColumnWidth( - view, - pluginState.activeHandle, - draggedWidth(pluginState.dragging, event, cellMinWidth), - ); - view.dispatch( - view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null }), - ); - } - } - - function move(event: MouseEvent): void { - if (!event.which) return finish(event); - const pluginState = columnResizingPluginKey.getState(view.state); - if (!pluginState) return; - if (pluginState.dragging) { - const dragged = draggedWidth(pluginState.dragging, event, cellMinWidth); - displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth); - } - } - - window.addEventListener('mouseup', finish); - window.addEventListener('mousemove', move); - event.preventDefault(); - return true; -} - -function currentColWidth( - view: EditorView, - cellPos: number, - { colspan, colwidth }: Attrs, -): number { - const width = colwidth && colwidth[colwidth.length - 1]; - if (width) return width; - const dom = view.domAtPos(cellPos); - const node = dom.node.childNodes[dom.offset] as HTMLElement; - let domWidth = node.offsetWidth, - parts = colspan; - if (colwidth) - for (let i = 0; i < colspan; i++) - if (colwidth[i]) { - domWidth -= colwidth[i]; - parts--; - } - return domWidth / parts; -} - -function domCellAround(target: HTMLElement | null): HTMLElement | null { - while (target && target.nodeName != 'TD' && target.nodeName != 'TH') - target = - target.classList && target.classList.contains('ProseMirror') - ? null - : (target.parentNode as HTMLElement); - return target; -} - -function edgeCell( - view: EditorView, - event: MouseEvent, - side: 'left' | 'right', - handleWidth: number, -): number { - // posAtCoords returns inconsistent positions when cursor is moving - // across a collapsed table border. Use an offset to adjust the - // target viewport coordinates away from the table border. - const offset = side == 'right' ? -handleWidth : handleWidth; - const found = view.posAtCoords({ - left: event.clientX + offset, - top: event.clientY, - }); - if (!found) return -1; - const { pos } = found; - const $cell = cellAround(view.state.doc.resolve(pos)); - if (!$cell) return -1; - if (side == 'right') return $cell.pos; - const map = TableMap.get($cell.node(-1)), - start = $cell.start(-1); - const index = map.map.indexOf($cell.pos - start); - return index % map.width == 0 ? -1 : start + map.map[index - 1]; -} - -function draggedWidth( - dragging: Dragging, - event: MouseEvent, - cellMinWidth: number, -): number { - const offset = event.clientX - dragging.startX; - return Math.max(cellMinWidth, dragging.startWidth + offset); -} - -function updateHandle(view: EditorView, value: number): void { - view.dispatch( - view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value }), - ); -} - -function updateColumnWidth( - view: EditorView, - cell: number, - width: number, -): void { - const $cell = view.state.doc.resolve(cell); - const table = $cell.node(-1), - map = TableMap.get(table), - start = $cell.start(-1); - const col = - map.colCount($cell.pos - start) + $cell.nodeAfter!.attrs.colspan - 1; - const tr = view.state.tr; - for (let row = 0; row < map.height; row++) { - const mapIndex = row * map.width + col; - // Rowspanning cell that has already been handled - if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue; - const pos = map.map[mapIndex]; - const attrs = table.nodeAt(pos)!.attrs as CellAttrs; - const index = attrs.colspan == 1 ? 0 : col - map.colCount(pos); - if (attrs.colwidth && attrs.colwidth[index] == width) continue; - const colwidth = attrs.colwidth - ? attrs.colwidth.slice() - : zeroes(attrs.colspan); - colwidth[index] = width; - tr.setNodeMarkup(start + pos, null, { ...attrs, colwidth: colwidth }); - } - if (tr.docChanged) view.dispatch(tr); -} - -function displayColumnWidth( - view: EditorView, - cell: number, - width: number, - cellMinWidth: number, -): void { - const $cell = view.state.doc.resolve(cell); - const table = $cell.node(-1), - start = $cell.start(-1); - const col = - TableMap.get(table).colCount($cell.pos - start) + - $cell.nodeAfter!.attrs.colspan - - 1; - let dom: Node | null = view.domAtPos($cell.start(-1)).node; - while (dom && dom.nodeName != 'TABLE') { - dom = dom.parentNode; - } - if (!dom) return; - updateColumnsOnResize( - table, - dom.firstChild as HTMLTableColElement, - dom as HTMLTableElement, - cellMinWidth, - col, - width, - ); -} - -function zeroes(n: number): 0[] { - return Array(n).fill(0); -} - -export function handleDecorations( - state: EditorState, - cell: number, -): DecorationSet { - const decorations = []; - const $cell = state.doc.resolve(cell); - const table = $cell.node(-1); - if (!table) { - return DecorationSet.empty; - } - const map = TableMap.get(table); - const start = $cell.start(-1); - const col = map.colCount($cell.pos - start) + $cell.nodeAfter!.attrs.colspan; - for (let row = 0; row < map.height; row++) { - const index = col + row * map.width - 1; - // For positions that have either a different cell or the end - // of the table to their right, and either the top of the table or - // a different cell above them, add a decoration - if ( - (col == map.width || map.map[index] != map.map[index + 1]) && - (row == 0 || map.map[index] != map.map[index - map.width]) - ) { - const cellPos = map.map[index]; - const pos = start + cellPos + table.nodeAt(cellPos)!.nodeSize - 1; - const dom = document.createElement('div'); - dom.className = 'column-resize-handle'; - decorations.push(Decoration.widget(pos, dom)); - } - } - return DecorationSet.create(state.doc, decorations); -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/commands.ts b/wax-prosemirror-services/src/TablesService/tableSrc/commands.ts deleted file mode 100644 index 2f8a9c704..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/commands.ts +++ /dev/null @@ -1,852 +0,0 @@ -// This file defines a number of table-related commands. - -import { Fragment, Node, NodeType, ResolvedPos } from 'prosemirror-model'; -import { - Command, - EditorState, - TextSelection, - Transaction, -} from 'prosemirror-state'; - -import { CellSelection } from './cellselection'; -import type { Direction } from './input'; -import { tableNodeTypes, TableRole } from './schema'; -import { Rect, TableMap } from './tablemap'; -import { - addColSpan, - cellAround, - CellAttrs, - cellWrapping, - columnIsHeader, - isInTable, - moveCellForward, - removeColSpan, - selectionCell, -} from './util'; - -/** - * @public - */ -export type TableRect = Rect & { - tableStart: number; - map: TableMap; - table: Node; -}; - -/** - * Helper to get the selected rectangle in a table, if any. Adds table - * map, table node, and table start offset to the object for - * convenience. - * - * @public - */ -export function selectedRect(state: EditorState): TableRect { - const sel = state.selection; - const $pos = selectionCell(state); - const table = $pos.node(-1); - const tableStart = $pos.start(-1); - const map = TableMap.get(table); - const rect = - sel instanceof CellSelection - ? map.rectBetween( - sel.$anchorCell.pos - tableStart, - sel.$headCell.pos - tableStart, - ) - : map.findCell($pos.pos - tableStart); - return { ...rect, tableStart, map, table }; -} - -/** - * Add a column at the given position in a table. - * - * @public - */ -export function addColumn( - tr: Transaction, - { map, tableStart, table }: TableRect, - col: number, -): Transaction { - let refColumn: number | null = col > 0 ? -1 : 0; - if (columnIsHeader(map, table, col + refColumn)) { - refColumn = col == 0 || col == map.width ? null : 0; - } - - for (let row = 0; row < map.height; row++) { - const index = row * map.width + col; - // If this position falls inside a col-spanning cell - if (col > 0 && col < map.width && map.map[index - 1] == map.map[index]) { - const pos = map.map[index]; - const cell = table.nodeAt(pos)!; - tr.setNodeMarkup( - tr.mapping.map(tableStart + pos), - null, - addColSpan(cell.attrs as CellAttrs, col - map.colCount(pos)), - ); - // Skip ahead if rowspan > 1 - row += cell.attrs.rowspan - 1; - } else { - const type = - refColumn == null - ? tableNodeTypes(table.type.schema).cell - : table.nodeAt(map.map[index + refColumn])!.type; - const pos = map.positionAt(row, col, table); - tr.insert(tr.mapping.map(tableStart + pos), type.createAndFill()!); - } - } - return tr; -} - -/** - * Command to add a column before the column with the selection. - * - * @public - */ -export function addColumnBefore( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - if (!isInTable(state)) return false; - if (dispatch) { - const rect = selectedRect(state); - dispatch(addColumn(state.tr, rect, rect.left)); - } - return true; -} - -/** - * Command to add a column after the column with the selection. - * - * @public - */ -export function addColumnAfter( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - if (!isInTable(state)) return false; - if (dispatch) { - const rect = selectedRect(state); - dispatch(addColumn(state.tr, rect, rect.right)); - } - return true; -} - -/** - * @public - */ -export function removeColumn( - tr: Transaction, - { map, table, tableStart }: TableRect, - col: number, -) { - const mapStart = tr.mapping.maps.length; - for (let row = 0; row < map.height; ) { - const index = row * map.width + col; - const pos = map.map[index]; - const cell = table.nodeAt(pos)!; - const attrs = cell.attrs as CellAttrs; - // If this is part of a col-spanning cell - if ( - (col > 0 && map.map[index - 1] == pos) || - (col < map.width - 1 && map.map[index + 1] == pos) - ) { - tr.setNodeMarkup( - tr.mapping.slice(mapStart).map(tableStart + pos), - null, - removeColSpan(attrs, col - map.colCount(pos)), - ); - } else { - const start = tr.mapping.slice(mapStart).map(tableStart + pos); - tr.delete(start, start + cell.nodeSize); - } - row += attrs.rowspan; - } -} - -/** - * Command function that removes the selected columns from a table. - * - * @public - */ -export function deleteColumn( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - if (!isInTable(state)) return false; - if (dispatch) { - const rect = selectedRect(state); - const tr = state.tr; - if (rect.left == 0 && rect.right == rect.map.width) return false; - for (let i = rect.right - 1; ; i--) { - removeColumn(tr, rect, i); - if (i == rect.left) break; - const table = rect.tableStart - ? tr.doc.nodeAt(rect.tableStart - 1) - : tr.doc; - if (!table) { - throw RangeError('No table found'); - } - rect.table = table; - rect.map = TableMap.get(table); - } - dispatch(tr); - } - return true; -} - -/** - * @public - */ -export function rowIsHeader(map: TableMap, table: Node, row: number): boolean { - const headerCell = tableNodeTypes(table.type.schema).header_cell; - for (let col = 0; col < map.width; col++) - if (table.nodeAt(map.map[col + row * map.width])?.type != headerCell) - return false; - return true; -} - -/** - * @public - */ -export function addRow( - tr: Transaction, - { map, tableStart, table }: TableRect, - row: number, -): Transaction { - let rowPos = tableStart; - for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; - const cells = []; - let refRow: number | null = row > 0 ? -1 : 0; - if (rowIsHeader(map, table, row + refRow)) - refRow = row == 0 || row == map.height ? null : 0; - for (let col = 0, index = map.width * row; col < map.width; col++, index++) { - // Covered by a rowspan cell - if ( - row > 0 && - row < map.height && - map.map[index] == map.map[index - map.width] - ) { - const pos = map.map[index]; - const attrs = table.nodeAt(pos)!.attrs; - tr.setNodeMarkup(tableStart + pos, null, { - ...attrs, - rowspan: attrs.rowspan + 1, - }); - col += attrs.colspan - 1; - } else { - const type = - refRow == null - ? tableNodeTypes(table.type.schema).cell - : table.nodeAt(map.map[index + refRow * map.width])?.type; - const node = type?.createAndFill(); - if (node) cells.push(node); - } - } - tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells)); - return tr; -} - -/** - * Add a table row before the selection. - * - * @public - */ -export function addRowBefore( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - if (!isInTable(state)) return false; - if (dispatch) { - const rect = selectedRect(state); - dispatch(addRow(state.tr, rect, rect.top)); - } - return true; -} - -/** - * Add a table row after the selection. - * - * @public - */ -export function addRowAfter( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - if (!isInTable(state)) return false; - if (dispatch) { - const rect = selectedRect(state); - dispatch(addRow(state.tr, rect, rect.bottom)); - } - return true; -} - -/** - * @public - */ -export function removeRow( - tr: Transaction, - { map, table, tableStart }: TableRect, - row: number, -): void { - let rowPos = 0; - for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; - const nextRow = rowPos + table.child(row).nodeSize; - - const mapFrom = tr.mapping.maps.length; - tr.delete(rowPos + tableStart, nextRow + tableStart); - - for (let col = 0, index = row * map.width; col < map.width; col++, index++) { - const pos = map.map[index]; - if (row > 0 && pos == map.map[index - map.width]) { - // If this cell starts in the row above, simply reduce its rowspan - const attrs = table.nodeAt(pos)!.attrs as CellAttrs; - tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + tableStart), null, { - ...attrs, - rowspan: attrs.rowspan - 1, - }); - col += attrs.colspan - 1; - } else if (row < map.width && pos == map.map[index + map.width]) { - // Else, if it continues in the row below, it has to be moved down - const cell = table.nodeAt(pos)!; - const attrs = cell.attrs as CellAttrs; - const copy = cell.type.create( - { ...attrs, rowspan: cell.attrs.rowspan - 1 }, - cell.content, - ); - const newPos = map.positionAt(row + 1, col, table); - tr.insert(tr.mapping.slice(mapFrom).map(tableStart + newPos), copy); - col += attrs.colspan - 1; - } - } -} - -/** - * Remove the selected rows from a table. - * - * @public - */ -export function deleteRow( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - if (!isInTable(state)) return false; - if (dispatch) { - const rect = selectedRect(state), - tr = state.tr; - if (rect.top == 0 && rect.bottom == rect.map.height) return false; - for (let i = rect.bottom - 1; ; i--) { - removeRow(tr, rect, i); - if (i == rect.top) break; - const table = rect.tableStart - ? tr.doc.nodeAt(rect.tableStart - 1) - : tr.doc; - if (!table) { - throw RangeError('No table found'); - } - rect.table = table; - rect.map = TableMap.get(rect.table); - } - dispatch(tr); - } - return true; -} - -function isEmpty(cell: Node): boolean { - const c = cell.content; - - return ( - c.childCount == 1 && c.child(0).isTextblock && c.child(0).childCount == 0 - ); -} - -function cellsOverlapRectangle({ width, height, map }: TableMap, rect: Rect) { - let indexTop = rect.top * width + rect.left, - indexLeft = indexTop; - let indexBottom = (rect.bottom - 1) * width + rect.left, - indexRight = indexTop + (rect.right - rect.left - 1); - for (let i = rect.top; i < rect.bottom; i++) { - if ( - (rect.left > 0 && map[indexLeft] == map[indexLeft - 1]) || - (rect.right < width && map[indexRight] == map[indexRight + 1]) - ) - return true; - indexLeft += width; - indexRight += width; - } - for (let i = rect.left; i < rect.right; i++) { - if ( - (rect.top > 0 && map[indexTop] == map[indexTop - width]) || - (rect.bottom < height && map[indexBottom] == map[indexBottom + width]) - ) - return true; - indexTop++; - indexBottom++; - } - return false; -} - -/** - * Merge the selected cells into a single cell. Only available when - * the selected cells' outline forms a rectangle. - * - * @public - */ -export function mergeCells( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - const sel = state.selection; - if ( - !(sel instanceof CellSelection) || - sel.$anchorCell.pos == sel.$headCell.pos - ) - return false; - const rect = selectedRect(state), - { map } = rect; - if (cellsOverlapRectangle(map, rect)) return false; - if (dispatch) { - const tr = state.tr; - const seen: Record<number, boolean> = {}; - let content = Fragment.empty; - let mergedPos: number | undefined; - let mergedCell: Node | undefined; - for (let row = rect.top; row < rect.bottom; row++) { - for (let col = rect.left; col < rect.right; col++) { - const cellPos = map.map[row * map.width + col]; - const cell = rect.table.nodeAt(cellPos); - if (seen[cellPos] || !cell) continue; - seen[cellPos] = true; - if (mergedPos == null) { - mergedPos = cellPos; - mergedCell = cell; - } else { - if (!isEmpty(cell)) content = content.append(cell.content); - const mapped = tr.mapping.map(cellPos + rect.tableStart); - tr.delete(mapped, mapped + cell.nodeSize); - } - } - } - if (mergedPos == null || mergedCell == null) { - return true; - } - - tr.setNodeMarkup(mergedPos + rect.tableStart, null, { - ...addColSpan( - mergedCell.attrs as CellAttrs, - mergedCell.attrs.colspan, - rect.right - rect.left - mergedCell.attrs.colspan, - ), - rowspan: rect.bottom - rect.top, - }); - if (content.size) { - const end = mergedPos + 1 + mergedCell.content.size; - const start = isEmpty(mergedCell) ? mergedPos + 1 : end; - tr.replaceWith(start + rect.tableStart, end + rect.tableStart, content); - } - tr.setSelection( - new CellSelection(tr.doc.resolve(mergedPos + rect.tableStart)), - ); - dispatch(tr); - } - return true; -} - -/** - * Split a selected cell, whose rowpan or colspan is greater than one, - * into smaller cells. Use the first cell type for the new cells. - * - * @public - */ -export function splitCell( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - const nodeTypes = tableNodeTypes(state.schema); - return splitCellWithType(({ node }) => { - return nodeTypes[node.type.spec.tableRole as TableRole]; - })(state, dispatch); -} - -/** - * @public - */ -export interface GetCellTypeOptions { - node: Node; - row: number; - col: number; -} - -/** - * Split a selected cell, whose rowpan or colspan is greater than one, - * into smaller cells with the cell type (th, td) returned by getType function. - * - * @public - */ -export function splitCellWithType( - getCellType: (options: GetCellTypeOptions) => NodeType, -): Command { - return (state, dispatch) => { - const sel = state.selection; - let cellNode: Node | null | undefined; - let cellPos: number | undefined; - if (!(sel instanceof CellSelection)) { - cellNode = cellWrapping(sel.$from); - if (!cellNode) return false; - cellPos = cellAround(sel.$from)?.pos; - } else { - if (sel.$anchorCell.pos != sel.$headCell.pos) return false; - cellNode = sel.$anchorCell.nodeAfter; - cellPos = sel.$anchorCell.pos; - } - if (cellNode == null || cellPos == null) { - return false; - } - if (cellNode.attrs.colspan == 1 && cellNode.attrs.rowspan == 1) { - return false; - } - if (dispatch) { - let baseAttrs = cellNode.attrs; - const attrs = []; - const colwidth = baseAttrs.colwidth; - if (baseAttrs.rowspan > 1) baseAttrs = { ...baseAttrs, rowspan: 1 }; - if (baseAttrs.colspan > 1) baseAttrs = { ...baseAttrs, colspan: 1 }; - const rect = selectedRect(state), - tr = state.tr; - for (let i = 0; i < rect.right - rect.left; i++) - attrs.push( - colwidth - ? { - ...baseAttrs, - colwidth: colwidth && colwidth[i] ? [colwidth[i]] : null, - } - : baseAttrs, - ); - let lastCell; - for (let row = rect.top; row < rect.bottom; row++) { - let pos = rect.map.positionAt(row, rect.left, rect.table); - if (row == rect.top) pos += cellNode.nodeSize; - for (let col = rect.left, i = 0; col < rect.right; col++, i++) { - if (col == rect.left && row == rect.top) continue; - tr.insert( - (lastCell = tr.mapping.map(pos + rect.tableStart, 1)), - getCellType({ node: cellNode, row, col }).createAndFill(attrs[i])!, - ); - } - } - tr.setNodeMarkup( - cellPos, - getCellType({ node: cellNode, row: rect.top, col: rect.left }), - attrs[0], - ); - if (sel instanceof CellSelection) - tr.setSelection( - new CellSelection( - tr.doc.resolve(sel.$anchorCell.pos), - lastCell ? tr.doc.resolve(lastCell) : undefined, - ), - ); - dispatch(tr); - } - return true; - }; -} - -/** - * Returns a command that sets the given attribute to the given value, - * and is only available when the currently selected cell doesn't - * already have that attribute set to that value. - * - * @public - */ -export function setCellAttr(name: string, value: unknown): Command { - return function (state, dispatch) { - if (!isInTable(state)) return false; - const $cell = selectionCell(state); - if ($cell.nodeAfter!.attrs[name] === value) return false; - if (dispatch) { - const tr = state.tr; - if (state.selection instanceof CellSelection) - state.selection.forEachCell((node, pos) => { - if (node.attrs[name] !== value) - tr.setNodeMarkup(pos, null, { - ...node.attrs, - [name]: value, - }); - }); - else - tr.setNodeMarkup($cell.pos, null, { - ...$cell.nodeAfter!.attrs, - [name]: value, - }); - dispatch(tr); - } - return true; - }; -} - -function deprecated_toggleHeader(type: ToggleHeaderType): Command { - return function (state, dispatch) { - if (!isInTable(state)) return false; - if (dispatch) { - const types = tableNodeTypes(state.schema); - const rect = selectedRect(state), - tr = state.tr; - const cells = rect.map.cellsInRect( - type == 'column' - ? { - left: rect.left, - top: 0, - right: rect.right, - bottom: rect.map.height, - } - : type == 'row' - ? { - left: 0, - top: rect.top, - right: rect.map.width, - bottom: rect.bottom, - } - : rect, - ); - const nodes = cells.map((pos) => rect.table.nodeAt(pos)!); - for ( - let i = 0; - i < cells.length; - i++ // Remove headers, if any - ) - if (nodes[i].type == types.header_cell) - tr.setNodeMarkup( - rect.tableStart + cells[i], - types.cell, - nodes[i].attrs, - ); - if (tr.steps.length == 0) - for ( - let i = 0; - i < cells.length; - i++ // No headers removed, add instead - ) - tr.setNodeMarkup( - rect.tableStart + cells[i], - types.header_cell, - nodes[i].attrs, - ); - dispatch(tr); - } - return true; - }; -} - -function isHeaderEnabledByType( - type: 'row' | 'column', - rect: TableRect, - types: Record<string, NodeType>, -): boolean { - // Get cell positions for first row or first column - const cellPositions = rect.map.cellsInRect({ - left: 0, - top: 0, - right: type == 'row' ? rect.map.width : 1, - bottom: type == 'column' ? rect.map.height : 1, - }); - - for (let i = 0; i < cellPositions.length; i++) { - const cell = rect.table.nodeAt(cellPositions[i]); - if (cell && cell.type !== types.header_cell) { - return false; - } - } - - return true; -} - -/** - * @public - */ -export type ToggleHeaderType = 'column' | 'row' | 'cell'; - -/** - * Toggles between row/column header and normal cells (Only applies to first row/column). - * For deprecated behavior pass `useDeprecatedLogic` in options with true. - * - * @public - */ -export function toggleHeader( - type: ToggleHeaderType, - options?: { useDeprecatedLogic: boolean } | undefined, -): Command { - options = options || { useDeprecatedLogic: false }; - - if (options.useDeprecatedLogic) return deprecated_toggleHeader(type); - - return function (state, dispatch) { - if (!isInTable(state)) return false; - if (dispatch) { - const types = tableNodeTypes(state.schema); - const rect = selectedRect(state), - tr = state.tr; - - const isHeaderRowEnabled = isHeaderEnabledByType('row', rect, types); - const isHeaderColumnEnabled = isHeaderEnabledByType( - 'column', - rect, - types, - ); - - const isHeaderEnabled = - type === 'column' - ? isHeaderRowEnabled - : type === 'row' - ? isHeaderColumnEnabled - : false; - - const selectionStartsAt = isHeaderEnabled ? 1 : 0; - - const cellsRect = - type == 'column' - ? { - left: 0, - top: selectionStartsAt, - right: 1, - bottom: rect.map.height, - } - : type == 'row' - ? { - left: selectionStartsAt, - top: 0, - right: rect.map.width, - bottom: 1, - } - : rect; - - const newType = - type == 'column' - ? isHeaderColumnEnabled - ? types.cell - : types.header_cell - : type == 'row' - ? isHeaderRowEnabled - ? types.cell - : types.header_cell - : types.cell; - - rect.map.cellsInRect(cellsRect).forEach((relativeCellPos) => { - const cellPos = relativeCellPos + rect.tableStart; - const cell = tr.doc.nodeAt(cellPos); - - if (cell) { - tr.setNodeMarkup(cellPos, newType, cell.attrs); - } - }); - - dispatch(tr); - } - return true; - }; -} - -/** - * Toggles whether the selected row contains header cells. - * - * @public - */ -export const toggleHeaderRow: Command = toggleHeader('row', { - useDeprecatedLogic: true, -}); - -/** - * Toggles whether the selected column contains header cells. - * - * @public - */ -export const toggleHeaderColumn: Command = toggleHeader('column', { - useDeprecatedLogic: true, -}); - -/** - * Toggles whether the selected cells are header cells. - * - * @public - */ -export const toggleHeaderCell: Command = toggleHeader('cell', { - useDeprecatedLogic: true, -}); - -function findNextCell($cell: ResolvedPos, dir: Direction): number | null { - if (dir < 0) { - const before = $cell.nodeBefore; - if (before) return $cell.pos - before.nodeSize; - for ( - let row = $cell.index(-1) - 1, rowEnd = $cell.before(); - row >= 0; - row-- - ) { - const rowNode = $cell.node(-1).child(row); - const lastChild = rowNode.lastChild; - if (lastChild) { - return rowEnd - 1 - lastChild.nodeSize; - } - rowEnd -= rowNode.nodeSize; - } - } else { - if ($cell.index() < $cell.parent.childCount - 1) { - return $cell.pos + $cell.nodeAfter!.nodeSize; - } - const table = $cell.node(-1); - for ( - let row = $cell.indexAfter(-1), rowStart = $cell.after(); - row < table.childCount; - row++ - ) { - const rowNode = table.child(row); - if (rowNode.childCount) return rowStart + 1; - rowStart += rowNode.nodeSize; - } - } - return null; -} - -/** - * Returns a command for selecting the next (direction=1) or previous - * (direction=-1) cell in a table. - * - * @public - */ -export function goToNextCell(direction: Direction): Command { - return function (state, dispatch) { - if (!isInTable(state)) return false; - const cell = findNextCell(selectionCell(state), direction); - if (cell == null) return false; - if (dispatch) { - const $cell = state.doc.resolve(cell); - dispatch( - state.tr - .setSelection(TextSelection.between($cell, moveCellForward($cell))) - .scrollIntoView(), - ); - } - return true; - }; -} - -/** - * Deletes the table around the selection, if any. - * - * @public - */ -export function deleteTable( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - const $pos = state.selection.$anchor; - for (let d = $pos.depth; d > 0; d--) { - const node = $pos.node(d); - if (node.type.spec.tableRole == 'table') { - if (dispatch) - dispatch( - state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView(), - ); - return true; - } - } - return false; -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/copypaste.ts b/wax-prosemirror-services/src/TablesService/tableSrc/copypaste.ts deleted file mode 100644 index 565bc815c..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/copypaste.ts +++ /dev/null @@ -1,381 +0,0 @@ -// Utilities used for copy/paste handling. -// -// This module handles pasting cell content into tables, or pasting -// anything into a cell selection, as replacing a block of cells with -// the content of the selection. When pasting cells into a cell, that -// involves placing the block of pasted content so that its top left -// aligns with the selection cell, optionally extending the table to -// the right or bottom to make sure it is large enough. Pasting into a -// cell selection is different, here the cells in the selection are -// clipped to the selection's rectangle, optionally repeating the -// pasted cells when they are smaller than the selection. - -import { Fragment, Node, NodeType, Schema, Slice } from 'prosemirror-model'; -import { Transform } from 'prosemirror-transform'; - -import { EditorState, Transaction } from 'prosemirror-state'; -import { CellSelection } from './cellselection'; -import { tableNodeTypes } from './schema'; -import { ColWidths, Rect, TableMap } from './tablemap'; -import { CellAttrs, removeColSpan } from './util'; - -/** - * @internal - */ -export type Area = { width: number; height: number; rows: Fragment[] }; - -// Utilities to help with copying and pasting table cells - -/** - * Get a rectangular area of cells from a slice, or null if the outer - * nodes of the slice aren't table cells or rows. - * - * @internal - */ -export function pastedCells(slice: Slice): Area | null { - if (!slice.size) return null; - let { content, openStart, openEnd } = slice; - while ( - content.childCount == 1 && - ((openStart > 0 && openEnd > 0) || - content.child(0).type.spec.tableRole == 'table') - ) { - openStart--; - openEnd--; - content = content.child(0).content; - } - const first = content.child(0); - const role = first.type.spec.tableRole; - const schema = first.type.schema, - rows = []; - if (role == 'row') { - for (let i = 0; i < content.childCount; i++) { - let cells = content.child(i).content; - const left = i ? 0 : Math.max(0, openStart - 1); - const right = i < content.childCount - 1 ? 0 : Math.max(0, openEnd - 1); - if (left || right) - cells = fitSlice( - tableNodeTypes(schema).row, - new Slice(cells, left, right), - ).content; - rows.push(cells); - } - } else if (role == 'cell' || role == 'header_cell') { - rows.push( - openStart || openEnd - ? fitSlice( - tableNodeTypes(schema).row, - new Slice(content, openStart, openEnd), - ).content - : content, - ); - } else { - return null; - } - return ensureRectangular(schema, rows); -} - -// Compute the width and height of a set of cells, and make sure each -// row has the same number of cells. -function ensureRectangular(schema: Schema, rows: Fragment[]): Area { - const widths: ColWidths = []; - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - for (let j = row.childCount - 1; j >= 0; j--) { - const { rowspan, colspan } = row.child(j).attrs; - for (let r = i; r < i + rowspan; r++) - widths[r] = (widths[r] || 0) + colspan; - } - } - let width = 0; - for (let r = 0; r < widths.length; r++) width = Math.max(width, widths[r]); - for (let r = 0; r < widths.length; r++) { - if (r >= rows.length) rows.push(Fragment.empty); - if (widths[r] < width) { - const empty = tableNodeTypes(schema).cell.createAndFill()!; - const cells = []; - for (let i = widths[r]; i < width; i++) { - cells.push(empty); - } - rows[r] = rows[r].append(Fragment.from(cells)); - } - } - return { height: rows.length, width, rows }; -} - -export function fitSlice(nodeType: NodeType, slice: Slice): Node { - const node = nodeType.createAndFill()!; - const tr = new Transform(node).replace(0, node.content.size, slice); - return tr.doc; -} - -/** - * Clip or extend (repeat) the given set of cells to cover the given - * width and height. Will clip rowspan/colspan cells at the edges when - * they stick out. - * - * @internal - */ -export function clipCells( - { width, height, rows }: Area, - newWidth: number, - newHeight: number, -): Area { - if (width != newWidth) { - const added: number[] = []; - const newRows: Fragment[] = []; - for (let row = 0; row < rows.length; row++) { - const frag = rows[row], - cells = []; - for (let col = added[row] || 0, i = 0; col < newWidth; i++) { - let cell = frag.child(i % frag.childCount); - if (col + cell.attrs.colspan > newWidth) - cell = cell.type.createChecked( - removeColSpan( - cell.attrs as CellAttrs, - cell.attrs.colspan, - col + cell.attrs.colspan - newWidth, - ), - cell.content, - ); - cells.push(cell); - col += cell.attrs.colspan; - for (let j = 1; j < cell.attrs.rowspan; j++) - added[row + j] = (added[row + j] || 0) + cell.attrs.colspan; - } - newRows.push(Fragment.from(cells)); - } - rows = newRows; - width = newWidth; - } - - if (height != newHeight) { - const newRows = []; - for (let row = 0, i = 0; row < newHeight; row++, i++) { - const cells = [], - source = rows[i % height]; - for (let j = 0; j < source.childCount; j++) { - let cell = source.child(j); - if (row + cell.attrs.rowspan > newHeight) - cell = cell.type.create( - { - ...cell.attrs, - rowspan: Math.max(1, newHeight - cell.attrs.rowspan), - }, - cell.content, - ); - cells.push(cell); - } - newRows.push(Fragment.from(cells)); - } - rows = newRows; - height = newHeight; - } - - return { width, height, rows }; -} - -// Make sure a table has at least the given width and height. Return -// true if something was changed. -function growTable( - tr: Transaction, - map: TableMap, - table: Node, - start: number, - width: number, - height: number, - mapFrom: number, -): boolean { - const schema = tr.doc.type.schema; - const types = tableNodeTypes(schema); - let empty; - let emptyHead; - if (width > map.width) { - for (let row = 0, rowEnd = 0; row < map.height; row++) { - const rowNode = table.child(row); - rowEnd += rowNode.nodeSize; - const cells: Node[] = []; - let add: Node; - if (rowNode.lastChild == null || rowNode.lastChild.type == types.cell) - add = empty || (empty = types.cell.createAndFill()!); - else add = emptyHead || (emptyHead = types.header_cell.createAndFill()!); - for (let i = map.width; i < width; i++) cells.push(add); - tr.insert(tr.mapping.slice(mapFrom).map(rowEnd - 1 + start), cells); - } - } - if (height > map.height) { - const cells = []; - for ( - let i = 0, start = (map.height - 1) * map.width; - i < Math.max(map.width, width); - i++ - ) { - const header = - i >= map.width - ? false - : table.nodeAt(map.map[start + i])!.type == types.header_cell; - cells.push( - header - ? emptyHead || (emptyHead = types.header_cell.createAndFill()!) - : empty || (empty = types.cell.createAndFill()!), - ); - } - - const emptyRow = types.row.create(null, Fragment.from(cells)), - rows = []; - for (let i = map.height; i < height; i++) rows.push(emptyRow); - tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 2), rows); - } - return !!(empty || emptyHead); -} - -// Make sure the given line (left, top) to (right, top) doesn't cross -// any rowspan cells by splitting cells that cross it. Return true if -// something changed. -function isolateHorizontal( - tr: Transaction, - map: TableMap, - table: Node, - start: number, - left: number, - right: number, - top: number, - mapFrom: number, -): boolean { - if (top == 0 || top == map.height) return false; - let found = false; - for (let col = left; col < right; col++) { - const index = top * map.width + col, - pos = map.map[index]; - if (map.map[index - map.width] == pos) { - found = true; - const cell = table.nodeAt(pos)!; - const { top: cellTop, left: cellLeft } = map.findCell(pos); - tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + start), null, { - ...cell.attrs, - rowspan: top - cellTop, - }); - tr.insert( - tr.mapping.slice(mapFrom).map(map.positionAt(top, cellLeft, table)), - cell.type.createAndFill({ - ...cell.attrs, - rowspan: cellTop + cell.attrs.rowspan - top, - })!, - ); - col += cell.attrs.colspan - 1; - } - } - return found; -} - -// Make sure the given line (left, top) to (left, bottom) doesn't -// cross any colspan cells by splitting cells that cross it. Return -// true if something changed. -function isolateVertical( - tr: Transaction, - map: TableMap, - table: Node, - start: number, - top: number, - bottom: number, - left: number, - mapFrom: number, -): boolean { - if (left == 0 || left == map.width) return false; - let found = false; - for (let row = top; row < bottom; row++) { - const index = row * map.width + left, - pos = map.map[index]; - if (map.map[index - 1] == pos) { - found = true; - const cell = table.nodeAt(pos)!; - const cellLeft = map.colCount(pos); - const updatePos = tr.mapping.slice(mapFrom).map(pos + start); - tr.setNodeMarkup( - updatePos, - null, - removeColSpan( - cell.attrs as CellAttrs, - left - cellLeft, - cell.attrs.colspan - (left - cellLeft), - ), - ); - tr.insert( - updatePos + cell.nodeSize, - cell.type.createAndFill( - removeColSpan(cell.attrs as CellAttrs, 0, left - cellLeft), - )!, - ); - row += cell.attrs.rowspan - 1; - } - } - return found; -} - -/** - * Insert the given set of cells (as returned by `pastedCells`) into a - * table, at the position pointed at by rect. - * - * @internal - */ -export function insertCells( - state: EditorState, - dispatch: (tr: Transaction) => void, - tableStart: number, - rect: Rect, - cells: Area, -): void { - let table = tableStart ? state.doc.nodeAt(tableStart - 1) : state.doc; - if (!table) { - throw new Error('No table found'); - } - let map = TableMap.get(table); - const { top, left } = rect; - const right = left + cells.width, - bottom = top + cells.height; - const tr = state.tr; - let mapFrom = 0; - - function recomp(): void { - table = tableStart ? tr.doc.nodeAt(tableStart - 1) : tr.doc; - if (!table) { - throw new Error('No table found'); - } - map = TableMap.get(table); - mapFrom = tr.mapping.maps.length; - } - - // Prepare the table to be large enough and not have any cells - // crossing the boundaries of the rectangle that we want to - // insert into. If anything about it changes, recompute the table - // map so that subsequent operations can see the current shape. - if (growTable(tr, map, table, tableStart, right, bottom, mapFrom)) recomp(); - if (isolateHorizontal(tr, map, table, tableStart, left, right, top, mapFrom)) - recomp(); - if ( - isolateHorizontal(tr, map, table, tableStart, left, right, bottom, mapFrom) - ) - recomp(); - if (isolateVertical(tr, map, table, tableStart, top, bottom, left, mapFrom)) - recomp(); - if (isolateVertical(tr, map, table, tableStart, top, bottom, right, mapFrom)) - recomp(); - - for (let row = top; row < bottom; row++) { - const from = map.positionAt(row, left, table), - to = map.positionAt(row, right, table); - tr.replace( - tr.mapping.slice(mapFrom).map(from + tableStart), - tr.mapping.slice(mapFrom).map(to + tableStart), - new Slice(cells.rows[row - top], 0, 0), - ); - } - recomp(); - tr.setSelection( - new CellSelection( - tr.doc.resolve(tableStart + map.positionAt(top, left, table)), - tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table)), - ), - ); - dispatch(tr); -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/fixtables.ts b/wax-prosemirror-services/src/TablesService/tableSrc/fixtables.ts deleted file mode 100644 index e35bc6520..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/fixtables.ts +++ /dev/null @@ -1,150 +0,0 @@ -// This file defines helpers for normalizing tables, making sure no -// cells overlap (which can happen, if you have the wrong col- and -// rowspans) and that each row has the same width. Uses the problems -// reported by `TableMap`. - -import { Node } from 'prosemirror-model'; -import { EditorState, PluginKey, Transaction } from 'prosemirror-state'; -import { tableNodeTypes, TableRole } from './schema'; -import { TableMap } from './tablemap'; -import { CellAttrs, removeColSpan } from './util'; - -/** - * @public - */ -export const fixTablesKey = new PluginKey<{ fixTables: boolean }>('fix-tables'); - -/** - * Helper for iterating through the nodes in a document that changed - * compared to the given previous document. Useful for avoiding - * duplicate work on each transaction. - * - * @public - */ -function changedDescendants( - old: Node, - cur: Node, - offset: number, - f: (node: Node, pos: number) => void, -): void { - const oldSize = old.childCount, - curSize = cur.childCount; - outer: for (let i = 0, j = 0; i < curSize; i++) { - const child = cur.child(i); - for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) { - if (old.child(scan) == child) { - j = scan + 1; - offset += child.nodeSize; - continue outer; - } - } - f(child, offset); - if (j < oldSize && old.child(j).sameMarkup(child)) - changedDescendants(old.child(j), child, offset + 1, f); - else child.nodesBetween(0, child.content.size, f, offset + 1); - offset += child.nodeSize; - } -} - -/** - * Inspect all tables in the given state's document and return a - * transaction that fixes them, if necessary. If `oldState` was - * provided, that is assumed to hold a previous, known-good state, - * which will be used to avoid re-scanning unchanged parts of the - * document. - * - * @public - */ -export function fixTables( - state: EditorState, - oldState?: EditorState, -): Transaction | undefined { - let tr: Transaction | undefined; - const check = (node: Node, pos: number) => { - if (node.type.spec.tableRole == 'table') - tr = fixTable(state, node, pos, tr); - }; - if (!oldState) state.doc.descendants(check); - else if (oldState.doc != state.doc) - changedDescendants(oldState.doc, state.doc, 0, check); - return tr; -} - -// Fix the given table, if necessary. Will append to the transaction -// it was given, if non-null, or create a new one if necessary. -export function fixTable( - state: EditorState, - table: Node, - tablePos: number, - tr: Transaction | undefined, -): Transaction | undefined { - const map = TableMap.get(table); - if (!map.problems) return tr; - if (!tr) tr = state.tr; - - // Track which rows we must add cells to, so that we can adjust that - // when fixing collisions. - const mustAdd: number[] = []; - for (let i = 0; i < map.height; i++) mustAdd.push(0); - for (let i = 0; i < map.problems.length; i++) { - const prob = map.problems[i]; - if (prob.type == 'collision') { - const cell = table.nodeAt(prob.pos); - if (!cell) continue; - const attrs = cell.attrs as CellAttrs; - for (let j = 0; j < attrs.rowspan; j++) mustAdd[prob.row + j] += prob.n; - tr.setNodeMarkup( - tr.mapping.map(tablePos + 1 + prob.pos), - null, - removeColSpan(attrs, attrs.colspan - prob.n, prob.n), - ); - } else if (prob.type == 'missing') { - mustAdd[prob.row] += prob.n; - } else if (prob.type == 'overlong_rowspan') { - const cell = table.nodeAt(prob.pos); - if (!cell) continue; - tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, { - ...cell.attrs, - rowspan: cell.attrs.rowspan - prob.n, - }); - } else if (prob.type == 'colwidth mismatch') { - const cell = table.nodeAt(prob.pos); - if (!cell) continue; - tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, { - ...cell.attrs, - colwidth: prob.colwidth, - }); - } - } - let first, last; - for (let i = 0; i < mustAdd.length; i++) - if (mustAdd[i]) { - if (first == null) first = i; - last = i; - } - // Add the necessary cells, using a heuristic for whether to add the - // cells at the start or end of the rows (if it looks like a 'bite' - // was taken out of the table, add cells at the start of the row - // after the bite. Otherwise add them at the end). - for (let i = 0, pos = tablePos + 1; i < map.height; i++) { - const row = table.child(i); - const end = pos + row.nodeSize; - const add = mustAdd[i]; - if (add > 0) { - let role: TableRole = 'cell'; - if (row.firstChild) { - role = row.firstChild.type.spec.tableRole; - } - const nodes: Node[] = []; - for (let j = 0; j < add; j++) { - const node = tableNodeTypes(state.schema)[role].createAndFill(); - - if (node) nodes.push(node); - } - const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1; - tr.insert(tr.mapping.map(side), nodes); - } - pos = end; - } - return tr.setMeta(fixTablesKey, { fixTables: true }); -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/index.ts b/wax-prosemirror-services/src/TablesService/tableSrc/index.ts deleted file mode 100644 index db21e4cb2..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -// This file defines a plugin that handles the drawing of cell -// selections and the basic user interactions for creating and working -// with such selections. It also makes sure that, after each -// transaction, the shapes of tables are normalized to be rectangular -// and not contain overlapping cells. - -import { Plugin } from 'prosemirror-state'; - -import { drawCellSelection, normalizeSelection } from './cellselection'; -import { fixTables, fixTablesKey } from './fixtables'; -import { - handleKeyDown, - handleMouseDown, - handlePaste, - handleTripleClick, -} from './input'; -import { tableEditingKey } from './util'; - -export { CellBookmark, CellSelection } from './cellselection'; -export type { CellSelectionJSON } from './cellselection'; -export { - columnResizing, - columnResizingPluginKey, - ResizeState, -} from './columnresizing'; -export type { ColumnResizingOptions, Dragging } from './columnresizing'; -export * from './commands'; -export { - clipCells as __clipCells, - insertCells as __insertCells, - pastedCells as __pastedCells, -} from './copypaste'; -export type { Area as __Area } from './copypaste'; -export type { Direction } from './input'; -export { tableNodes, tableNodeTypes } from './schema'; -export type { - CellAttributes, - getFromDOM, - setDOMAttr, - TableNodes, - TableNodesOptions, - TableRole, -} from './schema'; -export { TableMap } from './tablemap'; -export type { ColWidths, Problem, Rect } from './tablemap'; -export { TableView, updateColumnsOnResize } from './tableview'; -export { - addColSpan, - cellAround, - colCount, - columnIsHeader, - findCell, - inSameTable, - isInTable, - moveCellForward, - nextCell, - pointsAtCell, - removeColSpan, - selectionCell, -} from './util'; -export type { MutableAttrs } from './util'; -export { fixTables, handlePaste, fixTablesKey }; -export { tableEditingKey }; - -/** - * @public - */ -export type TableEditingOptions = { - allowTableNodeSelection?: boolean; -}; - -/** - * Creates a [plugin](http://prosemirror.net/docs/ref/#state.Plugin) - * that, when added to an editor, enables cell-selection, handles - * cell-based copy/paste, and makes sure tables stay well-formed (each - * row has the same width, and cells don't overlap). - * - * You should probably put this plugin near the end of your array of - * plugins, since it handles mouse and arrow key events in tables - * rather broadly, and other plugins, like the gap cursor or the - * column-width dragging plugin, might want to get a turn first to - * perform more specific behavior. - * - * @public - */ -export function tableEditing({ - allowTableNodeSelection = false, -}: TableEditingOptions = {}): Plugin { - return new Plugin({ - key: tableEditingKey, - - // This piece of state is used to remember when a mouse-drag - // cell-selection is happening, so that it can continue even as - // transactions (which might move its anchor cell) come in. - state: { - init() { - return null; - }, - apply(tr, cur) { - const set = tr.getMeta(tableEditingKey); - if (set != null) return set == -1 ? null : set; - if (cur == null || !tr.docChanged) return cur; - const { deleted, pos } = tr.mapping.mapResult(cur); - return deleted ? null : pos; - }, - }, - - props: { - decorations: drawCellSelection, - - handleDOMEvents: { - mousedown: handleMouseDown, - }, - - createSelectionBetween(view) { - return tableEditingKey.getState(view.state) != null - ? view.state.selection - : null; - }, - - handleTripleClick, - - handleKeyDown, - - handlePaste, - }, - - appendTransaction(_, oldState, state) { - return normalizeSelection( - state, - fixTables(state, oldState), - allowTableNodeSelection, - ); - }, - }); -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/input.ts b/wax-prosemirror-services/src/TablesService/tableSrc/input.ts deleted file mode 100644 index 96e7b63f3..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/input.ts +++ /dev/null @@ -1,310 +0,0 @@ -// This file defines a number of helpers for wiring up user input to -// table-related functionality. - -import { Fragment, ResolvedPos, Slice } from 'prosemirror-model'; -import { - Command, - EditorState, - Selection, - TextSelection, - Transaction, -} from 'prosemirror-state'; -import { keydownHandler } from 'prosemirror-keymap'; - -import { - cellAround, - inSameTable, - isInTable, - tableEditingKey, - nextCell, - selectionCell, -} from './util'; -import { CellSelection } from './cellselection'; -import { TableMap } from './tablemap'; -import { clipCells, fitSlice, insertCells, pastedCells } from './copypaste'; -import { tableNodeTypes } from './schema'; -import { EditorView } from 'prosemirror-view'; - -type Axis = 'horiz' | 'vert'; - -/** - * @public - */ -export type Direction = -1 | 1; - -export const handleKeyDown = keydownHandler({ - ArrowLeft: arrow('horiz', -1), - ArrowRight: arrow('horiz', 1), - ArrowUp: arrow('vert', -1), - ArrowDown: arrow('vert', 1), - - 'Shift-ArrowLeft': shiftArrow('horiz', -1), - 'Shift-ArrowRight': shiftArrow('horiz', 1), - 'Shift-ArrowUp': shiftArrow('vert', -1), - 'Shift-ArrowDown': shiftArrow('vert', 1), - - Backspace: deleteCellSelection, - 'Mod-Backspace': deleteCellSelection, - Delete: deleteCellSelection, - 'Mod-Delete': deleteCellSelection, -}); - -function maybeSetSelection( - state: EditorState, - dispatch: undefined | ((tr: Transaction) => void), - selection: Selection, -): boolean { - if (selection.eq(state.selection)) return false; - if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView()); - return true; -} - -function arrow(axis: Axis, dir: Direction): Command { - return (state, dispatch, view) => { - if (!view) return false; - const sel = state.selection; - if (sel instanceof CellSelection) { - return maybeSetSelection( - state, - dispatch, - Selection.near(sel.$headCell, dir), - ); - } - if (axis != 'horiz' && !sel.empty) return false; - const end = atEndOfCell(view, axis, dir); - if (end == null) return false; - if (axis == 'horiz') { - return maybeSetSelection( - state, - dispatch, - Selection.near(state.doc.resolve(sel.head + dir), dir), - ); - } else { - const $cell = state.doc.resolve(end); - const $next = nextCell($cell, axis, dir); - let newSel; - if ($next) newSel = Selection.near($next, 1); - else if (dir < 0) - newSel = Selection.near(state.doc.resolve($cell.before(-1)), -1); - else newSel = Selection.near(state.doc.resolve($cell.after(-1)), 1); - return maybeSetSelection(state, dispatch, newSel); - } - }; -} - -function shiftArrow(axis: Axis, dir: Direction): Command { - return (state, dispatch, view) => { - if (!view) return false; - const sel = state.selection; - let cellSel: CellSelection; - if (sel instanceof CellSelection) { - cellSel = sel; - } else { - const end = atEndOfCell(view, axis, dir); - if (end == null) return false; - cellSel = new CellSelection(state.doc.resolve(end)); - } - - const $head = nextCell(cellSel.$headCell, axis, dir); - if (!$head) return false; - return maybeSetSelection( - state, - dispatch, - new CellSelection(cellSel.$anchorCell, $head), - ); - }; -} - -function deleteCellSelection( - state: EditorState, - dispatch?: (tr: Transaction) => void, -): boolean { - const sel = state.selection; - if (!(sel instanceof CellSelection)) return false; - if (dispatch) { - const tr = state.tr; - const baseContent = tableNodeTypes(state.schema).cell.createAndFill()! - .content; - sel.forEachCell((cell, pos) => { - if (!cell.content.eq(baseContent)) - tr.replace( - tr.mapping.map(pos + 1), - tr.mapping.map(pos + cell.nodeSize - 1), - new Slice(baseContent, 0, 0), - ); - }); - if (tr.docChanged) dispatch(tr); - } - return true; -} - -export function handleTripleClick(view: EditorView, pos: number): boolean { - const doc = view.state.doc, - $cell = cellAround(doc.resolve(pos)); - if (!$cell) return false; - view.dispatch(view.state.tr.setSelection(new CellSelection($cell))); - return true; -} - -/** - * @public - */ -export function handlePaste( - view: EditorView, - _: ClipboardEvent, - slice: Slice, -): boolean { - if (!isInTable(view.state)) return false; - let cells = pastedCells(slice); - const sel = view.state.selection; - if (sel instanceof CellSelection) { - if (!cells) - cells = { - width: 1, - height: 1, - rows: [ - Fragment.from( - fitSlice(tableNodeTypes(view.state.schema).cell, slice), - ), - ], - }; - const table = sel.$anchorCell.node(-1); - const start = sel.$anchorCell.start(-1); - const rect = TableMap.get(table).rectBetween( - sel.$anchorCell.pos - start, - sel.$headCell.pos - start, - ); - cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top); - insertCells(view.state, view.dispatch, start, rect, cells); - return true; - } else if (cells) { - const $cell = selectionCell(view.state); - const start = $cell.start(-1); - insertCells( - view.state, - view.dispatch, - start, - TableMap.get($cell.node(-1)).findCell($cell.pos - start), - cells, - ); - return true; - } else { - return false; - } -} - -export function handleMouseDown( - view: EditorView, - startEvent: MouseEvent, -): void { - if (startEvent.ctrlKey || startEvent.metaKey) return; - - const startDOMCell = domInCell(view, startEvent.target as Node); - let $anchor; - if (startEvent.shiftKey && view.state.selection instanceof CellSelection) { - // Adding to an existing cell selection - setCellSelection(view.state.selection.$anchorCell, startEvent); - startEvent.preventDefault(); - } else if ( - startEvent.shiftKey && - startDOMCell && - ($anchor = cellAround(view.state.selection.$anchor)) != null && - cellUnderMouse(view, startEvent)?.pos != $anchor.pos - ) { - // Adding to a selection that starts in another cell (causing a - // cell selection to be created). - setCellSelection($anchor, startEvent); - startEvent.preventDefault(); - } else if (!startDOMCell) { - // Not in a cell, let the default behavior happen. - return; - } - - // Create and dispatch a cell selection between the given anchor and - // the position under the mouse. - function setCellSelection($anchor: ResolvedPos, event: MouseEvent): void { - let $head = cellUnderMouse(view, event); - const starting = tableEditingKey.getState(view.state) == null; - if (!$head || !inSameTable($anchor, $head)) { - if (starting) $head = $anchor; - else return; - } - const selection = new CellSelection($anchor, $head); - if (starting || !view.state.selection.eq(selection)) { - const tr = view.state.tr.setSelection(selection); - if (starting) tr.setMeta(tableEditingKey, $anchor.pos); - view.dispatch(tr); - } - } - - // Stop listening to mouse motion events. - function stop(): void { - view.root.removeEventListener('mouseup', stop); - view.root.removeEventListener('dragstart', stop); - view.root.removeEventListener('mousemove', move); - if (tableEditingKey.getState(view.state) != null) - view.dispatch(view.state.tr.setMeta(tableEditingKey, -1)); - } - - function move(_event: Event): void { - const event = _event as MouseEvent; - const anchor = tableEditingKey.getState(view.state); - let $anchor; - if (anchor != null) { - // Continuing an existing cross-cell selection - $anchor = view.state.doc.resolve(anchor); - } else if (domInCell(view, event.target as Node) != startDOMCell) { - // Moving out of the initial cell -- start a new cell selection - $anchor = cellUnderMouse(view, startEvent); - if (!$anchor) return stop(); - } - if ($anchor) setCellSelection($anchor, event); - } - - view.root.addEventListener('mouseup', stop); - view.root.addEventListener('dragstart', stop); - view.root.addEventListener('mousemove', move); -} - -// Check whether the cursor is at the end of a cell (so that further -// motion would move out of the cell) -function atEndOfCell(view: EditorView, axis: Axis, dir: number): null | number { - if (!(view.state.selection instanceof TextSelection)) return null; - const { $head } = view.state.selection; - for (let d = $head.depth - 1; d >= 0; d--) { - const parent = $head.node(d), - index = dir < 0 ? $head.index(d) : $head.indexAfter(d); - if (index != (dir < 0 ? 0 : parent.childCount)) return null; - if ( - parent.type.spec.tableRole == 'cell' || - parent.type.spec.tableRole == 'header_cell' - ) { - const cellPos = $head.before(d); - const dirStr: 'up' | 'down' | 'left' | 'right' = - axis == 'vert' ? (dir > 0 ? 'down' : 'up') : dir > 0 ? 'right' : 'left'; - return view.endOfTextblock(dirStr) ? cellPos : null; - } - } - return null; -} - -function domInCell(view: EditorView, dom: Node | null): Node | null { - for (; dom && dom != view.dom; dom = dom.parentNode) { - if (dom.nodeName == 'TD' || dom.nodeName == 'TH') { - return dom; - } - } - return null; -} - -function cellUnderMouse( - view: EditorView, - event: MouseEvent, -): ResolvedPos | null { - const mousePos = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - if (!mousePos) return null; - return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null; -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/schema.ts b/wax-prosemirror-services/src/TablesService/tableSrc/schema.ts deleted file mode 100644 index b9012f818..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/schema.ts +++ /dev/null @@ -1,197 +0,0 @@ -// Helper for creating a schema that supports tables. - -import { - AttributeSpec, - Attrs, - Node, - NodeSpec, - NodeType, - Schema, -} from 'prosemirror-model'; -import { CellAttrs, MutableAttrs } from './util'; - -function getCellAttrs(dom: HTMLElement | string, extraAttrs: Attrs): Attrs { - if (typeof dom === 'string') { - return {}; - } - - const widthAttr = dom.getAttribute('data-colwidth'); - const widths = - widthAttr && /^\d+(,\d+)*$/.test(widthAttr) - ? widthAttr.split(',').map((s) => Number(s)) - : null; - const colspan = Number(dom.getAttribute('colspan') || 1); - const result: MutableAttrs = { - colspan, - rowspan: Number(dom.getAttribute('rowspan') || 1), - colwidth: widths && widths.length == colspan ? widths : null, - } satisfies CellAttrs; - for (const prop in extraAttrs) { - const getter = extraAttrs[prop].getFromDOM; - const value = getter && getter(dom); - if (value != null) { - result[prop] = value; - } - } - return result; -} - -function setCellAttrs(node: Node, extraAttrs: Attrs): Attrs { - const attrs: MutableAttrs = {}; - if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan; - if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan; - if (node.attrs.colwidth) - attrs['data-colwidth'] = node.attrs.colwidth.join(','); - for (const prop in extraAttrs) { - const setter = extraAttrs[prop].setDOMAttr; - if (setter) setter(node.attrs[prop], attrs); - } - return attrs; -} - -/** - * @public - */ -export type getFromDOM = (dom: HTMLElement) => unknown; - -/** - * @public - */ -export type setDOMAttr = (value: unknown, attrs: MutableAttrs) => void; - -/** - * @public - */ -export interface CellAttributes { - /** - * The attribute's default value. - */ - default: unknown; - - /** - * A function to read the attribute's value from a DOM node. - */ - getFromDOM?: getFromDOM; - - /** - * A function to add the attribute's value to an attribute - * object that's used to render the cell's DOM. - */ - setDOMAttr?: setDOMAttr; -} - -/** - * @public - */ -export interface TableNodesOptions { - /** - * A group name (something like `"block"`) to add to the table - * node type. - */ - tableGroup?: string; - - /** - * The content expression for table cells. - */ - cellContent: string; - - /** - * Additional attributes to add to cells. Maps attribute names to - * objects with the following properties: - */ - cellAttributes: { [key: string]: CellAttributes }; -} - -/** - * @public - */ -export type TableNodes = Record< - 'table' | 'table_row' | 'table_cell' | 'table_header', - NodeSpec ->; - -/** - * This function creates a set of [node - * specs](http://prosemirror.net/docs/ref/#model.SchemaSpec.nodes) for - * `table`, `table_row`, and `table_cell` nodes types as used by this - * module. The result can then be added to the set of nodes when - * creating a schema. - * - * @public - */ -export function tableNodes(options: TableNodesOptions): TableNodes { - const extraAttrs = options.cellAttributes || {}; - const cellAttrs: Record<string, AttributeSpec> = { - colspan: { default: 1 }, - rowspan: { default: 1 }, - colwidth: { default: null }, - }; - for (const prop in extraAttrs) - cellAttrs[prop] = { default: extraAttrs[prop].default }; - - return { - table: { - content: 'table_row+', - tableRole: 'table', - isolating: true, - group: options.tableGroup, - parseDOM: [{ tag: 'table' }], - toDOM() { - return ['table', ['tbody', 0]]; - }, - }, - table_row: { - content: '(table_cell | table_header)*', - tableRole: 'row', - parseDOM: [{ tag: 'tr' }], - toDOM() { - return ['tr', 0]; - }, - }, - table_cell: { - content: options.cellContent, - attrs: cellAttrs, - tableRole: 'cell', - isolating: true, - parseDOM: [ - { tag: 'td', getAttrs: (dom) => getCellAttrs(dom, extraAttrs) }, - ], - toDOM(node) { - return ['td', setCellAttrs(node, extraAttrs), 0]; - }, - }, - table_header: { - content: options.cellContent, - attrs: cellAttrs, - tableRole: 'header_cell', - isolating: true, - parseDOM: [ - { tag: 'th', getAttrs: (dom) => getCellAttrs(dom, extraAttrs) }, - ], - toDOM(node) { - return ['th', setCellAttrs(node, extraAttrs), 0]; - }, - }, - }; -} - -/** - * @public - */ -export type TableRole = 'table' | 'row' | 'cell' | 'header_cell'; - -/** - * @public - */ -export function tableNodeTypes(schema: Schema): Record<TableRole, NodeType> { - let result = schema.cached.tableNodeTypes; - if (!result) { - result = schema.cached.tableNodeTypes = {}; - for (const name in schema.nodes) { - const type = schema.nodes[name], - role = type.spec.tableRole; - if (role) result[role] = type; - } - } - return result; -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/tablemap.ts b/wax-prosemirror-services/src/TablesService/tableSrc/tablemap.ts deleted file mode 100644 index bf633e25b..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/tablemap.ts +++ /dev/null @@ -1,377 +0,0 @@ -// Because working with row and column-spanning cells is not quite -// trivial, this code builds up a descriptive structure for a given -// table node. The structures are cached with the (persistent) table -// nodes as key, so that they only have to be recomputed when the -// content of the table changes. -// -// This does mean that they have to store table-relative, not -// document-relative positions. So code that uses them will typically -// compute the start position of the table and offset positions passed -// to or gotten from this structure by that amount. -import { Attrs, Node } from 'prosemirror-model'; -import { CellAttrs } from './util'; - -/** - * @public - */ -export type ColWidths = number[]; - -/** - * @public - */ -export type Problem = - | { - type: 'colwidth mismatch'; - pos: number; - colwidth: ColWidths; - } - | { - type: 'collision'; - pos: number; - row: number; - n: number; - } - | { - type: 'missing'; - row: number; - n: number; - } - | { - type: 'overlong_rowspan'; - pos: number; - n: number; - }; - -let readFromCache: (key: Node) => TableMap | undefined; -let addToCache: (key: Node, value: TableMap) => TableMap; - -// Prefer using a weak map to cache table maps. Fall back on a -// fixed-size cache if that's not supported. -if (typeof WeakMap != 'undefined') { - // eslint-disable-next-line - let cache = new WeakMap<Node, TableMap>(); - readFromCache = (key) => cache.get(key); - addToCache = (key, value) => { - cache.set(key, value); - return value; - }; -} else { - const cache: (Node | TableMap)[] = []; - const cacheSize = 10; - let cachePos = 0; - readFromCache = (key) => { - for (let i = 0; i < cache.length; i += 2) - if (cache[i] == key) return cache[i + 1] as TableMap; - }; - addToCache = (key, value) => { - if (cachePos == cacheSize) cachePos = 0; - cache[cachePos++] = key; - return (cache[cachePos++] = value); - }; -} - -/** - * @public - */ -export interface Rect { - left: number; - top: number; - right: number; - bottom: number; -} - -/** - * A table map describes the structure of a given table. To avoid - * recomputing them all the time, they are cached per table node. To - * be able to do that, positions saved in the map are relative to the - * start of the table, rather than the start of the document. - * - * @public - */ -export class TableMap { - constructor( - /** - * The number of columns - */ - public width: number, - /** - * The number of rows - */ - public height: number, - /** - * A width * height array with the start position of - * the cell covering that part of the table in each slot - */ - public map: number[], - /** - * An optional array of problems (cell overlap or non-rectangular - * shape) for the table, used by the table normalizer. - */ - public problems: Problem[] | null, - ) {} - - // Find the dimensions of the cell at the given position. - findCell(pos: number): Rect { - for (let i = 0; i < this.map.length; i++) { - const curPos = this.map[i]; - if (curPos != pos) continue; - - const left = i % this.width; - const top = (i / this.width) | 0; - let right = left + 1; - let bottom = top + 1; - - for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) { - right++; - } - for ( - let j = 1; - bottom < this.height && this.map[i + this.width * j] == curPos; - j++ - ) { - bottom++; - } - - return { left, top, right, bottom }; - } - throw new RangeError(`No cell with offset ${pos} found`); - } - - // Find the left side of the cell at the given position. - colCount(pos: number): number { - for (let i = 0; i < this.map.length; i++) { - if (this.map[i] == pos) { - return i % this.width; - } - } - throw new RangeError(`No cell with offset ${pos} found`); - } - - // Find the next cell in the given direction, starting from the cell - // at `pos`, if any. - nextCell(pos: number, axis: 'horiz' | 'vert', dir: number): null | number { - const { left, right, top, bottom } = this.findCell(pos); - if (axis == 'horiz') { - if (dir < 0 ? left == 0 : right == this.width) return null; - return this.map[top * this.width + (dir < 0 ? left - 1 : right)]; - } else { - if (dir < 0 ? top == 0 : bottom == this.height) return null; - return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)]; - } - } - - // Get the rectangle spanning the two given cells. - rectBetween(a: number, b: number): Rect { - const { - left: leftA, - right: rightA, - top: topA, - bottom: bottomA, - } = this.findCell(a); - const { - left: leftB, - right: rightB, - top: topB, - bottom: bottomB, - } = this.findCell(b); - return { - left: Math.min(leftA, leftB), - top: Math.min(topA, topB), - right: Math.max(rightA, rightB), - bottom: Math.max(bottomA, bottomB), - }; - } - - // Return the position of all cells that have the top left corner in - // the given rectangle. - cellsInRect(rect: Rect): number[] { - const result: number[] = []; - const seen: Record<number, boolean> = {}; - for (let row = rect.top; row < rect.bottom; row++) { - for (let col = rect.left; col < rect.right; col++) { - const index = row * this.width + col; - const pos = this.map[index]; - - if (seen[pos]) continue; - seen[pos] = true; - - if ( - (col == rect.left && col && this.map[index - 1] == pos) || - (row == rect.top && row && this.map[index - this.width] == pos) - ) { - continue; - } - result.push(pos); - } - } - return result; - } - - // Return the position at which the cell at the given row and column - // starts, or would start, if a cell started there. - positionAt(row: number, col: number, table: Node): number { - for (let i = 0, rowStart = 0; ; i++) { - const rowEnd = rowStart + table.child(i).nodeSize; - if (i == row) { - let index = col + row * this.width; - const rowEndIndex = (row + 1) * this.width; - // Skip past cells from previous rows (via rowspan) - while (index < rowEndIndex && this.map[index] < rowStart) index++; - return index == rowEndIndex ? rowEnd - 1 : this.map[index]; - } - rowStart = rowEnd; - } - } - - // Find the table map for the given table node. - static get(table: Node): TableMap { - return readFromCache(table) || addToCache(table, computeMap(table)); - } -} - -// Compute a table map. -function computeMap(table: Node): TableMap { - if (table.type.spec.tableRole != 'table') - throw new RangeError('Not a table node: ' + table.type.name); - const width = findWidth(table), - height = table.childCount; - const map = []; - let mapPos = 0; - let problems: Problem[] | null = null; - const colWidths: ColWidths = []; - for (let i = 0, e = width * height; i < e; i++) map[i] = 0; - - for (let row = 0, pos = 0; row < height; row++) { - const rowNode = table.child(row); - pos++; - for (let i = 0; ; i++) { - while (mapPos < map.length && map[mapPos] != 0) mapPos++; - if (i == rowNode.childCount) break; - const cellNode = rowNode.child(i); - const { colspan, rowspan, colwidth } = cellNode.attrs; - for (let h = 0; h < rowspan; h++) { - if (h + row >= height) { - (problems || (problems = [])).push({ - type: 'overlong_rowspan', - pos, - n: rowspan - h, - }); - break; - } - const start = mapPos + h * width; - for (let w = 0; w < colspan; w++) { - if (map[start + w] == 0) map[start + w] = pos; - else - (problems || (problems = [])).push({ - type: 'collision', - row, - pos, - n: colspan - w, - }); - const colW = colwidth && colwidth[w]; - if (colW) { - const widthIndex = ((start + w) % width) * 2, - prev = colWidths[widthIndex]; - if ( - prev == null || - (prev != colW && colWidths[widthIndex + 1] == 1) - ) { - colWidths[widthIndex] = colW; - colWidths[widthIndex + 1] = 1; - } else if (prev == colW) { - colWidths[widthIndex + 1]++; - } - } - } - } - mapPos += colspan; - pos += cellNode.nodeSize; - } - const expectedPos = (row + 1) * width; - let missing = 0; - while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++; - if (missing) - (problems || (problems = [])).push({ type: 'missing', row, n: missing }); - pos++; - } - - const tableMap = new TableMap(width, height, map, problems); - let badWidths = false; - - // For columns that have defined widths, but whose widths disagree - // between rows, fix up the cells whose width doesn't match the - // computed one. - for (let i = 0; !badWidths && i < colWidths.length; i += 2) - if (colWidths[i] != null && colWidths[i + 1] < height) badWidths = true; - if (badWidths) findBadColWidths(tableMap, colWidths, table); - - return tableMap; -} - -function findWidth(table: Node): number { - let width = -1; - let hasRowSpan = false; - for (let row = 0; row < table.childCount; row++) { - const rowNode = table.child(row); - let rowWidth = 0; - if (hasRowSpan) - for (let j = 0; j < row; j++) { - const prevRow = table.child(j); - for (let i = 0; i < prevRow.childCount; i++) { - const cell = prevRow.child(i); - if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan; - } - } - for (let i = 0; i < rowNode.childCount; i++) { - const cell = rowNode.child(i); - rowWidth += cell.attrs.colspan; - if (cell.attrs.rowspan > 1) hasRowSpan = true; - } - if (width == -1) width = rowWidth; - else if (width != rowWidth) width = Math.max(width, rowWidth); - } - return width; -} - -function findBadColWidths( - map: TableMap, - colWidths: ColWidths, - table: Node, -): void { - if (!map.problems) map.problems = []; - const seen: Record<number, boolean> = {}; - for (let i = 0; i < map.map.length; i++) { - const pos = map.map[i]; - if (seen[pos]) continue; - seen[pos] = true; - const node = table.nodeAt(pos); - if (!node) { - throw new RangeError(`No cell with offset ${pos} found`); - } - - let updated = null; - const attrs = node.attrs as CellAttrs; - for (let j = 0; j < attrs.colspan; j++) { - const col = (i + j) % map.width; - const colWidth = colWidths[col * 2]; - if ( - colWidth != null && - (!attrs.colwidth || attrs.colwidth[j] != colWidth) - ) - (updated || (updated = freshColWidth(attrs)))[j] = colWidth; - } - if (updated) - map.problems.unshift({ - type: 'colwidth mismatch', - pos, - colwidth: updated, - }); - } -} - -function freshColWidth(attrs: Attrs): ColWidths { - if (attrs.colwidth) return attrs.colwidth.slice(); - const result: ColWidths = []; - for (let i = 0; i < attrs.colspan; i++) result.push(0); - return result; -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/tableview.ts b/wax-prosemirror-services/src/TablesService/tableSrc/tableview.ts deleted file mode 100644 index c3130bc85..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/tableview.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Node } from 'prosemirror-model'; -import { NodeView } from 'prosemirror-view'; -import { CellAttrs } from './util'; - -/** - * @public - */ -export class TableView implements NodeView { - public dom: HTMLDivElement; - public table: HTMLTableElement; - public colgroup: HTMLTableColElement; - public contentDOM: HTMLTableSectionElement; - - constructor(public node: Node, public cellMinWidth: number) { - this.dom = document.createElement('div'); - this.dom.className = 'tableWrapper'; - this.table = this.dom.appendChild(document.createElement('table')); - this.colgroup = this.table.appendChild(document.createElement('colgroup')); - updateColumnsOnResize(node, this.colgroup, this.table, cellMinWidth); - this.contentDOM = this.table.appendChild(document.createElement('tbody')); - } - - update(node: Node): boolean { - if (node.type != this.node.type) return false; - this.node = node; - updateColumnsOnResize(node, this.colgroup, this.table, this.cellMinWidth); - return true; - } - - ignoreMutation(record: MutationRecord): boolean { - return ( - record.type == 'attributes' && - (record.target == this.table || this.colgroup.contains(record.target)) - ); - } -} - -/** - * @public - */ -export function updateColumnsOnResize( - node: Node, - colgroup: HTMLTableColElement, - table: HTMLTableElement, - cellMinWidth: number, - overrideCol?: number, - overrideValue?: number, -): void { - let totalWidth = 0; - let fixedWidth = true; - let nextDOM = colgroup.firstChild as HTMLElement; - const row = node.firstChild; - if (!row) return; - - for (let i = 0, col = 0; i < row.childCount; i++) { - const { colspan, colwidth } = row.child(i).attrs as CellAttrs; - for (let j = 0; j < colspan; j++, col++) { - const hasWidth = - overrideCol == col ? overrideValue : colwidth && colwidth[j]; - const cssWidth = hasWidth ? hasWidth + 'px' : ''; - totalWidth += hasWidth || cellMinWidth; - if (!hasWidth) fixedWidth = false; - if (!nextDOM) { - colgroup.appendChild(document.createElement('col')).style.width = - cssWidth; - } else { - if (nextDOM.style.width != cssWidth) nextDOM.style.width = cssWidth; - nextDOM = nextDOM.nextSibling as HTMLElement; - } - } - } - - while (nextDOM) { - const after = nextDOM.nextSibling; - nextDOM.parentNode?.removeChild(nextDOM); - nextDOM = after as HTMLElement; - } - - if (fixedWidth) { - table.style.width = totalWidth + 'px'; - table.style.minWidth = ''; - } else { - table.style.width = ''; - table.style.minWidth = totalWidth + 'px'; - } -} diff --git a/wax-prosemirror-services/src/TablesService/tableSrc/util.ts b/wax-prosemirror-services/src/TablesService/tableSrc/util.ts deleted file mode 100644 index 70fd658f9..000000000 --- a/wax-prosemirror-services/src/TablesService/tableSrc/util.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Various helper function for working with tables - -import { EditorState, NodeSelection, PluginKey } from 'prosemirror-state'; - -import { Attrs, Node, ResolvedPos } from 'prosemirror-model'; -import { CellSelection } from './cellselection'; -import { tableNodeTypes } from './schema'; -import { Rect, TableMap } from './tablemap'; - -/** - * @public - */ -export type MutableAttrs = Record<string, unknown>; - -/** - * @public - */ -export interface CellAttrs { - colspan: number; - rowspan: number; - colwidth: number[] | null; -} - -/** - * @public - */ -export const tableEditingKey = new PluginKey<number>('selectingCells'); - -/** - * @public - */ -export function cellAround($pos: ResolvedPos): ResolvedPos | null { - for (let d = $pos.depth - 1; d > 0; d--) - if ($pos.node(d).type.spec.tableRole == 'row') - return $pos.node(0).resolve($pos.before(d + 1)); - return null; -} - -export function cellWrapping($pos: ResolvedPos): null | Node { - for (let d = $pos.depth; d > 0; d--) { - // Sometimes the cell can be in the same depth. - const role = $pos.node(d).type.spec.tableRole; - if (role === 'cell' || role === 'header_cell') return $pos.node(d); - } - return null; -} - -/** - * @public - */ -export function isInTable(state: EditorState): boolean { - const $head = state.selection.$head; - for (let d = $head.depth; d > 0; d--) - if ($head.node(d).type.spec.tableRole == 'row') return true; - return false; -} - -/** - * @internal - */ -export function selectionCell(state: EditorState): ResolvedPos { - const sel = state.selection as CellSelection | NodeSelection; - if ('$anchorCell' in sel && sel.$anchorCell) { - return sel.$anchorCell.pos > sel.$headCell.pos - ? sel.$anchorCell - : sel.$headCell; - } else if ( - 'node' in sel && - sel.node && - sel.node.type.spec.tableRole == 'cell' - ) { - return sel.$anchor; - } - const $cell = cellAround(sel.$head) || cellNear(sel.$head); - if ($cell) { - return $cell; - } - throw new RangeError(`No cell found around position ${sel.head}`); -} - -function cellNear($pos: ResolvedPos): ResolvedPos | undefined { - for ( - let after = $pos.nodeAfter, pos = $pos.pos; - after; - after = after.firstChild, pos++ - ) { - const role = after.type.spec.tableRole; - if (role == 'cell' || role == 'header_cell') return $pos.doc.resolve(pos); - } - for ( - let before = $pos.nodeBefore, pos = $pos.pos; - before; - before = before.lastChild, pos-- - ) { - const role = before.type.spec.tableRole; - if (role == 'cell' || role == 'header_cell') - return $pos.doc.resolve(pos - before.nodeSize); - } -} - -/** - * @public - */ -export function pointsAtCell($pos: ResolvedPos): boolean { - return $pos.parent.type.spec.tableRole == 'row' && !!$pos.nodeAfter; -} - -/** - * @public - */ -export function moveCellForward($pos: ResolvedPos): ResolvedPos { - return $pos.node(0).resolve($pos.pos + $pos.nodeAfter!.nodeSize); -} - -/** - * @internal - */ -export function inSameTable($cellA: ResolvedPos, $cellB: ResolvedPos): boolean { - return ( - $cellA.depth == $cellB.depth && - $cellA.pos >= $cellB.start(-1) && - $cellA.pos <= $cellB.end(-1) - ); -} - -/** - * @public - */ -export function findCell($pos: ResolvedPos): Rect { - return TableMap.get($pos.node(-1)).findCell($pos.pos - $pos.start(-1)); -} - -/** - * @public - */ -export function colCount($pos: ResolvedPos): number { - return TableMap.get($pos.node(-1)).colCount($pos.pos - $pos.start(-1)); -} - -/** - * @public - */ -export function nextCell( - $pos: ResolvedPos, - axis: 'horiz' | 'vert', - dir: number, -): ResolvedPos | null { - const table = $pos.node(-1); - const map = TableMap.get(table); - const tableStart = $pos.start(-1); - - const moved = map.nextCell($pos.pos - tableStart, axis, dir); - return moved == null ? null : $pos.node(0).resolve(tableStart + moved); -} - -/** - * @public - */ -export function removeColSpan(attrs: CellAttrs, pos: number, n = 1): CellAttrs { - const result: CellAttrs = { ...attrs, colspan: attrs.colspan - n }; - - if (result.colwidth) { - result.colwidth = result.colwidth.slice(); - result.colwidth.splice(pos, n); - if (!result.colwidth.some((w) => w > 0)) result.colwidth = null; - } - return result; -} - -/** - * @public - */ -export function addColSpan(attrs: CellAttrs, pos: number, n = 1): Attrs { - const result = { ...attrs, colspan: attrs.colspan + n }; - if (result.colwidth) { - result.colwidth = result.colwidth.slice(); - for (let i = 0; i < n; i++) result.colwidth.splice(pos, 0, 0); - } - return result; -} - -/** - * @public - */ -export function columnIsHeader( - map: TableMap, - table: Node, - col: number, -): boolean { - const headerCell = tableNodeTypes(table.type.schema).header_cell; - for (let row = 0; row < map.height; row++) - if (table.nodeAt(map.map[col + row * map.width])!.type != headerCell) - return false; - return true; -} -- GitLab