refactor: table drag preview using decorations (#8597)
* refactor: table drag preview using decorations * fix: history meta for table drag state
This commit is contained in:
parent
8fa08b2506
commit
d497304de5
10 changed files with 124 additions and 37 deletions
|
|
@ -139,8 +139,6 @@ ATTRIBUTES = {
|
||||||
"rowspan",
|
"rowspan",
|
||||||
"colwidth",
|
"colwidth",
|
||||||
"background",
|
"background",
|
||||||
"hideContent",
|
|
||||||
"hidecontent",
|
|
||||||
"style",
|
"style",
|
||||||
},
|
},
|
||||||
"td": {
|
"td": {
|
||||||
|
|
@ -150,8 +148,6 @@ ATTRIBUTES = {
|
||||||
"background",
|
"background",
|
||||||
"textColor",
|
"textColor",
|
||||||
"textcolor",
|
"textcolor",
|
||||||
"hideContent",
|
|
||||||
"hidecontent",
|
|
||||||
"style",
|
"style",
|
||||||
},
|
},
|
||||||
"tr": {"background", "textColor", "textcolor", "style"},
|
"tr": {"background", "textColor", "textcolor", "style"},
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import {
|
||||||
updateColDragMarker,
|
updateColDragMarker,
|
||||||
updateColDropMarker,
|
updateColDropMarker,
|
||||||
} from "../marker-utils";
|
} from "../marker-utils";
|
||||||
import { updateCellContentVisibility } from "../utils";
|
import { showCellContent } from "../utils";
|
||||||
import { ColumnOptionsDropdown } from "./dropdown";
|
import { ColumnOptionsDropdown } from "./dropdown";
|
||||||
import { calculateColumnDropIndex, constructColumnDragPreview, getTableColumnNodesInfo } from "./utils";
|
import { calculateColumnDropIndex, constructColumnDragPreview, getTableColumnNodesInfo } from "./utils";
|
||||||
|
|
||||||
|
|
@ -152,8 +152,9 @@ export function ColumnDragHandle(props: ColumnDragHandleProps) {
|
||||||
hideDropMarker(dropMarker);
|
hideDropMarker(dropMarker);
|
||||||
hideDragMarker(dragMarker);
|
hideDragMarker(dragMarker);
|
||||||
|
|
||||||
|
// Show cell content by clearing decorations
|
||||||
if (isCellSelection(editor.state.selection)) {
|
if (isCellSelection(editor.state.selection)) {
|
||||||
updateCellContentVisibility(editor, false);
|
showCellContent(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (col !== dropIndex) {
|
if (col !== dropIndex) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { TableMap } from "@tiptap/pm/tables";
|
||||||
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
|
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
|
||||||
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
|
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
|
||||||
// local imports
|
// local imports
|
||||||
import { cloneTableCell, constructDragPreviewTable, updateCellContentVisibility } from "../utils";
|
import { cloneTableCell, constructDragPreviewTable, getSelectedCellPositions, hideCellContent } from "../utils";
|
||||||
|
|
||||||
type TableColumn = {
|
type TableColumn = {
|
||||||
left: number;
|
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;
|
return tableElement;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import {
|
||||||
updateRowDragMarker,
|
updateRowDragMarker,
|
||||||
updateRowDropMarker,
|
updateRowDropMarker,
|
||||||
} from "../marker-utils";
|
} from "../marker-utils";
|
||||||
import { updateCellContentVisibility } from "../utils";
|
import { showCellContent } from "../utils";
|
||||||
import { RowOptionsDropdown } from "./dropdown";
|
import { RowOptionsDropdown } from "./dropdown";
|
||||||
import { calculateRowDropIndex, constructRowDragPreview, getTableRowNodesInfo } from "./utils";
|
import { calculateRowDropIndex, constructRowDragPreview, getTableRowNodesInfo } from "./utils";
|
||||||
|
|
||||||
|
|
@ -152,8 +152,9 @@ export function RowDragHandle(props: RowDragHandleProps) {
|
||||||
hideDropMarker(dropMarker);
|
hideDropMarker(dropMarker);
|
||||||
hideDragMarker(dragMarker);
|
hideDragMarker(dragMarker);
|
||||||
|
|
||||||
|
// Show cell content by clearing decorations
|
||||||
if (isCellSelection(editor.state.selection)) {
|
if (isCellSelection(editor.state.selection)) {
|
||||||
updateCellContentVisibility(editor, false);
|
showCellContent(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row !== dropIndex) {
|
if (row !== dropIndex) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { TableMap } from "@tiptap/pm/tables";
|
||||||
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
|
import { getSelectedRect, isCellSelection } from "@/extensions/table/table/utilities/helpers";
|
||||||
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
|
import type { TableNodeLocation } from "@/extensions/table/table/utilities/helpers";
|
||||||
// local imports
|
// local imports
|
||||||
import { cloneTableCell, constructDragPreviewTable, updateCellContentVisibility } from "../utils";
|
import { cloneTableCell, constructDragPreviewTable, getSelectedCellPositions, hideCellContent } from "../utils";
|
||||||
|
|
||||||
type TableRow = {
|
type TableRow = {
|
||||||
top: number;
|
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;
|
return tableElement;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Editor } from "@tiptap/core";
|
import type { Editor } from "@tiptap/core";
|
||||||
|
import type { Selection } from "@tiptap/pm/state";
|
||||||
|
import { TableMap } from "@tiptap/pm/tables";
|
||||||
// constants
|
// constants
|
||||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
|
||||||
import { CORE_EDITOR_META } from "@/constants/meta";
|
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.
|
* @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.
|
* @description Get positions of all cells in the current selection.
|
||||||
* @param {Editor} editor - The editor instance.
|
* @param {Selection} selection - The selection.
|
||||||
* @param {boolean} hideContent - Whether to hide the content.
|
* @param {TableNodeLocation} table - The table node location.
|
||||||
* @returns {boolean} Whether the content visibility was updated.
|
* @returns {number[]} Array of cell positions.
|
||||||
*/
|
*/
|
||||||
export const updateCellContentVisibility = (editor: Editor, hideContent: boolean): boolean =>
|
export const getSelectedCellPositions = (selection: Selection, table: TableNodeLocation): number[] => {
|
||||||
editor
|
if (!isCellSelection(selection)) return [];
|
||||||
.chain()
|
|
||||||
.focus()
|
const tableMap = TableMap.get(table.node);
|
||||||
.setMeta(CORE_EDITOR_META.ADD_TO_HISTORY, false)
|
const selectedRect = getSelectedRect(selection, tableMap);
|
||||||
.updateAttributes(CORE_EXTENSIONS.TABLE_CELL, {
|
const cellsInSelection = tableMap.cellsInRect(selectedRect);
|
||||||
hideContent,
|
|
||||||
})
|
// Convert relative positions to absolute document positions
|
||||||
.updateAttributes(CORE_EXTENSIONS.TABLE_HEADER, {
|
return cellsInSelection.map((cellPos) => table.start + cellPos);
|
||||||
hideContent,
|
};
|
||||||
})
|
|
||||||
.run();
|
/**
|
||||||
|
* @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, false);
|
||||||
|
editor.view.dispatch(tr);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -53,9 +53,6 @@ export const TableCell = Node.create<TableCellOptions>({
|
||||||
textColor: {
|
textColor: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
hideContent: {
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -116,7 +113,6 @@ export const TableCell = Node.create<TableCellOptions>({
|
||||||
return [
|
return [
|
||||||
"td",
|
"td",
|
||||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
class: node.attrs.hideContent ? "content-hidden" : "",
|
|
||||||
style: `background-color: ${node.attrs.background}; color: ${node.attrs.textColor};`,
|
style: `background-color: ${node.attrs.background}; color: ${node.attrs.textColor};`,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,6 @@ export const TableHeader = Node.create<TableHeaderOptions>({
|
||||||
background: {
|
background: {
|
||||||
default: "none",
|
default: "none",
|
||||||
},
|
},
|
||||||
hideContent: {
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -63,7 +60,6 @@ export const TableHeader = Node.create<TableHeaderOptions>({
|
||||||
return [
|
return [
|
||||||
"th",
|
"th",
|
||||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||||
class: node.attrs.hideContent ? "content-hidden" : "",
|
|
||||||
style: `background-color: ${node.attrs.background};`,
|
style: `background-color: ${node.attrs.background};`,
|
||||||
}),
|
}),
|
||||||
0,
|
0,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
// constants
|
// constants
|
||||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||||
// local imports
|
// local imports
|
||||||
|
import { TableDragStatePlugin } from "../plugins/drag-state";
|
||||||
import { TableColumnDragHandlePlugin } from "../plugins/drag-handles/column/plugin";
|
import { TableColumnDragHandlePlugin } from "../plugins/drag-handles/column/plugin";
|
||||||
import { TableRowDragHandlePlugin } from "../plugins/drag-handles/row/plugin";
|
import { TableRowDragHandlePlugin } from "../plugins/drag-handles/row/plugin";
|
||||||
import { TableInsertPlugin } from "../plugins/insert-handlers/plugin";
|
import { TableInsertPlugin } from "../plugins/insert-handlers/plugin";
|
||||||
|
|
@ -281,6 +282,7 @@ export const Table = Node.create<TableOptions>({
|
||||||
tableEditing({
|
tableEditing({
|
||||||
allowTableNodeSelection: this.options.allowTableNodeSelection,
|
allowTableNodeSelection: this.options.allowTableNodeSelection,
|
||||||
}),
|
}),
|
||||||
|
TableDragStatePlugin,
|
||||||
TableInsertPlugin(this.editor),
|
TableInsertPlugin(this.editor),
|
||||||
TableColumnDragHandlePlugin(this.editor),
|
TableColumnDragHandlePlugin(this.editor),
|
||||||
TableRowDragHandlePlugin(this.editor),
|
TableRowDragHandlePlugin(this.editor),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue