Skip to content

Commit

Permalink
Table options (#5617)
Browse files Browse the repository at this point in the history
Signed-off-by: andrewkuryan <[email protected]>
  • Loading branch information
andrewkuryan committed May 21, 2024
1 parent 401ca99 commit 66a8fbe
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 36 deletions.
1 change: 1 addition & 0 deletions packages/text-editor/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"AddRowAfter": "Add after",
"DeleteRow": "Delete",
"DeleteTable": "Delete",
"Duplicate": "Duplicate",

"CategoryRow": "Rows",
"CategoryColumn": "Columns",
Expand Down
1 change: 1 addition & 0 deletions packages/text-editor/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"AddRowAfter": "Añadir después",
"DeleteRow": "Eliminar",
"DeleteTable": "Eliminar",
"Duplicate": "Duplicar",
"CategoryRow": "Filas",
"CategoryColumn": "Columnas",
"Table": "Tabla",
Expand Down
3 changes: 2 additions & 1 deletion packages/text-editor/lang/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"AddRowAfter": "Adicionar depois",
"DeleteRow": "Eliminar",
"DeleteTable": "Eliminar",
"Duplicate": "Duplicar",
"CategoryRow": "Linhas",
"CategoryColumn": "Colunas",
"Table": "Tabela",
Expand All @@ -49,4 +50,4 @@
"Image": "Imagem",
"SeparatorLine": "linha separadora"
}
}
}
1 change: 1 addition & 0 deletions packages/text-editor/lang/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"AddRowAfter": "Добавить после",
"DeleteRow": "Удалить",
"DeleteTable": "Удалить",
"Duplicate": "Дублировать",

"CategoryRow": "Строки",
"CategoryColumn": "Колонки",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,49 @@ export function moveRow (table: TableNodeLocation, from: number, to: number, tr:
return tr
}

function isNotNull<T> (value: T | null): value is T {
return value !== null
}

export function duplicateRows (table: TableNodeLocation, rowIndices: number[], tr: Transaction): Transaction {
const rows = tableToCells(table)

const { map, width } = TableMap.get(table.node)
const mapStart = tr.mapping.maps.length

const lastRowPos = map[rowIndices[rowIndices.length - 1] * width + width - 1]
const nextRowStart = lastRowPos + (table.node.nodeAt(lastRowPos)?.nodeSize ?? 0) + 1
const insertPos = tr.mapping.slice(mapStart).map(table.start + nextRowStart)

for (let i = rowIndices.length - 1; i >= 0; i--) {
tr.insert(insertPos, rows[rowIndices[i]].filter(isNotNull))
}

return tr
}

export function duplicateColumns (table: TableNodeLocation, columnIndices: number[], tr: Transaction): Transaction {
const rows = tableToCells(table)

const { map, width, height } = TableMap.get(table.node)
const mapStart = tr.mapping.maps.length

for (let row = 0; row < height; row++) {
const lastColumnPos = map[row * width + columnIndices[columnIndices.length - 1]]
const nextColumnStart = lastColumnPos + (table.node.nodeAt(lastColumnPos)?.nodeSize ?? 0)
const insertPos = tr.mapping.slice(mapStart).map(table.start + nextColumnStart)

for (let i = columnIndices.length - 1; i >= 0; i--) {
const copiedNode = rows[row][columnIndices[i]]
if (copiedNode !== null) {
tr.insert(insertPos, copiedNode)
}
}
}

return tr
}

function moveRowInplace (rows: TableRows, from: number, to: number): void {
rows.splice(to, 0, rows.splice(from, 1)[0])
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type AnySvelteComponent, ModernPopup, showPopup } from '@hcengineering/ui'
import { handleSvg } from './icons'

export interface OptionItem {
id: string
icon: AnySvelteComponent
label: string
action: () => void
}

