refactor: table drag preview using decorations

This commit is contained in:
Aaryan Khandelwal
2026-01-29 16:14:27 +05:30
parent b8d3b3c5eb
commit 2b3185d6cc
10 changed files with 124 additions and 37 deletions

View File

@@ -139,8 +139,6 @@ ATTRIBUTES = {
"rowspan",
"colwidth",
"background",
"hideContent",
"hidecontent",
"style",
},
"td": {
@@ -150,8 +148,6 @@ ATTRIBUTES = {
"background",
"textColor",
"textcolor",
"hideContent",
"hidecontent",
"style",
},
"tr": {"background", "textColor", "textcolor", "style"},

View File

@@ -42,7 +42,7 @@ import {
updateColDragMarker,
updateColDropMarker,
} from "../marker-utils";
import { updateCellContentVisibility } from "../utils";
import { showCellContent } from "../utils";
import { ColumnOptionsDropdown } from "./dropdown";
import { calculateColumnDropIndex, constructColumnDragPreview, getTableColumnNodesInfo } from "./utils";
@@ -152,8 +152,9 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
hideDropMarker(dropMarker);
hideDragMarker(dragMarker);
// Show cell content by clearing decorations
if (isCellSelection(editor.state.selection)) {
updateCellContentVisibility(editor, false);
showCellContent(editor);
}
if (col !== dropIndex) {

View File

@@ -11,7 +11,7 @@ import { TableMap } from "@tiptap/pm/tables";
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
// local imports
import { cloneTableCell, constructDragPreviewTable, updateCellContentVisibility } from "../utils";
import { cloneTableCell, constructDragPreviewTable, getSelectedCellPositions, hideCellContent } from "../utils";
type TableColumn = {
left: number;
@@ -151,7 +151,9 @@ export const constructColumnDragPreview = (
}
});
updateCellContentVisibility(editor, true);
// Hide the selected cells using decorations (local only, not persisted)
const cellPositions = getSelectedCellPositions(selection, table);
hideCellContent(editor, cellPositions);
return tableElement;
};

View File

@@ -42,7 +42,7 @@ import {
updateRowDragMarker,
updateRowDropMarker,
} from "../marker-utils";
import { updateCellContentVisibility } from "../utils";
import { showCellContent } from "../utils";
import { RowOptionsDropdown } from "./dropdown";
import { calculateRowDropIndex, constructRowDragPreview, getTableRowNodesInfo } from "./utils";
@@ -152,8 +152,9 @@ export function RowDragHandle(props: RowDragHandleProps) {
hideDropMarker(dropMarker);
hideDragMarker(dragMarker);
// Show cell content by clearing decorations
if (isCellSelection(editor.state.selection)) {
updateCellContentVisibility(editor, false);
showCellContent(editor);
}
if (row !== dropIndex) {

View File

@@ -11,7 +11,7 @@ import { TableMap } from "@tiptap/pm/tables";
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
// local imports
import { cloneTableCell, constructDragPreviewTable, updateCellContentVisibility } from "../utils";
import { cloneTableCell, constructDragPreviewTable, getSelectedCellPositions, hideCellContent } from "../utils";
type TableRow = {
top: number;
@@ -150,7 +150,9 @@ export const constructRowDragPreview = (
}
});
updateCellContentVisibility(editor, true);
// Hide the selected cells using decorations (local only, not persisted)
const cellPositions = getSelectedCellPositions(selection, table);
hideCellContent(editor, cellPositions);
return tableElement;
};

View File

