mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-14 19:36:15 +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.
|
// 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 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 { 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 DeleteCol from '../../../icons/table/DeleteCol.svelte'
|
||||||
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
||||||
|
import { duplicateColumns, moveSelectedColumns } from './actions'
|
||||||
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
||||||
import {
|
import {
|
||||||
dropMarkerWidthPx,
|
dropMarkerWidthPx,
|
||||||
@ -32,14 +31,176 @@ import {
|
|||||||
getDropMarker,
|
getDropMarker,
|
||||||
hideDragMarker,
|
hideDragMarker,
|
||||||
hideDropMarker,
|
hideDropMarker,
|
||||||
updateColDropMarker,
|
updateColDragMarker,
|
||||||
updateColDragMarker
|
updateColDropMarker
|
||||||
} from './tableDragMarkerDecoration'
|
} from './tableDragMarkerDecoration'
|
||||||
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||||
|
|
||||||
interface TableColumn {
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
leftPx: number
|
|
||||||
widthPx: number
|
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[] => [
|
const createOptionItems = (editor: Editor): OptionItem[] => [
|
||||||
@ -65,88 +226,9 @@ const createOptionItems = (editor: Editor): OptionItem[] => [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const columnHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
interface TableColumn {
|
||||||
const decorations: Decoration[] = []
|
leftPx: number
|
||||||
|
widthPx: number
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateColumnDropIndex (col: number, columns: TableColumn[], left: number): number {
|
function calculateColumnDropIndex (col: number, columns: TableColumn[], left: number): number {
|
||||||
|
@ -14,57 +14,130 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { type Editor } from '@tiptap/core'
|
import { type Editor } from '@tiptap/core'
|
||||||
import { type EditorState } from '@tiptap/pm/state'
|
|
||||||
import { TableMap } from '@tiptap/pm/tables'
|
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 { addSvg } from './icons'
|
||||||
import { type TableNodeLocation } from '../types'
|
|
||||||
import { insertColumn, isColumnSelected } from '../utils'
|
|
||||||
|
|
||||||
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
||||||
|
|
||||||
export const columnInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
interface TableColumnInsertDecorationPluginState {
|
||||||
|
decorations?: DecorationSet
|
||||||
|
}
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
export const TableColumnInsertDecorationPlugin = (editor: Editor): Plugin<TableColumnInsertDecorationPluginState> => {
|
||||||
const { width } = tableMap
|
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 tableMap = TableMap.get(table.node)
|
||||||
const show = col < width - 1 && !isColumnSelected(col, selection) && !isColumnSelected(col + 1, selection)
|
const { width } = tableMap
|
||||||
|
|
||||||
if (show) {
|
let isStale = false
|
||||||
const insert = document.createElement('div')
|
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||||
insert.classList.add('table-col-insert')
|
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')
|
if (!isStale) {
|
||||||
button.className = 'table-insert-button'
|
return { decorations: mapped }
|
||||||
button.innerHTML = addSvg
|
}
|
||||||
button.addEventListener('mousedown', (e) => {
|
|
||||||
handleMouseDown(col, table, e, editor)
|
|
||||||
})
|
|
||||||
insert.appendChild(button)
|
|
||||||
|
|
||||||
const marker = document.createElement('div')
|
for (let col = 0; col < width - 1; col++) {
|
||||||
marker.className = 'table-insert-marker'
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||||
marker.style.height = tableHeightPx + 'px'
|
const handler = new ColumnInsertHandler(editor, { col })
|
||||||
insert.appendChild(marker)
|
decorations.push(Decoration.widget(pos, () => handler.build(), { destroy: handler.destroy }))
|
||||||
|
}
|
||||||
|
|
||||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||||
decorations.push(Decoration.widget(pos, insert))
|
}
|
||||||
|
},
|
||||||
|
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 => {
|
const handle = document.createElement('div')
|
||||||
event.stopPropagation()
|
handle.classList.add('table-col-insert')
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
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.
|
// 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 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 { 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 DeleteRow from '../../../icons/table/DeleteRow.svelte'
|
||||||
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
import Duplicate from '../../../icons/table/Duplicate.svelte'
|
||||||
|
import { duplicateRows, moveSelectedRows } from './actions'
|
||||||
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
import { createCellsHandle, type OptionItem } from './cellsHandle'
|
||||||
import {
|
import {
|
||||||
dropMarkerWidthPx,
|
dropMarkerWidthPx,
|
||||||
@ -32,11 +32,176 @@ import {
|
|||||||
getRowDragMarker,
|
getRowDragMarker,
|
||||||
hideDragMarker,
|
hideDragMarker,
|
||||||
hideDropMarker,
|
hideDropMarker,
|
||||||
updateRowDropMarker,
|
updateRowDragMarker,
|
||||||
updateRowDragMarker
|
updateRowDropMarker
|
||||||
} from './tableDragMarkerDecoration'
|
} from './tableDragMarkerDecoration'
|
||||||
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
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 {
|
interface TableRow {
|
||||||
topPx: number
|
topPx: number
|
||||||
heightPx: 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 {
|
function calculateRowDropIndex (row: number, rows: TableRow[], top: number): number {
|
||||||
const rowCenterPx = top + rows[row].heightPx / 2
|
const rowCenterPx = top + rows[row].heightPx / 2
|
||||||
const index = rows.findIndex((p) => rowCenterPx <= p.topPx + p.heightPx)
|
const index = rows.findIndex((p) => rowCenterPx <= p.topPx + p.heightPx)
|
||||||
@ -159,9 +240,9 @@ function getTableRows (table: TableNodeLocation, editor: Editor): TableRow[] {
|
|||||||
const result = []
|
const result = []
|
||||||
let topPx = 0
|
let topPx = 0
|
||||||
|
|
||||||
const { map, height } = TableMap.get(table.node)
|
const tableMap = TableMap.get(table.node)
|
||||||
for (let row = 0; row < height; row++) {
|
for (let row = 0; row < tableMap.height; row++) {
|
||||||
const dom = editor.view.domAtPos(table.start + map[row] + 1)
|
const dom = editor.view.domAtPos(table.start + tableMap.map[row * tableMap.width])
|
||||||
if (dom.node instanceof HTMLElement) {
|
if (dom.node instanceof HTMLElement) {
|
||||||
const heightPx = dom.node.offsetHeight
|
const heightPx = dom.node.offsetHeight
|
||||||
result.push({ topPx, heightPx })
|
result.push({ topPx, heightPx })
|
||||||
|
@ -14,57 +14,129 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { type Editor } from '@tiptap/core'
|
import { type Editor } from '@tiptap/core'
|
||||||
import { type EditorState } from '@tiptap/pm/state'
|
|
||||||
import { TableMap } from '@tiptap/pm/tables'
|
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 { addSvg } from './icons'
|
||||||
import { type TableNodeLocation } from '../types'
|
|
||||||
import { insertRow, isRowSelected } from '../utils'
|
|
||||||
|
|
||||||
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||||
|
|
||||||
export const rowInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
interface TableRowInsertDecorationPluginState {
|
||||||
|
decorations?: DecorationSet
|
||||||
|
}
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
export const TableRowInsertDecorationPlugin = (editor: Editor): Plugin<TableRowInsertDecorationPluginState> => {
|
||||||
const { height } = tableMap
|
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++) {
|
let isStale = false
|
||||||
const show = row < height - 1 && !isRowSelected(row, selection) && !isRowSelected(row + 1, selection)
|
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) {
|
if (!isStale) {
|
||||||
const dot = document.createElement('div')
|
return { decorations: mapped }
|
||||||
dot.classList.add('table-row-insert')
|
}
|
||||||
|
|
||||||
const button = document.createElement('button')
|
const decorations: Decoration[] = []
|
||||||
button.className = 'table-insert-button'
|
|
||||||
button.innerHTML = addSvg
|
|
||||||
button.addEventListener('mousedown', (e) => {
|
|
||||||
handleMouseDown(row, table, e, editor)
|
|
||||||
})
|
|
||||||
dot.appendChild(button)
|
|
||||||
|
|
||||||
const marker = document.createElement('div')
|
for (let row = 0; row < height - 1; row++) {
|
||||||
marker.className = 'table-insert-marker'
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||||
marker.style.width = tableWidthPx + 'px'
|
const handler = new RowInsertHandler(editor, { row })
|
||||||
dot.appendChild(marker)
|
decorations.push(Decoration.widget(pos, () => handler.build(), { destroy: handler.destroy }))
|
||||||
|
}
|
||||||
|
|
||||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
return { decorations: DecorationSet.create(newState.doc, decorations) }
|
||||||
decorations.push(Decoration.widget(pos, dot))
|
}
|
||||||
|
},
|
||||||
|
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 => {
|
const handle = document.createElement('div')
|
||||||
event.stopPropagation()
|
handle.classList.add('table-row-insert')
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
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.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type EditorState } from '@tiptap/pm/state'
|
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||||
import { Decoration } from '@tiptap/pm/view'
|
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
import { type Editor } from '@tiptap/core'
|
||||||
|
import { findTable, haveTableRelatedChanges } from '../utils'
|
||||||
import { handleSvg } from './icons'
|
import { handleSvg } from './icons'
|
||||||
import { type TableNodeLocation } from '../types'
|
|
||||||
|
|
||||||
export const dropMarkerId = 'table-drop-marker'
|
export const dropMarkerId = 'table-drop-marker'
|
||||||
export const colDragMarkerId = 'table-col-drag-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 dropMarkerWidthPx = 1
|
||||||
|
|
||||||
export const tableDragMarkerDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
interface TableDragMarkerDecorationPluginState {
|
||||||
const dropMarker = document.createElement('div')
|
decorations?: DecorationSet
|
||||||
dropMarker.id = dropMarkerId
|
}
|
||||||
dropMarker.classList.add('table-drop-marker')
|
|
||||||
|
|
||||||
const colDragMarker = document.createElement('div')
|
export const TableDragMarkerDecorationPlugin = (editor: Editor): Plugin<TableDragMarkerDecorationPluginState> => {
|
||||||
colDragMarker.id = colDragMarkerId
|
const key = new PluginKey('table-cell-drag-marker-decoration-plugin')
|
||||||
colDragMarker.classList.add('table-col-drag-marker')
|
return new Plugin<TableDragMarkerDecorationPluginState>({
|
||||||
colDragMarker.style.display = 'none'
|
key,
|
||||||
const colDragMarkerBtn = colDragMarker.appendChild(document.createElement('button'))
|
state: {
|
||||||
colDragMarkerBtn.innerHTML = handleSvg
|
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')
|
if (prev.decorations !== undefined) {
|
||||||
rowDragMarker.id = rowDragMarkerId
|
const mapped = prev.decorations.map(tr.mapping, tr.doc)
|
||||||
rowDragMarker.classList.add('table-row-drag-marker')
|
const existing = mapped.find(table.start, table.start + 1)
|
||||||
rowDragMarker.style.display = 'none'
|
if (existing.length > 0) {
|
||||||
const rowDragMarkerBtn = rowDragMarker.appendChild(document.createElement('button'))
|
return { decorations: mapped }
|
||||||
rowDragMarkerBtn.innerHTML = handleSvg
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
const decorations = DecorationSet.create(newState.doc, [
|
||||||
Decoration.widget(table.start, dropMarker),
|
Decoration.widget(table.start, () => createMarkerContainer())
|
||||||
Decoration.widget(table.start, colDragMarker),
|
])
|
||||||
Decoration.widget(table.start, rowDragMarker)
|
|
||||||
]
|
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
|
export type DropMarkerHTMLElement = HTMLElement
|
||||||
|
@ -13,43 +13,71 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type EditorState } from '@tiptap/pm/state'
|
|
||||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
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[] => {
|
import { findTable, haveTableRelatedChanges } from '../utils'
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
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 { selection } = newState
|
||||||
const selected: number[] = []
|
if (!(selection instanceof CellSelection)) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
selection.forEachCell((_node, pos) => {
|
const decorations: Decoration[] = []
|
||||||
const start = pos - table.pos - 1
|
|
||||||
selected.push(start)
|
|
||||||
})
|
|
||||||
|
|
||||||
selection.forEachCell((node, pos) => {
|
const tableMap = TableMap.get(table.node)
|
||||||
const start = pos - table.pos - 1
|
|
||||||
const borders = getTableCellBorders(start, selected, tableMap)
|
|
||||||
|
|
||||||
const classes = ['table-cell-selected']
|
const selected: number[] = []
|
||||||
|
|
||||||
if (borders.top) classes.push('table-cell-selected__border-top')
|
selection.forEachCell((_node, pos) => {
|
||||||
if (borders.bottom) classes.push('table-cell-selected__border-bottom')
|
const start = pos - table.pos - 1
|
||||||
if (borders.left) classes.push('table-cell-selected__border-left')
|
selected.push(start)
|
||||||
if (borders.right) classes.push('table-cell-selected__border-right')
|
})
|
||||||
|
|
||||||
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 (
|
function getTableCellBorders (
|
||||||
|
@ -13,75 +13,34 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Editor } from '@tiptap/core'
|
|
||||||
import TiptapTableCell from '@tiptap/extension-table-cell'
|
import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||||
import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
|
import { Plugin } from '@tiptap/pm/state'
|
||||||
import { DecorationSet } from '@tiptap/pm/view'
|
|
||||||
|
|
||||||
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 { 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({
|
export const TableCell = TiptapTableCell.extend({
|
||||||
addProseMirrorPlugins () {
|
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 {
|
const TableSelectionNormalizerPlugin = (): Plugin<any> => {
|
||||||
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> => {
|
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
appendTransaction: (transactions, oldState, newState) => {
|
appendTransaction: (transactions, oldState, newState) => {
|
||||||
const selection = newState.selection
|
const selection = newState.selection
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { findParentNode } from '@tiptap/core'
|
import { type Editor, findParentNode } from '@tiptap/core'
|
||||||
import { type Selection, type Transaction } from '@tiptap/pm/state'
|
import { type EditorState, type Selection, type Transaction } from '@tiptap/pm/state'
|
||||||
import { CellSelection, type Rect, TableMap, addColumn, addRow } from '@tiptap/pm/tables'
|
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 {
|
export function insertColumn (table: TableNodeLocation, index: number, tr: Transaction): Transaction {
|
||||||
const map = TableMap.get(table.node)
|
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 => {
|
export const findTable = (selection: Selection): TableNodeLocation | undefined => {
|
||||||
return findParentNode((node) => node.type.spec.tableRole === 'table')(selection)
|
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