export function createCellsHandle (options: OptionItem[]): HTMLElement {
const handle = document.createElement('div')

const button = document.createElement('button')
button.innerHTML = handleSvg
button.addEventListener('click', () => {
button.classList.add('pressed')
showPopup(ModernPopup, { items: options }, button, (result) => {
const option = options.find((it) => it.id === result)
if (option !== undefined) {
option.action()
}
button.classList.remove('pressed')
})
})

handle.appendChild(button)

return handle
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ import { TableMap } from '@tiptap/pm/tables'
import { Decoration } from '@tiptap/pm/view'

import { type TableNodeLocation } from '../types'
import { isColumnSelected, selectColumn } from '../utils'
import { findTable, getSelectedColumns, isColumnSelected, selectColumn } from '../utils'

import { moveColumn } from './actions'
import { handleSvg } from './icons'
import { duplicateColumns, moveColumn } from './actions'
import DeleteCol from '../../../icons/table/DeleteCol.svelte'
import Duplicate from '../../../icons/table/Duplicate.svelte'
import textEditorPlugin from '../../../../plugin'
import { createCellsHandle, type OptionItem } from './cellsHandle'
import {
dropMarkerWidthPx,
getColDragMarker,
Expand All @@ -39,34 +42,66 @@ interface TableColumn {
widthPx: number
}

const createOptionItems = (editor: Editor): OptionItem[] => [
{
id: 'delete',
icon: DeleteCol,
label: textEditorPlugin.string.DeleteColumn,
action: () => editor.commands.deleteColumn()
},
{
id: 'duplicate',
icon: Duplicate,
label: textEditorPlugin.string.Duplicate,
action: () => {
const table = findTable(editor.state.selection)
if (table !== undefined) {
let tr = editor.state.tr
const selectedColumns = getSelectedColumns(editor.state.selection, TableMap.get(table.node))
tr = duplicateColumns(table, selectedColumns, tr)
editor.view.dispatch(tr)
}
}
}
]

export const columnHandlerDecoration = (state: EditorState, table: TableNodeLocation, editor: Editor): Decoration[] => {
const decorations: Decoration[] = []

const tableMap = TableMap.get(table.node)
for (let col = 0; col < tableMap.width; col++) {
const pos = getTableCellWidgetDecorationPos(table, tableMap, col)
const isSelected = isColumnSelected(col, state.selection)

const handle = document.createElement('div')
const handle = createCellsHandle(createOptionItems(editor))
handle.classList.add('table-col-handle')
if (isColumnSelected(col, state.selection)) {
if (isSelected) {
handle.classList.add('table-col-handle__selected')
}
handle.innerHTML = handleSvg
handle.addEventListener('mousedown', (e) => {
handleMouseDown(col, table, e, editor)
handleMouseDown(col, table, e, editor, isSelected)
})

decorations.push(Decoration.widget(pos, handle))
}

return decorations
}

const handleMouseDown = (col: number, table: TableNodeLocation, event: MouseEvent, editor: Editor): void => {
const handleMouseDown = (
col: number,
table: TableNodeLocation,
event: MouseEvent,
editor: Editor,
isSelected: boolean
): void => {
event.stopPropagation()
event.preventDefault()

// select column
editor.view.dispatch(selectColumn(table, col, editor.state.tr))
if (!isSelected) {
editor.view.dispatch(selectColumn(table, col, editor.state.tr))
}

// drag column
const tableWidthPx = getTableWidthPx(table, editor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ import { TableMap } from '@tiptap/pm/tables'
import { Decoration } from '@tiptap/pm/view'

import { type TableNodeLocation } from '../types'
import { isRowSelected, selectRow } from '../utils'
import { findTable, getSelectedRows, isRowSelected, selectRow } from '../utils'

import { moveRow } from './actions'
import { handleSvg } from './icons'
import { duplicateRows, moveRow } from './actions'
import DeleteRow from '../../../icons/table/DeleteRow.svelte'
import Duplicate from '../../../icons/table/Duplicate.svelte'
import textEditorPlugin from '../../../../plugin'
import { createCellsHandle, type OptionItem } from './cellsHandle'
import {
dropMarkerWidthPx,
getDropMarker,
Expand All @@ -39,34 +42,66 @@ interface TableRow {
heightPx: number
}

const createOptionItems = (editor: Editor): OptionItem[] => [
{
id: 'delete',
icon: DeleteRow,
label: textEditorPlugin.string.DeleteRow,
action: () => editor.commands.deleteRow()
},
{
id: 'duplicate',
icon: Duplicate,
label: textEditorPlugin.string.Duplicate,
action: () => {
const table = findTable(editor.state.selection)
if (table !== undefined) {
let tr = editor.state.tr
const selectedRows = getSelectedRows(editor.state.selection, TableMap.get(table.node))
tr = duplicateRows(table, selectedRows, tr)
editor.view.dispatch(tr)
}
}
}
]

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 = document.createElement('div')
const handle = createCellsHandle(createOptionItems(editor))
handle.classList.add('table-row-handle')
if (isRowSelected(row, state.selection)) {
if (isSelected) {
handle.classList.add('table-row-handle__selected')
}
handle.innerHTML = handleSvg
handle.addEventListener('mousedown', (e) => {
handleMouseDown(row, table, e, editor)
handleMouseDown(row, table, e, editor, isSelected)
})

decorations.push(Decoration.widget(pos, handle))
}

return decorations
}

const handleMouseDown = (row: number, table: TableNodeLocation, event: MouseEvent, editor: Editor): void => {
const handleMouseDown = (
row: number,
table: TableNodeLocation,
event: MouseEvent,
editor: Editor,
isSelected: boolean
): void => {
event.stopPropagation()
event.preventDefault()

// select row
editor.view.dispatch(selectRow(table, row, editor.state.tr))
if (!isSelected) {
editor.view.dispatch(selectRow(table, row, editor.state.tr))
}

// drag row
const tableHeightPx = getTableHeightPx(table, editor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { rowHandlerDecoration } from './decorations/rowHandlerDecoration'

export const TableCell = TiptapTableCell.extend({
addProseMirrorPlugins () {
return [...(this.parent?.() ?? []), tableCellDecorationPlugin(this.editor)]
return [tableCellDecorationPlugin(this.editor)]
}
})

Expand Down
28 changes: 24 additions & 4 deletions packages/text-editor/src/components/extension/table/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ export const isRowSelected = (rowIndex: number, selection: Selection): boolean =
return false
}

function getSelectedRect (selection: CellSelection, map: TableMap): Rect {
const start = selection.$anchorCell.start(-1)
return map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start)
}

export const getSelectedRows = (selection: Selection, map: TableMap): number[] => {
if (selection instanceof CellSelection && selection.isRowSelection()) {
const selectedRect = getSelectedRect(selection, map)
return [...Array(selectedRect.bottom - selectedRect.top).keys()].map((idx) => idx + selectedRect.top)
}

return []
}

export const getSelectedColumns = (selection: Selection, map: TableMap): number[] => {
if (selection instanceof CellSelection && selection.isColSelection()) {
const selectedRect = getSelectedRect(selection, map)
return [...Array(selectedRect.right - selectedRect.left).keys()].map((idx) => idx + selectedRect.left)
}

return []
}

export const isTableSelected = (selection: Selection): boolean => {
if (selection instanceof CellSelection) {
const { height, width } = TableMap.get(selection.$anchorCell.node(-1))
Expand All @@ -106,11 +129,8 @@ export const isTableSelected = (selection: Selection): boolean => {

export const isRectSelected = (rect: Rect, selection: CellSelection): boolean => {
const map = TableMap.get(selection.$anchorCell.node(-1))
const start = selection.$anchorCell.start(-1)
const cells = map.cellsInRect(rect)
const selectedCells = map.cellsInRect(
map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start)
)
const selectedCells = map.cellsInRect(getSelectedRect(selection, map))

return cells.every((cell) => selectedCells.includes(cell))
}
Expand Down
17 changes: 17 additions & 0 deletions packages/text-editor/src/components/icons/table/Duplicate.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>

<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 3C5 1.89543 5.89543 1 7 1H12C13.1046 1 14 1.89543 14 3V10C14 11.1046 13.1046 12 12 12H7C5.89543 12 5 11.1046 5 10V3ZM7 2C6.44772 2 6 2.44772 6 3V10C6 10.5523 6.44772 11 7 11H12C12.5523 11 13 10.5523 13 10V3C13 2.44772 12.5523 2 12 2H7Z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4 5C3.44772 5 3 5.44772 3 6V13C3 13.5523 3.44772 14 4 14H9C9.55228 14 10 13.5523 10 13V12H11V13C11 14.1046 10.1046 15 9 15H4C2.89543 15 2 14.1046 2 13V6C2 4.89543 2.89543 4 4 4H5V5H4Z"
/>
</svg>
1 change: 1 addition & 0 deletions packages/text-editor/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default plugin(textEditorId, {
AddRowAfter: '' as IntlString,
DeleteRow: '' as IntlString,
DeleteTable: '' as IntlString,
Duplicate: '' as IntlString,
CategoryRow: '' as IntlString,
CategoryColumn: '' as IntlString,
Table: '' as IntlString,
Expand Down
Loading

0 comments on commit 66a8fbe

Please sign in to comment.