mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 03:54:57 +02:00
Merge branch 'develop' into staging-new
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -56,6 +56,8 @@ function posAtLeftMenuElement (view: EditorView, leftMenuElement: HTMLElement, o
|
||||
function LeftMenu (options: LeftMenuOptions): Plugin {
|
||||
let leftMenuElement: HTMLElement | null = null
|
||||
const offsetX = options.width + options.marginX
|
||||
let rafId: number | null = null
|
||||
let styleCache = new WeakMap<HTMLElement, { lineHeight: number, paddingTop: number, marginTop: number }>()
|
||||
|
||||
function hideLeftMenu (): void {
|
||||
if (leftMenuElement !== null) {
|
||||
@@ -69,6 +71,19 @@ function LeftMenu (options: LeftMenuOptions): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
function getCachedStyle (node: HTMLElement): { lineHeight: number, paddingTop: number, marginTop: number } {
|
||||
let cached = styleCache.get(node)
|
||||
if (cached === undefined) {
|
||||
const compStyle = window.getComputedStyle(node)
|
||||
const lineHeight = parseInt(compStyle.lineHeight, 10)
|
||||
const paddingTop = parseInt(compStyle.paddingTop, 10)
|
||||
const marginTop = parseInt(compStyle.marginTop, 10)
|
||||
cached = { lineHeight, paddingTop, marginTop }
|
||||
styleCache.set(node, cached)
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
return new Plugin({
|
||||
key: new PluginKey('left-menu'),
|
||||
view: (view) => {
|
||||
@@ -117,8 +132,13 @@ function LeftMenu (options: LeftMenuOptions): Plugin {
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
leftMenuElement?.remove?.()
|
||||
leftMenuElement = null
|
||||
styleCache = new WeakMap()
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -129,52 +149,65 @@ function LeftMenu (options: LeftMenuOptions): Plugin {
|
||||
return
|
||||
}
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + offsetX,
|
||||
y: event.clientY
|
||||
if (rafId !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = null
|
||||
|
||||
const node = nodeDOMAtCoords({
|
||||
x: event.clientX + offsetX,
|
||||
y: event.clientY
|
||||
})
|
||||
|
||||
if (!(node instanceof HTMLElement) || node.nodeName === 'HR') {
|
||||
hideLeftMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const parent = node?.parentElement
|
||||
if (!(parent instanceof HTMLElement)) {
|
||||
hideLeftMenu()
|
||||
return
|
||||
}
|
||||
|
||||
// For some reason the offsetTop value for all elements is shifted by the first element's margin
|
||||
// so taking it into account here
|
||||
let firstMargin = 0
|
||||
const firstChild = parent.firstChild
|
||||
if (firstChild !== null && firstChild instanceof HTMLElement) {
|
||||
const { marginTop } = getCachedStyle(firstChild)
|
||||
firstMargin = marginTop
|
||||
}
|
||||
|
||||
const { lineHeight, paddingTop } = getCachedStyle(node)
|
||||
const left = -offsetX
|
||||
let top = node.offsetTop
|
||||
top += (lineHeight - options.height) / 2
|
||||
top += paddingTop
|
||||
top += firstMargin
|
||||
|
||||
if (leftMenuElement === null) return
|
||||
|
||||
leftMenuElement.style.left = `${left}px`
|
||||
leftMenuElement.style.top = `${top}px`
|
||||
|
||||
showLeftMenu()
|
||||
})
|
||||
|
||||
if (!(node instanceof HTMLElement) || node.nodeName === 'HR') {
|
||||
hideLeftMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const parent = node?.parentElement
|
||||
if (!(parent instanceof HTMLElement)) {
|
||||
hideLeftMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const compStyle = window.getComputedStyle(node)
|
||||
const lineHeight = parseInt(compStyle.lineHeight, 10)
|
||||
const paddingTop = parseInt(compStyle.paddingTop, 10)
|
||||
|
||||
// For some reason the offsetTop value for all elements is shifted by the first element's margin
|
||||
// so taking it into account here
|
||||
let firstMargin = 0
|
||||
const firstChild = parent.firstChild
|
||||
if (firstChild !== null) {
|
||||
const firstChildCompStyle = window.getComputedStyle(firstChild as HTMLElement)
|
||||
firstMargin = parseInt(firstChildCompStyle.marginTop, 10)
|
||||
}
|
||||
|
||||
const left = -offsetX
|
||||
let top = node.offsetTop
|
||||
top += (lineHeight - options.height) / 2
|
||||
top += paddingTop
|
||||
top += firstMargin
|
||||
|
||||
if (leftMenuElement === null) return
|
||||
|
||||
leftMenuElement.style.left = `${left}px`
|
||||
leftMenuElement.style.top = `${top}px`
|
||||
|
||||
showLeftMenu()
|
||||
},
|
||||
keydown: () => {
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
hideLeftMenu()
|
||||
},
|
||||
mousewheel: () => {
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
hideLeftMenu()
|
||||
},
|
||||
mouseleave: (view, event) => {
|
||||
|
||||
+38
-7
@@ -15,6 +15,7 @@
|
||||
|
||||
import textEditor from '@hcengineering/text-editor'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
@@ -34,12 +35,12 @@ import {
|
||||
updateColDragMarker,
|
||||
updateColDropMarker
|
||||
} from './tableDragMarkerDecoration'
|
||||
import { TableCachePluginKey } from './plugins'
|
||||
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
interface TableColumnHandlerDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
debounceTimeout?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
export const TableColumnHandlerDecorationPlugin = (editor: Editor): Plugin<TableColumnHandlerDecorationPluginState> => {
|
||||
@@ -52,11 +53,35 @@ export const TableColumnHandlerDecorationPlugin = (editor: Editor): Plugin<Table
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
|
||||
if (table === undefined && prev.debounceTimeout !== undefined) {
|
||||
clearTimeout(prev.debounceTimeout)
|
||||
return {}
|
||||
}
|
||||
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const cache = TableCachePluginKey.getState(newState)
|
||||
const tableMap = cache?.tableMap ?? TableMap.get(table.node)
|
||||
|
||||
if (tr.docChanged && tr.steps.length === 1 && !(newState.selection instanceof CellSelection)) {
|
||||
if (prev.debounceTimeout !== undefined) {
|
||||
clearTimeout(prev.debounceTimeout)
|
||||
}
|
||||
|
||||
const debounceTimeout = setTimeout(() => {
|
||||
editor.view.updateState(editor.state)
|
||||
}, 100)
|
||||
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
return { decorations: mapped, debounceTimeout }
|
||||
}
|
||||
|
||||
if (prev.debounceTimeout !== undefined) {
|
||||
clearTimeout(prev.debounceTimeout)
|
||||
}
|
||||
|
||||
let isStale = false
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
@@ -87,7 +112,15 @@ export const TableColumnHandlerDecorationPlugin = (editor: Editor): Plugin<Table
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
},
|
||||
view: () => ({
|
||||
destroy: () => {
|
||||
const state = key.getState(editor.state)
|
||||
if (state?.debounceTimeout !== undefined) {
|
||||
clearTimeout(state.debounceTimeout)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -176,8 +209,6 @@ class ColumnHandler {
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
window.removeEventListener('mouseup', handleFinish)
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
const handleMove = (event: MouseEvent): void => {
|
||||
@@ -195,7 +226,7 @@ class ColumnHandler {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', handleFinish)
|
||||
window.addEventListener('mouseup', handleFinish, { once: true })
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
})
|
||||
|
||||
|
||||
+20
-25
@@ -14,16 +14,16 @@
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { findTable, haveTableRelatedChanges, insertColumn } from '../utils'
|
||||
|
||||
import { addSvg } from './icons'
|
||||
|
||||
import { TableCachePluginKey } from './plugins'
|
||||
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
||||
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
interface TableColumnInsertDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
@@ -44,7 +44,8 @@ export const TableColumnInsertDecorationPlugin = (editor: Editor): Plugin<TableC
|
||||
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const cache = TableCachePluginKey.getState(newState)
|
||||
const tableMap = cache?.tableMap ?? TableMap.get(table.node)
|
||||
const { width } = tableMap
|
||||
|
||||
let isStale = false
|
||||
@@ -99,9 +100,24 @@ class ColumnInsertHandler {
|
||||
const handle = document.createElement('div')
|
||||
handle.classList.add('table-col-insert')
|
||||
|
||||
const marker = document.createElement('div')
|
||||
marker.className = 'table-insert-marker'
|
||||
handle.appendChild(marker)
|
||||
|
||||
const button = document.createElement('button')
|
||||
button.className = 'table-insert-button'
|
||||
button.innerHTML = addSvg
|
||||
handle.appendChild(button)
|
||||
|
||||
button.addEventListener('mouseenter', () => {
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
const tableHeightPx = getTableHeightPx(table, editor)
|
||||
marker.style.height = tableHeightPx + 'px'
|
||||
})
|
||||
|
||||
button.addEventListener('mousedown', (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
@@ -112,31 +128,10 @@ class ColumnInsertHandler {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { findTable } from '../utils'
|
||||
|
||||
export interface TableCachePluginState {
|
||||
tableMap?: TableMap
|
||||
tablePos?: number
|
||||
}
|
||||
|
||||
export const TableCachePluginKey = new PluginKey<TableCachePluginState>('tableCache')
|
||||
|
||||
export const TableCachePlugin = (): Plugin<TableCachePluginState> => {
|
||||
return new Plugin<TableCachePluginState>({
|
||||
key: TableCachePluginKey,
|
||||
state: {
|
||||
init: () => ({}),
|
||||
apply (tr, prev, _oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
if (table === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (prev.tablePos === table.pos && !tr.docChanged) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return {
|
||||
tableMap: TableMap.get(table.node),
|
||||
tablePos: table.pos
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+37
-5
@@ -35,10 +35,12 @@ import {
|
||||
updateRowDragMarker,
|
||||
updateRowDropMarker
|
||||
} from './tableDragMarkerDecoration'
|
||||
import { TableCachePluginKey } from './plugins'
|
||||
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
||||
|
||||
interface TableRowHandlerDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
debounceTimeout?: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
export const TableRowHandlerDecorationPlugin = (editor: Editor): Plugin<TableRowHandlerDecorationPluginState> => {
|
||||
@@ -51,11 +53,35 @@ export const TableRowHandlerDecorationPlugin = (editor: Editor): Plugin<TableRow
|
||||
},
|
||||
apply (tr, prev, oldState, newState) {
|
||||
const table = findTable(newState.selection)
|
||||
|
||||
if (table === undefined && prev.debounceTimeout !== undefined) {
|
||||
clearTimeout(prev.debounceTimeout)
|
||||
return {}
|
||||
}
|
||||
|
||||
if (!haveTableRelatedChanges(editor, table, oldState, newState, tr)) {
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const cache = TableCachePluginKey.getState(newState)
|
||||
const tableMap = cache?.tableMap ?? TableMap.get(table.node)
|
||||
|
||||
if (tr.docChanged && tr.steps.length === 1 && !(newState.selection instanceof CellSelection)) {
|
||||
if (prev.debounceTimeout !== undefined) {
|
||||
clearTimeout(prev.debounceTimeout)
|
||||
}
|
||||
|
||||
const debounceTimeout = setTimeout(() => {
|
||||
editor.view.updateState(editor.state)
|
||||
}, 100)
|
||||
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
return { decorations: mapped, debounceTimeout }
|
||||
}
|
||||
|
||||
if (prev.debounceTimeout !== undefined) {
|
||||
clearTimeout(prev.debounceTimeout)
|
||||
}
|
||||
|
||||
let isStale = false
|
||||
const mapped = prev.decorations?.map(tr.mapping, tr.doc)
|
||||
@@ -87,7 +113,15 @@ export const TableRowHandlerDecorationPlugin = (editor: Editor): Plugin<TableRow
|
||||
decorations (state) {
|
||||
return key.getState(state).decorations
|
||||
}
|
||||
}
|
||||
},
|
||||
view: () => ({
|
||||
destroy: () => {
|
||||
const state = key.getState(editor.state)
|
||||
if (state?.debounceTimeout !== undefined) {
|
||||
clearTimeout(state.debounceTimeout)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -175,8 +209,6 @@ class RowHandler {
|
||||
}
|
||||
editor.view.dispatch(tr)
|
||||
}
|
||||
window.removeEventListener('mouseup', handleFinish)
|
||||
window.removeEventListener('mousemove', handleMove)
|
||||
}
|
||||
|
||||
const handleMove = (event: MouseEvent): void => {
|
||||
@@ -194,7 +226,7 @@ class RowHandler {
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', handleFinish)
|
||||
window.addEventListener('mouseup', handleFinish, { once: true })
|
||||
window.addEventListener('mousemove', handleMove)
|
||||
})
|
||||
|
||||
|
||||
+20
-24
@@ -14,16 +14,16 @@
|
||||
//
|
||||
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { TableMap } from '@tiptap/pm/tables'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
|
||||
import { findTable, haveTableRelatedChanges, insertRow } from '../utils'
|
||||
|
||||
import { addSvg } from './icons'
|
||||
|
||||
import { TableCachePluginKey } from './plugins'
|
||||
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
interface TableRowInsertDecorationPluginState {
|
||||
decorations?: DecorationSet
|
||||
}
|
||||
@@ -42,7 +42,8 @@ export const TableRowInsertDecorationPlugin = (editor: Editor): Plugin<TableRowI
|
||||
return table !== undefined ? prev : {}
|
||||
}
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const cache = TableCachePluginKey.getState(newState)
|
||||
const tableMap = cache?.tableMap ?? TableMap.get(table.node)
|
||||
const { height } = tableMap
|
||||
|
||||
let isStale = false
|
||||
@@ -99,9 +100,24 @@ class RowInsertHandler {
|
||||
const handle = document.createElement('div')
|
||||
handle.classList.add('table-row-insert')
|
||||
|
||||
const marker = document.createElement('div')
|
||||
marker.className = 'table-insert-marker'
|
||||
handle.appendChild(marker)
|
||||
|
||||
const button = document.createElement('button')
|
||||
button.className = 'table-insert-button'
|
||||
button.innerHTML = addSvg
|
||||
handle.appendChild(button)
|
||||
|
||||
button.addEventListener('mouseenter', () => {
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
return
|
||||
}
|
||||
const tableWidthPx = getTableWidthPx(table, editor)
|
||||
marker.style.width = tableWidthPx + 'px'
|
||||
})
|
||||
|
||||
button.addEventListener('mousedown', (event) => {
|
||||
const table = findTable(editor.state.selection)
|
||||
if (table === undefined) {
|
||||
@@ -112,30 +128,10 @@ class RowInsertHandler {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+3
-1
@@ -19,6 +19,7 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
import { TableCachePluginKey } from './plugins'
|
||||
import { findTable, haveTableRelatedChanges } from '../utils'
|
||||
|
||||
interface TableSelectionDecorationPluginState {
|
||||
@@ -46,7 +47,8 @@ export const TableSelectionDecorationPlugin = (editor: Editor): Plugin<TableSele
|
||||
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
const cache = TableCachePluginKey.getState(newState)
|
||||
const tableMap = cache?.tableMap ?? TableMap.get(table.node)
|
||||
|
||||
const selected: number[] = []
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
|
||||
import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||
import { Plugin } from '@tiptap/pm/state'
|
||||
|
||||
import { type Node } from '@tiptap/pm/model'
|
||||
import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'
|
||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||
|
||||
import { TableCachePlugin } from './decorations/plugins'
|
||||
import { TableColumnHandlerDecorationPlugin } from './decorations/columnHandlerDecoration'
|
||||
import { TableColumnInsertDecorationPlugin } from './decorations/columnInsertDecoration'
|
||||
import { TableRowHandlerDecorationPlugin } from './decorations/rowHandlerDecoration'
|
||||
@@ -29,6 +30,7 @@ import { findTable } from './utils'
|
||||
export const TableCell = TiptapTableCell.extend({
|
||||
addProseMirrorPlugins () {
|
||||
return [
|
||||
TableCachePlugin(),
|
||||
TableSelectionNormalizerPlugin(),
|
||||
TableSelectionDecorationPlugin(this.editor),
|
||||
TableDragMarkerDecorationPlugin(this.editor),
|
||||
@@ -51,43 +53,70 @@ const TableSelectionNormalizerPlugin = (): Plugin<any> => {
|
||||
|
||||
const tableMap = TableMap.get(table.node)
|
||||
|
||||
let rect: Rect | undefined
|
||||
// Single pass to build initial rect and collect selected positions
|
||||
let minLeft = Infinity
|
||||
let minTop = Infinity
|
||||
let maxRight = -Infinity
|
||||
let maxBottom = -Infinity
|
||||
|
||||
const walkCell = (pos: number): void => {
|
||||
const cell = tableMap.findCell(pos)
|
||||
if (cell === undefined) return
|
||||
|
||||
if (rect === undefined) {
|
||||
rect = { ...cell }
|
||||
} else {
|
||||
rect.left = Math.min(rect.left, cell.left)
|
||||
rect.top = Math.min(rect.top, cell.top)
|
||||
|
||||
rect.right = Math.max(rect.right, cell.right)
|
||||
rect.bottom = Math.max(rect.bottom, cell.bottom)
|
||||
}
|
||||
}
|
||||
const selectedPositions = new Set<number>()
|
||||
|
||||
selection.forEachCell((_node, pos) => {
|
||||
walkCell(pos - table.pos - 1)
|
||||
})
|
||||
if (rect === undefined) return
|
||||
const relPos = pos - table.pos - 1
|
||||
selectedPositions.add(relPos)
|
||||
|
||||
const rectSelection: number[] = []
|
||||
for (let row = rect.top; row < rect.bottom; row++) {
|
||||
for (let col = rect.left; col < rect.right; col++) {
|
||||
rectSelection.push(tableMap.map[row * tableMap.width + col])
|
||||
const cell = tableMap.findCell(relPos)
|
||||
if (cell === undefined) return
|
||||
|
||||
minLeft = Math.min(minLeft, cell.left)
|
||||
minTop = Math.min(minTop, cell.top)
|
||||
maxRight = Math.max(maxRight, cell.right)
|
||||
maxBottom = Math.max(maxBottom, cell.bottom)
|
||||
})
|
||||
|
||||
if (!isFinite(minLeft)) return
|
||||
|
||||
// Expand rect to include all cells in the rectangular region
|
||||
// and check if we need to expand further
|
||||
let needsExpansion = false
|
||||
const { width } = tableMap
|
||||
|
||||
for (let row = minTop; row < maxBottom; row++) {
|
||||
for (let col = minLeft; col < maxRight; col++) {
|
||||
const pos = tableMap.map[row * width + col]
|
||||
if (!selectedPositions.has(pos)) {
|
||||
// Found a cell in rect that's not selected, need to expand
|
||||
const cell = tableMap.findCell(pos)
|
||||
if (cell !== undefined) {
|
||||
minLeft = Math.min(minLeft, cell.left)
|
||||
minTop = Math.min(minTop, cell.top)
|
||||
maxRight = Math.max(maxRight, cell.right)
|
||||
maxBottom = Math.max(maxBottom, cell.bottom)
|
||||
needsExpansion = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rectSelection.forEach((pos) => {
|
||||
walkCell(pos)
|
||||
})
|
||||
|
||||
if (rect === undefined) return
|
||||
// If we expanded, we need to check again (for merged cells)
|
||||
if (needsExpansion) {
|
||||
for (let row = minTop; row < maxBottom; row++) {
|
||||
for (let col = minLeft; col < maxRight; col++) {
|
||||
const pos = tableMap.map[row * width + col]
|
||||
const cell = tableMap.findCell(pos)
|
||||
if (cell !== undefined) {
|
||||
minLeft = Math.min(minLeft, cell.left)
|
||||
minTop = Math.min(minTop, cell.top)
|
||||
maxRight = Math.max(maxRight, cell.right)
|
||||
maxBottom = Math.max(maxBottom, cell.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Original promemirror implementation of TableMap.positionAt skips rowspawn cells, which leads to unpredictable selection behaviour
|
||||
const firstCellOffset = cellPositionAt(tableMap, rect.bottom - 1, rect.right - 1, table.node)
|
||||
const lastCellOffset = cellPositionAt(tableMap, rect.top, rect.left, table.node)
|
||||
// Original promemirror implementation of TableMap.positionAt skips rowspan cells, which leads to unpredictable selection behaviour
|
||||
const firstCellOffset = cellPositionAt(tableMap, maxBottom - 1, maxRight - 1, table.node)
|
||||
const lastCellOffset = cellPositionAt(tableMap, minTop, minLeft, table.node)
|
||||
|
||||
const firstCellPos = newState.doc.resolve(table.start + firstCellOffset)
|
||||
const lastCellPos = newState.doc.resolve(table.start + lastCellOffset)
|
||||
|
||||
@@ -146,5 +146,25 @@ export function haveTableRelatedChanges (
|
||||
newState: EditorState,
|
||||
tr: Transaction
|
||||
): table is TableNodeLocation {
|
||||
return editor.isEditable && table !== undefined && (tr.docChanged || !newState.selection.eq(oldState.selection))
|
||||
if (!editor.isEditable || table === undefined) return false
|
||||
|
||||
const selectionChanged = !newState.selection.eq(oldState.selection)
|
||||
|
||||
if (!tr.docChanged) {
|
||||
return selectionChanged
|
||||
}
|
||||
|
||||
const tableStart = table.pos
|
||||
const tableEnd = table.pos + table.node.nodeSize
|
||||
|
||||
let hasTableChange = false
|
||||
tr.mapping.maps.forEach((map) => {
|
||||
map.forEach((oldStart, oldEnd) => {
|
||||
if (oldStart < tableEnd && oldEnd > tableStart) {
|
||||
hasTableChange = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return hasTableChange || selectionChanged
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user