From e701dad7ef2d67403ef88c9a85e5d680b0c1ce2b Mon Sep 17 00:00:00 2001 From: Moonchild Date: Fri, 20 Mar 2026 14:45:11 +0100 Subject: [PATCH] Issue #3011 - Part 2: Switch spellchecker root to Shadow DOM. Set the root for spelling checker to shadow root if the contenteditable nodes are in the shadow DOM. Bail if the position can't be set properly. --- .../spellcheck/src/mozInlineSpellChecker.cpp | 7 ++- .../spellcheck/src/mozInlineSpellWordUtil.cpp | 54 ++++++++++++------- .../spellcheck/src/mozInlineSpellWordUtil.h | 35 ++++++------ 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/extensions/spellcheck/src/mozInlineSpellChecker.cpp b/extensions/spellcheck/src/mozInlineSpellChecker.cpp index 398059fc9b..a50d390037 100644 --- a/extensions/spellcheck/src/mozInlineSpellChecker.cpp +++ b/extensions/spellcheck/src/mozInlineSpellChecker.cpp @@ -1490,8 +1490,11 @@ nsresult mozInlineSpellChecker::DoSpellCheck(mozInlineSpellWordUtil& aWordUtil, return NS_OK; } - aWordUtil.SetEnd(endNode, endOffset); - aWordUtil.SetPosition(beginNode, beginOffset); + nsresult rv = aWordUtil.SetPositionAndEnd(beginNode, beginOffset, endNode, endOffset); + if (NS_FAILED(rv)) { + // Just bail out and don't try to spell-check this + return NS_OK; + } } // aWordUtil.SetPosition flushes pending notifications, check editor again. diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp index 460ac46b85..13e621c870 100644 --- a/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp +++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.cpp @@ -23,6 +23,8 @@ #include "nsIFrame.h" #include #include "mozilla/BinarySearch.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/dom/ShadowRoot.h" using namespace mozilla; @@ -69,8 +71,10 @@ mozInlineSpellWordUtil::Init(nsWeakPtr aWeakEditor) mDOMDocument = domDoc; mDocument = do_QueryInterface(domDoc); - // Find the root node for the editor. For contenteditable we'll need something - // cleverer here. + mIsContentEditableOrDesignMode = !!editor->AsHTMLEditor(); + + // Find the root node for the editor. For contenteditable the mRootNode could + // change to shadow root if the begin and end are inside the shadowDOM. nsCOMPtr rootElt; rv = editor->GetRootElement(getter_AddRefs(rootElt)); NS_ENSURE_SUCCESS(rv, rv); @@ -154,7 +158,7 @@ FindNextTextNode(nsINode* aNode, int32_t aOffset, nsINode* aRoot) return checkNode; } -// mozInlineSpellWordUtil::SetEnd +// mozInlineSpellWordUtil::SetPositionAndEnd // // We have two ranges "hard" and "soft". The hard boundary is simply // the scope of the root node. The soft boundary is that which is set @@ -172,34 +176,44 @@ FindNextTextNode(nsINode* aNode, int32_t aOffset, nsINode* aRoot) // position. nsresult -mozInlineSpellWordUtil::SetEnd(nsINode* aEndNode, int32_t aEndOffset) +mozInlineSpellWordUtil::SetPositionAndEnd(nsINode* aPositionNode, + int32_t aPositionOffset, + nsINode* aEndNode, + int32_t aEndOffset) { + MOZ_ASSERT(aPositionNode, "Null begin node?"); NS_PRECONDITION(aEndNode, "Null end node?"); NS_ASSERTION(mRootNode, "Not initialized"); + // Find a appropriate root if we are dealing with contenteditable nodes which + // are in the shadow DOM. See UXP Issue #3011 + if (mIsContentEditableOrDesignMode) { + nsINode* rootNode = aPositionNode->SubtreeRoot(); + if (rootNode != aEndNode->SubtreeRoot()) { + return NS_ERROR_FAILURE; + } + + if (mozilla::dom::ShadowRoot::FromNode(rootNode)) { + mRootNode = rootNode; + } + } + InvalidateWords(); + if (!IsTextNode(aPositionNode)) { + // Start at the start of the first text node after aNode/aOffset. + aPositionNode = FindNextTextNode(aPositionNode, aPositionOffset, mRootNode); + aPositionOffset = 0; + } + mSoftBegin = NodeOffset(aPositionNode, aPositionOffset); + if (!IsTextNode(aEndNode)) { // End at the start of the first text node after aEndNode/aEndOffset. aEndNode = FindNextTextNode(aEndNode, aEndOffset, mRootNode); aEndOffset = 0; } mSoftEnd = NodeOffset(aEndNode, aEndOffset); - return NS_OK; -} - -nsresult -mozInlineSpellWordUtil::SetPosition(nsINode* aNode, int32_t aOffset) -{ - InvalidateWords(); - - if (!IsTextNode(aNode)) { - // Start at the start of the first text node after aNode/aOffset. - aNode = FindNextTextNode(aNode, aOffset, mRootNode); - aOffset = 0; - } - mSoftBegin = NodeOffset(aNode, aOffset); nsresult rv = EnsureWords(); if (NS_FAILED(rv)) { @@ -207,8 +221,10 @@ mozInlineSpellWordUtil::SetPosition(nsINode* aNode, int32_t aOffset) } int32_t textOffset = MapDOMPositionToSoftTextOffset(mSoftBegin); - if (textOffset < 0) + if (textOffset < 0) { return NS_OK; + } + mNextWordIndex = FindRealWordContaining(textOffset, HINT_END, true); return NS_OK; } diff --git a/extensions/spellcheck/src/mozInlineSpellWordUtil.h b/extensions/spellcheck/src/mozInlineSpellWordUtil.h index b28d24ae5f..213fa52a16 100644 --- a/extensions/spellcheck/src/mozInlineSpellWordUtil.h +++ b/extensions/spellcheck/src/mozInlineSpellWordUtil.h @@ -29,12 +29,11 @@ class nsINode; * The basic operation is: * * 1. Call Init with the weak pointer to the editor that you're using. - * 2. Call SetEnd to set where you want to stop spellchecking. We'll stop - * at the word boundary after that. If SetEnd is not called, we'll stop - * at the end of the document's root element. - * 3. Call SetPosition to initialize the current position inside the - * previously given range. - * 4. Call GetNextWord over and over until it returns false. + * 2. Call SetPositionAndEnd to to initialize the current position inside the + * previously given range and set where you want to stop spellchecking. + * We'll stop at the word boundary after that. If SetEnd is not called, + * we'll stop at the end of the root element. + * 3. Call GetNextWord over and over until it returns false. */ class mozInlineSpellWordUtil @@ -57,17 +56,22 @@ public: }; mozInlineSpellWordUtil() - : mRootNode(nullptr), - mSoftBegin(nullptr, 0), mSoftEnd(nullptr, 0), - mNextWordIndex(-1), mSoftTextValid(false) {} + : mIsContentEditableOrDesignMode(false) + , mRootNode(nullptr) + , mSoftBegin(nullptr, 0) + , mSoftEnd(nullptr, 0) + , mNextWordIndex(-1) + , mSoftTextValid(false) + {} nsresult Init(nsWeakPtr aWeakEditor); - nsresult SetEnd(nsINode* aEndNode, int32_t aEndOffset); - - // sets the current position, this should be inside the range. If we are in - // the middle of a word, we'll move to its start. - nsresult SetPosition(nsINode* aNode, int32_t aOffset); + // Sets the current position and end. This should be inside the range. + // If we are in the middle of a word, we'll move to its start. + nsresult SetPositionAndEnd(nsINode* aPositionNode, + int32_t aPositionOffset, + nsINode* aEndNode, + int32_t aEndOffset); // Given a point inside or immediately following a word, this returns the // DOM range that exactly encloses that word's characters. The current @@ -100,7 +104,8 @@ private: // cached stuff for the editor, set by Init nsCOMPtr mDOMDocument; - nsCOMPtr mDocument; + nsCOMPtr mDocument; + bool mIsContentEditableOrDesignMode; // range to check, see SetPosition and SetEnd nsINode* mRootNode;