mirror of
https://github.com/docmost/docmost.git
synced 2026-05-19 07:54:05 +08:00
feat: table enhancement (#2191)
This commit is contained in:
@@ -60,6 +60,23 @@ function nodeDOMAtCoords(
|
||||
options: GlobalDragHandleOptions,
|
||||
view: EditorView,
|
||||
) {
|
||||
// Custom nodes (transclusion, …) render via tiptap's React node-view
|
||||
// renderer, which emits `class="react-renderer node-${name}"` on the
|
||||
// live wrapper — the `data-type` attribute is for static HTML
|
||||
// serialization only. Match both so we cover live and parsed DOM.
|
||||
// Inside a custom node, also match plain `p` so the first paragraph
|
||||
// (which doesn't match `:not(:first-child)`) still gets its own
|
||||
// handle; only hovers on the custom node's padding/border fall
|
||||
// through to the wrapper.
|
||||
const customSelectors = options.customNodes.flatMap((node) => [
|
||||
`[data-type=${node}]`,
|
||||
`.node-${node}`,
|
||||
]);
|
||||
const customParagraphSelectors = options.customNodes.flatMap((node) => [
|
||||
`[data-type=${node}] p`,
|
||||
`.node-${node} p`,
|
||||
]);
|
||||
|
||||
const selectors = [
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
@@ -71,7 +88,13 @@ function nodeDOMAtCoords(
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
...options.customNodes.map((node) => `[data-type=${node}]`),
|
||||
// Tables nested in another block (toggle, transclusion, …) have a
|
||||
// wrapper that isn't a direct child of .ProseMirror, so the
|
||||
// parent-check below skips it. Match the wrapper explicitly so the
|
||||
// handle shows up even with empty cells.
|
||||
".tableWrapper",
|
||||
...customParagraphSelectors,
|
||||
...customSelectors,
|
||||
].join(", ");
|
||||
return document
|
||||
.elementsFromPoint(coords.x, coords.y)
|
||||
@@ -99,6 +122,22 @@ function nodePosAtDOM(
|
||||
})?.inside;
|
||||
}
|
||||
|
||||
function isCustomNodeDOM(
|
||||
elem: Element | null | undefined,
|
||||
options: GlobalDragHandleOptions,
|
||||
): boolean {
|
||||
if (!elem) return false;
|
||||
for (const name of options.customNodes) {
|
||||
if (
|
||||
elem.getAttribute("data-type") === name ||
|
||||
elem.classList.contains(`node-${name}`)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function calcNodePos(pos: number, view: EditorView) {
|
||||
const $pos = view.state.doc.resolve(pos);
|
||||
if ($pos.depth > 1) return $pos.before($pos.depth);
|
||||
@@ -137,7 +176,6 @@ export function DragHandlePlugin(
|
||||
|
||||
const nodePos = view.state.doc.resolve(fromSelectionPos);
|
||||
|
||||
// Check if nodePos points to the top level node
|
||||
if (nodePos.node().type.name === "doc") differentNodeSelected = true;
|
||||
else {
|
||||
const nodeSelection = NodeSelection.create(
|
||||
@@ -166,14 +204,46 @@ export function DragHandlePlugin(
|
||||
} else {
|
||||
selection = NodeSelection.create(view.state.doc, draggedNodePos);
|
||||
|
||||
// if inline node is selected, e.g mention -> go to the parent node to select the whole node
|
||||
// if table row is selected, go to the parent node to select the whole node
|
||||
if (
|
||||
(selection as NodeSelection).node.type.isInline ||
|
||||
(selection as NodeSelection).node.type.name === "tableRow"
|
||||
) {
|
||||
let $pos = view.state.doc.resolve(selection.from);
|
||||
selection = NodeSelection.create(view.state.doc, $pos.before());
|
||||
const $sel = view.state.doc.resolve(selection.from);
|
||||
|
||||
if (isCustomNodeDOM(node, options)) {
|
||||
// The drag landed on a custom-node container (transclusion etc.).
|
||||
// Walk up to the matching node so the drag moves the whole
|
||||
// container, not whatever inner element the click landed on.
|
||||
const customTypes = new Set(options.customNodes);
|
||||
for (let d = $sel.depth; d > 0; d--) {
|
||||
if (customTypes.has($sel.node(d).type.name)) {
|
||||
selection = NodeSelection.create(
|
||||
view.state.doc,
|
||||
$sel.before(d),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the selected node lives inside a table (at any nesting
|
||||
// depth), promote to the whole table — the global drag handle is
|
||||
// meant to move the table as a single block, not a row/cell. The
|
||||
// earlier tableRow-only check only worked when the table sat at
|
||||
// the doc root; once wrapped in another node (toggle, layout,
|
||||
// etc.) the selection lands on a cell/paragraph and that check
|
||||
// never fired.
|
||||
let tableDepth = -1;
|
||||
for (let d = $sel.depth; d > 0; d--) {
|
||||
if ($sel.node(d).type.name === "table") {
|
||||
tableDepth = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tableDepth > 0) {
|
||||
selection = NodeSelection.create(
|
||||
view.state.doc,
|
||||
$sel.before(tableDepth),
|
||||
);
|
||||
} else if ((selection as NodeSelection).node.type.isInline) {
|
||||
// Inline node (e.g. mention): walk up to the parent block.
|
||||
selection = NodeSelection.create(view.state.doc, $sel.before());
|
||||
}
|
||||
}
|
||||
}
|
||||
view.dispatch(view.state.tr.setSelection(selection));
|
||||
@@ -313,6 +383,27 @@ export function DragHandlePlugin(
|
||||
return;
|
||||
}
|
||||
|
||||
const isCustomNode = isCustomNodeDOM(node, options);
|
||||
|
||||
// Custom nodes pin the handle to the inner NodeViewWrapper's top-left:
|
||||
// the natural anchor sits in transient/empty space outside the visible block.
|
||||
if (isCustomNode) {
|
||||
// tiptap React node-views emit an outer `.react-renderer` whose first
|
||||
// child is the visible NodeViewWrapper; walk to that outer first since
|
||||
// `node` may be either the outer or an inner element with data-type.
|
||||
const rendererOuter =
|
||||
(node.closest(".react-renderer") as HTMLElement | null) ?? node;
|
||||
const inner =
|
||||
(rendererOuter.firstElementChild as HTMLElement | null) ??
|
||||
rendererOuter;
|
||||
const innerRect = absoluteRect(inner);
|
||||
if (!dragHandleElement) return;
|
||||
dragHandleElement.style.left = `${innerRect.left + 4}px`;
|
||||
dragHandleElement.style.top = `${innerRect.top + 4}px`;
|
||||
showDragHandle();
|
||||
return;
|
||||
}
|
||||
|
||||
const compStyle = window.getComputedStyle(node);
|
||||
const parsedLineHeight = parseInt(compStyle.lineHeight, 10);
|
||||
const lineHeight = isNaN(parsedLineHeight)
|
||||
@@ -328,6 +419,13 @@ export function DragHandlePlugin(
|
||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||
rect.left -= options.dragHandleWidth;
|
||||
}
|
||||
// Tables: clear the table's own row-drag handle so the two
|
||||
// grips don't stack on each other. `nodeDOMAtCoords` returns
|
||||
// the wrapper for top-level hovers (wrapper is direct child of
|
||||
// .ProseMirror) and a descendant for deeper hovers — cover both.
|
||||
if (node.closest(".tableWrapper")) {
|
||||
rect.left -= options.dragHandleWidth;
|
||||
}
|
||||
rect.width = options.dragHandleWidth;
|
||||
|
||||
if (!dragHandleElement) return;
|
||||
|
||||
Reference in New Issue
Block a user