diff --git a/src/editor/contents.js b/src/editor/contents.js
index ac56e5a69..8f8f229c6 100644
--- a/src/editor/contents.js
+++ b/src/editor/contents.js
@@ -289,7 +289,7 @@ export default class Contents {
const node = $getNodeByKey(nodeKey)
if (!(node instanceof ActionTextAttachmentUploadNode)) return
- const replacementNodeKey = node.showUploadedAttachment(blob)
+ const replacementNodeKey = node.$showUploadedAttachment(blob)
if (replacementNodeKey) {
nodeKey = replacementNodeKey
}
diff --git a/src/editor/selection.js b/src/editor/selection.js
index 10102e658..eee32723b 100644
--- a/src/editor/selection.js
+++ b/src/editor/selection.js
@@ -1,7 +1,7 @@
import {
- $createParagraphNode, $getNearestNodeFromDOMNode, $getRoot, $getSelection, $isDecoratorNode, $isElementNode,
+ $addUpdateTag, $createParagraphNode, $getNearestNodeFromDOMNode, $getRoot, $getSelection, $isDecoratorNode, $isElementNode,
$isLineBreakNode, $isNodeSelection, $isRangeSelection, $isTextNode, $setSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, DELETE_CHARACTER_COMMAND,
- KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, SELECTION_CHANGE_COMMAND, isDOMNode
+ HISTORY_MERGE_TAG, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, SELECTION_CHANGE_COMMAND, isDOMNode
} from "lexical"
import { $getNearestNodeOfType } from "@lexical/utils"
import { $getListDepth, ListItemNode, ListNode } from "@lexical/list"
@@ -31,12 +31,6 @@ export default class Selection {
this.#clearStaleInlineCodeFormat()
}
- set current(selection) {
- this.editor.update(() => {
- this.#syncSelectedClasses()
- })
- }
-
get hasNodeSelection() {
return this.editor.getEditorState().read(() => {
const selection = $getSelection()
@@ -365,7 +359,7 @@ export default class Selection {
this.editor.registerCommand(DELETE_CHARACTER_COMMAND, this.#selectDecoratorNodeBeforeDeletion.bind(this), COMMAND_PRIORITY_LOW),
this.editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
- this.current = $getSelection()
+ this.#syncSelectedClasses()
}, COMMAND_PRIORITY_LOW)
)
}
@@ -563,6 +557,7 @@ export default class Selection {
#selectInLexical(node) {
if ($isDecoratorNode(node)) {
+ $addUpdateTag(HISTORY_MERGE_TAG)
const selection = $createNodeSelectionWith(node)
$setSelection(selection)
return selection
diff --git a/src/elements/editor.js b/src/elements/editor.js
index 9961fa9d8..fa58a38ee 100644
--- a/src/elements/editor.js
+++ b/src/elements/editor.js
@@ -1,4 +1,4 @@
-import { $addUpdateTag, $createParagraphNode, $getRoot, $getSelection, $isElementNode, $isLineBreakNode, $isRangeSelection, $isTextNode, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_NORMAL, KEY_ENTER_COMMAND, SKIP_DOM_SELECTION_TAG, TextNode } from "lexical"
+import { $createParagraphNode, $getRoot, $getSelection, $isElementNode, $isLineBreakNode, $isRangeSelection, $isTextNode, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, CLEAR_HISTORY_COMMAND, COMMAND_PRIORITY_NORMAL, HISTORY_MERGE_TAG, KEY_ENTER_COMMAND, SKIP_DOM_SELECTION_TAG, TextNode } from "lexical"
import { buildEditorFromExtensions } from "@lexical/extension"
import { ListItemNode, ListNode, registerList } from "@lexical/list"
import { AutoLinkNode, LinkNode } from "@lexical/link"
@@ -10,7 +10,6 @@ import { CodeHighlightNode, CodeNode, registerCodeHighlighting } from "@lexical/
import { TRANSFORMERS, registerMarkdownShortcuts } from "@lexical/markdown"
import { HORIZONTAL_DIVIDER } from "../editor/markdown/horizontal_divider_transformer"
import { registerMarkdownLeadingTagHandler } from "../editor/markdown/leading_tag_handler"
-import { createEmptyHistoryState, registerHistory } from "@lexical/history"
import theme from "../config/theme"
import { HorizontalDividerNode } from "../nodes/horizontal_divider_node"
@@ -35,6 +34,7 @@ import { ProvisionalParagraphExtension } from "../extensions/provisional_paragra
import { HighlightExtension } from "../extensions/highlight_extension"
import { TrixContentExtension } from "../extensions/trix_content_extension"
import { TablesExtension } from "../extensions/tables_extension"
+import { RewritableHistoryExtension } from "../extensions/rewritable_history_extension.js"
import { AttachmentsExtension } from "../extensions/attachments_extension.js"
import { FormatEscapeExtension } from "../extensions/format_escape_extension.js"
import { LinkOpenerExtension } from "../extensions/link_opener_extension.js"
@@ -48,11 +48,11 @@ export class LexicalEditorElement extends HTMLElement {
static observedAttributes = [ "connected", "required" ]
#initialValue = ""
- #initialValueLoaded = false
#validationTextArea = document.createElement("textarea")
#editorInitializedRafId = null
#listeners = new ListenerBin()
#disposables = []
+ #historyState = { undo: false, redo: false }
constructor() {
super()
@@ -144,6 +144,7 @@ export class LexicalEditorElement extends HTMLElement {
HighlightExtension,
TrixContentExtension,
TablesExtension,
+ RewritableHistoryExtension,
AttachmentsExtension,
FormatEscapeExtension,
LinkOpenerExtension
@@ -250,28 +251,22 @@ export class LexicalEditorElement extends HTMLElement {
}
set value(html) {
- const wasEmpty = !this.#initialValueLoaded
-
this.editor.update(() => {
- $addUpdateTag(SKIP_DOM_SELECTION_TAG)
- const root = $getRoot()
- root.clear()
- root.append(...this.#parseHtmlIntoLexicalNodes(html))
- root.selectEnd()
+ $getRoot()
+ .clear()
+ .selectEnd()
+ .insertNodes(this.#parseHtmlIntoLexicalNodes(html))
this.#toggleEmptyStatus()
+ }, { discrete: true, tag: SKIP_DOM_SELECTION_TAG })
+ }
- // The first time you set the value on an empty editor, Lexical can be
- // left in an inconsistent state until the next update (adding attachments
- // fails because no root node is detected). A no-op update works around
- // it. Only fire on the first load — subsequent set value calls don't hit
- // the inconsistent state and the extra reconciler cycle is pure overhead.
- if (wasEmpty) {
- requestAnimationFrame(() => this.editor?.update(() => { }))
- }
- })
+ get canUndo() {
+ return this.#historyState.undo
+ }
- this.#initialValueLoaded = true
+ get canRedo() {
+ return this.#historyState.redo
}
#parseHtmlIntoLexicalNodes(html) {
@@ -307,6 +302,7 @@ export class LexicalEditorElement extends HTMLElement {
this.#registerComponents()
this.#handleEnter()
this.#registerFocusEvents()
+ this.#registerHistoryEvents()
this.#attachDebugHooks()
this.#attachToolbar()
this.#configureSanitizer()
@@ -403,8 +399,10 @@ export class LexicalEditorElement extends HTMLElement {
}
#loadInitialValue() {
- const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "
"
- this.value = this.#initialValue = initialHtml
+ const initialHtml = this.valueBeforeDisconnect || this.getAttribute("value") || "
"
+ this.editor.update(() => {
+ this.value = this.#initialValue = initialHtml
+ }, { tag: HISTORY_MERGE_TAG })
}
#resetBeforeTurboCaches() {
@@ -454,8 +452,6 @@ export class LexicalEditorElement extends HTMLElement {
} else {
registered.push(registerPlainText(this.editor))
}
- this.historyState = createEmptyHistoryState()
- registered.push(registerHistory(this.editor, this.historyState, 20))
this.#listeners.track(...registered)
}
@@ -538,6 +534,12 @@ export class LexicalEditorElement extends HTMLElement {
}
}
+ #registerHistoryEvents() {
+ this.#listeners.track(
+ this.editor.registerCommand(CAN_UNDO_COMMAND, (enabled) => { this.#historyState.undo = enabled }, COMMAND_PRIORITY_NORMAL),
+ this.editor.registerCommand(CAN_REDO_COMMAND, (enabled) => { this.#historyState.redo = enabled }, COMMAND_PRIORITY_NORMAL)
+ )
+ }
#attachDebugHooks() {
if (!LexicalEditorElement.debug) return
@@ -635,8 +637,8 @@ export class LexicalEditorElement extends HTMLElement {
heading: { active: format.isInHeading, enabled: true },
"unordered-list": { active: format.isInList && format.listType === "bullet", enabled: true },
"ordered-list": { active: format.isInList && format.listType === "number", enabled: true },
- undo: { active: false, enabled: this.historyState?.undoStack.length > 0 },
- redo: { active: false, enabled: this.historyState?.redoStack.length > 0 }
+ undo: { active: false, enabled: this.canUndo },
+ redo: { active: false, enabled: this.canRedo }
}
linkHref = linkNode ? linkNode.getURL() : null
diff --git a/src/elements/toolbar.js b/src/elements/toolbar.js
index be76bd6a9..82f4310e0 100644
--- a/src/elements/toolbar.js
+++ b/src/elements/toolbar.js
@@ -1,6 +1,9 @@
import {
$getSelection,
$isRangeSelection,
+ CAN_REDO_COMMAND,
+ CAN_UNDO_COMMAND,
+ COMMAND_PRIORITY_LOW,
SKIP_DOM_SELECTION_TAG
} from "lexical"
import { getNonce } from "../helpers/csp_helper"
@@ -187,19 +190,10 @@ export class LexicalToolbarElement extends HTMLElement {
}
#monitorHistoryChanges() {
- this.#listeners.track(this.editor.registerUpdateListener(() => {
- this.#updateUndoRedoButtonStates()
- }))
- }
-
- #updateUndoRedoButtonStates() {
- this.editor.getEditorState().read(() => {
- const historyState = this.editorElement.historyState
- if (historyState) {
- this.#setButtonDisabled("undo", historyState.undoStack.length === 0)
- this.#setButtonDisabled("redo", historyState.redoStack.length === 0)
- }
- })
+ this.#listeners.track(
+ this.editor.registerCommand(CAN_UNDO_COMMAND, (enabled) => { this.#setButtonDisabled("undo", !enabled) }, COMMAND_PRIORITY_LOW),
+ this.editor.registerCommand(CAN_REDO_COMMAND, (enabled) => { this.#setButtonDisabled("redo", !enabled) }, COMMAND_PRIORITY_LOW),
+ )
}
#updateButtonStates() {
@@ -233,8 +227,6 @@ export class LexicalToolbarElement extends HTMLElement {
this.#setButtonPressed("code", isInCode)
this.#setButtonPressed("table", isInTable)
-
- this.#updateUndoRedoButtonStates()
}
#setButtonPressed(name, isPressed) {
@@ -445,11 +437,11 @@ export class LexicalToolbarElement extends HTMLElement {
-