mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-09 17:05:01 +00:00
Improved table performance in the text editor
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
6c5a6aae27
commit
f91001e056
@ -13,18 +13,17 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { findTable, getSelectedColumns, isColumnSelected, selectColumn } from '../utils'
|
||||
import { findTable, getSelectedColumns, haveTableRelatedChanges, isColumnSelected, selectColumn } from '../utils'
|
||||
|
||||
import { duplicateColumns, moveSelectedColumns } from './actions'
|
||||
import DeleteCol from '../../../icons/table/DeleteCol.svelte'
|
||||
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
||||
import { duplicateColumns, moveSelectedColumns } from './actions'
|
||||
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
||||
import {
|
||||
dropMarkerWidthPx,
|
||||
@ -32,14 +31,176 @@ import {
|
||||
getDropMarker,
|
||||
hideDragMarker,
|
||||
hideDropMarker,
|
||||
updateColDropMarker,
|
||||
updateColDragMarker
|
||||
updateColDragMarker,
|
||||
updateColDropMarker
|
||||
} from './tableDragMarkerDecoration'
|
||||
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||
|
||||
interface TableColumn {
|
||||
leftPx: number
|
||||
widthPx: number
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
interface TableColumnHandlerDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
|
||||
export const TableColumnHandlerDecorationPlugin = (editor: Editor): Plugin<TableColumnHandlerDecorationPluginState> => {
|
||||
const key = new PluginKey('tableColumnHandlerDecorationPlugin')
|
||||
return new Plugin<TableColumnHandlerDecorationPluginState>({
|
||||
key,
|
||||
state: {
|
||||
init: () => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
let isStale = false
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
for (let col = 0; col < tableMap.width; col++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||
if (mapped?.find(pos, pos + 1)?.length !== 1) {
|
||||
isStale = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStale) {
|
||||
return { decorations: mapped }
|
||||
}
|
||||
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
for (let col = 0; col < tableMap.width; col++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||
const handler = new ColumnHandler(editor, { col })
|
||||
decorations.push(Decoration.widget(pos, () => handler.build(), { destroy: () => handler.destroy?.() }))
|
||||
}
|
||||
|
||||
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface ColumnHandlerProps {
|
||||
col: number
|
||||
}
|
||||
|
||||
class ColumnHandler {
|
||||
editor: Editor
|
||||
props: ColumnHandlerProps
|
||||
destroy?: () => void
|
||||
|
||||
constructor (editor: Editor, props: ColumnHandlerProps) {
|
||||
this.editor = editor
|
||||
this.props = props
|
||||
}
|
||||
|
||||
build (): HTMLElement {
|
||||
const editor = this.editor
|
||||
const col = this.props.col
|
||||
|
||||
const handle = createCellsHandle(createOptionItems(editor))
|
||||
handle.classList.add('table-col-handle')
|
||||
|
||||
const selectionUpdate = (): boolean => {
|
||||
const isSelected = isColumnSelected(col, editor.state.selection)
|
||||
if (isSelected) {
|
||||
handle.classList.add('table-col-handle__selected')
|
||||
} else {
|
||||
handle.classList.remove('table-col-handle__selected')
|
||||
}
|
||||
return isSelected
|
||||
}
|
||||
|
||||
editor.on('selectionUpdate', selectionUpdate)
|
||||
|
||||
if (this.destroy !== undefined) {
|
||||
this.destroy()
|
||||
}
|
||||
this.destroy = (): void => {
|
||||
editor.off('selectionUpdate', selectionUpdate)
|
||||
}
|
||||
|
||||
handle.addEventListener('mousedown', (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const isSelected = selectionUpdate()
|
||||
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
// select column
|
||||
if (!isSelected) {
|
||||
editor.view.dispatch(selectColumn(table, col, editor.state.tr))
|
||||
}
|
||||
|
||||
// drag column
|
||||
const tableWidthPx = getTableWidthPx(table, editor)
|
||||
const columns = getTableColumns(table, editor)
|
||||
|
||||
let dropIndex = col
|
||||
const startLeft = columns[col].leftPx ?? 0
|
||||
const startX = event.clientX
|
||||
|
||||
const dropMarker = getDropMarker()
|
||||
const dragMarker = getColDragMarker()
|
||||
|
||||
const handleFinish = (): void => {
|
||||
if (dropMarker !== null) hideDropMarker(dropMarker)
|
||||
if (dragMarker !== null) hideDragMarker(dragMarker)
|
||||
|
||||
if (col !== dropIndex) {
|
||||
let tr = editor.state.tr
|
||||
const selection = editor.state.selection
|
||||
if (selection instanceof CellSelection) {
|
||||
const table = findTable(selection)
|
||||
if (table !== undefined) {
|
||||
tr = moveSelectedColumns(editor, table, selection, dropIndex, tr)
|
||||
}
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
window.removeEventListener('mouseup', handleFinish)
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
const handleMove = (event: MouseEvent): void => {
|
||||
if (dropMarker !== null && dragMarker !== null) {
|
||||
const currentLeft = startLeft + event.clientX - startX
|
||||
dropIndex = calculateColumnDropIndex(col, columns, currentLeft)
|
||||
|
||||
const dragMarkerWidthPx = columns[col].widthPx
|
||||
const dragMarkerLeftPx = Math.max(0, Math.min(currentLeft, tableWidthPx - dragMarkerWidthPx))
|
||||
const dropMarkerLeftPx =
|
||||
dropIndex <= col ? columns[dropIndex].leftPx : columns[dropIndex].leftPx + columns[dropIndex].widthPx
|
||||
|
||||
updateColDropMarker(dropMarker, dropMarkerLeftPx - Math.floor(dropMarkerWidthPx / 2) - 1, dropMarkerWidthPx)
|
||||
updateColDragMarker(dragMarker, dragMarkerLeftPx, dragMarkerWidthPx)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', handleFinish)
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
})
|
||||
|
||||
return handle
|
||||
}
|
||||
}
|
||||
|
||||
const createOptionItems = (editor: Editor): OptionItem[] => [
|
||||
@ -65,88 +226,9 @@ const createOptionItems = (editor: Editor): OptionItem[] => [
|
||||
}
|
||||
]
|
||||
|
||||
export const columnHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
for (let col = 0; col < tableMap.width; col++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||
const isSelected = isColumnSelected(col, state.selection)
|
||||
|
||||
const handle = createCellsHandle(createOptionItems(editor))
|
||||
handle.classList.add('table-col-handle')
|
||||
if (isSelected) {
|
||||
handle.classList.add('table-col-handle__selected')
|
||||
}
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
handleMouseDown(col, table, e, editor, isSelected)
|
||||
})
|
||||
|
||||
decorations.push(Decoration.widget(pos, handle))
|
||||
}
|
||||
|
||||
return decorations
|
||||
}
|
||||
|
||||
const handleMouseDown = (
|
||||
col: number,
|
||||
table: TableNodeLocation,
|
||||
event: MouseEvent,
|
||||
editor: Editor,
|
||||
isSelected: boolean
|
||||
): void => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
// select column
|
||||
if (!isSelected) {
|
||||
editor.view.dispatch(selectColumn(table, col, editor.state.tr))
|
||||
}
|
||||
|
||||
// drag column
|
||||
const tableWidthPx = getTableWidthPx(table, editor)
|
||||
const columns = getTableColumns(table, editor)
|
||||
|
||||
let dropIndex = col
|
||||
const startLeft = columns[col].leftPx ?? 0
|
||||
const startX = event.clientX
|
||||
|
||||
const dropMarker = getDropMarker()
|
||||
const dragMarker = getColDragMarker()
|
||||
|
||||
function handleFinish (): void {
|
||||
if (dropMarker !== null) hideDropMarker(dropMarker)
|
||||
if (dragMarker !== null) hideDragMarker(dragMarker)
|
||||
|
||||
if (col !== dropIndex) {
|
||||
let tr = editor.state.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)
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
function handleMove (event: MouseEvent): void {
|
||||
if (dropMarker !== null && dragMarker !== null) {
|
||||
const currentLeft = startLeft + event.clientX - startX
|
||||
dropIndex = calculateColumnDropIndex(col, columns, currentLeft)
|
||||
|
||||
const dragMarkerWidthPx = columns[col].widthPx
|
||||
const dragMarkerLeftPx = Math.max(0, Math.min(currentLeft, tableWidthPx - dragMarkerWidthPx))
|
||||
const dropMarkerLeftPx =
|
||||
dropIndex <= col ? columns[dropIndex].leftPx : columns[dropIndex].leftPx + columns[dropIndex].widthPx
|
||||
|
||||
updateColDropMarker(dropMarker, dropMarkerLeftPx - Math.floor(dropMarkerWidthPx / 2) - 1, dropMarkerWidthPx)
|
||||
updateColDragMarker(dragMarker, dragMarkerLeftPx, dragMarkerWidthPx)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', handleFinish)
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
interface TableColumn {
|
||||
leftPx: number
|
||||
widthPx: number
|
||||
}
|
||||
|
||||
function calculateColumnDropIndex (col: number, columns: TableColumn[], left: number): number {
|
||||
|
@ -14,57 +14,130 @@
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { findTable, haveTableRelatedChanges, insertColumn } from '../utils'
|
||||
import { addSvg } from './icons'
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { insertColumn, isColumnSelected } from '../utils'
|
||||
|
||||
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
||||
|
||||
export const columnInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||
const decorations: Decoration[] = []
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
const { selection } = state
|
||||
interface TableColumnInsertDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const { width } = tableMap
|
||||
export const TableColumnInsertDecorationPlugin = (editor: Editor): Plugin<TableColumnInsertDecorationPluginState> => {
|
||||
const key = new PluginKey('tableColumnInsertDecorationPlugin')
|
||||
return new Plugin<TableColumnInsertDecorationPluginState>({
|
||||
key,
|
||||
state: {
|
||||
init: () => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableHeightPx = getTableHeightPx(table, editor)
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
for (let col = 0; col < width; col++) {
|
||||
const show = col < width - 1 && !isColumnSelected(col, selection) && !isColumnSelected(col + 1, selection)
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const { width } = tableMap
|
||||
|
||||
if (show) {
|
||||
const insert = document.createElement('div')
|
||||
insert.classList.add('table-col-insert')
|
||||
let isStale = false
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
for (let col = 0; col < width - 1; col++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||
if (mapped?.find(pos, pos + 1)?.length !== 1) {
|
||||
isStale = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const button = document.createElement('button')
|
||||
button.className = 'table-insert-button'
|
||||
button.innerHTML = addSvg
|
||||
button.addEventListener('mousedown', (e) => {
|
||||
handleMouseDown(col, table, e, editor)
|
||||
})
|
||||
insert.appendChild(button)
|
||||
if (!isStale) {
|
||||
return { decorations: mapped }
|
||||
}
|
||||
|
||||
const marker = document.createElement('div')
|
||||
marker.className = 'table-insert-marker'
|
||||
marker.style.height = tableHeightPx + 'px'
|
||||
insert.appendChild(marker)
|
||||
for (let col = 0; col < width - 1; col++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||
const handler = new ColumnInsertHandler(editor, { col })
|
||||
decorations.push(Decoration.widget(pos, () => handler.build(), { destroy: handler.destroy }))
|
||||
}
|
||||
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||
decorations.push(Decoration.widget(pos, insert))
|
||||
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface ColumnInsertHandlerProps {
|
||||
col: number
|
||||
}
|
||||
|
||||
class ColumnInsertHandler {
|
||||
editor: Editor
|
||||
props: ColumnInsertHandlerProps
|
||||
destroy?: () => void
|
||||
|
||||
constructor (editor: Editor, props: ColumnInsertHandlerProps) {
|
||||
this.editor = editor
|
||||
this.props = props
|
||||
}
|
||||
|
||||
return decorations
|
||||
}
|
||||
build (): HTMLElement {
|
||||
const editor = this.editor
|
||||
const col = this.props.col
|
||||
|
||||
const handleMouseDown = (col: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const handle = document.createElement('div')
|
||||
handle.classList.add('table-col-insert')
|
||||
|
||||
editor.view.dispatch(insertColumn(table, col + 1, editor.state.tr))
|
||||
const button = document.createElement('button')
|
||||
button.className = 'table-insert-button'
|
||||
button.innerHTML = addSvg
|
||||
button.addEventListener('mousedown', (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
editor.view.dispatch(insertColumn(table, col + 1, editor.state.tr))
|
||||
})
|
||||
handle.appendChild(button)
|
||||
|
||||
const marker = document.createElement('div')
|
||||
marker.className = 'table-insert-marker'
|
||||
|
||||
handle.appendChild(marker)
|
||||
|
||||
const updateMarkerHeight = (): void => {
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
const tableHeightPx = getTableHeightPx(table, editor)
|
||||
marker.style.height = tableHeightPx + 'px'
|
||||
}
|
||||
|
||||
updateMarkerHeight()
|
||||
editor.on('update', updateMarkerHeight)
|
||||
|
||||
if (this.destroy !== undefined) {
|
||||
this.destroy()
|
||||
}
|
||||
this.destroy = () => {
|
||||
editor.off('update', updateMarkerHeight)
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
}
|
||||
|
@ -13,18 +13,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { findTable, getSelectedRows, isRowSelected, selectRow } from '../utils'
|
||||
import { findTable, getSelectedRows, haveTableRelatedChanges, isRowSelected, selectRow } from '../utils'
|
||||
|
||||
import { duplicateRows, moveSelectedRows } from './actions'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import DeleteRow from '../../../icons/table/DeleteRow.svelte'
|
||||
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
||||
import { duplicateRows, moveSelectedRows } from './actions'
|
||||
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
||||
import {
|
||||
dropMarkerWidthPx,
|
||||
@ -32,11 +32,176 @@ import {
|
||||
getRowDragMarker,
|
||||
hideDragMarker,
|
||||
hideDropMarker,
|
||||
updateRowDropMarker,
|
||||
updateRowDragMarker
|
||||
updateRowDragMarker,
|
||||
updateRowDropMarker
|
||||
} from './tableDragMarkerDecoration'
|
||||
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
||||
|
||||
interface TableRowHandlerDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
|
||||
export const TableRowHandlerDecorationPlugin = (editor: Editor): Plugin<TableRowHandlerDecorationPluginState> => {
|
||||
const key = new PluginKey('tableRowHandlerDecorationPlugin')
|
||||
return new Plugin<TableRowHandlerDecorationPluginState>({
|
||||
key,
|
||||
state: {
|
||||
init: () => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
let isStale = false
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
for (let row = 0; row < tableMap.height; row++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||
if (mapped?.find(pos, pos + 1)?.length !== 1) {
|
||||
isStale = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStale) {
|
||||
return { decorations: mapped }
|
||||
}
|
||||
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
for (let row = 0; row < tableMap.height; row++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||
|
||||
const handler = new RowHandler(editor, { row })
|
||||
decorations.push(Decoration.widget(pos, () => handler.build(), { destroy: () => handler.destroy?.() }))
|
||||
}
|
||||
|
||||
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface RowHandlerProps {
|
||||
row: number
|
||||
}
|
||||
|
||||
class RowHandler {
|
||||
editor: Editor
|
||||
props: RowHandlerProps
|
||||
destroy?: () => void
|
||||
|
||||
constructor (editor: Editor, props: RowHandlerProps) {
|
||||
this.editor = editor
|
||||
this.props = props
|
||||
}
|
||||
|
||||
build (): HTMLElement {
|
||||
const editor = this.editor
|
||||
const selection = editor.state.selection
|
||||
const row = this.props.row
|
||||
|
||||
const handle = createCellsHandle(createOptionItems(editor))
|
||||
handle.classList.add('table-row-handle')
|
||||
|
||||
const selectionUpdate = (): boolean => {
|
||||
const isSelected = isRowSelected(row, editor.state.selection)
|
||||
if (isSelected) {
|
||||
handle.classList.add('table-row-handle__selected')
|
||||
} else {
|
||||
handle.classList.add('table-row-handle__selected')
|
||||
}
|
||||
return isSelected
|
||||
}
|
||||
|
||||
editor.on('selectionUpdate', selectionUpdate)
|
||||
|
||||
if (this.destroy !== undefined) {
|
||||
this.destroy()
|
||||
}
|
||||
this.destroy = (): void => {
|
||||
editor.off('selectionUpdate', selectionUpdate)
|
||||
}
|
||||
|
||||
handle.addEventListener('mousedown', (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const isSelected = isRowSelected(row, selection)
|
||||
|
||||
// select row
|
||||
if (!isSelected) {
|
||||
editor.view.dispatch(selectRow(table, row, editor.state.tr))
|
||||
}
|
||||
|
||||
// drag row
|
||||
const tableHeightPx = getTableHeightPx(table, editor)
|
||||
const rows = getTableRows(table, editor)
|
||||
console.log(rows)
|
||||
|
||||
let dropIndex = row
|
||||
const startTop = rows[row].topPx ?? 0
|
||||
const startY = event.clientY
|
||||
|
||||
const dropMarker = getDropMarker()
|
||||
const dragMarker = getRowDragMarker()
|
||||
|
||||
const handleFinish = (): void => {
|
||||
if (dropMarker !== null) hideDropMarker(dropMarker)
|
||||
if (dragMarker !== null) hideDragMarker(dragMarker)
|
||||
|
||||
if (row !== dropIndex) {
|
||||
let tr = editor.state.tr
|
||||
const selection = editor.state.selection
|
||||
if (selection instanceof CellSelection) {
|
||||
const table = findTable(selection)
|
||||
if (table !== undefined) {
|
||||
tr = moveSelectedRows(editor, table, selection, dropIndex, tr)
|
||||
}
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
window.removeEventListener('mouseup', handleFinish)
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
const handleMove = (event: MouseEvent): void => {
|
||||
if (dropMarker !== null && dragMarker !== null) {
|
||||
const cursorTop = startTop + event.clientY - startY
|
||||
dropIndex = calculateRowDropIndex(row, rows, cursorTop)
|
||||
|
||||
const dragMarkerHeightPx = rows[row].heightPx
|
||||
const dragMarkerTopPx = Math.max(0, Math.min(cursorTop, tableHeightPx - dragMarkerHeightPx))
|
||||
const dropMarkerTopPx =
|
||||
dropIndex <= row ? rows[dropIndex].topPx : rows[dropIndex].topPx + rows[dropIndex].heightPx
|
||||
|
||||
updateRowDropMarker(dropMarker, dropMarkerTopPx - dropMarkerWidthPx / 2, dropMarkerWidthPx)
|
||||
updateRowDragMarker(dragMarker, dragMarkerTopPx, dragMarkerHeightPx)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', handleFinish)
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
})
|
||||
|
||||
return handle
|
||||
}
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
topPx: number
|
||||
heightPx: number
|
||||
@ -65,90 +230,6 @@ const createOptionItems = (editor: Editor): OptionItem[] => [
|
||||
}
|
||||
]
|
||||
|
||||
export const rowHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
for (let row = 0; row < tableMap.height; row++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||
const isSelected = isRowSelected(row, state.selection)
|
||||
|
||||
const handle = createCellsHandle(createOptionItems(editor))
|
||||
handle.classList.add('table-row-handle')
|
||||
if (isSelected) {
|
||||
handle.classList.add('table-row-handle__selected')
|
||||
}
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
handleMouseDown(row, table, e, editor, isSelected)
|
||||
})
|
||||
|
||||
decorations.push(Decoration.widget(pos, handle))
|
||||
}
|
||||
|
||||
return decorations
|
||||
}
|
||||
|
||||
const handleMouseDown = (
|
||||
row: number,
|
||||
table: TableNodeLocation,
|
||||
event: MouseEvent,
|
||||
editor: Editor,
|
||||
isSelected: boolean
|
||||
): void => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
// select row
|
||||
if (!isSelected) {
|
||||
editor.view.dispatch(selectRow(table, row, editor.state.tr))
|
||||
}
|
||||
|
||||
// drag row
|
||||
const tableHeightPx = getTableHeightPx(table, editor)
|
||||
const rows = getTableRows(table, editor)
|
||||
|
||||
let dropIndex = row
|
||||
const startTop = rows[row].topPx ?? 0
|
||||
const startY = event.clientY
|
||||
|
||||
const dropMarker = getDropMarker()
|
||||
const dragMarker = getRowDragMarker()
|
||||
|
||||
function handleFinish (): void {
|
||||
if (dropMarker !== null) hideDropMarker(dropMarker)
|
||||
if (dragMarker !== null) hideDragMarker(dragMarker)
|
||||
|
||||
if (row !== dropIndex) {
|
||||
let tr = editor.state.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)
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
function handleMove (event: MouseEvent): void {
|
||||
if (dropMarker !== null && dragMarker !== null) {
|
||||
const cursorTop = startTop + event.clientY - startY
|
||||
dropIndex = calculateRowDropIndex(row, rows, cursorTop)
|
||||
|
||||
const dragMarkerHeightPx = rows[row].heightPx
|
||||
const dragMarkerTopPx = Math.max(0, Math.min(cursorTop, tableHeightPx - dragMarkerHeightPx))
|
||||
const dropMarkerTopPx =
|
||||
dropIndex <= row ? rows[dropIndex].topPx : rows[dropIndex].topPx + rows[dropIndex].heightPx
|
||||
|
||||
updateRowDropMarker(dropMarker, dropMarkerTopPx - dropMarkerWidthPx / 2, dropMarkerWidthPx)
|
||||
updateRowDragMarker(dragMarker, dragMarkerTopPx, dragMarkerHeightPx)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', handleFinish)
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
function calculateRowDropIndex (row: number, rows: TableRow[], top: number): number {
|
||||
const rowCenterPx = top + rows[row].heightPx / 2
|
||||
const index = rows.findIndex((p) => rowCenterPx <= p.topPx + p.heightPx)
|
||||
@ -159,9 +240,9 @@ function getTableRows (table: TableNodeLocation, editor: Editor): TableRow[] {
|
||||
const result = []
|
||||
let topPx = 0
|
||||
|
||||
const { map, height } = TableMap.get(table.node)
|
||||
for (let row = 0; row < height; row++) {
|
||||
const dom = editor.view.domAtPos(table.start + map[row] + 1)
|
||||
const tableMap = TableMap.get(table.node)
|
||||
for (let row = 0; row < tableMap.height; row++) {
|
||||
const dom = editor.view.domAtPos(table.start + tableMap.map[row * tableMap.width])
|
||||
if (dom.node instanceof HTMLElement) {
|
||||
const heightPx = dom.node.offsetHeight
|
||||
result.push({ topPx, heightPx })
|
||||
|
@ -14,57 +14,129 @@
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { findTable, haveTableRelatedChanges, insertRow } from '../utils'
|
||||
import { addSvg } from './icons'
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { insertRow, isRowSelected } from '../utils'
|
||||
|
||||
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||
|
||||
export const rowInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
||||
const decorations: Decoration[] = []
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
const { selection } = state
|
||||
interface TableRowInsertDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const { height } = tableMap
|
||||
export const TableRowInsertDecorationPlugin = (editor: Editor): Plugin<TableRowInsertDecorationPluginState> => {
|
||||
const key = new PluginKey('tableRowInsertDecorationPlugin')
|
||||
return new Plugin<TableRowInsertDecorationPluginState>({
|
||||
key,
|
||||
state: {
|
||||
init: () => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableWidthPx = getTableWidthPx(table, editor)
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const { height } = tableMap
|
||||
|
||||
for (let row = 0; row < height; row++) {
|
||||
const show = row < height - 1 && !isRowSelected(row, selection) && !isRowSelected(row + 1, selection)
|
||||
let isStale = false
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
for (let row = 0; row < height - 1; row++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||
if (mapped?.find(pos, pos + 1)?.length !== 1) {
|
||||
isStale = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (show) {
|
||||
const dot = document.createElement('div')
|
||||
dot.classList.add('table-row-insert')
|
||||
if (!isStale) {
|
||||
return { decorations: mapped }
|
||||
}
|
||||
|
||||
const button = document.createElement('button')
|
||||
button.className = 'table-insert-button'
|
||||
button.innerHTML = addSvg
|
||||
button.addEventListener('mousedown', (e) => {
|
||||
handleMouseDown(row, table, e, editor)
|
||||
})
|
||||
dot.appendChild(button)
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
const marker = document.createElement('div')
|
||||
marker.className = 'table-insert-marker'
|
||||
marker.style.width = tableWidthPx + 'px'
|
||||
dot.appendChild(marker)
|
||||
for (let row = 0; row < height - 1; row++) {
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||
const handler = new RowInsertHandler(editor, { row })
|
||||
decorations.push(Decoration.widget(pos, () => handler.build(), { destroy: handler.destroy }))
|
||||
}
|
||||
|
||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||
decorations.push(Decoration.widget(pos, dot))
|
||||
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface RowInsertHandlerProps {
|
||||
row: number
|
||||
}
|
||||
|
||||
class RowInsertHandler {
|
||||
editor: Editor
|
||||
props: RowInsertHandlerProps
|
||||
destroy?: () => void
|
||||
|
||||
constructor (editor: Editor, props: RowInsertHandlerProps) {
|
||||
this.editor = editor
|
||||
this.props = props
|
||||
}
|
||||
|
||||
return decorations
|
||||
}
|
||||
build (): HTMLElement {
|
||||
const editor = this.editor
|
||||
const row = this.props.row
|
||||
|
||||
const handleMouseDown = (row: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const handle = document.createElement('div')
|
||||
handle.classList.add('table-row-insert')
|
||||
|
||||
editor.view.dispatch(insertRow(table, row + 1, editor.state.tr))
|
||||
const button = document.createElement('button')
|
||||
button.className = 'table-insert-button'
|
||||
button.innerHTML = addSvg
|
||||
button.addEventListener('mousedown', (event) => {
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
||||
editor.view.dispatch(insertRow(table, row + 1, editor.state.tr))
|
||||
})
|
||||
handle.appendChild(button)
|
||||
|
||||
const marker = document.createElement('div')
|
||||
marker.className = 'table-insert-marker'
|
||||
handle.appendChild(marker)
|
||||
|
||||
const updateMarkerHeight = (): void => {
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
const tableWidthPx = getTableWidthPx(table, editor)
|
||||
marker.style.width = tableWidthPx + 'px'
|
||||
}
|
||||
|
||||
updateMarkerHeight()
|
||||
editor.on('update', updateMarkerHeight)
|
||||
|
||||
if (this.destroy !== undefined) {
|
||||
this.destroy()
|
||||
}
|
||||
this.destroy = () => {
|
||||
editor.off('update', updateMarkerHeight)
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
}
|
||||
|
@ -13,11 +13,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { findTable, haveTableRelatedChanges } from '../utils'
|
||||
import { handleSvg } from './icons'
|
||||
import { type TableNodeLocation } from '../types'
|
||||
|
||||
export const dropMarkerId = 'table-drop-marker'
|
||||
export const colDragMarkerId = 'table-col-drag-marker'
|
||||
@ -25,30 +26,85 @@ export const rowDragMarkerId = 'table-row-drag-marker'
|
||||
|
||||
export const dropMarkerWidthPx = 1
|
||||
|
||||
export const tableDragMarkerDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
||||
const dropMarker = document.createElement('div')
|
||||
dropMarker.id = dropMarkerId
|
||||
dropMarker.classList.add('table-drop-marker')
|
||||
interface TableDragMarkerDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
|
||||
const colDragMarker = document.createElement('div')
|
||||
colDragMarker.id = colDragMarkerId
|
||||
colDragMarker.classList.add('table-col-drag-marker')
|
||||
colDragMarker.style.display = 'none'
|
||||
const colDragMarkerBtn = colDragMarker.appendChild(document.createElement('button'))
|
||||
colDragMarkerBtn.innerHTML = handleSvg
|
||||
export const TableDragMarkerDecorationPlugin = (editor: Editor): Plugin<TableDragMarkerDecorationPluginState> => {
|
||||
const key = new PluginKey('table-cell-drag-marker-decoration-plugin')
|
||||
return new Plugin<TableDragMarkerDecorationPluginState>({
|
||||
key,
|
||||
state: {
|
||||
init: () => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const rowDragMarker = document.createElement('div')
|
||||
rowDragMarker.id = rowDragMarkerId
|
||||
rowDragMarker.classList.add('table-row-drag-marker')
|
||||
rowDragMarker.style.display = 'none'
|
||||
const rowDragMarkerBtn = rowDragMarker.appendChild(document.createElement('button'))
|
||||
rowDragMarkerBtn.innerHTML = handleSvg
|
||||
if (prev.decorations !== undefined) {
|
||||
const mapped = prev.decorations.map(tr.mapping, tr.doc)
|
||||
const existing = mapped.find(table.start, table.start + 1)
|
||||
if (existing.length > 0) {
|
||||
return { decorations: mapped }
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
Decoration.widget(table.start, dropMarker),
|
||||
Decoration.widget(table.start, colDragMarker),
|
||||
Decoration.widget(table.start, rowDragMarker)
|
||||
]
|
||||
const decorations = DecorationSet.create(newState.doc, [
|
||||
Decoration.widget(table.start, () => createMarkerContainer())
|
||||
])
|
||||
|
||||
return { decorations }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function createMarkerContainer (): HTMLElement {
|
||||
const el = document.createElement('div')
|
||||
el.classList.add('table-drag-marker-container')
|
||||
el.appendChild(createDropMarker())
|
||||
el.appendChild(createColDragMarker())
|
||||
el.appendChild(createRowDragMarker())
|
||||
return el
|
||||
}
|
||||
|
||||
function createDropMarker (): DropMarkerHTMLElement {
|
||||
const el = document.createElement('div')
|
||||
el.id = dropMarkerId
|
||||
el.classList.add('table-drop-marker')
|
||||
return el
|
||||
}
|
||||
|
||||
function createColDragMarker (): DragMarkerHTMLElement {
|
||||
const el = document.createElement('div')
|
||||
el.id = colDragMarkerId
|
||||
el.classList.add('table-col-drag-marker')
|
||||
el.style.display = 'none'
|
||||
|
||||
const btn = el.appendChild(document.createElement('button'))
|
||||
btn.innerHTML = handleSvg
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
function createRowDragMarker (): DragMarkerHTMLElement {
|
||||
const el = document.createElement('div')
|
||||
el.id = rowDragMarkerId
|
||||
el.classList.add('table-row-drag-marker')
|
||||
el.style.display = 'none'
|
||||
|
||||
const btn = el.appendChild(document.createElement('button'))
|
||||
btn.innerHTML = handleSvg
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
export type DropMarkerHTMLElement = HTMLElement
|
||||
|
@ -13,43 +13,71 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type EditorState } from '@tiptap/pm/state'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration } from '@tiptap/pm/view'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { type TableNodeLocation } from '../types'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
export const tableSelectionDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
||||
const decorations: Decoration[] = []
|
||||
import { findTable, haveTableRelatedChanges } from '../utils'
|
||||
|
||||
const { selection } = state
|
||||
interface TableSelectionDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
export const TableSelectionDecorationPlugin = (editor: Editor): Plugin<TableSelectionDecorationPluginState> => {
|
||||
const key = new PluginKey('tableSelectionDecorationPlugin')
|
||||
return new Plugin<TableSelectionDecorationPluginState>({
|
||||
key,
|
||||
state: {
|
||||
init: () => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
if (selection instanceof CellSelection) {
|
||||
const selected: number[] = []
|
||||
const { selection } = newState
|
||||
if (!(selection instanceof CellSelection)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
selection.forEachCell((_node, pos) => {
|
||||
const start = pos - table.pos - 1
|
||||
selected.push(start)
|
||||
})
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
selection.forEachCell((node, pos) => {
|
||||
const start = pos - table.pos - 1
|
||||
const borders = getTableCellBorders(start, selected, tableMap)
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
const classes = ['table-cell-selected']
|
||||
const selected: number[] = []
|
||||
|
||||
if (borders.top) classes.push('table-cell-selected__border-top')
|
||||
if (borders.bottom) classes.push('table-cell-selected__border-bottom')
|
||||
if (borders.left) classes.push('table-cell-selected__border-left')
|
||||
if (borders.right) classes.push('table-cell-selected__border-right')
|
||||
selection.forEachCell((_node, pos) => {
|
||||
const start = pos - table.pos - 1
|
||||
selected.push(start)
|
||||
})
|
||||
|
||||
decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(' ') }))
|
||||
})
|
||||
}
|
||||
selection.forEachCell((node, pos) => {
|
||||
const start = pos - table.pos - 1
|
||||
const borders = getTableCellBorders(start, selected, tableMap)
|
||||
|
||||
return decorations
|
||||
const classes = ['table-cell-selected']
|
||||
|
||||
if (borders.top) classes.push('table-cell-selected__border-top')
|
||||
if (borders.bottom) classes.push('table-cell-selected__border-bottom')
|
||||
if (borders.left) classes.push('table-cell-selected__border-left')
|
||||
if (borders.right) classes.push('table-cell-selected__border-right')
|
||||
|
||||
decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(' ') }))
|
||||
})
|
||||
|
||||
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getTableCellBorders (
|
||||
|
@ -13,75 +13,34 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||
import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
|
||||
import { DecorationSet } from '@tiptap/pm/view'
|
||||
import { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
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 { findTable } from './utils'
|
||||
import { type Node } from '@tiptap/pm/model'
|
||||
import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
|
||||
import { TableColumnHandlerDecorationPlugin } from './decorations/columnHandlerDecoration'
|
||||
import { TableColumnInsertDecorationPlugin } from './decorations/columnInsertDecoration'
|
||||
import { TableRowHandlerDecorationPlugin } from './decorations/rowHandlerDecoration'
|
||||
import { TableRowInsertDecorationPlugin } from './decorations/rowInsertDecoration'
|
||||
import { TableDragMarkerDecorationPlugin } from './decorations/tableDragMarkerDecoration'
|
||||
import { TableSelectionDecorationPlugin } from './decorations/tableSelectionDecoration'
|
||||
import { findTable } from './utils'
|
||||
|
||||
export const TableCell = TiptapTableCell.extend({
|
||||
addProseMirrorPlugins () {
|
||||
return [tableCellDecorationPlugin(this.editor), tableSelectionNormalizer()]
|
||||
return [
|
||||
TableSelectionNormalizerPlugin(),
|
||||
TableSelectionDecorationPlugin(this.editor),
|
||||
TableDragMarkerDecorationPlugin(this.editor),
|
||||
TableColumnHandlerDecorationPlugin(this.editor),
|
||||
TableColumnInsertDecorationPlugin(this.editor),
|
||||
TableRowHandlerDecorationPlugin(this.editor),
|
||||
TableRowInsertDecorationPlugin(this.editor)
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
interface TableCellDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
selection?: Selection
|
||||
}
|
||||
|
||||
const tableCellDecorationPlugin = (editor: Editor): Plugin<TableCellDecorationPluginState> => {
|
||||
const key = new PluginKey('table-cell-decoration-plugin')
|
||||
return new Plugin({
|
||||
key,
|
||||
state: {
|
||||
init: (): TableCellDecorationPluginState => {
|
||||
return {}
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
if (!editor.isEditable) {
|
||||
return { selection: newState.selection, decorations: DecorationSet.empty }
|
||||
}
|
||||
|
||||
const newTable = findTable(newState.selection)
|
||||
|
||||
if (newTable === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (prev.selection === newState.selection) {
|
||||
return prev
|
||||
}
|
||||
|
||||
const decorations = DecorationSet.create(newState.doc, [
|
||||
...tableSelectionDecoration(newState, newTable),
|
||||
...tableDragMarkerDecoration(newState, newTable),
|
||||
...columnHandlerDecoration(newState, newTable, editor),
|
||||
...columnInsertDecoration(newState, newTable, editor),
|
||||
...rowHandlerDecoration(newState, newTable, editor),
|
||||
...rowInsertDecoration(newState, newTable, editor)
|
||||
])
|
||||
return { selection: newState.selection, decorations }
|
||||
}
|
||||
},
|
||||
props: {
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const tableSelectionNormalizer = (): Plugin<any> => {
|
||||
const TableSelectionNormalizerPlugin = (): Plugin<any> => {
|
||||
return new Plugin({
|
||||
appendTransaction: (transactions, oldState, newState) => {
|
||||
const selection = newState.selection
|
||||
|
@ -13,11 +13,11 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { findParentNode } from '@tiptap/core'
|
||||
import { type Selection, type Transaction } from '@tiptap/pm/state'
|
||||
import { type Editor, findParentNode } from '@tiptap/core'
|
||||
import { type EditorState, type Selection, type Transaction } from '@tiptap/pm/state'
|
||||
import { CellSelection, type Rect, TableMap, addColumn, addRow } from '@tiptap/pm/tables'
|
||||
|
||||
import { TableSelection, type TableNodeLocation } from './types'
|
||||
import { type TableNodeLocation, TableSelection } from './types'
|
||||
|
||||
export function insertColumn (table: TableNodeLocation, index: number, tr: Transaction): Transaction {
|
||||
const map = TableMap.get(table.node)
|
||||
@ -138,3 +138,13 @@ export const isRectSelected = (rect: Rect, selection: CellSelection): boolean =>
|
||||
export const findTable = (selection: Selection): TableNodeLocation | undefined => {
|
||||
return findParentNode((node) => node.type.spec.tableRole === 'table')(selection)
|
||||
}
|
||||
|
||||
export function haveTableRelatedChanges (
|
||||
editor: Editor,
|
||||
table: TableNodeLocation | undefined,
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
tr: Transaction
|
||||
): table is TableNodeLocation {
|
||||
return editor.isEditable && table !== undefined && (tr.docChanged || !newState.selection.eq(oldState.selection))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user