@@ -5,9 +5,15 @@
*/
import type { Editor } from "@tiptap/core";
import type { Selection } from "@tiptap/pm/state";
import { TableMap } from "@tiptap/pm/tables";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
import { CORE_EDITOR_META } from "@/constants/meta";
// extensions
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
// local imports
import { updateTransactionMeta } from "../drag-state";
/**
* @description Construct a pseudo table element which will act as a parent for column and row drag previews.
@@ -47,20 +53,41 @@ export const cloneTableCell = (
};
/**
* @description This function updates the `hideContent` attribute of the table cells and headers.
* @param {Editor} editor - The editor instance.
* @param {boolean} hideContent - Whether to hide the content.
* @returns {boolean} Whether the content visibility was updated.
* @description Get positions of all cells in the current selection.
* @param {Selection} selection - The selection.
* @param {TableNodeLocation} table - The table node location.
* @returns {number[]} Array of cell positions.
*/
export const updateCellContentVisibility = (editor: Editor, hideContent: boolean): boolean =>
editor
.chain()
.focus()
.setMeta(CORE_EDITOR_META.ADD_TO_HISTORY, false)
.updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
hideContent,
})
.updateAttributes(CORE_EXTENSIONS.TABLE_HEADER, {
hideContent,
})
.run();
export const getSelectedCellPositions = (selection: Selection, table: TableNodeLocation): number[] => {
if (!isCellSelection(selection)) return [];
const tableMap = TableMap.get(table.node);
const selectedRect = getSelectedRect(selection, tableMap);
const cellsInSelection = tableMap.cellsInRect(selectedRect);
// Convert relative positions to absolute document positions
return cellsInSelection.map((cellPos) => table.start + cellPos);
};
/**
* @description Hide cell content using decorations (local only, not persisted).
* @param {Editor} editor - The editor instance.
* @param {number[]} cellPositions - Array of cell positions to hide.
*/
export const hideCellContent = (editor: Editor, cellPositions: number[]): void => {
const tr = editor.view.state.tr;
updateTransactionMeta(tr, cellPositions);
tr.setMeta(CORE_EDITOR_META.ADD_TO_HISTORY, false);
editor.view.dispatch(tr);
};
/**
* @description Show cell content by clearing decorations.
* @param {Editor} editor - The editor instance.
*/
export const showCellContent = (editor: Editor): void => {
const tr = editor.view.state.tr;
updateTransactionMeta(tr, null);
tr.setMeta(CORE_EDITOR_META.ADD_TO_HISTORY, true);
editor.view.dispatch(tr);
};

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Transaction } from "@tiptap/pm/state";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
const TABLE_DRAG_STATE_PLUGIN_KEY = new PluginKey("tableDragState");
export const updateTransactionMeta = (tr: Transaction, hiddenCellPositions: number[] | null) => {
tr.setMeta(TABLE_DRAG_STATE_PLUGIN_KEY, hiddenCellPositions);
};
/**
* @description Plugin to manage table drag state using decorations.
* This allows hiding cell content during drag operations without modifying the document.
* Decorations are local to each user and not persisted or shared.
*/
export const TableDragStatePlugin = new Plugin({
key: TABLE_DRAG_STATE_PLUGIN_KEY,
state: {
init() {
return DecorationSet.empty;
},
apply(tr, oldState) {
// Get metadata about which cells to hide
const hiddenCellPositions = tr.getMeta(TABLE_DRAG_STATE_PLUGIN_KEY) as number[] | null;
if (hiddenCellPositions === undefined) {
// No change, map decorations through the transaction
return oldState.map(tr.mapping, tr.doc);
}
if (hiddenCellPositions === null || !Array.isArray(hiddenCellPositions) || hiddenCellPositions.length === 0) {
// Clear all decorations
return DecorationSet.empty;
}
// Create decorations for hidden cells
const decorations: Decoration[] = [];
hiddenCellPositions.forEach((pos) => {
if (typeof pos !== "number") return;
const node = tr.doc.nodeAt(pos);
if (node) {
decorations.push(
Decoration.node(pos, pos + node.nodeSize, {
class: "content-hidden",
})
);
}
});
return DecorationSet.create(tr.doc, decorations);
},
},
props: {
decorations(state) {
return this.getState(state);
},
},
});

View File

@@ -53,9 +53,6 @@ export const TableCell = Node.create<TableCellOptions>({
textColor: {
default: null,
},
hideContent: {
default: false,
},
};
},
@@ -116,7 +113,6 @@ export const TableCell = Node.create<TableCellOptions>({
return [
"td",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: node.attrs.hideContent ? "content-hidden" : "",
style: `background-color: ${node.attrs.background}; color: ${node.attrs.textColor};`,
}),
0,

View File

@@ -45,9 +45,6 @@ export const TableHeader = Node.create<TableHeaderOptions>({
background: {
default: "none",
},
hideContent: {
default: false,
},
};
},
@@ -63,7 +60,6 @@ export const TableHeader = Node.create<TableHeaderOptions>({
return [
"th",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: node.attrs.hideContent ? "content-hidden" : "",
style: `background-color: ${node.attrs.background};`,
}),
0,

View File

@@ -28,6 +28,7 @@ import {
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// local imports
import { TableDragStatePlugin } from "../plugins/drag-state";
import { TableColumnDragHandlePlugin } from "../plugins/drag-handles/column/plugin";
import { TableRowDragHandlePlugin } from "../plugins/drag-handles/row/plugin";
import { TableInsertPlugin } from "../plugins/insert-handlers/plugin";
@@ -281,6 +282,7 @@ export const Table = Node.create<TableOptions>({
tableEditing({
allowTableNodeSelection: this.options.allowTableNodeSelection,
}),
TableDragStatePlugin,
TableInsertPlugin(this.editor),
TableColumnDragHandlePlugin(this.editor),
TableRowDragHandlePlugin(this.editor),