diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index e01b4e8f4..9928ad8bf 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -1,4 +1,4 @@ -import {createEditor, LexicalEditor} from 'lexical'; +import {createEditor} from 'lexical'; import {createEmptyHistoryState, registerHistory} from '@lexical/history'; import {registerRichText} from '@lexical/rich-text'; import {mergeRegister} from '@lexical/utils'; @@ -89,6 +89,9 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st window.debugEditorState = () => { return editor.getEditorState().toJSON(); }; + context.manager.onSelectionChange((selection) => { + console.log(selection, context.editor.getEditorState()); + }); registerCommonNodeMutationListeners(context); diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts index 6d79c01cc..95a723469 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts @@ -383,6 +383,14 @@ export class LexicalNode { return isSelected; } + /** + * Indicate if this node should be selected directly instead of the default + * where the selection would descend to the nearest initial child element. + */ + shouldSelectDirectly(): boolean { + return false; + } + /** * Returns this nodes key. */ diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index 297286a4b..7051336d5 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -476,12 +476,12 @@ export class RangeSelection implements BaseSelection { const startOffset = firstPoint.offset; const endOffset = lastPoint.offset; - if ($isElementNode(firstNode)) { + if ($isElementNode(firstNode) && !firstNode.shouldSelectDirectly()) { const firstNodeDescendant = firstNode.getDescendantByIndex(startOffset); firstNode = firstNodeDescendant != null ? firstNodeDescendant : firstNode; } - if ($isElementNode(lastNode)) { + if ($isElementNode(lastNode) && !lastNode.shouldSelectDirectly()) { let lastNodeDescendant = lastNode.getDescendantByIndex(endOffset); // We don't want to over-select, as node selection infers the child before @@ -499,7 +499,7 @@ export class RangeSelection implements BaseSelection { let nodes: Array; if (firstNode.is(lastNode)) { - if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0) { + if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0 && !firstNode.shouldSelectDirectly()) { nodes = []; } else { nodes = [firstNode]; diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts index a27603773..ce4dbc467 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts @@ -150,6 +150,20 @@ export class ElementNode extends LexicalNode { } return node; } + getFirstSelectableDescendant(): null | T { + if (this.shouldSelectDirectly()) { + return null; + } + let node = this.getFirstChild(); + while ($isElementNode(node) && !node.shouldSelectDirectly()) { + const child = node.getFirstChild(); + if (child === null) { + break; + } + node = child; + } + return node; + } getLastDescendant(): null | T { let node = this.getLastChild(); while ($isElementNode(node)) { @@ -161,6 +175,20 @@ export class ElementNode extends LexicalNode { } return node; } + getLastSelectableDescendant(): null | T { + if (this.shouldSelectDirectly()) { + return null; + } + let node = this.getLastChild(); + while ($isElementNode(node) && !node.shouldSelectDirectly()) { + const child = node.getLastChild(); + if (child === null) { + break; + } + node = child; + } + return node; + } getDescendantByIndex(index: number): null | T { const children = this.getChildren(); const childrenLength = children.length; @@ -319,11 +347,11 @@ export class ElementNode extends LexicalNode { return selection; } selectStart(): RangeSelection { - const firstNode = this.getFirstDescendant(); + const firstNode = this.getFirstSelectableDescendant(); return firstNode ? firstNode.selectStart() : this.select(); } selectEnd(): RangeSelection { - const lastNode = this.getLastDescendant(); + const lastNode = this.getLastSelectableDescendant(); return lastNode ? lastNode.selectEnd() : this.select(); } clear(): this { diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts index 3c845359a..5e5d6b735 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts @@ -75,6 +75,9 @@ export class DetailsNode extends ElementNode { if (this.__open) { el.setAttribute('open', 'true'); + el.removeAttribute('contenteditable'); + } else { + el.setAttribute('contenteditable', 'false'); } const summary = document.createElement('summary'); @@ -84,7 +87,7 @@ export class DetailsNode extends ElementNode { event.preventDefault(); _editor.update(() => { this.select(); - }) + }); }); el.append(summary); @@ -96,6 +99,11 @@ export class DetailsNode extends ElementNode { if (prevNode.__open !== this.__open) { dom.toggleAttribute('open', this.__open); + if (this.__open) { + dom.removeAttribute('contenteditable'); + } else { + dom.setAttribute('contenteditable', 'false'); + } } return prevNode.__id !== this.__id @@ -144,6 +152,7 @@ export class DetailsNode extends ElementNode { } element.removeAttribute('open'); + element.removeAttribute('contenteditable'); return {element}; } @@ -165,6 +174,10 @@ export class DetailsNode extends ElementNode { return node; } + shouldSelectDirectly(): boolean { + return true; + } + } export function $createDetailsNode() { diff --git a/resources/js/wysiwyg/services/mouse-handling.ts b/resources/js/wysiwyg/services/mouse-handling.ts index 058efc8d2..100dd44ff 100644 --- a/resources/js/wysiwyg/services/mouse-handling.ts +++ b/resources/js/wysiwyg/services/mouse-handling.ts @@ -1,31 +1,41 @@ import {EditorUiContext} from "../ui/framework/core"; import { - $createParagraphNode, $getRoot, - $getSelection, + $createParagraphNode, $getNearestNodeFromDOMNode, $getRoot, $isDecoratorNode, CLICK_COMMAND, - COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, - KEY_BACKSPACE_COMMAND, - KEY_DELETE_COMMAND, - KEY_ENTER_COMMAND, KEY_TAB_COMMAND, - LexicalEditor, + COMMAND_PRIORITY_LOW, ElementNode, LexicalNode } from "lexical"; import {$isImageNode} from "@lexical/rich-text/LexicalImageNode"; import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode"; -import {getLastSelection} from "../utils/selection"; -import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes"; -import {$setInsetForSelection} from "../utils/lists"; -import {$isListItemNode} from "@lexical/list"; -import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; import {$isDiagramNode} from "../utils/diagrams"; import {$isTableNode} from "@lexical/table"; +import {$isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; function isHardToEscapeNode(node: LexicalNode): boolean { - return $isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node) || $isTableNode(node); + return $isDecoratorNode(node) + || $isImageNode(node) + || $isMediaNode(node) + || $isDiagramNode(node) + || $isTableNode(node) + || $isDetailsNode(node); +} + +function $getContextNode(event: MouseEvent): ElementNode { + if (event.target instanceof HTMLElement) { + const nearestDetails = event.target.closest('details'); + if (nearestDetails) { + const detailsNode = $getNearestNodeFromDOMNode(nearestDetails); + if ($isDetailsNode(detailsNode)) { + return detailsNode; + } + } + } + return $getRoot(); } function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boolean { - const lastNode = $getRoot().getLastChild(); + const contextNode = $getContextNode(event); + const lastNode = contextNode.getLastChild(); if (!lastNode || !isHardToEscapeNode(lastNode)) { return false; } @@ -40,7 +50,7 @@ function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boole if (isClickBelow) { context.editor.update(() => { const newNode = $createParagraphNode(); - $getRoot().append(newNode); + contextNode.append(newNode); newNode.select(); }); return true; @@ -49,7 +59,6 @@ function insertBelowLastNode(context: EditorUiContext, event: MouseEvent): boole return false; } - export function registerMouseHandling(context: EditorUiContext): () => void { const unregisterClick = context.editor.registerCommand(CLICK_COMMAND, (event): boolean => { insertBelowLastNode(context, event); diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts index 41f6061b6..1f78f4487 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts @@ -193,6 +193,8 @@ export const details: EditorButtonDefinition = { .filter(n => n !== null) as ElementNode[]; const uniqueTopLevels = [...new Set(topLevels)]; + detailsNode.setOpen(true); + if (uniqueTopLevels.length > 0) { uniqueTopLevels[0].insertAfter(detailsNode); } else { diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index a7f5ab387..966c11d3a 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -437,6 +437,16 @@ body.editor-is-fullscreen { .editor-node-resizer.active .editor-node-resizer-ghost { display: block; } +.editor-content-area details[contenteditable="false"], +.editor-content-area summary[contenteditable="false"] { + user-select: none; +} +.editor-content-area details[contenteditable="false"] > details * { + pointer-events: none; +} +.editor-content-area details summary { + caret-color: transparent; +} .editor-table-marker { position: fixed;