diff --git a/app/assets/javascript/lexxy.js b/app/assets/javascript/lexxy.js index 88f0e83d..89eac480 100644 --- a/app/assets/javascript/lexxy.js +++ b/app/assets/javascript/lexxy.js @@ -7688,6 +7688,11 @@ class CommandDispatcher { const selection = Lr$1(); if (!yr$1(selection)) return + if (as(selection.anchor.getNode())) { + selection.insertNodes([ St$3("h2") ]); + return + } + const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow(); let nextTag = "h2"; if (It$3(topLevelElement)) { @@ -9023,6 +9028,11 @@ class Contents { const selection = Lr$1(); if (!yr$1(selection)) return + if (as(selection.anchor.getNode())) { + selection.insertNodes([ newNodeFn() ]); + return + } + const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow(); // Check if format is already applied @@ -9296,9 +9306,19 @@ class Contents { #unwrap(node) { const children = node.getChildren(); - children.forEach((child) => { - node.insertBefore(child); - }); + if (children.length == 0) { + node.insertBefore(Li()); + } else { + children.forEach((child) => { + if (lr(child) && child.getTextContent().trim() !== "") { + const newParagraph = Li(); + newParagraph.append(child); + node.insertBefore(newParagraph); + } else if (!jn(child)) { + node.insertBefore(child); + } + }); + } node.remove(); } @@ -9312,6 +9332,12 @@ class Contents { if (selectedNodes.length === 0) { return } + + if (as(selectedNodes[0])) { + selection.insertNodes([ newNodeFn() ]); + return + } + const topLevelElements = new Set(); selectedNodes.forEach((node) => { const topLevel = node.getTopLevelElementOrThrow(); @@ -9382,6 +9408,12 @@ class Contents { #wrapCurrentLine(selection, newNodeFn) { const anchorNode = selection.anchor.getNode(); + + if (as(anchorNode)) { + selection.insertNodes([ newNodeFn() ]); + return + } + const topLevelElement = anchorNode.getTopLevelElementOrThrow(); if (topLevelElement.getTextContent()) { @@ -10143,7 +10175,7 @@ class LexicalEditorElement extends HTMLElement { const root = No$1(); root.clear(); if (html !== "") root.append(...this.#parseHtmlIntoLexicalNodes(html)); - root.select(); + root.selectEnd(); this.#toggleEmptyStatus(); @@ -10167,6 +10199,7 @@ class LexicalEditorElement extends HTMLElement { #parseHtmlIntoLexicalNodes(html) { if (!html) html = "

"; const nodes = m$1(this.editor, parseHtml(`
${html}
`)); + // Custom decorator block elements such action-text-attachments get wrapped into

automatically by Lexical. // We flatten those. return nodes.map(node => { diff --git a/app/assets/stylesheets/lexxy-content.css b/app/assets/stylesheets/lexxy-content.css index a7ad0051..7f2df9ae 100644 --- a/app/assets/stylesheets/lexxy-content.css +++ b/app/assets/stylesheets/lexxy-content.css @@ -64,6 +64,10 @@ font-style: italic; margin: var(--lexxy-content-margin) 0; padding: 0.5lh 2ch; + + p:last-child { + margin-block-end: 0; + } } p:empty { diff --git a/src/editor/command_dispatcher.js b/src/editor/command_dispatcher.js index 897d4d21..b47069e4 100644 --- a/src/editor/command_dispatcher.js +++ b/src/editor/command_dispatcher.js @@ -2,6 +2,7 @@ import { $createTextNode, $getSelection, $isRangeSelection, + $isRootOrShadowRoot, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, FORMAT_TEXT_COMMAND, @@ -173,6 +174,11 @@ export class CommandDispatcher { const selection = $getSelection() if (!$isRangeSelection(selection)) return + if ($isRootOrShadowRoot(selection.anchor.getNode())) { + selection.insertNodes([ $createHeadingNode("h2") ]) + return + } + const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow() let nextTag = "h2" if ($isHeadingNode(topLevelElement)) { diff --git a/src/editor/contents.js b/src/editor/contents.js index 12e18163..38243507 100644 --- a/src/editor/contents.js +++ b/src/editor/contents.js @@ -351,9 +351,19 @@ export default class Contents { #unwrap(node) { const children = node.getChildren() - children.forEach((child) => { - node.insertBefore(child) - }) + if (children.length == 0) { + node.insertBefore($createParagraphNode()) + } else { + children.forEach((child) => { + if ($isTextNode(child) && child.getTextContent().trim() !== "") { + const newParagraph = $createParagraphNode() + newParagraph.append(child) + node.insertBefore(newParagraph) + } else if (!$isLineBreakNode(child)) { + node.insertBefore(child) + } + }) + } node.remove() } @@ -367,6 +377,7 @@ export default class Contents { if (selectedNodes.length === 0) { return } + const topLevelElements = new Set() selectedNodes.forEach((node) => { const topLevel = node.getTopLevelElementOrThrow() @@ -437,6 +448,7 @@ export default class Contents { #wrapCurrentLine(selection, newNodeFn) { const anchorNode = selection.anchor.getNode() + const topLevelElement = anchorNode.getTopLevelElementOrThrow() if (topLevelElement.getTextContent()) { diff --git a/src/elements/editor.js b/src/elements/editor.js index 4e59df7f..e56312f4 100644 --- a/src/elements/editor.js +++ b/src/elements/editor.js @@ -1,4 +1,4 @@ -import { $addUpdateTag, $getNodeByKey, $getRoot, BLUR_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_NORMAL, DecoratorNode, FOCUS_COMMAND, KEY_ENTER_COMMAND, SKIP_DOM_SELECTION_TAG, createEditor } from "lexical" +import { $addUpdateTag, $createParagraphNode, $getNodeByKey, $getRoot, BLUR_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_NORMAL, DecoratorNode, FOCUS_COMMAND, KEY_ENTER_COMMAND, SKIP_DOM_SELECTION_TAG, createEditor } from "lexical" import { ListItemNode, ListNode, registerList } from "@lexical/list" import { AutoLinkNode, LinkNode } from "@lexical/link" import { registerPlainText } from "@lexical/plain-text" @@ -79,6 +79,20 @@ export default class LexicalEditorElement extends HTMLElement { this.editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined) } + focus() { + this.editor.focus() + } + + toString() { + if (!this.cachedStringValue) { + this.editor?.getEditorState().read(() => { + this.cachedStringValue = $getRoot().getTextContent() + }) + } + + return this.cachedStringValue + } + get form() { return this.internals.form } @@ -130,10 +144,6 @@ export default class LexicalEditorElement extends HTMLElement { return parseInt(this.editorContentElement?.getAttribute("tabindex") ?? "0") } - focus() { - this.editor.focus() - } - get value() { if (!this.cachedValue) { this.editor?.getEditorState().read(() => { @@ -149,8 +159,8 @@ export default class LexicalEditorElement extends HTMLElement { $addUpdateTag(SKIP_DOM_SELECTION_TAG) const root = $getRoot() root.clear() - if (html !== "") root.append(...this.#parseHtmlIntoLexicalNodes(html)) - root.select() + root.append(...this.#parseHtmlIntoLexicalNodes(html)) + root.selectEnd() this.#toggleEmptyStatus() @@ -161,19 +171,14 @@ export default class LexicalEditorElement extends HTMLElement { }) } - toString() { - if (!this.cachedStringValue) { - this.editor?.getEditorState().read(() => { - this.cachedStringValue = $getRoot().getTextContent() - }) - } - - return this.cachedStringValue - } - #parseHtmlIntoLexicalNodes(html) { if (!html) html = "

" const nodes = $generateNodesFromDOM(this.editor, parseHtml(`
${html}
`)) + + if (nodes.length === 0) { + return [ $createParagraphNode() ] + } + // Custom decorator block elements such action-text-attachments get wrapped into

automatically by Lexical. // We flatten those. return nodes.map(node => { diff --git a/src/elements/toolbar.js b/src/elements/toolbar.js index 47d27f62..58b07fbc 100644 --- a/src/elements/toolbar.js +++ b/src/elements/toolbar.js @@ -404,11 +404,11 @@ export default class LexicalToolbarElement extends HTMLElement {

- - diff --git a/test/system/editor_to_string_test.rb b/test/system/editor_to_string_test.rb index 6c238af4..8b52bce4 100644 --- a/test/system/editor_to_string_test.rb +++ b/test/system/editor_to_string_test.rb @@ -28,13 +28,13 @@ class EditorValueMethodsTest < ApplicationSystemTestCase end sleep 0.1 - assert_editor_plain_text "\n\n[example.png]\n\n" + assert_editor_plain_text "[example.png]\n\n" find("figcaption textarea").click.send_keys("Example Image") find_editor.click sleep 0.1 - assert_editor_plain_text "\n\n[Example Image]\n\n" + assert_editor_plain_text "[Example Image]\n\n" end test "toString returns content for custom_action_text_attachment (mention)" do diff --git a/test/system/form_test.rb b/test/system/form_test.rb index ebbe6131..04bc0fb6 100644 --- a/test/system/form_test.rb +++ b/test/system/form_test.rb @@ -39,7 +39,7 @@ class ActionTextLoadTest < ApplicationSystemTestCase click_on "Edit this post" - assert_equal_html "


That

", find_editor.value + assert_equal_html "

That

", find_editor.value end test "resets editor to initial state when form is reset" do diff --git a/test/system/page_refreshes_test.rb b/test/system/page_refreshes_test.rb index 8efc4275..298b0b62 100644 --- a/test/system/page_refreshes_test.rb +++ b/test/system/page_refreshes_test.rb @@ -9,6 +9,9 @@ class PageRefreshesTest < ApplicationSystemTestCase wait_for_editor + # Prompt is only opened if trigger follows a space or newline + find_editor.send "\n" + find_editor.send "1" click_on_prompt "Peter Johnson" assert_mention_attachment people(:peter) diff --git a/test/test_helpers/editor_handler.rb b/test/test_helpers/editor_handler.rb index f38f4120..21979e6f 100644 --- a/test/test_helpers/editor_handler.rb +++ b/test/test_helpers/editor_handler.rb @@ -51,6 +51,7 @@ def send_key(key, ctrl: false) }); this.dispatchEvent(event); JS + sleep 0.1 end def send_tab(shift: false)