mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-08 10:47:42 +02:00
Table cell merge & various related fixes (#7599)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
@@ -175,7 +175,7 @@
|
||||
}
|
||||
|
||||
&__col {
|
||||
right: -1.5rem;
|
||||
right: calc(var(--table-offscreen-spacing) - 1.5rem);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 1.25rem 0;
|
||||
@@ -188,7 +188,7 @@
|
||||
&__row {
|
||||
bottom: -0.25rem;
|
||||
left: var(--table-offscreen-spacing);
|
||||
right: 0;
|
||||
right: var(--table-offscreen-spacing);
|
||||
|
||||
.table-button {
|
||||
height: 1.25rem;
|
||||
|
||||
+70
-37
@@ -13,25 +13,76 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { Fragment, type Node, type Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import type { Transaction } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { type CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import type { TableNodeLocation } from '../types'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
|
||||
type TableRow = Array<ProseMirrorNode | null>
|
||||
type TableRows = TableRow[]
|
||||
|
||||
export function moveColumn (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction {
|
||||
const cols = transpose(tableToCells(table))
|
||||
moveRowInplace(cols, from, to)
|
||||
tableFromCells(table, transpose(cols), tr)
|
||||
export function moveSelectedColumns (
|
||||
editor: Editor,
|
||||
table: TableNodeLocation,
|
||||
selection: CellSelection,
|
||||
to: number,
|
||||
tr: Transaction
|
||||
): Transaction {
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
let columnStart = -1
|
||||
let columnEnd = -1
|
||||
|
||||
selection.forEachCell((node, pos) => {
|
||||
const cell = tableMap.findCell(pos - table.pos - 1)
|
||||
for (let i = cell.left; i < cell.right; i++) {
|
||||
columnStart = columnStart >= 0 ? Math.min(cell.left, columnStart) : cell.left
|
||||
columnEnd = columnEnd >= 0 ? Math.max(cell.right, columnEnd) : cell.right
|
||||
}
|
||||
})
|
||||
|
||||
if (to < 0 || to > tableMap.width || (to >= columnStart && to < columnEnd)) return tr
|
||||
|
||||
const rows = tableToCells(table)
|
||||
for (const row of rows) {
|
||||
const range = row.splice(columnStart, columnEnd - columnStart)
|
||||
const offset = to > columnStart ? to - (columnEnd - columnStart - 1) : to
|
||||
row.splice(offset, 0, ...range)
|
||||
}
|
||||
|
||||
tableFromCells(editor, table, rows, tr)
|
||||
return tr
|
||||
}
|
||||
|
||||
export function moveRow (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction {
|
||||
export function moveSelectedRows (
|
||||
editor: Editor,
|
||||
table: TableNodeLocation,
|
||||
selection: CellSelection,
|
||||
to: number,
|
||||
tr: Transaction
|
||||
): Transaction {
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
let rowStart = -1
|
||||
let rowEnd = -1
|
||||
|
||||
selection.forEachCell((node, pos) => {
|
||||
const cell = tableMap.findCell(pos - table.pos - 1)
|
||||
for (let i = cell.top; i < cell.bottom; i++) {
|
||||
rowStart = rowStart >= 0 ? Math.min(cell.top, rowStart) : cell.top
|
||||
rowEnd = rowEnd >= 0 ? Math.max(cell.bottom, rowEnd) : cell.bottom
|
||||
}
|
||||
})
|
||||
|
||||
if (to < 0 || to > tableMap.height || (to >= rowStart && to < rowEnd)) return tr
|
||||
|
||||
const rows = tableToCells(table)
|
||||
moveRowInplace(rows, from, to)
|
||||
tableFromCells(table, rows, tr)
|
||||
const range = rows.splice(rowStart, rowEnd - rowStart)
|
||||
const offset = to > rowStart ? to - (rowEnd - rowStart - 1) : to
|
||||
rows.splice(offset, 0, ...range)
|
||||
|
||||
tableFromCells(editor, table, rows, tr)
|
||||
return tr
|
||||
}
|
||||
|
||||
@@ -78,23 +129,17 @@ export function duplicateColumns (table: TableNodeLocation, columnIndices: numbe
|
||||
return tr
|
||||
}
|
||||
|
||||
function moveRowInplace (rows: TableRows, from: number, to: number): void {
|
||||
rows.splice(to, 0, rows.splice(from, 1)[0])
|
||||
}
|
||||
|
||||
function transpose (rows: TableRows): TableRows {
|
||||
return rows[0].map((_, colIdx) => rows.map((row) => row[colIdx]))
|
||||
}
|
||||
|
||||
function tableToCells (table: TableNodeLocation): TableRows {
|
||||
const { map, width, height } = TableMap.get(table.node)
|
||||
|
||||
const visitedCells = new Set<number>()
|
||||
const rows = []
|
||||
for (let row = 0; row < height; row++) {
|
||||
const cells = []
|
||||
for (let col = 0; col < width; col++) {
|
||||
const pos = map[row * width + col]
|
||||
cells.push(table.node.nodeAt(pos))
|
||||
cells.push(!visitedCells.has(pos) ? table.node.nodeAt(pos) : null)
|
||||
visitedCells.add(pos)
|
||||
}
|
||||
rows.push(cells)
|
||||
}
|
||||
@@ -102,23 +147,11 @@ function tableToCells (table: TableNodeLocation): TableRows {
|
||||
return rows
|
||||
}
|
||||
|
||||
function tableFromCells (table: TableNodeLocation, rows: TableRows, tr: Transaction): void {
|
||||
const { map, width, height } = TableMap.get(table.node)
|
||||
const mapStart = tr.mapping.maps.length
|
||||
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
const pos = map[row * width + col]
|
||||
|
||||
const oldCell = table.node.nodeAt(pos)
|
||||
const newCell = rows[row][col]
|
||||
|
||||
if (oldCell !== null && newCell !== null && oldCell !== newCell) {
|
||||
const start = tr.mapping.slice(mapStart).map(table.start + pos)
|
||||
const end = start + oldCell.nodeSize
|
||||
|
||||
tr.replaceWith(start, end, newCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
function tableFromCells (editor: Editor, table: TableNodeLocation, rows: TableRows, tr: Transaction): void {
|
||||
const schema = editor.schema.nodes
|
||||
const newRowNodes = rows.map((row) =>
|
||||
schema.tableRow.create(null, row.filter((cell) => cell !== null) as readonly Node[])
|
||||
)
|
||||
const newTableNode = table.node.copy(Fragment.from(newRowNodes))
|
||||
tr.replaceWith(table.pos, table.pos + table.node.nodeSize, newTableNode)
|
||||
}
|
||||
|
||||
+6
-4
@@ -15,14 +15,14 @@
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { findTable, getSelectedColumns, isColumnSelected, selectColumn } from '../utils'
|
||||
|
||||
import { duplicateColumns, moveColumn } from './actions'
|
||||
import { duplicateColumns, moveSelectedColumns } from './actions'
|
||||
import DeleteCol from '../../../icons/table/DeleteCol.svelte'
|
||||
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
||||
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
||||
@@ -120,8 +120,10 @@ const handleMouseDown = (
|
||||
|
||||
if (col !== dropIndex) {
|
||||
let tr = editor.state.tr
|
||||
tr = selectColumn(table, dropIndex, tr)
|
||||
tr = moveColumn(table, col, dropIndex, tr)
|
||||
const selection = editor.state.selection
|
||||
if (selection instanceof CellSelection) {
|
||||
tr = moveSelectedColumns(editor, table, selection, dropIndex, tr)
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
window.removeEventListener('mouseup', handleFinish)
|
||||
|
||||
+6
-4
@@ -15,14 +15,14 @@
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { findTable, getSelectedRows, isRowSelected, selectRow } from '../utils'
|
||||
|
||||
import { duplicateRows, moveRow } from './actions'
|
||||
import { duplicateRows, moveSelectedRows } from './actions'
|
||||
import DeleteRow from '../../../icons/table/DeleteRow.svelte'
|
||||
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
||||
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
||||
@@ -120,8 +120,10 @@ const handleMouseDown = (
|
||||
|
||||
if (row !== dropIndex) {
|
||||
let tr = editor.state.tr
|
||||
tr = selectRow(table, dropIndex, tr)
|
||||
tr = moveRow(table, row, dropIndex, tr)
|
||||
const selection = editor.state.selection
|
||||
if (selection instanceof CellSelection) {
|
||||
tr = moveSelectedRows(editor, table, selection, dropIndex, tr)
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
window.removeEventListener('mouseup', handleFinish)
|
||||
|
||||
+10
-3
@@ -60,10 +60,17 @@ function getTableCellBorders (
|
||||
const { width, height } = tableMap
|
||||
const cellIndex = tableMap.map.indexOf(cell)
|
||||
|
||||
const rect = tableMap.findCell(cell)
|
||||
const cellW = rect.right - rect.left
|
||||
const cellH = rect.bottom - rect.top
|
||||
|
||||
const testRight = cellW
|
||||
const testBottom = width * cellH
|
||||
|
||||
const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined
|
||||
const bottomCell = cellIndex < width * height - width ? tableMap.map[cellIndex + width] : undefined
|
||||
const leftCell = cellIndex % width !== 0 ? tableMap.map[cellIndex - 1] : undefined
|
||||
const rightCell = cellIndex % width !== width - 1 ? tableMap.map[cellIndex + 1] : undefined
|
||||
const bottomCell = cellIndex < width * height - testBottom ? tableMap.map[cellIndex + testBottom] : undefined
|
||||
const leftCell = cellIndex % width > 0 ? tableMap.map[cellIndex - 1] : undefined
|
||||
const rightCell = cellIndex % width < width - testRight ? tableMap.map[cellIndex + testRight] : undefined
|
||||
|
||||
return {
|
||||
top: topCell === undefined || !selection.includes(topCell),
|
||||
|
||||
@@ -116,6 +116,24 @@ export async function openTableOptions (editor: Editor, event: MouseEvent): Prom
|
||||
label: textEditor.string.CategoryRow
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#mergeCells',
|
||||
icon: textEditor.icon.MergeCells,
|
||||
label: textEditor.string.MergeCells,
|
||||
action: () => editor.commands.mergeCells(),
|
||||
category: {
|
||||
label: textEditor.string.CategoryCell
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#splitCell',
|
||||
icon: textEditor.icon.SplitCells,
|
||||
label: textEditor.string.SplitCells,
|
||||
action: () => editor.commands.splitCell(),
|
||||
category: {
|
||||
label: textEditor.string.CategoryCell
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '#deleteTable',
|
||||
icon: DeleteTable,
|
||||
|
||||
@@ -18,17 +18,19 @@ import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||
import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
|
||||
import { DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { findTable } from './utils'
|
||||
import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
|
||||
import { columnHandlerDecoration } from './decorations/columnHandlerDecoration'
|
||||
import { columnInsertDecoration } from './decorations/columnInsertDecoration'
|
||||
import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'
|
||||
import { rowInsertDecoration } from './decorations/rowInsertDecoration'
|
||||
import { tableDragMarkerDecoration } from './decorations/tableDragMarkerDecoration'
|
||||
import { tableSelectionDecoration } from './decorations/tableSelectionDecoration'
|
||||
import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'
|
||||
import { findTable } from './utils'
|
||||
import { type Node } from '@tiptap/pm/model'
|
||||
|
||||
export const TableCell = TiptapTableCell.extend({
|
||||
addProseMirrorPlugins () {
|
||||
return [tableCellDecorationPlugin(this.editor)]
|
||||
return [tableCellDecorationPlugin(this.editor), tableSelectionNormalizer()]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -78,3 +80,80 @@ const tableCellDecorationPlugin = (editor: Editor): Plugin<TableCellDecorationPl
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const tableSelectionNormalizer = (): Plugin<any> => {
|
||||
return new Plugin({
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const selection = newState.selection
|
||||
if (selection.eq(oldState.selection) || !(selection instanceof CellSelection)) return
|
||||
|
||||
const table = findTable(newState.selection)
|
||||
if (table === undefined) return
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
let rect: Rect | undefined
|
||||
|
||||
const walkCell = (pos: number): void => {
|
||||
const cell = tableMap.findCell(pos)
|
||||
if (cell === undefined) return
|
||||
|
||||
if (rect === undefined) {
|
||||
rect = { ...cell }
|
||||
} else {
|
||||
rect.left = Math.min(rect.left, cell.left)
|
||||
rect.top = Math.min(rect.top, cell.top)
|
||||
|
||||
rect.right = Math.max(rect.right, cell.right)
|
||||
rect.bottom = Math.max(rect.bottom, cell.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
selection.forEachCell((_node, pos) => {
|
||||
walkCell(pos - table.pos - 1)
|
||||
})
|
||||
if (rect === undefined) return
|
||||
|
||||
const rectSelection: number[] = []
|
||||
for (let row = rect.top; row < rect.bottom; row++) {
|
||||
for (let col = rect.left; col < rect.right; col++) {
|
||||
rectSelection.push(tableMap.map[row * tableMap.width + col])
|
||||
}
|
||||
}
|
||||
rectSelection.forEach((pos) => {
|
||||
walkCell(pos)
|
||||
})
|
||||
|
||||
if (rect === undefined) return
|
||||
|
||||
// Original promemirror implementation of TableMap.positionAt skips rowspawn cells, which leads to unpredictable selection behaviour
|
||||
const firstCellOffset = cellPositionAt(tableMap, rect.bottom - 1, rect.right - 1, table.node)
|
||||
const lastCellOffset = cellPositionAt(tableMap, rect.top, rect.left, table.node)
|
||||
|
||||
const firstCellPos = newState.doc.resolve(table.start + firstCellOffset)
|
||||
const lastCellPos = newState.doc.resolve(table.start + lastCellOffset)
|
||||
|
||||
const reverseOrder = selection.$anchorCell.pos > selection.$headCell.pos
|
||||
const $head = reverseOrder ? lastCellPos : firstCellPos
|
||||
const $anchor = reverseOrder ? firstCellPos : lastCellPos
|
||||
|
||||
const newSelection = new CellSelection($anchor, $head)
|
||||
|
||||
if (newSelection.eq(newState.selection)) return
|
||||
|
||||
return newState.tr.setSelection(new CellSelection($anchor, $head))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function cellPositionAt (tableMap: TableMap, row: number, col: number, table: Node): number {
|
||||
for (let i = 0, rowStart = 0; ; i++) {
|
||||
const rowEnd = rowStart + table.child(i).nodeSize
|
||||
if (i === row) {
|
||||
const index = col + row * tableMap.width
|
||||
const rowEndIndex = (row + 1) * tableMap.width
|
||||
return index === rowEndIndex ? rowEnd - 1 : tableMap.map[index]
|
||||
}
|
||||
rowStart = rowEnd
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user