mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 03:40:48 +00:00
Table options (#5617)
Signed-off-by: andrewkuryan <Andrew.Kuryan@tut.by>
This commit is contained in:
parent
401ca99604
commit
66a8fbeac1
@ -39,6 +39,7 @@
|
||||
"AddRowAfter": "Add after",
|
||||
"DeleteRow": "Delete",
|
||||
"DeleteTable": "Delete",
|
||||
"Duplicate": "Duplicate",
|
||||
|
||||
"CategoryRow": "Rows",
|
||||
"CategoryColumn": "Columns",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"AddRowAfter": "Añadir después",
|
||||
"DeleteRow": "Eliminar",
|
||||
"DeleteTable": "Eliminar",
|
||||
"Duplicate": "Duplicar",
|
||||
"CategoryRow": "Filas",
|
||||
"CategoryColumn": "Columnas",
|
||||
"Table": "Tabla",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"AddRowAfter": "Adicionar depois",
|
||||
"DeleteRow": "Eliminar",
|
||||
"DeleteTable": "Eliminar",
|
||||
"Duplicate": "Duplicar",
|
||||
"CategoryRow": "Linhas",
|
||||
"CategoryColumn": "Colunas",
|
||||
"Table": "Tabela",
|
||||
@ -49,4 +50,4 @@
|
||||
"Image": "Imagem",
|
||||
"SeparatorLine": "linha separadora"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,7 @@
|
||||
"AddRowAfter": "Добавить после",
|
||||
"DeleteRow": "Удалить",
|
||||
"DeleteTable": "Удалить",
|
||||
"Duplicate": "Дублировать",
|
||||
|
||||
"CategoryRow": "Строки",
|
||||
"CategoryColumn": "Колонки",
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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,
|
||||
@ -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)
|
||||
|
@ -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,
|
||||
@ -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)
|
||||
|
@ -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)]
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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))
|
||||
@ -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))
|
||||
}
|
||||
|
@ -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>
|
@ -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,
|
||||
|
@ -1,14 +1,14 @@
|
||||
//
|
||||
// Copyright © 2022, 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.
|
||||
//
|
||||
@ -95,9 +95,15 @@ table.proseTable {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
color: var(--theme-button-contrast-hovered);
|
||||
button {
|
||||
background-color: transparent;
|
||||
border-radius: var(--table-selection-border-radius);
|
||||
opacity: 0;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
|
||||
svg {
|
||||
color: var(--theme-button-contrast-hovered);
|
||||
}
|
||||
}
|
||||
|
||||
&__selected {
|
||||
@ -115,10 +121,22 @@ table.proseTable {
|
||||
right: var(--table-selection-border-indent);
|
||||
}
|
||||
|
||||
svg {
|
||||
z-index: var(--table-handlers-z-index);
|
||||
color: white;
|
||||
button {
|
||||
opacity: 1;
|
||||
z-index: var(--table-handlers-z-index);
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-button-hovered);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.pressed {
|
||||
background-color: var(--primary-button-pressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,12 +148,19 @@ table.proseTable {
|
||||
top: var(--table-handle-indent);
|
||||
left: 0;
|
||||
|
||||
button {
|
||||
height: 70%;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.table-col-handle__selected) {
|
||||
background-color: var(--theme-button-hovered);
|
||||
}
|
||||
|
||||
svg { opacity: 1; }
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__selected {
|
||||
@ -157,8 +182,13 @@ table.proseTable {
|
||||
top: 0;
|
||||
left: var(--table-handle-indent);
|
||||
|
||||
svg {
|
||||
transform: rotate(90deg);
|
||||
button {
|
||||
width: 70%;
|
||||
padding: 4px 0;
|
||||
|
||||
svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@ -166,7 +196,7 @@ table.proseTable {
|
||||
background-color: var(--theme-button-hovered);
|
||||
}
|
||||
|
||||
svg {
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user