diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index 64cd601a9..81378e944 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -112,7 +112,7 @@ export class PageEditor extends Component { } savePage() { - this.container.closest('form').submit(); + this.container.closest('form').requestSubmit(); } async saveDraft() { diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index 56dbe8d7c..5a2581900 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -25,6 +25,7 @@ export class WysiwygEditor extends Component { textDirection: this.$opts.textDirection, translations, }); + window.wysiwyg = this.editor; }); let handlingFormSubmit = false; @@ -38,7 +39,9 @@ export class WysiwygEditor extends Component { handlingFormSubmit = true; this.editor.getContentAsHtml().then(html => { this.input.value = html; - this.input.form.submit(); + setTimeout(() => { + this.input.form.requestSubmit(); + }, 5); }); } else { handlingFormSubmit = false; diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index d54a64ce8..6a8e45724 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -37,6 +37,7 @@ import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode"; import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; import {EditorUiContext} from "../../../../ui/framework/core"; import {EditorUIManager} from "../../../../ui/framework/manager"; +import {ImageNode} from "@lexical/rich-text/LexicalImageNode"; type TestEnv = { readonly container: HTMLDivElement; @@ -484,6 +485,9 @@ export function createTestContext(): EditorUiContext { const editor = createTestEditor({ namespace: 'testing', theme: {}, + nodes: [ + ImageNode, + ] }); editor.setRootElement(editorDOM); diff --git a/resources/js/wysiwyg/lexical/list/LexicalListNode.ts b/resources/js/wysiwyg/lexical/list/LexicalListNode.ts index 6edf0d64a..b5c83addd 100644 --- a/resources/js/wysiwyg/lexical/list/LexicalListNode.ts +++ b/resources/js/wysiwyg/lexical/list/LexicalListNode.ts @@ -332,7 +332,19 @@ function isDomChecklist(domNode: HTMLElement) { } // if children are checklist items, the node is a checklist ul. Applicable for googledoc checklist pasting. for (const child of domNode.childNodes) { - if (isHTMLElement(child) && child.hasAttribute('aria-checked')) { + if (!isHTMLElement(child)) { + continue; + } + + if (child.hasAttribute('aria-checked')) { + return true; + } + + if (child.classList.contains('task-list-item')) { + return true; + } + + if (child.firstElementChild && child.firstElementChild.matches('input[type="checkbox"]')) { return true; } } diff --git a/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts b/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts index 8c7729dbf..b85383e7d 100644 --- a/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts +++ b/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts @@ -6,7 +6,7 @@ * */ import {ParagraphNode, TextNode} from 'lexical'; -import {initializeUnitTest} from 'lexical/__tests__/utils'; +import {createTestContext} from 'lexical/__tests__/utils'; import { $createListItemNode, @@ -16,6 +16,7 @@ import { ListItemNode, ListNode, } from '../..'; +import {$htmlToBlockNodes} from "../../../../utils/nodes"; const editorConfig = Object.freeze({ namespace: '', @@ -46,123 +47,122 @@ const editorConfig = Object.freeze({ }); describe('LexicalListNode tests', () => { - initializeUnitTest((testEnv) => { - test('ListNode.constructor', async () => { - const {editor} = testEnv; + test('ListNode.constructor', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - expect(listNode.getType()).toBe('list'); - expect(listNode.getTag()).toBe('ul'); - expect(listNode.getTextContent()).toBe(''); - }); - - // @ts-expect-error - expect(() => $createListNode()).toThrow(); + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + expect(listNode.getType()).toBe('list'); + expect(listNode.getTag()).toBe('ul'); + expect(listNode.getTextContent()).toBe(''); }); - test('ListNode.getTag()', async () => { - const {editor} = testEnv; + // @ts-expect-error + expect(() => $createListNode()).toThrow(); + }); - await editor.update(() => { - const ulListNode = $createListNode('bullet', 1); - expect(ulListNode.getTag()).toBe('ul'); - const olListNode = $createListNode('number', 1); - expect(olListNode.getTag()).toBe('ol'); - const checkListNode = $createListNode('check', 1); - expect(checkListNode.getTag()).toBe('ul'); - }); + test('ListNode.getTag()', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const ulListNode = $createListNode('bullet', 1); + expect(ulListNode.getTag()).toBe('ul'); + const olListNode = $createListNode('number', 1); + expect(olListNode.getTag()).toBe('ol'); + const checkListNode = $createListNode('check', 1); + expect(checkListNode.getTag()).toBe('ul'); }); + }); - test('ListNode.createDOM()', async () => { - const {editor} = testEnv; + test('ListNode.createDOM()', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - expect(listNode.createDOM(editorConfig).outerHTML).toBe( + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + expect(listNode.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-1"></ul>', - ); - expect( + ); + expect( listNode.createDOM({ namespace: '', theme: { list: {}, }, }).outerHTML, - ).toBe('<ul></ul>'); - expect( + ).toBe('<ul></ul>'); + expect( listNode.createDOM({ namespace: '', theme: {}, }).outerHTML, - ).toBe('<ul></ul>'); - }); + ).toBe('<ul></ul>'); }); + }); - test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => { - const {editor} = testEnv; + test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode1 = $createListNode('bullet'); - const listNode2 = $createListNode('bullet'); - const listNode3 = $createListNode('bullet'); - const listNode4 = $createListNode('bullet'); - const listNode5 = $createListNode('bullet'); - const listNode6 = $createListNode('bullet'); - const listNode7 = $createListNode('bullet'); + await editor.update(() => { + const listNode1 = $createListNode('bullet'); + const listNode2 = $createListNode('bullet'); + const listNode3 = $createListNode('bullet'); + const listNode4 = $createListNode('bullet'); + const listNode5 = $createListNode('bullet'); + const listNode6 = $createListNode('bullet'); + const listNode7 = $createListNode('bullet'); - const listItem1 = $createListItemNode(); - const listItem2 = $createListItemNode(); - const listItem3 = $createListItemNode(); - const listItem4 = $createListItemNode(); + const listItem1 = $createListItemNode(); + const listItem2 = $createListItemNode(); + const listItem3 = $createListItemNode(); + const listItem4 = $createListItemNode(); - listNode1.append(listItem1); - listItem1.append(listNode2); - listNode2.append(listItem2); - listItem2.append(listNode3); - listNode3.append(listItem3); - listItem3.append(listNode4); - listNode4.append(listItem4); - listNode4.append(listNode5); - listNode5.append(listNode6); - listNode6.append(listNode7); + listNode1.append(listItem1); + listItem1.append(listNode2); + listNode2.append(listItem2); + listItem2.append(listNode3); + listNode3.append(listItem3); + listItem3.append(listNode4); + listNode4.append(listItem4); + listNode4.append(listNode5); + listNode5.append(listNode6); + listNode6.append(listNode7); - expect(listNode1.createDOM(editorConfig).outerHTML).toBe( + expect(listNode1.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-1"></ul>', - ); - expect( + ); + expect( listNode1.createDOM({ namespace: '', theme: { list: {}, }, }).outerHTML, - ).toBe('<ul></ul>'); - expect( + ).toBe('<ul></ul>'); + expect( listNode1.createDOM({ namespace: '', theme: {}, }).outerHTML, - ).toBe('<ul></ul>'); - expect(listNode2.createDOM(editorConfig).outerHTML).toBe( + ).toBe('<ul></ul>'); + expect(listNode2.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-2"></ul>', - ); - expect(listNode3.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode3.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-3"></ul>', - ); - expect(listNode4.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode4.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-4"></ul>', - ); - expect(listNode5.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode5.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-5"></ul>', - ); - expect(listNode6.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode6.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-6"></ul>', - ); - expect(listNode7.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode7.createDOM(editorConfig).outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-7"></ul>', - ); - expect( + ); + expect( listNode5.createDOM({ namespace: '', theme: { @@ -176,123 +176,135 @@ describe('LexicalListNode tests', () => { }, }, }).outerHTML, - ).toBe('<ul class="my-ul-list-class my-ul-list-class-2"></ul>'); - }); + ).toBe('<ul class="my-ul-list-class my-ul-list-class-2"></ul>'); }); + }); - test('ListNode.updateDOM()', async () => { - const {editor} = testEnv; + test('ListNode.updateDOM()', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - const domElement = listNode.createDOM(editorConfig); + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + const domElement = listNode.createDOM(editorConfig); - expect(domElement.outerHTML).toBe( + expect(domElement.outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-1"></ul>', - ); + ); - const newListNode = $createListNode('number', 1); - const result = newListNode.updateDOM( + const newListNode = $createListNode('number', 1); + const result = newListNode.updateDOM( listNode, domElement, editorConfig, - ); + ); - expect(result).toBe(true); - expect(domElement.outerHTML).toBe( + expect(result).toBe(true); + expect(domElement.outerHTML).toBe( '<ul class="my-ul-list-class my-ul-list-class-1"></ul>', - ); - }); - }); - - test('ListNode.append() should properly transform a ListItemNode', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode = new ListNode('bullet', 1); - const listItemNode = new ListItemNode(); - const textNode = new TextNode('Hello'); - - listItemNode.append(textNode); - const nodesToAppend = [listItemNode]; - - expect(listNode.append(...nodesToAppend)).toBe(listNode); - expect(listNode.getFirstChild()).toBe(listItemNode); - expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); - }); - }); - - test('ListNode.append() should properly transform a ListNode', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode = new ListNode('bullet', 1); - const nestedListNode = new ListNode('bullet', 1); - const listItemNode = new ListItemNode(); - const textNode = new TextNode('Hello'); - - listItemNode.append(textNode); - nestedListNode.append(listItemNode); - - const nodesToAppend = [nestedListNode]; - - expect(listNode.append(...nodesToAppend)).toBe(listNode); - expect($isListItemNode(listNode.getFirstChild())).toBe(true); - expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe( - nestedListNode, - ); - }); - }); - - test('ListNode.append() should properly transform a ParagraphNode', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode = new ListNode('bullet', 1); - const paragraph = new ParagraphNode(); - const textNode = new TextNode('Hello'); - paragraph.append(textNode); - const nodesToAppend = [paragraph]; - - expect(listNode.append(...nodesToAppend)).toBe(listNode); - expect($isListItemNode(listNode.getFirstChild())).toBe(true); - expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); - }); - }); - - test('$createListNode()', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - const createdListNode = $createListNode('bullet'); - - expect(listNode.__type).toEqual(createdListNode.__type); - expect(listNode.__parent).toEqual(createdListNode.__parent); - expect(listNode.__tag).toEqual(createdListNode.__tag); - expect(listNode.__key).not.toEqual(createdListNode.__key); - }); - }); - - test('$isListNode()', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - - expect($isListNode(listNode)).toBe(true); - }); - }); - - test('$createListNode() with tag name (backward compatibility)', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const numberList = $createListNode('number', 1); - const bulletList = $createListNode('bullet', 1); - expect(numberList.__listType).toBe('number'); - expect(bulletList.__listType).toBe('bullet'); - }); + ); }); }); + + test('ListNode.append() should properly transform a ListItemNode', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = new ListNode('bullet', 1); + const listItemNode = new ListItemNode(); + const textNode = new TextNode('Hello'); + + listItemNode.append(textNode); + const nodesToAppend = [listItemNode]; + + expect(listNode.append(...nodesToAppend)).toBe(listNode); + expect(listNode.getFirstChild()).toBe(listItemNode); + expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); + }); + }); + + test('ListNode.append() should properly transform a ListNode', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = new ListNode('bullet', 1); + const nestedListNode = new ListNode('bullet', 1); + const listItemNode = new ListItemNode(); + const textNode = new TextNode('Hello'); + + listItemNode.append(textNode); + nestedListNode.append(listItemNode); + + const nodesToAppend = [nestedListNode]; + + expect(listNode.append(...nodesToAppend)).toBe(listNode); + expect($isListItemNode(listNode.getFirstChild())).toBe(true); + expect(listNode.getFirstChild<ListItemNode>()!.getFirstChild()).toBe( + nestedListNode, + ); + }); + }); + + test('ListNode.append() should properly transform a ParagraphNode', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = new ListNode('bullet', 1); + const paragraph = new ParagraphNode(); + const textNode = new TextNode('Hello'); + paragraph.append(textNode); + const nodesToAppend = [paragraph]; + + expect(listNode.append(...nodesToAppend)).toBe(listNode); + expect($isListItemNode(listNode.getFirstChild())).toBe(true); + expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); + }); + }); + + test('$createListNode()', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + const createdListNode = $createListNode('bullet'); + + expect(listNode.__type).toEqual(createdListNode.__type); + expect(listNode.__parent).toEqual(createdListNode.__parent); + expect(listNode.__tag).toEqual(createdListNode.__tag); + expect(listNode.__key).not.toEqual(createdListNode.__key); + }); + }); + + test('$isListNode()', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + + expect($isListNode(listNode)).toBe(true); + }); + }); + + test('$createListNode() with tag name (backward compatibility)', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const numberList = $createListNode('number', 1); + const bulletList = $createListNode('bullet', 1); + expect(numberList.__listType).toBe('number'); + expect(bulletList.__listType).toBe('bullet'); + }); + }); + + test('importDOM handles old editor expected task list format', async () => { + const {editor} = createTestContext(); + + let list!: ListNode; + editor.update(() => { + const nodes = $htmlToBlockNodes(editor, `<ul><li class="task-list-item"><input checked="" disabled="" type="checkbox"> A</li></ul>`); + list = nodes[0] as ListNode; + }); + + expect(list).toBeInstanceOf(ListNode); + expect(list.getListType()).toBe('check'); + }); }); diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts index 9f42ad732..40f4ab711 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts @@ -133,7 +133,7 @@ export class ImageNode extends ElementNode { element.addEventListener('click', e => { _editor.update(() => { - $selectSingleNode(this); + this.select(); }); }); diff --git a/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts b/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts index 736c3573c..cd4235f2f 100644 --- a/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts +++ b/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts @@ -1,7 +1,7 @@ import { createTestContext, destroyFromContext, dispatchKeydownEventForNode, - dispatchKeydownEventForSelectedNode, + dispatchKeydownEventForSelectedNode, expectNodeShapeToMatch, } from "lexical/__tests__/utils"; import { $createParagraphNode, $createTextNode, @@ -13,6 +13,7 @@ import {registerKeyboardHandling} from "../keyboard-handling"; import {registerRichText} from "@lexical/rich-text"; import {EditorUiContext} from "../../ui/framework/core"; import {$createListItemNode, $createListNode, ListItemNode, ListNode} from "@lexical/list"; +import {$createImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode"; describe('Keyboard-handling service tests', () => { @@ -127,4 +128,34 @@ describe('Keyboard-handling service tests', () => { expect(selectedNode?.getKey()).toBe(innerList.getChildren()[0].getKey()); }); }); + + test('Images: up on selected image creates new paragraph if none above', () => { + let image!: ImageNode; + editor.updateAndCommit(() => { + const root = $getRoot(); + const imageWrap = $createParagraphNode(); + image = $createImageNode('https://example.com/cat.png'); + imageWrap.append(image); + root.append(imageWrap); + image.select(); + }); + + expectNodeShapeToMatch(editor, [{ + type: 'paragraph', + children: [ + {type: 'image'} + ], + }]); + + dispatchKeydownEventForNode(image, editor, 'ArrowUp'); + + expectNodeShapeToMatch(editor, [{ + type: 'paragraph', + }, { + type: 'paragraph', + children: [ + {type: 'image'} + ], + }]); + }); }); \ No newline at end of file diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index ff6117b2b..a7f1ec7f0 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -3,7 +3,7 @@ import { $createParagraphNode, $getSelection, $isDecoratorNode, - COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, + COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, @@ -43,7 +43,7 @@ function deleteSingleSelectedNode(editor: LexicalEditor) { } /** - * Insert a new empty node after the selection if the selection contains a single + * Insert a new empty node before/after the selection if the selection contains a single * selected node (like image, media etc...). */ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean { @@ -67,6 +67,34 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve return false; } +function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: KeyboardEvent|null, after: boolean = true): boolean { + const selectionNodes = getLastSelection(editor)?.getNodes() || []; + if (!isSingleSelectedNode(selectionNodes)) { + return false; + } + + event?.preventDefault(); + + const node = selectionNodes[0]; + const nearestBlock = $getNearestNodeBlockParent(node) || node; + let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling(); + + editor.update(() => { + if (!target) { + target = $createParagraphNode(); + if (after) { + nearestBlock.insertAfter(target) + } else { + nearestBlock.insertBefore(target); + } + } + + target.selectStart(); + }); + + return true; +} + /** * Insert a new node after a details node, if inside a details node that's * the last element, and if the cursor is at the last block within the details node. @@ -199,8 +227,13 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void { return handleInsetOnTab(context.editor, event); }, COMMAND_PRIORITY_LOW); + const unregisterUp = context.editor.registerCommand(KEY_ARROW_UP_COMMAND, (event): boolean => { + return focusAdjacentOrInsertForSingleSelectNode(context.editor, event, false); + }, COMMAND_PRIORITY_LOW); + const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => { - return insertAfterDetails(context.editor, event); + return insertAfterDetails(context.editor, event) + || focusAdjacentOrInsertForSingleSelectNode(context.editor, event, true) }, COMMAND_PRIORITY_LOW); return () => { @@ -208,6 +241,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void { unregisterDelete(); unregisterEnter(); unregisterTab(); + unregisterUp(); unregisterDown(); }; } \ No newline at end of file diff --git a/resources/js/wysiwyg/utils/lists.ts b/resources/js/wysiwyg/utils/lists.ts index 005b05f98..3deb9dfb6 100644 --- a/resources/js/wysiwyg/utils/lists.ts +++ b/resources/js/wysiwyg/utils/lists.ts @@ -1,6 +1,6 @@ import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; -import {nodeHasInset} from "./nodes"; +import {$sortNodes, nodeHasInset} from "./nodes"; import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list"; @@ -49,16 +49,11 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { } const laterSiblings = node.getNextSiblings(); - parentListItem.insertAfter(node); if (list.getChildren().length === 0) { list.remove(); } - if (parentListItem.getChildren().length === 0) { - parentListItem.remove(); - } - if (laterSiblings.length > 0) { const childList = $createListNode(list.getListType()); childList.append(...laterSiblings); @@ -69,23 +64,54 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { list.remove(); } + if (parentListItem.getChildren().length === 0) { + parentListItem.remove(); + } + return node; } function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] { const nodes = selection?.getNodes() || []; - const listItemNodes = []; + let [start, end] = selection?.getStartEndPoints() || [null, null]; + // Ensure we ignore parent list items of the top-most list item since, + // although technically part of the selection, from a user point of + // view the selection does not spread to encompass this outer element. + const itemsToIgnore: Set<string> = new Set(); + if (selection && start) { + if (selection.isBackward() && end) { + [end, start] = [start, end]; + } + + const startParents = start.getNode().getParents(); + let foundList = false; + for (const parent of startParents) { + if ($isListItemNode(parent)) { + if (foundList) { + itemsToIgnore.add(parent.getKey()); + } else { + foundList = true; + } + } + } + } + + const listItemNodes = []; outer: for (const node of nodes) { if ($isListItemNode(node)) { - listItemNodes.push(node); + if (!itemsToIgnore.has(node.getKey())) { + listItemNodes.push(node); + } continue; } const parents = node.getParents(); for (const parent of parents) { if ($isListItemNode(parent)) { - listItemNodes.push(parent); + if (!itemsToIgnore.has(parent.getKey())) { + listItemNodes.push(parent); + } continue outer; } } @@ -110,7 +136,8 @@ function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[ } } - return Object.values(listItemMap); + const items = Object.values(listItemMap); + return $sortNodes(items) as ListItemNode[]; } export function $setInsetForSelection(editor: LexicalEditor, change: number): void { diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index b5cc78955..591232ea3 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -94,6 +94,30 @@ export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null return $findMatchingParent(node, isBlockNode); } +export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] { + const idChain: string[] = []; + const addIds = (n: ElementNode) => { + for (const child of n.getChildren()) { + idChain.push(child.getKey()) + if ($isElementNode(child)) { + addIds(child) + } + } + }; + + const root = $getRoot(); + addIds(root); + + const sorted = Array.from(nodes); + sorted.sort((a, b) => { + const aIndex = idChain.indexOf(a.getKey()); + const bIndex = idChain.indexOf(b.getKey()); + return aIndex - bIndex; + }); + + return sorted; +} + export function nodeHasAlignment(node: object): node is NodeHasAlignment { return '__alignment' in node; } diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 9f7694e85..35f11c5a2 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -370,8 +370,10 @@ body.editor-is-fullscreen { display: inline-block; outline: 2px dashed var(--editor-color-primary); direction: ltr; + pointer-events: none; } .editor-node-resizer-handle { + pointer-events: auto; position: absolute; display: block; width: 10px;