Lexical: Fixed ability to cut/copy/drag+drop details nodes

Required updating the selection management to ensure they are selected
upon mousedown, for when a drag operation starts.
Also added draggable management, which we have to avoid having there
when open unless a drag is started, otherwise contents are not editable
in chromium browsers.
This commit is contained in:
Dan Brown
2026-09-07 19:37:26 +01:00
parent 88e7748621
commit 1902a291b3
5 changed files with 27 additions and 5 deletions
@@ -1282,7 +1282,7 @@ export class RangeSelection implements BaseSelection {
if (
insertedParagraph &&
$isElementNode(lastInsertedBlock) &&
(insertedParagraph.canMergeWhenEmpty() || INTERNAL_$isBlock(lastToInsert))
(insertedParagraph.canMergeWhenEmpty() || INTERNAL_$isBlock(lastToInsert) || ($isElementNode(lastToInsert) && lastToInsert.isShadowRoot()))
) {
lastInsertedBlock.append(...insertedParagraph.getChildren());
insertedParagraph.remove();
@@ -5,7 +5,7 @@ import {
LexicalEditor,
LexicalNode,
SerializedElementNode, Spread,
EditorConfig, DOMExportOutput,
EditorConfig, DOMExportOutput, $getSelection,
} from 'lexical';
import {extractDirectionFromElement} from "lexical/nodes/common";
@@ -82,6 +82,7 @@ export class DetailsNode extends ElementNode {
el.setAttribute('open', 'true');
el.removeAttribute('contenteditable');
} else {
el.setAttribute('draggable', 'true');
el.setAttribute('contenteditable', 'false');
}
@@ -94,6 +95,20 @@ export class DetailsNode extends ElementNode {
this.select();
});
});
summary.addEventListener('pointerdown', event => {
_editor.update(() => {
this.select();
});
// We juggle the draggable so that the details element can be dragged when started from the summary,
// but details element otherwise remains undraggable when open to allow editing of its contents
el.draggable = true;
const pointerUpCallback = () => {
document.removeEventListener('pointerup', pointerUpCallback);
el.draggable = false;
};
document.addEventListener('pointerup', pointerUpCallback);
});
summary.addEventListener('dblclick', event => {
event.preventDefault();
@@ -116,8 +131,10 @@ export class DetailsNode extends ElementNode {
dom.toggleAttribute('open', this.__open);
if (this.__open) {
dom.removeAttribute('contenteditable');
dom.removeAttribute('draggable');
} else {
dom.setAttribute('contenteditable', 'false');
dom.setAttribute('draggable', 'true');
}
}
@@ -168,6 +185,7 @@ export class DetailsNode extends ElementNode {
element.removeAttribute('open');
element.removeAttribute('contenteditable');
element.removeAttribute('draggable');
return {element};
}
@@ -7,7 +7,6 @@ import {
} from "lexical";
import type {EditorConfig} from "lexical/LexicalEditor";
import {CommonBlockAlignment, extractAlignmentFromElement} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedElementNode} from "lexical/nodes/LexicalElementNode";
export interface ImageNodeOptions {
@@ -56,12 +56,12 @@ function insertHtmlToEditor(editor: LexicalEditor, html: string, isFromInternal:
if (isFromInternal) {
const selected = $getSelection();
if ($isRangeSelection(selected)) {
selected.removeText();
// Handle single selected node scenarios
const singleSelectedNode = $getSingleSelectableNode(selected.getNodes());
if (singleSelectedNode) {
singleSelectedNode.remove();
} else {
selected.removeText();
}
// Clear out empty blocks
+5
View File
@@ -18,6 +18,7 @@ import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
import {$isDiagramNode} from "./diagrams";
import {$isLinkedImageNode} from "./images";
import {$isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
function wrapTextNodes(nodes: LexicalNode[]): LexicalNode[] {
return nodes.map(node => {
@@ -176,6 +177,10 @@ export function $isSingleSelectableNode(nodes: LexicalNode[]): boolean {
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node)) {
return true;
}
if ($isDetailsNode(node)) {
return true;
}
}
return false;