import { Node as PmNode } from 'prosemirror-model'; import { Selection, Transaction } from 'prosemirror-state'; import { TableMap } from '@atlaskit/editor-tables/table-map'; import { ContentNodeWithPos } from 'prosemirror-utils'; import { findTable, getCellsInRow, getSelectionRect, } from '@atlaskit/editor-tables/utils'; import { Decoration, DecorationSet } from 'prosemirror-view'; import { CellAttributes } from '@atlaskit/adf-schema'; import { nonNullable } from '../../../utils'; import { Cell, CellColumnPositioning, TableCssClassName as ClassName, TableDecorations, } from '../types'; const filterDecorationByKey = ( key: TableDecorations, decorationSet: DecorationSet, ): Decoration[] => decorationSet.find( undefined, undefined, (spec) => spec.key.indexOf(key) > -1, ); export const findColumnControlSelectedDecoration = ( decorationSet: DecorationSet, ): Decoration[] => filterDecorationByKey(TableDecorations.COLUMN_SELECTED, decorationSet); export const findControlsHoverDecoration = ( decorationSet: DecorationSet, ): Decoration[] => filterDecorationByKey(TableDecorations.ALL_CONTROLS_HOVER, decorationSet); export const createCellHoverDecoration = ( cells: Cell[], type: 'warning', ): Decoration[] => cells.map((cell) => Decoration.node( cell.pos, cell.pos + cell.node.nodeSize, { class: ClassName.HOVERED_CELL_WARNING, }, { key: TableDecorations.CELL_CONTROLS_HOVER, }, ), ); export const createControlsHoverDecoration = ( cells: Cell[], type: 'row' | 'column' | 'table', danger?: boolean, ): Decoration[] => cells.map((cell) => { const classes = [ClassName.HOVERED_CELL]; if (danger) { classes.push(ClassName.HOVERED_CELL_IN_DANGER); } classes.push( type === 'column' ? ClassName.HOVERED_COLUMN : type === 'row' ? ClassName.HOVERED_ROW : ClassName.HOVERED_TABLE, ); let key: TableDecorations; switch (type) { case 'row': key = TableDecorations.ROW_CONTROLS_HOVER; break; case 'column': key = TableDecorations.COLUMN_CONTROLS_HOVER; break; default: key = TableDecorations.TABLE_CONTROLS_HOVER; break; } return Decoration.node( cell.pos, cell.pos + cell.node.nodeSize, { class: classes.join(' '), }, { key }, ); }); export const createColumnSelectedDecoration = ( tr: Transaction, ): Decoration[] => { const { selection, doc } = tr; const table = findTable(selection); const rect = getSelectionRect(selection); if (!table || !rect) { return []; } const map = TableMap.get(table.node); const cellPositions = map.cellsInRect(rect); return cellPositions.map((pos, index) => { const cell = doc.nodeAt(pos + table.start); return Decoration.node( pos + table.start, pos + table.start + cell!.nodeSize, { class: ClassName.COLUMN_SELECTED, }, { key: `${TableDecorations.COLUMN_SELECTED}_${index}`, }, ); }); }; export const createColumnControlsDecoration = ( selection: Selection, ): Decoration[] => { const cells: ContentNodeWithPos[] = getCellsInRow(0)(selection) || []; let index = 0; return cells.map((cell) => { const colspan = (cell.node.attrs as CellAttributes).colspan || 1; const element = document.createElement('div'); element.classList.add(ClassName.COLUMN_CONTROLS_DECORATIONS); element.dataset.startIndex = `${index}`; index += colspan; element.dataset.endIndex = `${index}`; return Decoration.widget( cell.pos + 1, // Do not delay the rendering for this Decoration // because we need to always render all controls // to keep the order safe element, { key: `${TableDecorations.COLUMN_CONTROLS_DECORATIONS}_${index}`, // this decoration should be the first one, even before gap cursor. side: -100, }, ); }); }; export const updateDecorations = ( node: PmNode, decorationSet: DecorationSet, decorations: Decoration[], key: TableDecorations, ): DecorationSet => { const filteredDecorations = filterDecorationByKey(key, decorationSet); const decorationSetFiltered = decorationSet.remove(filteredDecorations); return decorationSetFiltered.add(node, decorations); }; const makeArray = (n: number) => Array.from(Array(n).keys()); /* * This function will create two specific decorations for each cell in a column index target, * for example given that table: * * ``` * 0 1 2 3 * _____________________ _______ * | | | | * | B1 | C1 | A1 | * |______|______ ______|______| * | | | | * | B2 | | A2 | * |______ ______| |______| * | | | D1 | | * | B3 | C2 | | A3 | * |______|______|______|______| * ^ ^ ^ ^ * | | | | * | | | | * | | | | * 0 1 3 4 * \ | | / * \ | | / * \ | | / * \ | | / * \ | | / * columnEndIndexTarget === CellColumnPositioning.right * ``` * * When a user wants to resize a cell, * they need to grab and hold the end of that column, * and this will be the `columnEndIndexTarget` using * the CellColumnPositioning interface. * * Let's say the `columnEndIndexTarget.right` is 3, * so this function will return two types of decorations for each cell on that column, * that means 2 `resizerHandle` and 2 `lastCellElement`, * here is the explanation for each one of them : * * - resizerHandle: * * Given the cell C1, this decoration will add a div to create this area * ``` * ▁▁▁▁▁▁▁▁▁▁▁▁▁ * | ▒▒| * | C1 ▒▒| * | ▒▒| * ▔▔▔▔▔▔▔▔▔▔▔▔▔ * ``` * This ▒ represents the area where table resizing will start, * and you can follow that using checking the class name `ClassName.RESIZE_HANDLE_DECORATION` on the code * * - lastCellElementDecoration * * Given the content of the cell C1 * ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ * | | * | _____________ | * | | | | * | |
| |
* | |_____________| |
* | |
* | _____________ |
* | | | |
* | |