mirror of
https://github.com/docmost/docmost.git
synced 2026-07-25 02:44:42 +10:00
feat: table row/column drag and drop (#1467)
* chore: add dev container * feat: add drag handle when hovering cell * feat: add column drag and drop * feat: add support for row drag and drop * refactor: extract preview controllers * fix: hover issue * refactor: add handle controller * chore: f * chore: remove log * chore: remove dev files * feat: hide other drop indicators when table dnd working * feat: add auto scroll and bug fix * chore: f * fix: firefox
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
/**
|
||||
* Convert an array of rows to a table node.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function convertArrayOfRowsToTableNode(
|
||||
tableNode: Node,
|
||||
arrayOfNodes: (Node | null)[][],
|
||||
): Node {
|
||||
const rowsPM = []
|
||||
const map = TableMap.get(tableNode)
|
||||
for (let rowIndex = 0; rowIndex < map.height; rowIndex++) {
|
||||
const row = tableNode.child(rowIndex)
|
||||
const rowCells = []
|
||||
|
||||
for (let colIndex = 0; colIndex < map.width; colIndex++) {
|
||||
if (!arrayOfNodes[rowIndex][colIndex]) continue
|
||||
|
||||
const cellPos = map.map[rowIndex * map.width + colIndex]
|
||||
|
||||
const cell = arrayOfNodes[rowIndex][colIndex]!
|
||||
const oldCell = tableNode.nodeAt(cellPos)!
|
||||
const newCell = oldCell.type.createChecked(
|
||||
Object.assign({}, cell.attrs),
|
||||
cell.content,
|
||||
cell.marks,
|
||||
)
|
||||
rowCells.push(newCell)
|
||||
}
|
||||
|
||||
rowsPM.push(row.type.createChecked(row.attrs, rowCells, row.marks))
|
||||
}
|
||||
|
||||
const newTable = tableNode.type.createChecked(
|
||||
tableNode.attrs,
|
||||
rowsPM,
|
||||
tableNode.marks,
|
||||
)
|
||||
|
||||
return newTable
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
/**
|
||||
* This function will transform the table node into a matrix of rows and columns
|
||||
* respecting merged cells, for example this table:
|
||||
*
|
||||
* ```
|
||||
* ┌──────┬──────┬─────────────┐
|
||||
* │ A1 │ B1 │ C1 │
|
||||
* ├──────┼──────┴──────┬──────┤
|
||||
* │ A2 │ B2 │ │
|
||||
* ├──────┼─────────────┤ D1 │
|
||||
* │ A3 │ B3 │ C3 │ │
|
||||
* └──────┴──────┴──────┴──────┘
|
||||
* ```
|
||||
*
|
||||
* will be converted to the below:
|
||||
*
|
||||
* ```javascript
|
||||
* [
|
||||
* [A1, B1, C1, null],
|
||||
* [A2, B2, null, D1],
|
||||
* [A3, B3, C3, null],
|
||||
* ]
|
||||
* ```
|
||||
* @internal
|
||||
*/
|
||||
export function convertTableNodeToArrayOfRows(tableNode: Node): (Node | null)[][] {
|
||||
const map = TableMap.get(tableNode)
|
||||
const rows: (Node | null)[][] = []
|
||||
const rowCount = map.height
|
||||
const colCount = map.width
|
||||
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
||||
const row: (Node | null)[] = []
|
||||
for (let colIndex = 0; colIndex < colCount; colIndex++) {
|
||||
let cellIndex = rowIndex * colCount + colIndex
|
||||
let cellPos = map.map[cellIndex]
|
||||
if (rowIndex > 0) {
|
||||
const topCellIndex = cellIndex - colCount
|
||||
const topCellPos = map.map[topCellIndex]
|
||||
if (cellPos === topCellPos) {
|
||||
row.push(null)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (colIndex > 0) {
|
||||
const leftCellIndex = cellIndex - 1
|
||||
const leftCellPos = map.map[leftCellIndex]
|
||||
if (cellPos === leftCellPos) {
|
||||
row.push(null)
|
||||
continue
|
||||
}
|
||||
}
|
||||
row.push(tableNode.nodeAt(cellPos))
|
||||
}
|
||||
rows.push(row)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
import { findTable } from './query'
|
||||
import type { CellPos } from './types'
|
||||
|
||||
/**
|
||||
* Returns an array of cells in a column(s), where `columnIndex` could be a column index or an array of column indexes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getCellsInColumn(columnIndexes: number | number[], selection: Selection): CellPos[] | undefined {
|
||||
const table = findTable(selection.$from)
|
||||
if (!table) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = TableMap.get(table.node)
|
||||
const indexes = Array.isArray(columnIndexes) ? columnIndexes : [columnIndexes]
|
||||
|
||||
return indexes
|
||||
.filter((index) => index >= 0 && index <= map.width - 1)
|
||||
.flatMap((index) => {
|
||||
const cells = map.cellsInRect({
|
||||
left: index,
|
||||
right: index + 1,
|
||||
top: 0,
|
||||
bottom: map.height,
|
||||
})
|
||||
return cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos)!
|
||||
const pos = nodePos + table.start
|
||||
return { pos, start: pos + 1, node, depth: table.depth + 2 }
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
import { findTable } from './query'
|
||||
import type { CellPos } from './types'
|
||||
|
||||
/**
|
||||
* Returns an array of cells in a row(s), where `rowIndex` could be a row index or an array of row indexes.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getCellsInRow(rowIndex: number | number[], selection: Selection): CellPos[] | undefined {
|
||||
const table = findTable(selection.$from)
|
||||
if (!table) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = TableMap.get(table.node)
|
||||
const indexes = Array.isArray(rowIndex) ? rowIndex : [rowIndex]
|
||||
|
||||
return indexes
|
||||
.filter((index) => index >= 0 && index <= map.height - 1)
|
||||
.flatMap((index) => {
|
||||
const cells = map.cellsInRect({
|
||||
left: 0,
|
||||
right: map.width,
|
||||
top: index,
|
||||
bottom: index + 1,
|
||||
})
|
||||
return cells.map((nodePos) => {
|
||||
const node = table.node.nodeAt(nodePos)!
|
||||
const pos = nodePos + table.start
|
||||
return { pos, start: pos + 1, node, depth: table.depth + 2 }
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
|
||||
import { getCellsInColumn } from './get-cells-in-column'
|
||||
import { getCellsInRow } from './get-cells-in-row'
|
||||
import type { CellSelectionRange } from './types'
|
||||
|
||||
/**
|
||||
* Returns a range of rectangular selection spanning all merged cells around a
|
||||
* column at index `columnIndex`.
|
||||
*
|
||||
* Original implementation from Atlassian (Apache License 2.0)
|
||||
*
|
||||
* https://bitbucket.org/atlassian/atlassian-frontend-mirror/src/5f91cb871e8248bc3bae5ddc30bb9fd9200fadbb/editor/editor-tables/src/utils/get-selection-range-in-column.ts#editor/editor-tables/src/utils/get-selection-range-in-column.ts
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getSelectionRangeInColumn(tr: Transaction, startColIndex: number, endColIndex: number = startColIndex): CellSelectionRange | undefined {
|
||||
let startIndex = startColIndex
|
||||
let endIndex = endColIndex
|
||||
|
||||
// looking for selection start column (startIndex)
|
||||
for (let i = startColIndex; i >= 0; i--) {
|
||||
const cells = getCellsInColumn(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.colspan + i - 1
|
||||
if (maybeEndIndex >= startIndex) {
|
||||
startIndex = i
|
||||
}
|
||||
if (maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// looking for selection end column (endIndex)
|
||||
for (let i = startColIndex; i <= endIndex; i++) {
|
||||
const cells = getCellsInColumn(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.colspan + i - 1
|
||||
if (cell.node.attrs.colspan > 1 && maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// filter out columns without cells (where all rows have colspan > 1 in the same column)
|
||||
const indexes = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const maybeCells = getCellsInColumn(i, tr.selection)
|
||||
if (maybeCells && maybeCells.length > 0) {
|
||||
indexes.push(i)
|
||||
}
|
||||
}
|
||||
startIndex = indexes[0]
|
||||
endIndex = indexes[indexes.length - 1]
|
||||
|
||||
const firstSelectedColumnCells = getCellsInColumn(startIndex, tr.selection)
|
||||
const firstRowCells = getCellsInRow(0, tr.selection)
|
||||
if (!firstSelectedColumnCells || !firstRowCells) {
|
||||
return
|
||||
}
|
||||
|
||||
const $anchor = tr.doc.resolve(
|
||||
firstSelectedColumnCells[firstSelectedColumnCells.length - 1].pos,
|
||||
)
|
||||
|
||||
let headCell
|
||||
for (let i = endIndex; i >= startIndex; i--) {
|
||||
const columnCells = getCellsInColumn(i, tr.selection)
|
||||
if (columnCells && columnCells.length > 0) {
|
||||
for (let j = firstRowCells.length - 1; j >= 0; j--) {
|
||||
if (firstRowCells[j].pos === columnCells[0].pos) {
|
||||
headCell = columnCells[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (headCell) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!headCell) {
|
||||
return
|
||||
}
|
||||
|
||||
const $head = tr.doc.resolve(headCell.pos)
|
||||
return { $anchor, $head, indexes }
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
|
||||
import { getCellsInColumn } from './get-cells-in-column'
|
||||
import { getCellsInRow } from './get-cells-in-row'
|
||||
import type { CellSelectionRange } from './types'
|
||||
|
||||
/**
|
||||
* Returns a range of rectangular selection spanning all merged cells around a
|
||||
* row at index `rowIndex`.
|
||||
*
|
||||
* Original implementation from Atlassian (Apache License 2.0)
|
||||
*
|
||||
* https://bitbucket.org/atlassian/atlassian-frontend-mirror/src/5f91cb871e8248bc3bae5ddc30bb9fd9200fadbb/editor/editor-tables/src/utils/get-selection-range-in-row.ts#editor/editor-tables/src/utils/get-selection-range-in-row.ts
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function getSelectionRangeInRow(tr: Transaction, startRowIndex: number, endRowIndex: number = startRowIndex): CellSelectionRange | undefined {
|
||||
let startIndex = startRowIndex
|
||||
let endIndex = endRowIndex
|
||||
|
||||
// looking for selection start row (startIndex)
|
||||
for (let i = startRowIndex; i >= 0; i--) {
|
||||
const cells = getCellsInRow(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.rowspan + i - 1
|
||||
if (maybeEndIndex >= startIndex) {
|
||||
startIndex = i
|
||||
}
|
||||
if (maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// looking for selection end row (endIndex)
|
||||
for (let i = startRowIndex; i <= endIndex; i++) {
|
||||
const cells = getCellsInRow(i, tr.selection)
|
||||
if (cells) {
|
||||
cells.forEach((cell) => {
|
||||
const maybeEndIndex = cell.node.attrs.rowspan + i - 1
|
||||
if (cell.node.attrs.rowspan > 1 && maybeEndIndex > endIndex) {
|
||||
endIndex = maybeEndIndex
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// filter out rows without cells (where all columns have rowspan > 1 in the same row)
|
||||
const indexes = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const maybeCells = getCellsInRow(i, tr.selection)
|
||||
if (maybeCells && maybeCells.length > 0) {
|
||||
indexes.push(i)
|
||||
}
|
||||
}
|
||||
startIndex = indexes[0]
|
||||
endIndex = indexes[indexes.length - 1]
|
||||
|
||||
const firstSelectedRowCells = getCellsInRow(startIndex, tr.selection)
|
||||
const firstColumnCells = getCellsInColumn(0, tr.selection)
|
||||
if (!firstSelectedRowCells || !firstColumnCells) {
|
||||
return
|
||||
}
|
||||
|
||||
const $anchor = tr.doc.resolve(firstSelectedRowCells[firstSelectedRowCells.length - 1].pos)
|
||||
|
||||
let headCell
|
||||
for (let i = endIndex; i >= startIndex; i--) {
|
||||
const rowCells = getCellsInRow(i, tr.selection)
|
||||
if (rowCells && rowCells.length > 0) {
|
||||
for (let j = firstColumnCells.length - 1; j >= 0; j--) {
|
||||
if (firstColumnCells[j].pos === rowCells[0].pos) {
|
||||
headCell = rowCells[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (headCell) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!headCell) {
|
||||
return
|
||||
}
|
||||
|
||||
const $head = tr.doc.resolve(headCell.pos)
|
||||
return { $anchor, $head, indexes }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './move-column'
|
||||
export * from './move-row'
|
||||
export * from './query'
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
} from '@tiptap/pm/tables'
|
||||
|
||||
import { convertArrayOfRowsToTableNode } from './convert-array-of-rows-to-table-node'
|
||||
import { convertTableNodeToArrayOfRows } from './convert-table-node-to-array-of-rows'
|
||||
import { getSelectionRangeInColumn } from './get-selection-range-in-column'
|
||||
import { moveRowInArrayOfRows } from './move-row-in-array-of-rows'
|
||||
import { findTable } from './query'
|
||||
import { transpose } from './transpose'
|
||||
|
||||
export interface MoveColumnParams {
|
||||
tr: Transaction
|
||||
originIndex: number
|
||||
targetIndex: number
|
||||
select: boolean
|
||||
pos: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a column from index `origin` to index `target`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function moveColumn(moveColParams: MoveColumnParams): boolean {
|
||||
const { tr, originIndex, targetIndex, select, pos } = moveColParams
|
||||
const $pos = tr.doc.resolve(pos)
|
||||
const table = findTable($pos)
|
||||
if (!table) return false
|
||||
|
||||
const indexesOriginColumn = getSelectionRangeInColumn(tr, originIndex)?.indexes
|
||||
const indexesTargetColumn = getSelectionRangeInColumn(tr, targetIndex)?.indexes
|
||||
|
||||
if (!indexesOriginColumn || !indexesTargetColumn) return false
|
||||
|
||||
if (indexesOriginColumn.includes(targetIndex)) return false
|
||||
|
||||
const newTable = moveTableColumn(
|
||||
table.node,
|
||||
indexesOriginColumn,
|
||||
indexesTargetColumn,
|
||||
0,
|
||||
)
|
||||
|
||||
tr.replaceWith(
|
||||
table.pos,
|
||||
table.pos + table.node.nodeSize,
|
||||
newTable,
|
||||
)
|
||||
|
||||
if (!select) return true
|
||||
|
||||
const map = TableMap.get(newTable)
|
||||
const start = table.start
|
||||
const index = targetIndex
|
||||
const lastCell = map.positionAt(map.height - 1, index, newTable)
|
||||
const $lastCell = tr.doc.resolve(start + lastCell)
|
||||
|
||||
const firstCell = map.positionAt(0, index, newTable)
|
||||
const $firstCell = tr.doc.resolve(start + firstCell)
|
||||
|
||||
tr.setSelection(CellSelection.colSelection($lastCell, $firstCell))
|
||||
return true
|
||||
}
|
||||
|
||||
function moveTableColumn(
|
||||
table: Node,
|
||||
indexesOrigin: number[],
|
||||
indexesTarget: number[],
|
||||
direction: -1 | 1 | 0,
|
||||
) {
|
||||
let rows = transpose(convertTableNodeToArrayOfRows(table))
|
||||
|
||||
rows = moveRowInArrayOfRows(rows, indexesOrigin, indexesTarget, direction)
|
||||
rows = transpose(rows)
|
||||
|
||||
return convertArrayOfRowsToTableNode(table, rows)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Move a row in an array of rows.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function moveRowInArrayOfRows<T>(
|
||||
rows: T[],
|
||||
indexesOrigin: number[],
|
||||
indexesTarget: number[],
|
||||
directionOverride: -1 | 1 | 0,
|
||||
): T[] {
|
||||
const direction = indexesOrigin[0] > indexesTarget[0] ? -1 : 1
|
||||
|
||||
const rowsExtracted = rows.splice(indexesOrigin[0], indexesOrigin.length)
|
||||
const positionOffset = rowsExtracted.length % 2 === 0 ? 1 : 0
|
||||
let target: number
|
||||
|
||||
if (directionOverride === -1 && direction === 1) {
|
||||
target = indexesTarget[0] - 1
|
||||
} else if (directionOverride === 1 && direction === -1) {
|
||||
target = indexesTarget[indexesTarget.length - 1] - positionOffset + 1
|
||||
} else {
|
||||
target = direction === -1
|
||||
? indexesTarget[0]
|
||||
: indexesTarget[indexesTarget.length - 1] - positionOffset
|
||||
}
|
||||
|
||||
rows.splice(target, 0, ...rowsExtracted)
|
||||
return rows
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Node } from '@tiptap/pm/model'
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
} from '@tiptap/pm/tables'
|
||||
|
||||
import { convertArrayOfRowsToTableNode } from './convert-array-of-rows-to-table-node'
|
||||
import { convertTableNodeToArrayOfRows } from './convert-table-node-to-array-of-rows'
|
||||
import { getSelectionRangeInRow } from './get-selection-range-in-row'
|
||||
import { moveRowInArrayOfRows } from './move-row-in-array-of-rows'
|
||||
import { findTable } from './query'
|
||||
|
||||
export interface MoveRowParams {
|
||||
tr: Transaction
|
||||
originIndex: number
|
||||
targetIndex: number
|
||||
select: boolean
|
||||
pos: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a row from index `origin` to index `target`.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function moveRow(moveRowParams: MoveRowParams): boolean {
|
||||
const { tr, originIndex, targetIndex, select, pos } = moveRowParams
|
||||
const $pos = tr.doc.resolve(pos)
|
||||
const table = findTable($pos)
|
||||
if (!table) return false
|
||||
|
||||
const indexesOriginRow = getSelectionRangeInRow(tr, originIndex)?.indexes
|
||||
const indexesTargetRow = getSelectionRangeInRow(tr, targetIndex)?.indexes
|
||||
|
||||
if (!indexesOriginRow || !indexesTargetRow) return false
|
||||
|
||||
if (indexesOriginRow.includes(targetIndex)) return false
|
||||
|
||||
const newTable = moveTableRow(table.node, indexesOriginRow, indexesTargetRow, 0)
|
||||
|
||||
tr.replaceWith(
|
||||
table.pos,
|
||||
table.pos + table.node.nodeSize,
|
||||
newTable,
|
||||
)
|
||||
|
||||
if (!select) return true
|
||||
|
||||
const map = TableMap.get(newTable)
|
||||
const start = table.start
|
||||
const index = targetIndex
|
||||
const lastCell = map.positionAt(index, map.width - 1, newTable)
|
||||
const $lastCell = tr.doc.resolve(start + lastCell)
|
||||
|
||||
const firstCell = map.positionAt(index, 0, newTable)
|
||||
const $firstCell = tr.doc.resolve(start + firstCell)
|
||||
|
||||
tr.setSelection(CellSelection.rowSelection($lastCell, $firstCell))
|
||||
return true
|
||||
}
|
||||
|
||||
function moveTableRow(
|
||||
table: Node,
|
||||
indexesOrigin: number[],
|
||||
indexesTarget: number[],
|
||||
direction: -1 | 1 | 0,
|
||||
) {
|
||||
let rows = convertTableNodeToArrayOfRows(table)
|
||||
|
||||
rows = moveRowInArrayOfRows(rows, indexesOrigin, indexesTarget, direction)
|
||||
|
||||
return convertArrayOfRowsToTableNode(table, rows)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import type {
|
||||
Node,
|
||||
ResolvedPos,
|
||||
} from '@tiptap/pm/model'
|
||||
import type { Selection } from '@tiptap/pm/state'
|
||||
import {
|
||||
cellAround,
|
||||
cellNear,
|
||||
CellSelection,
|
||||
inSameTable,
|
||||
} from '@tiptap/pm/tables'
|
||||
|
||||
/**
|
||||
* Checks if the given object is a `CellSelection` instance.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function isCellSelection(value: unknown): value is CellSelection {
|
||||
return value instanceof CellSelection
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest table node.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findTable($pos: ResolvedPos): FindParentNodeResult | undefined {
|
||||
return findParentNode((node) => node.type.spec.tableRole === 'table', $pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find the anchor and head cell in the same table by using the given
|
||||
* anchor and head as hit points, or fallback to the selection's anchor and
|
||||
* head.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findCellRange(
|
||||
selection: Selection,
|
||||
anchorHit?: number,
|
||||
headHit?: number,
|
||||
): [ResolvedPos, ResolvedPos] | undefined {
|
||||
if (anchorHit == null && headHit == null && isCellSelection(selection)) {
|
||||
return [selection.$anchorCell, selection.$headCell]
|
||||
}
|
||||
|
||||
const anchor: number = anchorHit ?? headHit ?? selection.anchor
|
||||
const head: number = headHit ?? anchorHit ?? selection.head
|
||||
|
||||
const doc = selection.$head.doc
|
||||
|
||||
const $anchorCell = findCellPos(doc, anchor)
|
||||
const $headCell = findCellPos(doc, head)
|
||||
|
||||
if ($anchorCell && $headCell && inSameTable($anchorCell, $headCell)) {
|
||||
return [$anchorCell, $headCell]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find a resolved pos of a cell by using the given pos as a hit point.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findCellPos(
|
||||
doc: Node,
|
||||
pos: number,
|
||||
): ResolvedPos | undefined {
|
||||
const $pos = doc.resolve(pos)
|
||||
return cellAround($pos) || cellNear($pos)
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface FindParentNodeResult {
|
||||
/**
|
||||
* The closest parent node that satisfies the predicate.
|
||||
*/
|
||||
node: Node
|
||||
|
||||
/**
|
||||
* The position directly before the node.
|
||||
*/
|
||||
pos: number
|
||||
|
||||
/**
|
||||
* The position at the start of the node.
|
||||
*/
|
||||
start: number
|
||||
|
||||
/**
|
||||
* The depth of the node.
|
||||
*/
|
||||
depth: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest parent node that satisfies the predicate.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function findParentNode(
|
||||
/**
|
||||
* The predicate to test the parent node.
|
||||
*/
|
||||
predicate: (node: Node) => boolean,
|
||||
/**
|
||||
* The position to start searching from.
|
||||
*/
|
||||
$pos: ResolvedPos,
|
||||
): FindParentNodeResult | undefined {
|
||||
for (let depth = $pos.depth; depth >= 0; depth -= 1) {
|
||||
const node = $pos.node(depth)
|
||||
|
||||
if (predicate(node)) {
|
||||
const pos = depth === 0 ? 0 : $pos.before(depth)
|
||||
const start = $pos.start(depth)
|
||||
return { node, pos, start, depth }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Transposes a 2D array by flipping columns to rows.
|
||||
*
|
||||
* Transposition is a familiar algebra concept where the matrix is flipped
|
||||
* along its diagonal. For more details, see:
|
||||
* https://en.wikipedia.org/wiki/Transpose
|
||||
*
|
||||
* @example
|
||||
* ```javascript
|
||||
* const arr = [
|
||||
* ['a1', 'a2', 'a3'],
|
||||
* ['b1', 'b2', 'b3'],
|
||||
* ['c1', 'c2', 'c3'],
|
||||
* ['d1', 'd2', 'd3'],
|
||||
* ];
|
||||
*
|
||||
* const result = transpose(arr);
|
||||
* result === [
|
||||
* ['a1', 'b1', 'c1', 'd1'],
|
||||
* ['a2', 'b2', 'c2', 'd2'],
|
||||
* ['a3', 'b3', 'c3', 'd3'],
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function transpose<T>(array: T[][]): T[][] {
|
||||
return array[0].map((_, i) => {
|
||||
return array.map((column) => column[i])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type {
|
||||
Node,
|
||||
ResolvedPos,
|
||||
} from '@tiptap/pm/model'
|
||||
|
||||
export type CellPos = {
|
||||
pos: number
|
||||
start: number
|
||||
depth: number
|
||||
node: Node
|
||||
}
|
||||
|
||||
export type CellSelectionRange = {
|
||||
$anchor: ResolvedPos
|
||||
$head: ResolvedPos
|
||||
// an array of column/row indexes
|
||||
indexes: number[]
|
||||
}
|
||||
Reference in New Issue
Block a user