From dce79dbac756bc3363d5aa8108cd28385442d210 Mon Sep 17 00:00:00 2001 From: d3m1d0v Date: Tue, 30 Sep 2025 20:18:30 +0300 Subject: [PATCH] feat(YfmTable): change border color for selected cells --- .../plugins/YfmTableControls/const.ts | 4 +- .../plugins/YfmTableControls/dnd/dnd.scss | 82 ++++++++++++++++--- .../plugins/YfmTableControls/dnd/dnd.ts | 18 +--- .../nodeviews/yfm-table-cell-view.tsx | 14 +--- .../YfmTableControls/plugins/dnd-plugin.ts | 60 +++++++++----- .../plugins/YfmTableControls/utils.ts | 70 ++++++++++++++++ 6 files changed, 189 insertions(+), 59 deletions(-) create mode 100644 src/extensions/yfm/YfmTable/plugins/YfmTableControls/utils.ts diff --git a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/const.ts b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/const.ts index 29dc9601..d29f5f67 100644 --- a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/const.ts +++ b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/const.ts @@ -10,9 +10,9 @@ export enum YfmTableDecorationType { OpenRowMenu = 'cell--open-row-menu', // sign of opening the row menu in the cell OpenColumnMenu = 'cell--open-column-menu', // sign of opening the column menu in the cell - ActivateRow = 'row--active', + ActivateRowCells = 'cell--active-row', ActivateColumnCells = 'cell--active-column', - ActivateDangerRow = 'row--danger', + ActivateDangerRowCells = 'cell--danger-row', ActivateDangerColumnCells = 'cell--danger-column', } diff --git a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss index e928769c..347f0e48 100644 --- a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss +++ b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.scss @@ -41,19 +41,75 @@ z-index: 2; } -.yfm.ProseMirror { - .g-md-yfm-table-dnd-dragged-row, - .g-md-yfm-table-dnd-dragged-column-cell { - opacity: 0.3; - background-color: var(--g-color-base-selection); - // --yfm-color-border: var(--g-color-line-brand); - } -} +.yfm.ProseMirror table { + td { + $selectedCell: 'g-md-yfm-table-selected-cell'; + + position: relative; + + &.#{$selectedCell} { + overflow: unset; + + border-color: var(--g-color-line-brand); + background-color: var(--g-color-base-selection); + + &::after { + position: absolute; + z-index: 2; + inset: -1px; + + display: inline-block; + + content: ''; + pointer-events: none; + + border: 1px solid var(--g-color-line-brand); + } + + &_first-row::after { + top: 0; + } -.yfm.ProseMirror { - .g-md-yfm-table-active-row, - .g-md-yfm-table-active-column-cell { - background-color: var(--g-color-base-selection); - // --yfm-color-border: var(--g-color-line-brand); + &_last-row::after { + bottom: 0; + } + + &_first-column::after { + left: 0; + } + + &_last-column::after { + right: 0; + } + + &_first-row.#{$selectedCell}_first-column::after { + border-top-left-radius: 8px; + } + + &_first-row.#{$selectedCell}_last-column::after { + border-top-right-radius: 8px; + } + + &_last-row.#{$selectedCell}_first-column::after { + border-bottom-left-radius: 8px; + } + + &_last-row.#{$selectedCell}_last-column::after { + border-bottom-right-radius: 8px; + } + + &.dragged-cell::before { + position: absolute; + inset: 0; + + display: inline-block; + + content: ''; + pointer-events: none; + + opacity: 0.7; + background-color: var(--g-color-base-background); + } + } } } diff --git a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts index 019a596e..c3c1f67f 100644 --- a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts +++ b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/dnd/dnd.ts @@ -2,7 +2,7 @@ import {type Node, Slice} from '#pm/model'; import {TextSelection} from '#pm/state'; import {findParentNodeClosestToPos} from '#pm/utils'; import type {EditorView} from '#pm/view'; -import {debounce, range as iterate} from 'src/lodash'; +import {debounce} from 'src/lodash'; import type {Logger2} from 'src/logger'; import {isTableNode} from 'src/table-utils'; import { @@ -17,6 +17,7 @@ import { import {YfmTableNode} from '../../../YfmTableSpecs'; import {clearAllSelections, selectDraggedColumn, selectDraggedRow} from '../plugins/dnd-plugin'; import {hideHoverDecos} from '../plugins/focus-plugin'; +import {getSelectedCellsForColumns, getSelectedCellsForRows} from '../utils'; import { type DropCursorParams, @@ -212,12 +213,7 @@ class YfmTableRowDnDHandler extends YfmTableDnDAbstractHandler { { const {tr} = this._editorView.state; hideHoverDecos(tr); - selectDraggedRow( - tr, - iterate(currRowRange.startIdx, currRowRange.endIdx + 1).map((rowIdx) => - tableDesc.getPosForRow(rowIdx), - ), - ); + selectDraggedRow(tr, getSelectedCellsForRows(info.tableDesc, currRowRange)); this._editorView.dispatch(tr); } @@ -415,15 +411,9 @@ class YfmTableColumnDnDHandler extends YfmTableDnDAbstractHandler { this._logger.event({event: 'column-drag-start'}); { - const columnCellsPos: CellPos[] = []; - for (const i of iterate(currColumnRange.startIdx, currColumnRange.endIdx + 1)) { - columnCellsPos.push(...tableDesc.getPosForColumn(i)); - } - const realPos = columnCellsPos.filter((cell) => cell.type === 'real'); - const {tr} = this._editorView.state; hideHoverDecos(tr); - selectDraggedColumn(tr, realPos); + selectDraggedColumn(tr, getSelectedCellsForColumns(tableDesc, currColumnRange)); this._editorView.dispatch(tr); } { diff --git a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx index adb9f718..2a9e0bd7 100644 --- a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx +++ b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/nodeviews/yfm-table-cell-view.tsx @@ -8,7 +8,6 @@ import type {Logger2} from 'src/logger'; import {ErrorLoggerBoundary} from 'src/react-utils/ErrorBoundary'; import {isTableNode} from 'src/table-utils'; import { - type CellPos, type TableColumnRange, TableDesc, type TableDescBinded, @@ -35,6 +34,7 @@ import { deactivateColumn, deactivateRow, } from '../plugins/dnd-plugin'; +import {getSelectedCellsForColumns, getSelectedCellsForRows} from '../utils'; const dropCursorParams: DropCursorParams = { color: 'var(--g-color-line-brand)', @@ -241,9 +241,7 @@ class YfmTableCellView implements NodeView { const rowRange = info.tableDesc.base.getRowRangeByRowIdx(info.cell.row); const tr = activateRows(this._view.state.tr, { controlCell: {from: info.pos, to: info.pos + this._node.nodeSize}, - rows: iterate(rowRange.startIdx, rowRange.endIdx + 1).map((rowIdx) => - info.tableDesc.getPosForRow(rowIdx), - ), + cells: getSelectedCellsForRows(info.tableDesc, rowRange), uniqKey: this._decoRowUniqKey, }); this._view.dispatch(tr); @@ -264,14 +262,10 @@ class YfmTableCellView implements NodeView { if (!info) return; this._decoColumnUniqKey = Date.now(); - const currColumnRange = info.tableDesc.base.getColumnRangeByColumnIdx(info.cell.column); - const cells: CellPos[] = []; - for (const i of iterate(currColumnRange.startIdx, currColumnRange.endIdx + 1)) { - cells.push(...info.tableDesc.getPosForColumn(i)); - } + const columnRange = info.tableDesc.base.getColumnRangeByColumnIdx(info.cell.column); const tr = activateColumns(this._view.state.tr, { controlCell: {from: info.pos, to: info.pos + this._node.nodeSize}, - cells: cells.filter((pos) => pos.type === 'real'), + cells: getSelectedCellsForColumns(info.tableDesc, columnRange), uniqKey: this._decoColumnUniqKey, }); this._view.dispatch(tr); diff --git a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/dnd-plugin.ts b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/dnd-plugin.ts index 95e80959..9e61e3a0 100644 --- a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/dnd-plugin.ts +++ b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/plugins/dnd-plugin.ts @@ -1,5 +1,6 @@ import {Plugin, PluginKey, type Transaction} from '#pm/state'; import {Decoration, DecorationSet} from '#pm/view'; +import {cn} from 'src/classname'; import { YfmTableDecorationType, @@ -7,25 +8,43 @@ import { YfmTableDecorationUniqKey, } from '../const'; +const b = cn('yfm-table-selected-cell'); + type FromTo = { from: number; to: number; }; +type CellMods = { + 'first-row'?: boolean; + 'last-row'?: boolean; + 'first-column'?: boolean; + 'last-column'?: boolean; +}; + +export type SelectedCellPos = FromTo & { + mods: CellMods; +}; + type DndMeta = - | {action: 'row-control-active'; controlCell: FromTo; rows: FromTo[]; uniqKey: number} - | {action: 'column-control-active'; controlCell: FromTo; cells: FromTo[]; uniqKey: number} + | {action: 'row-control-active'; controlCell: FromTo; cells: SelectedCellPos[]; uniqKey: number} + | { + action: 'column-control-active'; + controlCell: FromTo; + cells: SelectedCellPos[]; + uniqKey: number; + } | {action: 'row-control-non-active'; uniqKey: number} | {action: 'column-control-non-active'; uniqKey: number} - | {action: 'drag-rows'; rows: FromTo[]} - | {action: 'drag-columns'; cells: FromTo[]} + | {action: 'drag-rows'; cells: SelectedCellPos[]} + | {action: 'drag-columns'; cells: SelectedCellPos[]} | {action: 'hide'}; const key = new PluginKey('yfm-table-dnd-decos'); export function activateRows( tr: Transaction, - params: {controlCell: FromTo; rows: FromTo[]; uniqKey: number}, + params: {controlCell: FromTo; cells: SelectedCellPos[]; uniqKey: number}, ): Transaction { const meta: DndMeta = {action: 'row-control-active', ...params}; tr.setMeta(key, meta); @@ -40,7 +59,7 @@ export function deactivateRow(tr: Transaction, uniqKey: number): Transaction { export function activateColumns( tr: Transaction, - params: {controlCell: FromTo; cells: FromTo[]; uniqKey: number}, + params: {controlCell: FromTo; cells: SelectedCellPos[]; uniqKey: number}, ): Transaction { const meta: DndMeta = {action: 'column-control-active', ...params}; tr.setMeta(key, meta); @@ -53,13 +72,13 @@ export function deactivateColumn(tr: Transaction, uniqKey: number): Transaction return tr; } -export function selectDraggedRow(tr: Transaction, rows: FromTo[]): Transaction { - const meta: DndMeta = {action: 'drag-rows', rows}; +export function selectDraggedRow(tr: Transaction, cells: SelectedCellPos[]): Transaction { + const meta: DndMeta = {action: 'drag-rows', cells}; tr.setMeta(key, meta); return tr; } -export function selectDraggedColumn(tr: Transaction, cells: FromTo[]): Transaction { +export function selectDraggedColumn(tr: Transaction, cells: SelectedCellPos[]): Transaction { const meta: DndMeta = {action: 'drag-columns', cells}; tr.setMeta(key, meta); return tr; @@ -86,7 +105,7 @@ export const yfmTableDndPlugin = () => { } if (meta?.action === 'row-control-active') { - const {controlCell, rows, uniqKey} = meta; + const {controlCell, cells, uniqKey} = meta; return DecorationSet.create(tr.doc, [ Decoration.node( controlCell.from, @@ -97,14 +116,15 @@ export const yfmTableDndPlugin = () => { [YfmTableDecorationTypeKey]: YfmTableDecorationType.OpenRowMenu, }, ), - ...rows.map((row) => + ...cells.map((cell) => Decoration.node( - row.from, - row.to, - {class: 'g-md-yfm-table-active-row'}, + cell.from, + cell.to, + {class: b(cell.mods)}, { [YfmTableDecorationUniqKey]: uniqKey, - [YfmTableDecorationTypeKey]: YfmTableDecorationType.ActivateRow, + [YfmTableDecorationTypeKey]: + YfmTableDecorationType.ActivateRowCells, }, ), ), @@ -137,7 +157,7 @@ export const yfmTableDndPlugin = () => { Decoration.node( pos.from, pos.to, - {class: 'g-md-yfm-table-active-column-cell'}, + {class: b(pos.mods)}, { [YfmTableDecorationUniqKey]: uniqKey, [YfmTableDecorationTypeKey]: @@ -161,9 +181,9 @@ export const yfmTableDndPlugin = () => { if (meta?.action === 'drag-rows') { return DecorationSet.create( tr.doc, - meta.rows.map((row) => - Decoration.node(row.from, row.to, { - class: 'g-md-yfm-table-dnd-dragged-row', + meta.cells.map((cell) => + Decoration.node(cell.from, cell.to, { + class: b(cell.mods, 'dragged-cell'), }), ), ); @@ -174,7 +194,7 @@ export const yfmTableDndPlugin = () => { tr.doc, meta.cells.map((cell) => Decoration.node(cell.from, cell.to, { - class: 'g-md-yfm-table-dnd-dragged-column-cell', + class: b(cell.mods, 'dragged-cell'), }), ), ); diff --git a/src/extensions/yfm/YfmTable/plugins/YfmTableControls/utils.ts b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/utils.ts new file mode 100644 index 00000000..a057fa68 --- /dev/null +++ b/src/extensions/yfm/YfmTable/plugins/YfmTableControls/utils.ts @@ -0,0 +1,70 @@ +import type { + TableCellRealDesc, + TableColumnRange, + TableDescBinded, + TableRowRange, +} from 'src/table-utils/table-desc'; + +import type {SelectedCellPos} from './plugins/dnd-plugin'; + +export function getSelectedCellsForRows( + tableDesc: TableDescBinded, + range: TableRowRange, +): SelectedCellPos[] { + const cells: SelectedCellPos[] = []; + + for (let rowIdx = range.startIdx; rowIdx <= range.endIdx; rowIdx++) { + for (let colIdx = 0; colIdx < tableDesc.cols; colIdx++) { + const cell = getSelectedCellPos(tableDesc, rowIdx, colIdx); + if (cell) cells.push(cell); + } + } + + return cells; +} + +export function getSelectedCellsForColumns( + tableDesc: TableDescBinded, + range: TableColumnRange, +): SelectedCellPos[] { + const cells: SelectedCellPos[] = []; + + for (let rowIdx = 0; rowIdx < tableDesc.rows; rowIdx++) { + for (let colIdx = range.startIdx; colIdx <= range.endIdx; colIdx++) { + const cell = getSelectedCellPos(tableDesc, rowIdx, colIdx); + if (cell) cells.push(cell); + } + } + + return cells; +} + +function getSelectedCellPos( + tableDesc: TableDescBinded, + rowIdx: number, + colIdx: number, +): SelectedCellPos | null { + const pos = tableDesc.getPosForCell(rowIdx, colIdx); + if (pos.type === 'virtual') return null; + + const cell: SelectedCellPos = { + from: pos.from, + to: pos.to, + mods: { + 'first-row': rowIdx === 0, + 'last-row': rowIdx === tableDesc.rows - 1, + 'first-column': colIdx === 0, + 'last-column': colIdx === tableDesc.cols - 1, + }, + }; + + const {rowspan, colspan} = tableDesc.base.rowsDesc[rowIdx].cells[colIdx] as TableCellRealDesc; + if (rowspan && rowspan > 0 && rowIdx + rowspan >= tableDesc.rows) { + cell.mods['last-row'] = true; + } + if (colspan && colspan > 0 && colIdx + colspan >= tableDesc.cols) { + cell.mods['last-column'] = true; + } + + return cell; +}