commit 5a9aae988c77e85395f94537fd06f4be51cf197d
parent 6ba307dd8f601fe1b58f2fd12a04d73eb720c663
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date: Wed, 12 Nov 2025 22:52:08 +0000
Bug 1992288 - Make `AutoEditActionDataSetter` manage whether the destroyed editor was reinitialized r=m_kato
Once the editor is destroyed while dispatching `beforeinput` event,
e.g., by a reframe of the text control frame, `AutoEditActionDataSetter`
marks "the editor has destroyed during my lifetime". Then, nested
`EditAction`s always fail because the parent `AutoEditActionDataSetter`
thinks the editor is not available.
However, the editor can be reinitialized immediately. Then, the nested
`EditAction`s should work (note that it does not mean that another user
input is handled, `AutoEditActionDataSetter` is used by some query
methods too). Therefore, this patch adds a new `bool` member to store
whether the editor is initialized again.
I think we could reset `mEditorWasDestroyedDuringHandlingEditAction`,
but I'm afraid to do that because it forgets the destroyed thing
completely and some features may need to abort handling something even
if the editor is restored since the editable nodes are recreated.
Therefore, this patch adds a new `bool` member for making this change
safer and anybody can access the details of the state.
Differential Revision: https://phabricator.services.mozilla.com/D271559
Diffstat:
5 files changed, 86 insertions(+), 22 deletions(-)
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp
@@ -6620,16 +6620,11 @@ EditorBase::AutoEditActionDataSetter::AutoEditActionDataSetter(
mParentData(aEditorBase.mEditActionData),
mData(VoidString()),
mRawEditAction(aEditAction),
- mTopLevelEditSubAction(EditSubAction::eNone),
- mAborted(false),
- mHasTriedToDispatchBeforeInputEvent(false),
- mBeforeInputEventCanceled(false),
- mMakeBeforeInputEventNonCancelable(false),
- mHasTriedToDispatchClipboardEvent(false),
mEditorWasDestroyedDuringHandlingEditAction(
mParentData &&
mParentData->mEditorWasDestroyedDuringHandlingEditAction),
- mHandled(false) {
+ mEditorWasReinitialized(mParentData &&
+ mParentData->mEditorWasReinitialized) {
// If we're nested edit action, copies necessary data from the parent.
if (mParentData) {
mSelection = mParentData->mSelection;
diff --git a/editor/libeditor/EditorBase.h b/editor/libeditor/EditorBase.h
@@ -984,6 +984,8 @@ class EditorBase : public nsIEditor,
AutoEditActionDataSetter(const EditorBase& aEditorBase,
EditAction aEditAction,
nsIPrincipal* aPrincipal = nullptr);
+ AutoEditActionDataSetter() = delete;
+ AutoEditActionDataSetter(const AutoEditActionDataSetter& aOther) = delete;
~AutoEditActionDataSetter();
void SetSelectionCreatedByDoubleclick(bool aSelectionCreatedByDoubleclick) {
@@ -1013,11 +1015,11 @@ class EditorBase : public nsIEditor,
[[nodiscard]] bool CanHandle() const {
#ifdef DEBUG
mHasCanHandleChecked = true;
-#endif // #ifdefn DEBUG
+#endif // #ifdef DEBUG
// Don't allow to run new edit action when an edit action caused
// destroying the editor while it's being handled.
if (mEditAction != EditAction::eInitializing &&
- mEditorWasDestroyedDuringHandlingEditAction) {
+ HasEditorDestroyedDuringHandlingEditActionAndNotYetReinitialized()) {
NS_WARNING("Editor was destroyed during an edit action being handled");
return false;
}
@@ -1214,14 +1216,38 @@ class EditorBase : public nsIEditor,
// something other unexpected event listeners. In the cases, new child
// edit action shouldn't been aborted.
mEditorWasDestroyedDuringHandlingEditAction = true;
+ mEditorWasReinitialized = false;
}
if (mParentData) {
mParentData->OnEditorDestroy();
}
}
- bool HasEditorDestroyedDuringHandlingEditAction() const {
+ void OnEditorInitialized() {
+ if (mEditorWasDestroyedDuringHandlingEditAction) {
+ mEditorWasReinitialized = true;
+ }
+ if (mParentData) {
+ mParentData->OnEditorInitialized();
+ }
+ }
+ /**
+ * Return true if the editor was destroyed at least once while the
+ * EditAction is being handled. Note that the editor may have already been
+ * reinitialized even if this returns true.
+ */
+ [[nodiscard]] bool HasEditorDestroyedDuringHandlingEditAction() const {
return mEditorWasDestroyedDuringHandlingEditAction;
}
+ /**
+ * Return true if the editor was destroyed while the EditAction is being
+ * handled and has not been reinitialized. I.e., the editor is still under
+ * the destroyed state.
+ */
+ [[nodiscard]] bool
+ HasEditorDestroyedDuringHandlingEditActionAndNotYetReinitialized() const {
+ return mEditorWasDestroyedDuringHandlingEditAction &&
+ !mEditorWasReinitialized;
+ }
void SetTopLevelEditSubAction(EditSubAction aEditSubAction,
EDirection aDirection = eNone) {
@@ -1457,40 +1483,40 @@ class EditorBase : public nsIEditor,
// instance's mTopLevelEditSubAction member since it's copied from the
// parent instance at construction and it's always cleared before this
// won't be overwritten and cleared before destruction.
- EditSubAction mTopLevelEditSubAction;
+ EditSubAction mTopLevelEditSubAction = EditSubAction::eNone;
- EDirection mDirectionOfTopLevelEditSubAction;
+ EDirection mDirectionOfTopLevelEditSubAction = nsIEditor::eNone;
- bool mAborted;
+ bool mAborted = false;
// Set to true when this handles "beforeinput" event dispatching. Note
// that even if "beforeinput" event shouldn't be dispatched for this,
// instance, this is set to true when it's considered.
- bool mHasTriedToDispatchBeforeInputEvent;
+ bool mHasTriedToDispatchBeforeInputEvent = false;
// Set to true if "beforeinput" event was dispatched and it's canceled.
- bool mBeforeInputEventCanceled;
+ bool mBeforeInputEventCanceled = false;
// Set to true if `beforeinput` event must not be cancelable even if
// its inputType is defined as cancelable by the standards.
- bool mMakeBeforeInputEventNonCancelable;
+ bool mMakeBeforeInputEventNonCancelable = false;
// Set to true when the edit action handler tries to dispatch a clipboard
// event.
- bool mHasTriedToDispatchClipboardEvent;
+ bool mHasTriedToDispatchClipboardEvent = false;
// The editor instance may be destroyed once temporarily if `document.write`
// etc runs. In such case, we should mark this flag of being handled
// edit action.
bool mEditorWasDestroyedDuringHandlingEditAction;
+ // This is set to `true` if the editor was destroyed but now, it's
+ // initialized again.
+ bool mEditorWasReinitialized;
// This is set before dispatching `input` event and notifying editor
// observers.
- bool mHandled;
+ bool mHandled = false;
// Whether the editor is dispatching a `beforeinput` or `input` event.
bool mDispatchingInputEvent = false;
#ifdef DEBUG
mutable bool mHasCanHandleChecked = false;
#endif // #ifdef DEBUG
-
- AutoEditActionDataSetter() = delete;
- AutoEditActionDataSetter(const AutoEditActionDataSetter& aOther) = delete;
};
void UpdateEditActionData(const nsAString& aData) {
diff --git a/editor/libeditor/HTMLEditor.cpp b/editor/libeditor/HTMLEditor.cpp
@@ -426,6 +426,7 @@ nsresult HTMLEditor::Init(Document& aDocument,
MOZ_ASSERT(!mInitSucceeded, "HTMLEditor::Init() shouldn't be nested");
mInitSucceeded = true;
+ editActionData.OnEditorInitialized();
return NS_OK;
}
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp
@@ -199,11 +199,12 @@ nsresult TextEditor::Init(Document& aDocument, Element& aAnonymousDivElement,
return NS_ERROR_FAILURE;
}
- // We set mInitSucceeded here rather than at the end of the function,
+ // We set the initialized state here rather than at the end of the function,
// since InitEditorContentAndSelection() can perform some transactions
// and can warn if mInitSucceeded is still false.
MOZ_ASSERT(!mInitSucceeded, "TextEditor::Init() shouldn't be nested");
mInitSucceeded = true;
+ editActionData.OnEditorInitialized();
rv = InitEditorContentAndSelection();
if (NS_FAILED(rv)) {
@@ -212,6 +213,7 @@ nsresult TextEditor::Init(Document& aDocument, Element& aAnonymousDivElement,
// XXX Shouldn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this
// is a public method?
mInitSucceeded = false;
+ editActionData.OnEditorDestroy();
return EditorBase::ToGenericNSResult(rv);
}
diff --git a/testing/web-platform/tests/editing/other/restyle-textcontrol-during-beforeinput.html b/testing/web-platform/tests/editing/other/restyle-textcontrol-during-beforeinput.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Restyle text control at `beforeinput`</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="../include/editor-test-utils.js"></script>
+<script>
+"use strict";
+
+addEventListener("load", () => {
+ for (const selector of ["input", "textarea"]) {
+ promise_test(async () => {
+ const textControl = document.querySelector(selector);
+ textControl.focus();
+ textControl.addEventListener("beforeinput", () => {
+ textControl.style.display = textControl.style.display == "block" ? "inline" : "block";
+ textControl.getBoundingClientRect(); // Flush the restyle before back to the builtin editor
+ });
+ await new test_driver.Actions()
+ .keyDown("a")
+ .keyUp("a")
+ .keyDown("b")
+ .keyUp("b")
+ .send();
+ assert_equals(textControl.value, "ab");
+ }, `${selector} should accept text input even after restyled`);
+ }
+}, {once: true});
+</script>
+</head>
+<body>
+ <input>
+ <textarea></textarea>
+</body>
+</html>