mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 11:42:30 +03:00
EZQMS-381 Table rows / columns drag and drop (#4176)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
4c5d490228
commit
d54422d737
@ -104,21 +104,22 @@
|
|||||||
.table-wrapper {
|
.table-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 1.25rem 0;
|
padding: 1.25rem 0;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -1.25rem;
|
top: 0;
|
||||||
bottom: -1.25rem;
|
bottom: 0;
|
||||||
left: -1.25rem;
|
left: 0;
|
||||||
right: -1.25rem;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.table-selected {
|
&.table-selected {
|
||||||
&::before {
|
&::before {
|
||||||
border: 1.25rem var(--theme-button-default) solid;
|
border: 1.25rem var(--theme-button-default) solid;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
|
inset: 0 -1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +138,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__row {
|
&__row {
|
||||||
bottom: -1.25rem;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 type { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||||
|
import type { Transaction } from '@tiptap/pm/state'
|
||||||
|
import { TableMap } from '@tiptap/pm/tables'
|
||||||
|
import type { TableNodeLocation } from '../types'
|
||||||
|
|
||||||
|
type TableRow = Array<ProseMirrorNode | null>
|
||||||
|
type TableRows = TableRow[]
|
||||||
|
|
||||||
|
export function moveColumn (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction {
|
||||||
|
const cols = transpose(tableToCells(table))
|
||||||
|
moveRowInplace(cols, from, to)
|
||||||
|
tableFromCells(table, transpose(cols), tr)
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveRow (table: TableNodeLocation, from: number, to: number, tr: Transaction): Transaction {
|
||||||
|
const rows = tableToCells(table)
|
||||||
|
moveRowInplace(rows, from, to)
|
||||||
|
tableFromCells(table, rows, tr)
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRowInplace (rows: TableRows, from: number, to: number): void {
|
||||||
|
rows.splice(to, 0, rows.splice(from, 1)[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function transpose (rows: TableRows): TableRows {
|
||||||
|
return rows[0].map((_, colIdx) => rows.map((row) => row[colIdx]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableToCells (table: TableNodeLocation): TableRows {
|
||||||
|
const { map, width, height } = TableMap.get(table.node)
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
const cells = []
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
const pos = map[row * width + col]
|
||||||
|
cells.push(table.node.nodeAt(pos))
|
||||||
|
}
|
||||||
|
rows.push(cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableFromCells (table: TableNodeLocation, rows: TableRows, tr: Transaction): void {
|
||||||
|
const { map, width, height } = TableMap.get(table.node)
|
||||||
|
const mapStart = tr.mapping.maps.length
|
||||||
|
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
const pos = map[row * width + col]
|
||||||
|
|
||||||
|
const oldCell = table.node.nodeAt(pos)
|
||||||
|
const newCell = rows[row][col]
|
||||||
|
|
||||||
|
if (oldCell !== null && newCell !== null && oldCell !== newCell) {
|
||||||
|
const start = tr.mapping.slice(mapStart).map(table.start + pos)
|
||||||
|
const end = start + oldCell.nodeSize
|
||||||
|
|
||||||
|
tr.replaceWith(start, end, newCell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,139 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { 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 { type TableNodeLocation } from '../types'
|
||||||
|
import { isColumnSelected, selectColumn } from '../utils'
|
||||||
|
|
||||||
|
import { moveColumn } from './actions'
|
||||||
|
import { handleSvg } from './icons'
|
||||||
|
import {
|
||||||
|
dropMarkerWidthPx,
|
||||||
|
getColDragMarker,
|
||||||
|
getDropMarker,
|
||||||
|
hideDragMarker,
|
||||||
|
hideDropMarker,
|
||||||
|
updateColDropMarker,
|
||||||
|
updateColDragMarker
|
||||||
|
} from './tableDragMarkerDecoration'
|
||||||
|
import { getTableCellWidgetDecorationPos, getTableWidthPx } from './utils'
|
||||||
|
|
||||||
|
interface TableColumn {
|
||||||
|
leftPx: number
|
||||||
|
widthPx: number
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handle = document.createElement('div')
|
||||||
|
handle.classList.add('table-col-handle')
|
||||||
|
if (isColumnSelected(col, state.selection)) {
|
||||||
|
handle.classList.add('table-col-handle__selected')
|
||||||
|
}
|
||||||
|
handle.innerHTML = handleSvg
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
handleMouseDown(col, table, e, editor)
|
||||||
|
})
|
||||||
|
decorations.push(Decoration.widget(pos, handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = (col: number, table: TableNodeLocation, event: MouseEvent, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
// select column
|
||||||
|
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
|
||||||
|
tr = selectColumn(table, dropIndex, tr)
|
||||||
|
tr = moveColumn(table, col, 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 - dropMarkerWidthPx / 2, dropMarkerWidthPx)
|
||||||
|
updateColDragMarker(dragMarker, dragMarkerLeftPx, dragMarkerWidthPx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', handleFinish)
|
||||||
|
window.addEventListener('mousemove', handleMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateColumnDropIndex (col: number, columns: TableColumn[], left: number): number {
|
||||||
|
const colCenterPx = left + columns[col].widthPx / 2
|
||||||
|
const index = columns.findIndex((p) => colCenterPx < p.leftPx + p.widthPx / 2)
|
||||||
|
return index !== -1 ? (index > col ? index - 1 : index) : columns.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableColumns (table: TableNodeLocation, editor: Editor): TableColumn[] {
|
||||||
|
const result = []
|
||||||
|
let leftPx = 0
|
||||||
|
|
||||||
|
const { map, width } = TableMap.get(table.node)
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
const dom = editor.view.domAtPos(table.start + map[col] + 1)
|
||||||
|
if (dom.node instanceof HTMLElement) {
|
||||||
|
if (col === 0) {
|
||||||
|
leftPx = dom.node.offsetLeft
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
leftPx: dom.node.offsetLeft - leftPx,
|
||||||
|
widthPx: dom.node.offsetWidth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { 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 { 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[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
const { width } = tableMap
|
||||||
|
|
||||||
|
const tableHeightPx = getTableHeightPx(table, editor)
|
||||||
|
|
||||||
|
for (let col = 0; col < width; col++) {
|
||||||
|
const show = col < width - 1 && !isColumnSelected(col, selection) && !isColumnSelected(col + 1, selection)
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
const insert = document.createElement('div')
|
||||||
|
insert.classList.add('table-col-insert')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
const marker = document.createElement('div')
|
||||||
|
marker.className = 'table-insert-marker'
|
||||||
|
marker.style.height = tableHeightPx + 'px'
|
||||||
|
insert.appendChild(marker)
|
||||||
|
|
||||||
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
||||||
|
decorations.push(Decoration.widget(pos, insert))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = (col: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
editor.view.dispatch(insertColumn(table, col + 1, editor.state.tr))
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { 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 { type TableNodeLocation } from '../types'
|
||||||
|
import { isRowSelected, selectRow } from '../utils'
|
||||||
|
|
||||||
|
import { moveRow } from './actions'
|
||||||
|
import { handleSvg } from './icons'
|
||||||
|
import {
|
||||||
|
dropMarkerWidthPx,
|
||||||
|
getDropMarker,
|
||||||
|
getRowDragMarker,
|
||||||
|
hideDragMarker,
|
||||||
|
hideDropMarker,
|
||||||
|
updateRowDropMarker,
|
||||||
|
updateRowDragMarker
|
||||||
|
} from './tableDragMarkerDecoration'
|
||||||
|
import { getTableCellWidgetDecorationPos, getTableHeightPx } from './utils'
|
||||||
|
|
||||||
|
interface TableRow {
|
||||||
|
topPx: number
|
||||||
|
heightPx: number
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handle = document.createElement('div')
|
||||||
|
handle.classList.add('table-row-handle')
|
||||||
|
if (isRowSelected(row, state.selection)) {
|
||||||
|
handle.classList.add('table-row-handle__selected')
|
||||||
|
}
|
||||||
|
handle.innerHTML = handleSvg
|
||||||
|
handle.addEventListener('mousedown', (e) => {
|
||||||
|
handleMouseDown(row, table, e, editor)
|
||||||
|
})
|
||||||
|
decorations.push(Decoration.widget(pos, handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = (row: number, table: TableNodeLocation, event: MouseEvent, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
// select row
|
||||||
|
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
|
||||||
|
tr = selectRow(table, dropIndex, tr)
|
||||||
|
tr = moveRow(table, row, 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)
|
||||||
|
return index !== -1 ? (index > row ? index - 1 : index) : rows.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (dom.node instanceof HTMLElement) {
|
||||||
|
const heightPx = dom.node.offsetHeight
|
||||||
|
result.push({ topPx, heightPx })
|
||||||
|
topPx += heightPx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { 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 { 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[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
const { height } = tableMap
|
||||||
|
|
||||||
|
const tableWidthPx = getTableWidthPx(table, editor)
|
||||||
|
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
const show = row < height - 1 && !isRowSelected(row, selection) && !isRowSelected(row + 1, selection)
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
const dot = document.createElement('div')
|
||||||
|
dot.classList.add('table-row-insert')
|
||||||
|
|
||||||
|
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 marker = document.createElement('div')
|
||||||
|
marker.className = 'table-insert-marker'
|
||||||
|
marker.style.width = tableWidthPx + 'px'
|
||||||
|
dot.appendChild(marker)
|
||||||
|
|
||||||
|
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
||||||
|
decorations.push(Decoration.widget(pos, dot))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseDown = (row: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
editor.view.dispatch(insertRow(table, row + 1, editor.state.tr))
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { type EditorState } from '@tiptap/pm/state'
|
||||||
|
import { Decoration } from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
import { handleSvg } from './icons'
|
||||||
|
import { type TableNodeLocation } from '../types'
|
||||||
|
|
||||||
|
export const dropMarkerId = 'table-drop-marker'
|
||||||
|
export const colDragMarkerId = 'table-col-drag-marker'
|
||||||
|
export const rowDragMarkerId = 'table-row-drag-marker'
|
||||||
|
|
||||||
|
export const dropMarkerWidthPx = 2
|
||||||
|
|
||||||
|
export const tableDragMarkerDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
||||||
|
const dropMarker = document.createElement('div')
|
||||||
|
dropMarker.id = dropMarkerId
|
||||||
|
dropMarker.classList.add('table-drop-marker')
|
||||||
|
|
||||||
|
const colDragMarker = document.createElement('div')
|
||||||
|
colDragMarker.id = colDragMarkerId
|
||||||
|
colDragMarker.classList.add('table-col-drag-marker')
|
||||||
|
colDragMarker.innerHTML = handleSvg
|
||||||
|
colDragMarker.style.display = 'none'
|
||||||
|
|
||||||
|
const rowDragMarker = document.createElement('div')
|
||||||
|
rowDragMarker.id = rowDragMarkerId
|
||||||
|
rowDragMarker.classList.add('table-row-drag-marker')
|
||||||
|
rowDragMarker.innerHTML = handleSvg
|
||||||
|
rowDragMarker.style.display = 'none'
|
||||||
|
|
||||||
|
return [
|
||||||
|
Decoration.widget(table.start, dropMarker),
|
||||||
|
Decoration.widget(table.start, colDragMarker),
|
||||||
|
Decoration.widget(table.start, rowDragMarker)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DropMarkerHTMLElement = HTMLElement
|
||||||
|
|
||||||
|
export function getDropMarker (): DropMarkerHTMLElement | null {
|
||||||
|
return document.getElementById(dropMarkerId) as DropMarkerHTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideDropMarker (element: DropMarkerHTMLElement): void {
|
||||||
|
element.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateColDropMarker (element: DropMarkerHTMLElement, left: number, width: number): void {
|
||||||
|
element.style.height = '100%'
|
||||||
|
element.style.width = `${width}px`
|
||||||
|
element.style.top = '0'
|
||||||
|
element.style.left = `${left}px`
|
||||||
|
element.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRowDropMarker (element: DropMarkerHTMLElement, top: number, height: number): void {
|
||||||
|
element.style.width = '100%'
|
||||||
|
element.style.height = `${height}px`
|
||||||
|
element.style.left = '0'
|
||||||
|
element.style.top = `${top}px`
|
||||||
|
element.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DragMarkerHTMLElement = HTMLElement
|
||||||
|
|
||||||
|
export function getColDragMarker (): DragMarkerHTMLElement | null {
|
||||||
|
return document.getElementById(colDragMarkerId) as DragMarkerHTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRowDragMarker (): DragMarkerHTMLElement | null {
|
||||||
|
return document.getElementById(rowDragMarkerId) as DragMarkerHTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDragMarker (element: DragMarkerHTMLElement): void {
|
||||||
|
element.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideDragMarker (element: DragMarkerHTMLElement): void {
|
||||||
|
element.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateColDragMarker (element: DragMarkerHTMLElement, left: number, width: number): void {
|
||||||
|
element.style.width = `${width}px`
|
||||||
|
element.style.left = `${left}px`
|
||||||
|
element.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRowDragMarker (element: DragMarkerHTMLElement, top: number, height: number): void {
|
||||||
|
element.style.height = `${height}px`
|
||||||
|
element.style.top = `${top}px`
|
||||||
|
element.style.display = 'block'
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { type EditorState } from '@tiptap/pm/state'
|
||||||
|
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
||||||
|
import { Decoration } from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
import { type TableNodeLocation } from '../types'
|
||||||
|
|
||||||
|
export const tableSelectionDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
const { selection } = state
|
||||||
|
|
||||||
|
const tableMap = TableMap.get(table.node)
|
||||||
|
|
||||||
|
if (selection instanceof CellSelection) {
|
||||||
|
const selected: number[] = []
|
||||||
|
|
||||||
|
selection.forEachCell((_node, pos) => {
|
||||||
|
const start = pos - table.pos - 1
|
||||||
|
selected.push(start)
|
||||||
|
})
|
||||||
|
|
||||||
|
selection.forEachCell((node, pos) => {
|
||||||
|
const start = pos - table.pos - 1
|
||||||
|
const borders = getTableCellBorders(start, selected, tableMap)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableCellBorders (
|
||||||
|
cell: number,
|
||||||
|
selection: number[],
|
||||||
|
tableMap: TableMap
|
||||||
|
): { top: boolean, bottom: boolean, left: boolean, right: boolean } {
|
||||||
|
const { width, height } = tableMap
|
||||||
|
const cellIndex = tableMap.map.indexOf(cell)
|
||||||
|
|
||||||
|
const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined
|
||||||
|
const bottomCell = cellIndex < width * height - width ? tableMap.map[cellIndex + width] : undefined
|
||||||
|
const leftCell = cellIndex % width !== 0 ? tableMap.map[cellIndex - 1] : undefined
|
||||||
|
const rightCell = cellIndex % width !== width - 1 ? tableMap.map[cellIndex + 1] : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: topCell === undefined || !selection.includes(topCell),
|
||||||
|
bottom: bottomCell === undefined || !selection.includes(bottomCell),
|
||||||
|
left: leftCell === undefined || !selection.includes(leftCell),
|
||||||
|
right: rightCell === undefined || !selection.includes(rightCell)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2023 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 { type Editor } from '@tiptap/core'
|
||||||
|
import { type TableMap } from '@tiptap/pm/tables'
|
||||||
|
import { type TableNodeLocation } from '../types'
|
||||||
|
|
||||||
|
export function getTableCellWidgetDecorationPos (table: TableNodeLocation, map: TableMap, index: number): number {
|
||||||
|
return table.start + map.map[index] + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTableHeightPx (table: TableNodeLocation, editor: Editor): number {
|
||||||
|
const dom = editor.view.domAtPos(table.start)
|
||||||
|
return dom.node.parentElement?.offsetHeight ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTableWidthPx (table: TableNodeLocation, editor: Editor): number {
|
||||||
|
const dom = editor.view.domAtPos(table.start)
|
||||||
|
return dom.node.parentElement?.offsetWidth ?? 0
|
||||||
|
}
|
@ -15,13 +15,16 @@
|
|||||||
|
|
||||||
import { type Editor } from '@tiptap/core'
|
import { type Editor } from '@tiptap/core'
|
||||||
import TiptapTableCell from '@tiptap/extension-table-cell'
|
import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||||
import { type EditorState, Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
|
import { Plugin, PluginKey, type Selection } from '@tiptap/pm/state'
|
||||||
import { CellSelection, TableMap } from '@tiptap/pm/tables'
|
import { DecorationSet } from '@tiptap/pm/view'
|
||||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
|
||||||
|
|
||||||
import { addSvg, handleSvg } from './icons'
|
import { findTable } from './utils'
|
||||||
import { type TableNodeLocation } from './types'
|
import { columnHandlerDecoration } from './decorations/columnHandlerDecoration'
|
||||||
import { insertColumn, insertRow, findTable, isColumnSelected, isRowSelected, selectColumn, selectRow } from './utils'
|
import { columnInsertDecoration } from './decorations/columnInsertDecoration'
|
||||||
|
import { rowInsertDecoration } from './decorations/rowInsertDecoration'
|
||||||
|
import { tableDragMarkerDecoration } from './decorations/tableDragMarkerDecoration'
|
||||||
|
import { tableSelectionDecoration } from './decorations/tableSelectionDecoration'
|
||||||
|
import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'
|
||||||
|
|
||||||
export const TableCell = TiptapTableCell.extend({
|
export const TableCell = TiptapTableCell.extend({
|
||||||
addProseMirrorPlugins () {
|
addProseMirrorPlugins () {
|
||||||
@ -58,11 +61,12 @@ const tableCellDecorationPlugin = (editor: Editor): Plugin<TableCellDecorationPl
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decorations = DecorationSet.create(newState.doc, [
|
const decorations = DecorationSet.create(newState.doc, [
|
||||||
|
...tableSelectionDecoration(newState, newTable),
|
||||||
|
...tableDragMarkerDecoration(newState, newTable),
|
||||||
...columnHandlerDecoration(newState, newTable, editor),
|
...columnHandlerDecoration(newState, newTable, editor),
|
||||||
...columnInsertDecoration(newState, newTable, editor),
|
...columnInsertDecoration(newState, newTable, editor),
|
||||||
...rowHandlerDecoration(newState, newTable, editor),
|
...rowHandlerDecoration(newState, newTable, editor),
|
||||||
...rowInsertDecoration(newState, newTable, editor),
|
...rowInsertDecoration(newState, newTable, editor)
|
||||||
...selectionDecoration(newState, newTable)
|
|
||||||
])
|
])
|
||||||
return { selection: newState.selection, decorations }
|
return { selection: newState.selection, decorations }
|
||||||
}
|
}
|
||||||
@ -74,216 +78,3 @@ const tableCellDecorationPlugin = (editor: Editor): Plugin<TableCellDecorationPl
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
|
||||||
for (let col = 0; col < tableMap.width; col++) {
|
|
||||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
|
||||||
|
|
||||||
const handle = document.createElement('div')
|
|
||||||
handle.classList.add('table-col-handle')
|
|
||||||
if (isColumnSelected(col, selection)) {
|
|
||||||
handle.classList.add('table-col-handle__selected')
|
|
||||||
}
|
|
||||||
handle.innerHTML = handleSvg
|
|
||||||
handle.addEventListener('mousedown', (e) => {
|
|
||||||
handleColHandleMouseDown(col, table, e, editor)
|
|
||||||
})
|
|
||||||
decorations.push(Decoration.widget(pos, handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
return decorations
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
|
||||||
const { width } = tableMap
|
|
||||||
|
|
||||||
const dom = editor.view.domAtPos(table.start)
|
|
||||||
const tableHeightPx = dom.node.parentElement?.clientHeight ?? 0
|
|
||||||
|
|
||||||
for (let col = 0; col < width; col++) {
|
|
||||||
const show = col < width - 1 && !isColumnSelected(col, selection) && !isColumnSelected(col + 1, selection)
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
const insert = document.createElement('div')
|
|
||||||
insert.classList.add('table-col-insert')
|
|
||||||
|
|
||||||
const button = document.createElement('button')
|
|
||||||
button.className = 'table-insert-button'
|
|
||||||
button.innerHTML = addSvg
|
|
||||||
button.addEventListener('mousedown', (e) => {
|
|
||||||
handleColInsertMouseDown(col, table, e, editor)
|
|
||||||
})
|
|
||||||
insert.appendChild(button)
|
|
||||||
|
|
||||||
const marker = document.createElement('div')
|
|
||||||
marker.className = 'table-insert-marker'
|
|
||||||
marker.style.height = tableHeightPx + 'px'
|
|
||||||
insert.appendChild(marker)
|
|
||||||
|
|
||||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
|
|
||||||
decorations.push(Decoration.widget(pos, insert))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return decorations
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleColHandleMouseDown = (col: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
editor.view.dispatch(selectColumn(table, col, editor.state.tr))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleColInsertMouseDown = (col: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
editor.view.dispatch(insertColumn(table, col + 1, editor.state.tr))
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
|
||||||
for (let row = 0; row < tableMap.height; row++) {
|
|
||||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
|
||||||
|
|
||||||
const handle = document.createElement('div')
|
|
||||||
handle.classList.add('table-row-handle')
|
|
||||||
if (isRowSelected(row, selection)) {
|
|
||||||
handle.classList.add('table-row-handle__selected')
|
|
||||||
}
|
|
||||||
handle.innerHTML = handleSvg
|
|
||||||
handle.addEventListener('mousedown', (e) => {
|
|
||||||
handleRowHandleMouseDown(row, table, e, editor)
|
|
||||||
})
|
|
||||||
decorations.push(Decoration.widget(pos, handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
return decorations
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowInsertDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
|
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
|
||||||
const { height } = tableMap
|
|
||||||
|
|
||||||
const dom = editor.view.domAtPos(table.start)
|
|
||||||
const tableWidthPx = dom.node.parentElement?.clientWidth ?? 0
|
|
||||||
|
|
||||||
for (let row = 0; row < height; row++) {
|
|
||||||
const show = row < height - 1 && !isRowSelected(row, selection) && !isRowSelected(row + 1, selection)
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
const dot = document.createElement('div')
|
|
||||||
dot.classList.add('table-row-insert')
|
|
||||||
|
|
||||||
const button = document.createElement('button')
|
|
||||||
button.className = 'table-insert-button'
|
|
||||||
button.innerHTML = addSvg
|
|
||||||
button.addEventListener('mousedown', (e) => {
|
|
||||||
handleRowInsertMouseDown(row, table, e, editor)
|
|
||||||
})
|
|
||||||
dot.appendChild(button)
|
|
||||||
|
|
||||||
const marker = document.createElement('div')
|
|
||||||
marker.className = 'table-insert-marker'
|
|
||||||
marker.style.width = tableWidthPx + 'px'
|
|
||||||
dot.appendChild(marker)
|
|
||||||
|
|
||||||
const pos = getTableCellWidgetDecorationPos(table, tableMap, row * tableMap.width)
|
|
||||||
decorations.push(Decoration.widget(pos, dot))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return decorations
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRowHandleMouseDown = (row: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
editor.view.dispatch(selectRow(table, row, editor.state.tr))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRowInsertMouseDown = (row: number, table: TableNodeLocation, event: Event, editor: Editor): void => {
|
|
||||||
event.stopPropagation()
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
editor.view.dispatch(insertRow(table, row + 1, editor.state.tr))
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectionDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => {
|
|
||||||
const decorations: Decoration[] = []
|
|
||||||
|
|
||||||
const { selection } = state
|
|
||||||
|
|
||||||
const tableMap = TableMap.get(table.node)
|
|
||||||
|
|
||||||
if (selection instanceof CellSelection) {
|
|
||||||
const selected: number[] = []
|
|
||||||
|
|
||||||
selection.forEachCell((_node, pos) => {
|
|
||||||
const start = pos - table.pos - 1
|
|
||||||
selected.push(start)
|
|
||||||
})
|
|
||||||
|
|
||||||
selection.forEachCell((node, pos) => {
|
|
||||||
const start = pos - table.pos - 1
|
|
||||||
const borders = getTableCellBorders(start, selected, tableMap)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTableCellWidgetDecorationPos (table: TableNodeLocation, map: TableMap, index: number): number {
|
|
||||||
const pos = table.node.resolve(map.map[index] + 1)
|
|
||||||
return table.start + pos.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTableCellBorders (
|
|
||||||
cell: number,
|
|
||||||
selection: number[],
|
|
||||||
tableMap: TableMap
|
|
||||||
): { top: boolean, bottom: boolean, left: boolean, right: boolean } {
|
|
||||||
const { width, height } = tableMap
|
|
||||||
const cellIndex = tableMap.map.indexOf(cell)
|
|
||||||
|
|
||||||
const topCell = cellIndex >= width ? tableMap.map[cellIndex - width] : undefined
|
|
||||||
const bottomCell = cellIndex < width * height - width ? tableMap.map[cellIndex + width] : undefined
|
|
||||||
const leftCell = cellIndex % width !== 0 ? tableMap.map[cellIndex - 1] : undefined
|
|
||||||
const rightCell = cellIndex % width !== width - 1 ? tableMap.map[cellIndex + 1] : undefined
|
|
||||||
|
|
||||||
return {
|
|
||||||
top: topCell === undefined || !selection.includes(topCell),
|
|
||||||
bottom: bottomCell === undefined || !selection.includes(bottomCell),
|
|
||||||
left: leftCell === undefined || !selection.includes(leftCell),
|
|
||||||
right: rightCell === undefined || !selection.includes(rightCell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -298,6 +298,7 @@
|
|||||||
|
|
||||||
--text-editor-toc-default-color: rgba(255, 255, 255, 0.1);
|
--text-editor-toc-default-color: rgba(255, 255, 255, 0.1);
|
||||||
--text-editor-toc-hovered-color: rgba(255, 255, 255, 0.4);
|
--text-editor-toc-hovered-color: rgba(255, 255, 255, 0.4);
|
||||||
|
--text-editor-drag-marker-bg-color: #444248;
|
||||||
|
|
||||||
--theme-clockface-back: radial-gradient(farthest-corner at 50% 0%, #bbb, #fff 100%);
|
--theme-clockface-back: radial-gradient(farthest-corner at 50% 0%, #bbb, #fff 100%);
|
||||||
--theme-clockface-shadow: inset 0 -3px 10px #aaa;
|
--theme-clockface-shadow: inset 0 -3px 10px #aaa;
|
||||||
@ -524,6 +525,7 @@
|
|||||||
|
|
||||||
--text-editor-toc-default-color: rgba(0, 0, 0, 0.1);
|
--text-editor-toc-default-color: rgba(0, 0, 0, 0.1);
|
||||||
--text-editor-toc-hovered-color: rgba(0, 0, 0, 0.4);
|
--text-editor-toc-hovered-color: rgba(0, 0, 0, 0.4);
|
||||||
|
--text-editor-drag-marker-bg-color: #444248;
|
||||||
|
|
||||||
--theme-clockface-back: radial-gradient(farthest-corner at 50% 0%, #606060, #000 100%);
|
--theme-clockface-back: radial-gradient(farthest-corner at 50% 0%, #606060, #000 100%);
|
||||||
--theme-clockface-shadow: inset 0 -3px 10px #000;
|
--theme-clockface-shadow: inset 0 -3px 10px #000;
|
||||||
|
@ -15,14 +15,21 @@
|
|||||||
|
|
||||||
/* Table */
|
/* Table */
|
||||||
table.proseTable {
|
table.proseTable {
|
||||||
--table-selection-border-indent: -0.125rem;
|
--table-selection-border-width: 2px;
|
||||||
--table-selection-border-radius: 0.125rem;
|
--table-selection-border-indent: -2px;
|
||||||
--table-selection-border-width: 0.125rem;
|
--table-selection-border-radius: 2px;
|
||||||
|
--table-selection-border-width: 2px;
|
||||||
|
--table-handle-size: 1.25rem;
|
||||||
|
--table-handle-indent: calc(-1.25rem - 1px);
|
||||||
|
|
||||||
|
--table-selection-z-index: 100;
|
||||||
|
--table-drag-and-drop-z-index: 110;
|
||||||
|
--table-handlers-z-index: 120;
|
||||||
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
td,
|
td,
|
||||||
@ -59,7 +66,7 @@ table.proseTable {
|
|||||||
border-radius: var(--table-selection-border-radius);
|
border-radius: var(--table-selection-border-radius);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 110;
|
z-index: var(--table-selection-z-index);
|
||||||
top: var(--table-selection-border-indent);
|
top: var(--table-selection-border-indent);
|
||||||
bottom: var(--table-selection-border-indent);
|
bottom: var(--table-selection-border-indent);
|
||||||
left: var(--table-selection-border-indent);
|
left: var(--table-selection-border-indent);
|
||||||
@ -91,7 +98,6 @@ table.proseTable {
|
|||||||
svg {
|
svg {
|
||||||
color: var(--theme-button-contrast-hovered);
|
color: var(--theme-button-contrast-hovered);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 120;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__selected {
|
&__selected {
|
||||||
@ -102,7 +108,7 @@ table.proseTable {
|
|||||||
border-radius: var(--table-selection-border-radius);
|
border-radius: var(--table-selection-border-radius);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 110;
|
z-index: var(--table-handlers-z-index);
|
||||||
top: var(--table-selection-border-indent);
|
top: var(--table-selection-border-indent);
|
||||||
bottom: var(--table-selection-border-indent);
|
bottom: var(--table-selection-border-indent);
|
||||||
left: var(--table-selection-border-indent);
|
left: var(--table-selection-border-indent);
|
||||||
@ -110,6 +116,7 @@ table.proseTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
z-index: var(--table-handlers-z-index);
|
||||||
color: white;
|
color: white;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@ -119,8 +126,8 @@ table.proseTable {
|
|||||||
.table-col-handle {
|
.table-col-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: calc(100% + 1px);
|
width: calc(100% + 1px);
|
||||||
height: 1.25rem;
|
height: var(--table-handle-size);
|
||||||
top: calc(-1.25rem - 1px);
|
top: var(--table-handle-indent);
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -134,18 +141,21 @@ table.proseTable {
|
|||||||
&__selected {
|
&__selected {
|
||||||
&::before {
|
&::before {
|
||||||
right: -1px;
|
right: -1px;
|
||||||
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border-bottom-width: 0;
|
border-bottom-width: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-row-handle {
|
.table-row-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1.25rem;
|
width: var(--table-handle-size);
|
||||||
height: calc(100% + 1px);
|
height: calc(100% + 1px);
|
||||||
top: 0;
|
top: 0;
|
||||||
left: calc(-1.25rem - 1px);
|
left: var(--table-handle-indent);
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
@ -164,8 +174,11 @@ table.proseTable {
|
|||||||
&__selected {
|
&__selected {
|
||||||
&::before {
|
&::before {
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-right-width: 0;
|
border-right-width: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,7 +196,7 @@ table.proseTable {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
top: -1.25rem;
|
top: var(--table-handle-indent);
|
||||||
right: -0.625rem;
|
right: -0.625rem;
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
|
|
||||||
@ -197,7 +210,7 @@ table.proseTable {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
left: -1.25rem;
|
left: var(--table-handle-indent);
|
||||||
bottom: -0.625rem;
|
bottom: -0.625rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
|
|
||||||
@ -241,6 +254,50 @@ table.proseTable {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-drop-marker {
|
||||||
|
background-color: var(--primary-button-focused);
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--table-drag-and-drop-z-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-col-drag-marker,
|
||||||
|
.table-row-drag-marker {
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--table-drag-and-drop-z-index);
|
||||||
|
opacity: 0.5;
|
||||||
|
background-color: var(--text-editor-drag-marker-bg-color);
|
||||||
|
border: var(--table-selection-border-width) solid var(--text-editor-drag-marker-bg-color);
|
||||||
|
border-radius: var(--table-selection-border-radius);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: white;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-col-drag-marker {
|
||||||
|
height: var(--table-handle-size);
|
||||||
|
top: calc(var(--table-handle-indent) + 1px);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row-drag-marker {
|
||||||
|
width: var(--table-handle-size);
|
||||||
|
left: calc(var(--table-handle-indent) + 1px / 2);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 100%;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.proseCode {
|
.proseCode {
|
||||||
@ -273,4 +330,4 @@ pre.proseCodeBlock > pre.proseCode {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.proseHeading { line-height: 110%; }
|
.proseHeading { line-height: 110%; }
|
||||||
|
Loading…
Reference in New Issue
Block a user