diff --git a/models/text-editor/src/index.ts b/models/text-editor/src/index.ts index 5b4675878a..ed38407599 100644 --- a/models/text-editor/src/index.ts +++ b/models/text-editor/src/index.ts @@ -299,14 +299,23 @@ export function createModel (builder: Builder): void { // Table category builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { - action: textEditor.function.OpenTableOptions, - icon: textEditor.icon.TableProps, - visibilityTester: textEditor.function.IsEditableTableActive, - label: textEditor.string.TableOptions, + action: textEditor.function.SelectTable, + icon: textEditor.icon.SelectTable, + visibilityTester: textEditor.function.IsTableToolbarContext, + label: textEditor.string.SelectTable, category: 70, index: 5 }) + builder.createDoc(textEditor.class.TextEditorAction, core.space.Model, { + action: textEditor.function.OpenTableOptions, + icon: textEditor.icon.TableProps, + visibilityTester: textEditor.function.IsTableToolbarContext, + label: textEditor.string.TableOptions, + category: 70, + index: 10 + }) + // Image align category createImageAlignmentAction(builder, 'left') createImageAlignmentAction(builder, 'center') diff --git a/models/text-editor/src/plugin.ts b/models/text-editor/src/plugin.ts index 22190c7b71..5540a9240a 100644 --- a/models/text-editor/src/plugin.ts +++ b/models/text-editor/src/plugin.ts @@ -25,6 +25,7 @@ export default mergeIds(textEditorId, textEditor, { function: { FormatLink: '' as Resource, OpenTableOptions: '' as Resource, + SelectTable: '' as Resource, OpenImage: '' as Resource, ExpandImage: '' as Resource, MoreImageActions: '' as Resource, @@ -32,6 +33,7 @@ export default mergeIds(textEditorId, textEditor, { DownloadImage: '' as Resource, IsEditableTableActive: '' as Resource, + IsTableToolbarContext: '' as Resource, IsEditableNote: '' as Resource, IsEditable: '' as Resource, IsHeadingVisible: '' as Resource, diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index a78a677c6b..5003092d10 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -309,7 +309,6 @@ --text-editor-toc-default-color: rgba(255, 255, 255, 0.1); --text-editor-toc-hovered-color: rgba(255, 255, 255, 0.4); - --text-editor-drag-marker-bg-color: #444248; --text-editor-table-header-color: rgba(255, 255, 255, 0.06); --theme-clockface-back: radial-gradient(farthest-corner at 50% 0%, #bbb, #fff 100%); @@ -568,7 +567,6 @@ --text-editor-toc-default-color: rgba(0, 0, 0, 0.1); --text-editor-toc-hovered-color: rgba(0, 0, 0, 0.4); - --text-editor-drag-marker-bg-color: #444248; --text-editor-table-header-color: rgba(0, 0, 0, 0.06); --theme-clockface-back: radial-gradient(farthest-corner at 50% 0%, #606060, #000 100%); diff --git a/packages/theme/styles/prose.scss b/packages/theme/styles/prose.scss index 07723fdc7a..0dd1f8f85d 100644 --- a/packages/theme/styles/prose.scss +++ b/packages/theme/styles/prose.scss @@ -15,14 +15,17 @@ /* Table */ table.proseTable { - --table-selection-border-width: 2px; - --table-selection-border-indent: -2px; + --table-selection-border-width: 1px; + --table-selection-border-indent: -1px; --table-selection-border-radius: 2px; - --table-handle-size: 1.25rem; - --table-handle-indent: calc(-1.25rem - 1px); + --table-handle-size: 0.875rem; + --table-handle-indent: calc(var(--table-handle-size) * -1 - 1px); + --table-handle-col-indent: calc(var(--table-handle-size) * -0.5); + --table-handle-row-indent: calc(var(--table-handle-size) * -1 - 0.75rem); + --table-insert-marker-indent: calc(-1.25rem - 1px); --table-selection-z-index: 100; - --table-drag-and-drop-z-index: 110; + --table-drag-and-drop-z-index: 130; --table-handlers-z-index: 120; border-collapse: collapse; @@ -63,7 +66,6 @@ table.proseTable { &::before { content: ''; border: 0 solid var(--primary-button-focused); - border-radius: var(--table-selection-border-radius); pointer-events: none; position: absolute; z-index: var(--table-selection-z-index); @@ -100,10 +102,12 @@ table.proseTable { align-items: center; button { - background-color: transparent; + background-color: var(--button-border-color); border-radius: var(--table-selection-border-radius); opacity: 0; - transition: background-color 0.3s ease-in-out; + transition-property: opacity, background-color; + transition-timing-function: ease-in-out; + transition-duration: 0.1s; svg { color: var(--theme-button-contrast-hovered); @@ -113,8 +117,6 @@ table.proseTable { &__selected { &::before { content: ''; - background-color: var(--primary-button-focused); - border: var(--table-selection-border-width) solid var(--primary-button-focused); border-radius: var(--table-selection-border-radius); pointer-events: none; position: absolute; @@ -125,9 +127,10 @@ table.proseTable { right: var(--table-selection-border-indent); } - button { + &:hover button { opacity: 1; z-index: var(--table-handlers-z-index); + background-color: var(--primary-button-default); svg { color: white; @@ -147,20 +150,20 @@ table.proseTable { .table-col-handle { position: absolute; - width: calc(100% + 1px); height: var(--table-handle-size); - top: var(--table-handle-indent); - left: 0; + top: var(--table-handle-col-indent); + left: -1px; + right: -1px; button { - height: 70%; + height: 100%; padding: 0 4px; } &:hover { - &:not(.table-col-handle__selected) { - background-color: var(--theme-button-hovered); - } + border-radius: var(--table-selection-border-radius); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; button { opacity: 1; @@ -168,8 +171,9 @@ table.proseTable { } &__selected { + left: 0; &::before { - right: -1px; + right: 0; top: 0; bottom: 0; border-bottom-width: 0; @@ -182,12 +186,15 @@ table.proseTable { .table-row-handle { position: absolute; width: var(--table-handle-size); - height: calc(100% + 1px); - top: 0; - left: var(--table-handle-indent); + top: -1px; + bottom: -1px; + left: var(--table-handle-row-indent); + border-radius: var(--table-selection-border-radius); + border-top-right-radius: 0; + border-bottom-right-radius: 0; button { - width: 70%; + width: 100%; padding: 4px 0; svg { @@ -197,7 +204,6 @@ table.proseTable { &:hover { &:not(.table-row-handle__selected) { - background-color: var(--theme-button-hovered); } button { @@ -206,8 +212,9 @@ table.proseTable { } &__selected { + top: 0; &::before { - bottom: -1px; + bottom: 0; left: 0; right: 0; border-right-width: 0; @@ -230,12 +237,12 @@ table.proseTable { flex-direction: column; justify-content: flex-start; align-items: center; - top: var(--table-handle-indent); + top: var(--table-insert-marker-indent); right: -0.625rem; width: 1.25rem; .table-insert-marker { - width: 0.125rem; + width: 1px; } } @@ -244,12 +251,12 @@ table.proseTable { flex-direction: row; justify-content: flex-start; align-items: center; - left: var(--table-handle-indent); + left: var(--table-insert-marker-indent); bottom: -0.625rem; height: 1.25rem; .table-insert-marker { - height: 0.125rem; + height: 1px; } } @@ -258,11 +265,14 @@ table.proseTable { width: 1.25rem; height: 1.25rem; + opacity: 0; + svg { color: white; } &:hover { + opacity: 1; border-radius: 50%; background-color: var(--primary-button-focused); } @@ -301,24 +311,41 @@ table.proseTable { .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); + background-color: transparent; display: flex; justify-content: center; align-items: center; + button { + margin: auto; + background-color: var(--button-border-color); + border-radius: var(--table-selection-border-radius); + } + svg { color: white; - margin: auto; } } .table-col-drag-marker { height: var(--table-handle-size); - top: calc(var(--table-handle-indent) + 1px); + top: var(--table-handle-col-indent); + + &::before { + content: ''; + position: absolute; + top: 50%; + width: 100%; + height: 1px; + background-color: var(--primary-button-focused); + z-index: -1; + } + + button { + height: 100%; + padding: 0 4px; + } svg { width: 100%; @@ -327,7 +354,12 @@ table.proseTable { .table-row-drag-marker { width: var(--table-handle-size); - left: calc(var(--table-handle-indent) + 1px / 2); + left: var(--table-handle-row-indent); + + button { + width: 100%; + padding: 4px 0; + } svg { height: 100%; diff --git a/plugins/text-editor-assets/assets/icons.svg b/plugins/text-editor-assets/assets/icons.svg index 239fe4387f..0de43e2d89 100644 --- a/plugins/text-editor-assets/assets/icons.svg +++ b/plugins/text-editor-assets/assets/icons.svg @@ -201,4 +201,10 @@ + + + + + + \ No newline at end of file diff --git a/plugins/text-editor-assets/lang/en.json b/plugins/text-editor-assets/lang/en.json index e9e19ce94f..cceeb8ebd7 100644 --- a/plugins/text-editor-assets/lang/en.json +++ b/plugins/text-editor-assets/lang/en.json @@ -52,6 +52,7 @@ "Table": "Table", "InsertTable": "Insert table", "TableOptions": "Customize table", + "SelectTable": "Select table", "Width": "Width", "Height": "Height", "Unset": "Unset", diff --git a/plugins/text-editor-assets/lang/ru.json b/plugins/text-editor-assets/lang/ru.json index bf64f35c1d..e1cea576aa 100644 --- a/plugins/text-editor-assets/lang/ru.json +++ b/plugins/text-editor-assets/lang/ru.json @@ -52,6 +52,7 @@ "Table": "Таблица", "InsertTable": "Добавить таблицу", "TableOptions": "Настроить таблицу", + "SelectTable": "Выделить таблицу", "Width": "Ширина", "Height": "Высота", "Unset": "Убрать", diff --git a/plugins/text-editor-assets/src/index.ts b/plugins/text-editor-assets/src/index.ts index 0b5a98d852..43a450ce20 100644 --- a/plugins/text-editor-assets/src/index.ts +++ b/plugins/text-editor-assets/src/index.ts @@ -40,5 +40,6 @@ loadMetadata(textEditor.icon, { ScaleOut: `${icons}#scaleOut`, Download: `${icons}#download`, Note: `${icons}#note`, - Comment: `${icons}#comment` + Comment: `${icons}#comment`, + SelectTable: `${icons}#move` }) diff --git a/plugins/text-editor-resources/src/components/TextActionButton.svelte b/plugins/text-editor-resources/src/components/TextActionButton.svelte index f49c779d10..7405371e40 100644 --- a/plugins/text-editor-resources/src/components/TextActionButton.svelte +++ b/plugins/text-editor-resources/src/components/TextActionButton.svelte @@ -23,6 +23,7 @@ export let size: IconSize export let editor: Editor export let actionCtx: ActionContext + export let blockMouseEvents = true const dispatch = createEventDispatcher() let selected: boolean = false @@ -44,6 +45,11 @@ } async function handleClick (event: MouseEvent): Promise { + if (blockMouseEvents) { + event.preventDefault() + event.stopPropagation() + } + const handler = action.action if (typeof handler === 'string') { @@ -67,7 +73,7 @@ use:tooltip={{ label: action.label }} tabindex="0" data-id={'btn' + action.label.split(':').pop()} - on:click|preventDefault|stopPropagation={handleClick} + on:click={handleClick} > diff --git a/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte b/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte index bc237606a5..7d31a324b0 100644 --- a/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte +++ b/plugins/text-editor-resources/src/components/extension/table/TableNodeView.svelte @@ -20,6 +20,7 @@ import { NodeViewContent, NodeViewProps, NodeViewWrapper } from '../../node-view' import { findTable, insertColumn, insertRow } from './utils' import { TableMap } from '@tiptap/pm/tables' + import TableToolbar from './TableToolbar.svelte' export let node: NodeViewProps['node'] export let getPos: NodeViewProps['getPos'] @@ -77,6 +78,10 @@ {#if editable && focused} +
+ +
+
@@ -115,41 +120,14 @@ right: 0; } - &.table-selected { - &::before { - border: 1.25rem var(--theme-button-default) solid; - border-radius: 1.25rem; - inset: 0 -1.25rem; - } - } - .table-button-container { position: absolute; transition: opacity 0.15s ease-in-out 0.15s; - &__col { - right: -1.25rem; - top: 0; - bottom: 0; - margin: 1.25rem 0; - - .table-button { - width: 1.25rem; - } - } - - &__row { - bottom: 0; - left: 0; - right: 0; - - .table-button { - height: 1.25rem; - } - } - .table-button { + border-radius: 2px; background-color: transparent; + color: var(--theme-button-contrast-hovered); &:hover { background-color: var(--theme-button-hovered); @@ -161,6 +139,7 @@ height: 0.25rem; border-radius: 50%; background-color: var(--text-editor-table-marker-color); + display: none; } .table-button__icon { @@ -175,6 +154,38 @@ display: block; } } + + &__col { + right: -1.25rem; + top: 0; + bottom: 0; + margin: 1.25rem 0; + + .table-button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + width: 1.25rem; + } + } + + &__row { + bottom: 0; + left: 0; + right: 0; + + .table-button { + border-top-left-radius: 0; + border-top-right-radius: 0; + height: 1.25rem; + } + } } } + + .table-toolbar-container { + position: absolute; + top: -1.5rem; + right: 0; + z-index: 200; + } diff --git a/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte b/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte new file mode 100644 index 0000000000..b2fc07a074 --- /dev/null +++ b/plugins/text-editor-resources/src/components/extension/table/TableToolbar.svelte @@ -0,0 +1,71 @@ + + + +
+ {#each actions as action} + + {/each} +
+ + diff --git a/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts b/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts index babed2a35c..e3cf2ae58c 100644 --- a/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts +++ b/plugins/text-editor-resources/src/components/extension/table/decorations/columnHandlerDecoration.ts @@ -138,7 +138,7 @@ const handleMouseDown = ( const dropMarkerLeftPx = dropIndex <= col ? columns[dropIndex].leftPx : columns[dropIndex].leftPx + columns[dropIndex].widthPx - updateColDropMarker(dropMarker, dropMarkerLeftPx - dropMarkerWidthPx / 2, dropMarkerWidthPx) + updateColDropMarker(dropMarker, dropMarkerLeftPx - Math.floor(dropMarkerWidthPx / 2) - 1, dropMarkerWidthPx) updateColDragMarker(dragMarker, dragMarkerLeftPx, dragMarkerWidthPx) } } diff --git a/plugins/text-editor-resources/src/components/extension/table/decorations/tableDragMarkerDecoration.ts b/plugins/text-editor-resources/src/components/extension/table/decorations/tableDragMarkerDecoration.ts index bb2ae7087a..c11b1ec840 100644 --- a/plugins/text-editor-resources/src/components/extension/table/decorations/tableDragMarkerDecoration.ts +++ b/plugins/text-editor-resources/src/components/extension/table/decorations/tableDragMarkerDecoration.ts @@ -23,7 +23,7 @@ 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 dropMarkerWidthPx = 1 export const tableDragMarkerDecoration = (state: EditorState, table: TableNodeLocation): Decoration[] => { const dropMarker = document.createElement('div') @@ -33,14 +33,16 @@ export const tableDragMarkerDecoration = (state: EditorState, table: TableNodeLo const colDragMarker = document.createElement('div') colDragMarker.id = colDragMarkerId colDragMarker.classList.add('table-col-drag-marker') - colDragMarker.innerHTML = handleSvg colDragMarker.style.display = 'none' + const colDragMarkerBtn = colDragMarker.appendChild(document.createElement('button')) + colDragMarkerBtn.innerHTML = handleSvg const rowDragMarker = document.createElement('div') rowDragMarker.id = rowDragMarkerId rowDragMarker.classList.add('table-row-drag-marker') - rowDragMarker.innerHTML = handleSvg rowDragMarker.style.display = 'none' + const rowDragMarkerBtn = rowDragMarker.appendChild(document.createElement('button')) + rowDragMarkerBtn.innerHTML = handleSvg return [ Decoration.widget(table.start, dropMarker), diff --git a/plugins/text-editor-resources/src/components/extension/table/table.ts b/plugins/text-editor-resources/src/components/extension/table/table.ts index 55b6965923..c130cdf7bd 100644 --- a/plugins/text-editor-resources/src/components/extension/table/table.ts +++ b/plugins/text-editor-resources/src/components/extension/table/table.ts @@ -17,11 +17,11 @@ import { type Editor } from '@tiptap/core' import TiptapTable from '@tiptap/extension-table' import { CellSelection } from '@tiptap/pm/tables' import { getEventPositionElement, SelectPopup, showPopup } from '@hcengineering/ui' -import textEditor from '@hcengineering/text-editor' +import textEditor, { type ActionContext } from '@hcengineering/text-editor' import { SvelteNodeViewRenderer } from '../../node-view' import TableNodeView from './TableNodeView.svelte' -import { isTableSelected } from './utils' +import { findTable, isTableSelected, selectTable as selectTableNode } from './utils' import AddColAfter from '../../icons/table/AddColAfter.svelte' import AddColBefore from '../../icons/table/AddColBefore.svelte' import AddRowAfter from '../../icons/table/AddRowAfter.svelte' @@ -31,6 +31,8 @@ import DeleteRow from '../../icons/table/DeleteRow.svelte' import DeleteTable from '../../icons/table/DeleteTable.svelte' export const Table = TiptapTable.extend({ + draggable: true, + addKeyboardShortcuts () { return { 'Mod-Backspace': () => handleDelete(this.editor), @@ -145,6 +147,19 @@ export async function openTableOptions (editor: Editor, event: MouseEvent): Prom }) } +export async function selectTable (editor: Editor, event: MouseEvent): Promise { + const table = findTable(editor.state.selection) + if (table === undefined) return + + event.preventDefault() + + editor.view.dispatch(selectTableNode(table, editor.state.tr)) +} + export async function isEditableTableActive (editor: Editor): Promise { return editor.isEditable && editor.isActive('table') } + +export async function isTableToolbarContext (editor: Editor, context: ActionContext): Promise { + return editor.isEditable && editor.isActive('table') && context.tag === 'table-toolbar' +} diff --git a/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts b/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts index 0d96457a9d..b65fbd131c 100644 --- a/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts +++ b/plugins/text-editor-resources/src/components/node-view/svelte-node-view-renderer.ts @@ -110,6 +110,13 @@ class SvelteNodeView extends NodeView => ({ function: { FormatLink: formatLink, OpenTableOptions: openTableOptions, + SelectTable: selectTable, OpenImage: openImage, ExpandImage: expandImage, DownloadImage: downloadImage, MoreImageActions: moreImageActions, ConfigureNote: configureNote, IsEditableTableActive: isEditableTableActive, + IsTableToolbarContext: isTableToolbarContext, IsEditableNote: isEditableNote, IsEditable: isEditable, IsHeadingVisible: isHeadingVisible, diff --git a/plugins/text-editor/src/plugin.ts b/plugins/text-editor/src/plugin.ts index 125f8500b5..c3e95c1692 100644 --- a/plugins/text-editor/src/plugin.ts +++ b/plugins/text-editor/src/plugin.ts @@ -91,6 +91,7 @@ export default plugin(textEditorId, { CategoryColumn: '' as IntlString, Table: '' as IntlString, TableOptions: '' as IntlString, + SelectTable: '' as IntlString, Width: '' as IntlString, Height: '' as IntlString, Unset: '' as IntlString, @@ -123,6 +124,7 @@ export default plugin(textEditorId, { ScaleOut: '' as Asset, Download: '' as Asset, Note: '' as Asset, - Comment: '' as Asset + Comment: '' as Asset, + SelectTable: '' as Asset } }) diff --git a/plugins/text-editor/src/types.ts b/plugins/text-editor/src/types.ts index fb10f04a13..5df5a107ec 100644 --- a/plugins/text-editor/src/types.ts +++ b/plugins/text-editor/src/types.ts @@ -108,6 +108,7 @@ export interface ActionContext { objectId?: Ref objectClass?: Ref> objectSpace?: Ref + tag?: string } /** diff --git a/tests/sanity/tests/model/documents/document-content-page.ts b/tests/sanity/tests/model/documents/document-content-page.ts index 84e679bffe..11c7cb237b 100644 --- a/tests/sanity/tests/model/documents/document-content-page.ts +++ b/tests/sanity/tests/model/documents/document-content-page.ts @@ -32,8 +32,7 @@ export class DocumentContentPage extends CommonPage { readonly buttonInsertColumn = (col: number = 0): Locator => this.page.locator('div.table-col-insert').nth(col).locator('button') - readonly buttonInsertLastRow = (): Locator => - this.page.locator('table.proseTable + div.table-button-container__col + div.table-button-container__row') + readonly buttonInsertLastRow = (): Locator => this.page.locator('div.table-button-container__row') readonly buttonInsertInnerRow = (row: number = 0): Locator => this.page.locator('table.proseTable').locator('tr').nth(row).locator('div.table-row-insert button')