diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js
index 64cd601a920..81378e944c0 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 56dbe8d7c5f..5a258190069 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 d54a64ce89a..6a8e457248b 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 6edf0d64a2e..b5c83adddce 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 8c7729dbff1..b85383e7da5 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;
-
- 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();
+ 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('');
});
- test('ListNode.getTag()', async () => {
- const {editor} = testEnv;
-
- 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');
- });
+ // @ts-expect-error
+ expect(() => $createListNode()).toThrow();
+ });
+
+ 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(
'
',
- );
- expect(
+ );
+ expect(
listNode.createDOM({
namespace: '',
theme: {
list: {},
},
}).outerHTML,
- ).toBe('');
- expect(
+ ).toBe('');
+ expect(
listNode.createDOM({
namespace: '',
theme: {},
}).outerHTML,
- ).toBe('');
- });
+ ).toBe('');
});
+ });
- test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => {
- const {editor} = testEnv;
-
- 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();
-
- 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(
+ 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');
+
+ 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);
+
+ expect(listNode1.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(
+ );
+ expect(
listNode1.createDOM({
namespace: '',
theme: {
list: {},
},
}).outerHTML,
- ).toBe('');
- expect(
+ ).toBe('');
+ expect(
listNode1.createDOM({
namespace: '',
theme: {},
}).outerHTML,
- ).toBe('');
- expect(listNode2.createDOM(editorConfig).outerHTML).toBe(
+ ).toBe('');
+ expect(listNode2.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(listNode3.createDOM(editorConfig).outerHTML).toBe(
+ );
+ expect(listNode3.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(listNode4.createDOM(editorConfig).outerHTML).toBe(
+ );
+ expect(listNode4.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(listNode5.createDOM(editorConfig).outerHTML).toBe(
+ );
+ expect(listNode5.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(listNode6.createDOM(editorConfig).outerHTML).toBe(
+ );
+ expect(listNode6.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(listNode7.createDOM(editorConfig).outerHTML).toBe(
+ );
+ expect(listNode7.createDOM(editorConfig).outerHTML).toBe(
'',
- );
- expect(
+ );
+ expect(
listNode5.createDOM({
namespace: '',
theme: {
@@ -176,123 +176,135 @@ describe('LexicalListNode tests', () => {
},
},
}).outerHTML,
- ).toBe('');
- });
+ ).toBe('');
});
+ });
- 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(
'',
- );
+ );
- 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(
'',
- );
- });
+ );
});
+ });
- test('ListNode.append() should properly transform a ListItemNode', async () => {
- const {editor} = testEnv;
+ 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');
+ await editor.update(() => {
+ const listNode = new ListNode('bullet', 1);
+ const listItemNode = new ListItemNode();
+ const textNode = new TextNode('Hello');
- listItemNode.append(textNode);
- const nodesToAppend = [listItemNode];
+ listItemNode.append(textNode);
+ const nodesToAppend = [listItemNode];
- expect(listNode.append(...nodesToAppend)).toBe(listNode);
- expect(listNode.getFirstChild()).toBe(listItemNode);
- expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');
- });
+ 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;
+ 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');
+ 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);
+ listItemNode.append(textNode);
+ nestedListNode.append(listItemNode);
- const nodesToAppend = [nestedListNode];
+ const nodesToAppend = [nestedListNode];
- expect(listNode.append(...nodesToAppend)).toBe(listNode);
- expect($isListItemNode(listNode.getFirstChild())).toBe(true);
- expect(listNode.getFirstChild()!.getFirstChild()).toBe(
+ expect(listNode.append(...nodesToAppend)).toBe(listNode);
+ expect($isListItemNode(listNode.getFirstChild())).toBe(true);
+ expect(listNode.getFirstChild()!.getFirstChild()).toBe(
nestedListNode,
- );
- });
+ );
});
+ });
- test('ListNode.append() should properly transform a ParagraphNode', async () => {
- const {editor} = testEnv;
+ 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];
+ 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');
- });
+ expect(listNode.append(...nodesToAppend)).toBe(listNode);
+ expect($isListItemNode(listNode.getFirstChild())).toBe(true);
+ expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello');
});
+ });
- test('$createListNode()', async () => {
- const {editor} = testEnv;
+ test('$createListNode()', async () => {
+ const {editor} = createTestContext();
- await editor.update(() => {
- const listNode = $createListNode('bullet', 1);
- const createdListNode = $createListNode('bullet');
+ 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);
- });
+ 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);
- test('$isListNode()', async () => {
- const {editor} = testEnv;
+ expect($isListNode(listNode)).toBe(true);
+ });
+ });
- await editor.update(() => {
- const listNode = $createListNode('bullet', 1);
+ test('$createListNode() with tag name (backward compatibility)', async () => {
+ const {editor} = createTestContext();
- expect($isListNode(listNode)).toBe(true);
- });
+ await editor.update(() => {
+ const numberList = $createListNode('number', 1);
+ const bulletList = $createListNode('bullet', 1);
+ expect(numberList.__listType).toBe('number');
+ expect(bulletList.__listType).toBe('bullet');
});
+ });
- test('$createListNode() with tag name (backward compatibility)', async () => {
- const {editor} = testEnv;
+ test('importDOM handles old editor expected task list format', 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');
- });
+ let list!: ListNode;
+ editor.update(() => {
+ const nodes = $htmlToBlockNodes(editor, ``);
+ 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 9f42ad73204..40f4ab71165 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 736c3573c5b..cd4235f2f06 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 ff6117b2b87..a7f1ec7f0b0 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 005b05f9816..3deb9dfb6e9 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 = 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 b5cc789550c..591232ea385 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 9f7694e858c..35f11c5a279 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;