import { splice, zeroes } from "@heydovetail/array"; import { Node, ResolvedPos } from "prosemirror-model"; import { EditorState, NodeSelection, Selection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { closest } from "../pquery"; import { CellAttrs } from "../schema"; import { Bias, CartesianAxis, DirString, DocNode, TableAxis, TableNode, TablePosOfCell, TableStart } from "../types"; import * as dom from "../util/dom"; import { CellSelection } from "./CellSelection"; import { EdgeRect } from "./EdgeRect"; import { TableMap } from "./TableMap"; export function editable(view: EditorView): boolean { return view.props.editable != null ? view.props.editable(view.state) : true; } export function cellAround($pos: ResolvedPos) { for (let d = $pos.depth; d > 0; d--) { if ($pos.node(d).type.name === "tr") { return $pos.node(0).resolve($pos.before(d + 1)); } } return null; } export function isInTable(state: EditorState): boolean { const $head = state.selection.$head; for (let d = $head.depth; d > 0; d--) { if ($head.node(d).type.name === "tr") { return true; } } return false; } export function selectionCell(state: EditorState): ResolvedPos | null { const sel = state.selection; if (sel instanceof CellSelection) { return sel.$anchorCell.pos > sel.$headCell.pos ? sel.$anchorCell : sel.$headCell; } if (sel instanceof NodeSelection) { const typeName = sel.node.type.name; if (typeName === "td" || typeName === "th") { return sel.$anchor; } } const cellAroundResult = cellAround(sel.$head); if (cellAroundResult !== null) { return cellAroundResult; } const cellNearResult = cellNear(sel.$head); if (cellNearResult !== null) { return cellNearResult; } return null; } function cellNear($pos: ResolvedPos): ResolvedPos | null { for (let after = $pos.nodeAfter, pos = $pos.pos; after != null; after = after.firstChild, pos++) { const typeName = after.type.name; if (typeName === "td" || typeName == "th") { return $pos.doc.resolve(pos); } } for (let before = $pos.nodeBefore, pos = $pos.pos; before != null; before = before.lastChild, pos--) { const typeName = before.type.name; if (typeName === "td" || typeName === "th") { return $pos.doc.resolve(pos - before.nodeSize); } } return null; } export function pointsAtCell($pos: ResolvedPos): boolean { return $pos.parent.type.name === "tr" ? $pos.nodeAfter != null : false; } export function moveCellForward($pos: ResolvedPos) { const nodeAfter = $pos.nodeAfter; return nodeAfter == null ? $pos : $pos.node(0).resolve($pos.pos + nodeAfter.nodeSize); } export function inSameTable($a: ResolvedPos, $b: ResolvedPos): boolean { return $a.depth == $b.depth && $a.pos >= $b.start(-1) && $a.pos <= $b.end(-1); } export function isColumnHeader(tableMap: TableMap, table: TableNode, col: number): boolean { const headerCell = table.type.schema.nodes["th"]; for (let row = 0; row < tableMap.height; row++) { if (table.nodeAt(tableMap.map[col + row * tableMap.width])!.type != headerCell) { return false; } } return true; } export function findCell($pos: ResolvedPos): EdgeRect { const table = $pos.node(-1) as TableNode; const tableStart = $pos.start(-1) as TableStart; const cellPos = ($pos.pos - tableStart) as TablePosOfCell; return TableMap.get(table).findCell(cellPos); } export function colCount($pos: ResolvedPos): number { const table = $pos.node(-1) as TableNode; const tableStart = $pos.start(-1) as TableStart; const cellPos = ($pos.pos - tableStart) as TablePosOfCell; return TableMap.get(table).columnIndex(cellPos); } export function nextCell($cellPos: ResolvedPos, axis: CartesianAxis, bias: Bias) { const tableStart = $cellPos.start(-1) as TableStart; const table = $cellPos.node(-1) as TableNode; const map = TableMap.get(table); const cellPos = ($cellPos.pos - tableStart) as TablePosOfCell; const moved = map.nextCell(cellPos, axis, bias); return moved == null ? null : $cellPos.node(0).resolve(tableStart + moved); } /** * Reduce a cell's `colspan` attribute, omitting unused `colwidth` elements. */ export function shrinkColSpan(attrs: CellAttrs, columnIndex: number, count = 1): CellAttrs { const newColspan = attrs.colspan - count; const newColwidth = attrs.colwidth != null ? splice(attrs.colwidth, columnIndex, count) : null; return { ...attrs, colwidth: newColwidth !== null && newColwidth.some(w => w > 0) ? newColwidth : null, colspan: newColspan }; } export function expandColSpan(attrs: CellAttrs, pos: number, n = 1) { const newColspan = attrs.colspan + n; return { ...attrs, colwidth: attrs.colwidth != null ? splice(attrs.colwidth, pos, 0, ...zeroes(n)) : null, colspan: newColspan }; } export function closestDomCell( view: EditorView, searchStartNode: dom.Node ): HTMLTableHeaderCellElement | HTMLTableDataCellElement | null { let currentCandidate: dom.Node = searchStartNode; while (currentCandidate != view.dom) { if (currentCandidate.nodeName == "TD" || currentCandidate.nodeName == "TH") { return currentCandidate as HTMLTableHeaderCellElement | HTMLTableDataCellElement; } const nextCandidate = currentCandidate.parentNode; if (nextCandidate === null) { break; } else { currentCandidate = nextCandidate; } } return null; } export const enum CellNearMouseStrategy { CARTESIAN_PROXIMITY, DOM_HIERARCHY } export function cellNearMouse( view: EditorView, event: MouseEvent, strategy = CellNearMouseStrategy.CARTESIAN_PROXIMITY ): ResolvedPos | null { const mousePos = view.posAtCoords({ left: event.clientX, top: event.clientY }); return mousePos != null ? cellAround( view.state.doc.resolve(strategy === CellNearMouseStrategy.CARTESIAN_PROXIMITY ? mousePos.pos : mousePos.inside) ) : null; } export function axisSelectionForCell($cell: ResolvedPos, axis: TableAxis): CellSelection { switch (axis) { case TableAxis.COLUMN: { return CellSelection.colSelection($cell); } case TableAxis.ROW: { return CellSelection.rowSelection($cell); } default: { const exhausted: never = axis; throw exhausted; } } } /** * Find a table node in a doc given the table’s start position. * * This method supports the doc itself being a table. * * @param doc * @param start Start position of the table (first position inside) relative to doc. */ export function findTable(doc: DocNode, start: TableStart): TableNode | null { if (start === 0) { return (doc as Node) as TableNode; } const node = doc.nodeAt(start - 1); if (node != null && node.type.name === "tbl") { return node as TableNode; } return null; } export interface TableDescriptor { node: TableNode; start: TableStart; } /** * Find the closest table to the current selection ancestors. * * One of the challenges (performance) with applying decoration to certain types * of elements is finding all of those elements. Instead of searching the entire * document, a trick is to just find decorate elements that are ancestors of the * current selection. */ export function closestTableFromSelection(selection: Selection): TableDescriptor | null | void { if (selection instanceof CellSelection) { return closestTable(selection.$anchorCell); } else if (selection instanceof TextSelection) { return closestTable(selection.$anchor); } } /** * Find the closest ancestor table from a position in a document. */ export function closestTable($pos: ResolvedPos): TableDescriptor | null | void { return closest($pos, node => node.type.name === "tbl"); } /** * Check whether the cursor is at the end of a cell (so that further motion * would move out of the cell) */ export function atEndOfCell(view: EditorView, axis: CartesianAxis, bias: Bias) { if (view.state.selection instanceof TextSelection) { const { $head } = view.state.selection; for (let d = $head.depth - 1; d >= 0; d--) { const parent = $head.node(d); const index = bias == Bias.BACKWARD ? $head.index(d) : $head.indexAfter(d); if (index != (bias == Bias.BACKWARD ? 0 : parent.childCount)) { break; } if (parent.type.name === "td" || parent.type.name === "th") { const cellPos = $head.before(d); const dirStr: DirString = axis == CartesianAxis.VERTICAL ? (bias == Bias.FORWARD ? "down" : "up") : bias == Bias.FORWARD ? "right" : "left"; if (view.endOfTextblock(dirStr)) { return cellPos; } break; } } } return null; }