From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/base/Selection.cpp | 4118 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 4118 insertions(+) create mode 100644 dom/base/Selection.cpp (limited to 'dom/base/Selection.cpp') diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp new file mode 100644 index 0000000000..f46f4409cd --- /dev/null +++ b/dom/base/Selection.cpp @@ -0,0 +1,4118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Implementation of mozilla::dom::Selection + */ + +#include "Selection.h" + +#include "ErrorList.h" +#include "LayoutConstants.h" +#include "mozilla/AccessibleCaretEventHub.h" +#include "mozilla/Assertions.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/AutoCopyListener.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentIterator.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/SelectionBinding.h" +#include "mozilla/dom/ShadowRoot.h" +#include "mozilla/dom/StaticRange.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/Logging.h" +#include "mozilla/PresShell.h" +#include "mozilla/RangeBoundary.h" +#include "mozilla/RangeUtils.h" +#include "mozilla/StackWalk.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Telemetry.h" + +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsDirection.h" +#include "nsString.h" +#include "nsFrameSelection.h" +#include "nsISelectionListener.h" +#include "nsContentCID.h" +#include "nsDeviceContext.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsRange.h" +#include "nsITableCellLayout.h" +#include "nsTArray.h" +#include "nsTableWrapperFrame.h" +#include "nsTableCellFrame.h" +#include "nsIScrollableFrame.h" +#include "nsCCUncollectableMarker.h" +#include "nsIDocumentEncoder.h" +#include "nsTextFragment.h" +#include +#include "nsContentUtils.h" + +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" +#include "nsBidiPresUtils.h" +#include "nsTextFrame.h" + +#include "nsThreadUtils.h" + +#include "nsPresContext.h" +#include "nsCaret.h" + +#include "nsITimer.h" +#include "mozilla/dom/Document.h" +#include "nsINamed.h" + +#include "nsISelectionController.h" //for the enums +#include "nsCopySupport.h" +#include "nsIFrameInlines.h" +#include "nsRefreshDriver.h" + +#include "nsError.h" +#include "nsViewManager.h" + +#include "nsFocusManager.h" +#include "nsPIDOMWindow.h" + +namespace mozilla { +// "Selection" logs only the calls of AddRangesForSelectableNodes and +// NotifySelectionListeners in debug level. +static LazyLogModule sSelectionLog("Selection"); +// "SelectionAPI" logs all API calls (both internal ones and exposed to script +// ones) of normal selection which may change selection ranges. +// 3. Info: Calls of APIs +// 4. Debug: Call stacks with 7 ancestor callers of APIs +// 5. Verbose: Complete call stacks of APIs. +LazyLogModule sSelectionAPILog("SelectionAPI"); + +MOZ_ALWAYS_INLINE bool NeedsToLogSelectionAPI(dom::Selection& aSelection) { + return aSelection.Type() == SelectionType::eNormal && + MOZ_LOG_TEST(sSelectionAPILog, LogLevel::Info); +} + +void LogStackForSelectionAPI() { + if (!MOZ_LOG_TEST(sSelectionAPILog, LogLevel::Debug)) { + return; + } + static nsAutoCString* sBufPtr = nullptr; + MOZ_ASSERT(!sBufPtr); + nsAutoCString buf; + sBufPtr = &buf; + auto writer = [](const char* aBuf) { sBufPtr->Append(aBuf); }; + const LogLevel logLevel = MOZ_LOG_TEST(sSelectionAPILog, LogLevel::Verbose) + ? LogLevel::Verbose + : LogLevel::Debug; + MozWalkTheStackWithWriter(writer, CallerPC(), + logLevel == LogLevel::Verbose + ? 0u /* all */ + : 8u /* 8 inclusive ancestors */); + MOZ_LOG(sSelectionAPILog, logLevel, ("\n%s", buf.get())); + sBufPtr = nullptr; +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s()", aSelection, aFuncName)); +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aArgName, + const nsINode* aNode) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s)", aSelection, aFuncName, aArgName, + aNode ? ToString(*aNode).c_str() : "nullptr")); +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aArgName, + const dom::AbstractRange& aRange) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s)", aSelection, aFuncName, aArgName, + ToString(aRange).c_str())); +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aArgName1, + const nsINode* aNode, const char* aArgName2, + uint32_t aOffset) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s, %s=%u)", aSelection, aFuncName, aArgName1, + aNode ? ToString(*aNode).c_str() : "nullptr", aArgName2, aOffset)); +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aArgName, + const RawRangeBoundary& aBoundary) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s)", aSelection, aFuncName, aArgName, + ToString(aBoundary).c_str())); +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aArgName1, + const nsAString& aStr1, const char* aArgName2, + const nsAString& aStr2, const char* aArgName3, + const nsAString& aStr3) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s, %s=%s, %s=%s)", aSelection, aFuncName, + aArgName1, NS_ConvertUTF16toUTF8(aStr1).get(), aArgName2, + NS_ConvertUTF16toUTF8(aStr2).get(), aArgName3, + NS_ConvertUTF16toUTF8(aStr3).get())); +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aNodeArgName1, + const nsINode& aNode1, const char* aOffsetArgName1, + uint32_t aOffset1, const char* aNodeArgName2, + const nsINode& aNode2, const char* aOffsetArgName2, + uint32_t aOffset2) { + if (&aNode1 == &aNode2 && aOffset1 == aOffset2) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s=%s, %s=%s=%u)", aSelection, aFuncName, + aNodeArgName1, aNodeArgName2, ToString(aNode1).c_str(), + aOffsetArgName1, aOffsetArgName2, aOffset1)); + } else { + MOZ_LOG( + sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s, %s=%u, %s=%s, %s=%u)", aSelection, aFuncName, + aNodeArgName1, ToString(aNode1).c_str(), aOffsetArgName1, aOffset1, + aNodeArgName2, ToString(aNode2).c_str(), aOffsetArgName2, aOffset2)); + } +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aNodeArgName1, + const nsINode& aNode1, const char* aOffsetArgName1, + uint32_t aOffset1, const char* aNodeArgName2, + const nsINode& aNode2, const char* aOffsetArgName2, + uint32_t aOffset2, const char* aDirArgName, + nsDirection aDirection, const char* aReasonArgName, + int16_t aReason) { + if (&aNode1 == &aNode2 && aOffset1 == aOffset2) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s=%s, %s=%s=%u, %s=%s, %s=%d)", aSelection, + aFuncName, aNodeArgName1, aNodeArgName2, ToString(aNode1).c_str(), + aOffsetArgName1, aOffsetArgName2, aOffset1, aDirArgName, + ToString(aDirection).c_str(), aReasonArgName, aReason)); + } else { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s, %s=%u, %s=%s, %s=%u, %s=%s, %s=%d)", + aSelection, aFuncName, aNodeArgName1, ToString(aNode1).c_str(), + aOffsetArgName1, aOffset1, aNodeArgName2, ToString(aNode2).c_str(), + aOffsetArgName2, aOffset2, aDirArgName, + ToString(aDirection).c_str(), aReasonArgName, aReason)); + } +} + +static void LogSelectionAPI(const dom::Selection* aSelection, + const char* aFuncName, const char* aArgName1, + const RawRangeBoundary& aBoundary1, + const char* aArgName2, + const RawRangeBoundary& aBoundary2) { + if (aBoundary1 == aBoundary2) { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s=%s)", aSelection, aFuncName, aArgName1, + aArgName2, ToString(aBoundary1).c_str())); + } else { + MOZ_LOG(sSelectionAPILog, LogLevel::Info, + ("%p Selection::%s(%s=%s, %s=%s)", aSelection, aFuncName, aArgName1, + ToString(aBoundary1).c_str(), aArgName2, + ToString(aBoundary2).c_str())); + } +} +} // namespace mozilla + +using namespace mozilla; +using namespace mozilla::dom; + +// #define DEBUG_TABLE 1 + +#ifdef PRINT_RANGE +static void printRange(nsRange* aDomRange); +# define DEBUG_OUT_RANGE(x) printRange(x) +#else +# define DEBUG_OUT_RANGE(x) +#endif // PRINT_RANGE + +static constexpr nsLiteralCString kNoDocumentTypeNodeError = + "DocumentType nodes are not supported"_ns; +static constexpr nsLiteralCString kNoRangeExistsError = + "No selection range exists"_ns; + +namespace mozilla { + +/****************************************************************************** + * Utility methods defined in nsISelectionController.idl + ******************************************************************************/ + +const char* ToChar(SelectionType aSelectionType) { + switch (aSelectionType) { + case SelectionType::eInvalid: + return "SelectionType::eInvalid"; + case SelectionType::eNone: + return "SelectionType::eNone"; + case SelectionType::eNormal: + return "SelectionType::eNormal"; + case SelectionType::eSpellCheck: + return "SelectionType::eSpellCheck"; + case SelectionType::eIMERawClause: + return "SelectionType::eIMERawClause"; + case SelectionType::eIMESelectedRawClause: + return "SelectionType::eIMESelectedRawClause"; + case SelectionType::eIMEConvertedClause: + return "SelectionType::eIMEConvertedClause"; + case SelectionType::eIMESelectedClause: + return "SelectionType::eIMESelectedClause"; + case SelectionType::eAccessibility: + return "SelectionType::eAccessibility"; + case SelectionType::eFind: + return "SelectionType::eFind"; + case SelectionType::eURLSecondary: + return "SelectionType::eURLSecondary"; + case SelectionType::eURLStrikeout: + return "SelectionType::eURLStrikeout"; + default: + return "Invalid SelectionType"; + } +} + +/****************************************************************************** + * Utility methods defined in nsISelectionListener.idl + ******************************************************************************/ + +nsCString SelectionChangeReasonsToCString(int16_t aReasons) { + nsCString reasons; + if (!aReasons) { + reasons.AssignLiteral("NO_REASON"); + return reasons; + } + auto EnsureSeparator = [](nsCString& aString) -> void { + if (!aString.IsEmpty()) { + aString.AppendLiteral(" | "); + } + }; + struct ReasonData { + int16_t mReason; + const char* mReasonStr; + + ReasonData(int16_t aReason, const char* aReasonStr) + : mReason(aReason), mReasonStr(aReasonStr) {} + }; + for (const ReasonData& reason : + {ReasonData(nsISelectionListener::DRAG_REASON, "DRAG_REASON"), + ReasonData(nsISelectionListener::MOUSEDOWN_REASON, "MOUSEDOWN_REASON"), + ReasonData(nsISelectionListener::MOUSEUP_REASON, "MOUSEUP_REASON"), + ReasonData(nsISelectionListener::KEYPRESS_REASON, "KEYPRESS_REASON"), + ReasonData(nsISelectionListener::SELECTALL_REASON, "SELECTALL_REASON"), + ReasonData(nsISelectionListener::COLLAPSETOSTART_REASON, + "COLLAPSETOSTART_REASON"), + ReasonData(nsISelectionListener::COLLAPSETOEND_REASON, + "COLLAPSETOEND_REASON"), + ReasonData(nsISelectionListener::IME_REASON, "IME_REASON"), + ReasonData(nsISelectionListener::JS_REASON, "JS_REASON")}) { + if (aReasons & reason.mReason) { + EnsureSeparator(reasons); + reasons.Append(reason.mReasonStr); + } + } + return reasons; +} + +} // namespace mozilla + +// #define DEBUG_SELECTION // uncomment for printf describing every collapse and +// extend. #define DEBUG_NAVIGATION + +// #define DEBUG_TABLE_SELECTION 1 + +struct CachedOffsetForFrame { + CachedOffsetForFrame() + : mCachedFrameOffset(0, 0) // nsPoint ctor + , + mLastCaretFrame(nullptr), + mLastContentOffset(0), + mCanCacheFrameOffset(false) {} + + nsPoint mCachedFrameOffset; // cached frame offset + nsIFrame* mLastCaretFrame; // store the frame the caret was last drawn in. + int32_t mLastContentOffset; // store last content offset + bool mCanCacheFrameOffset; // cached frame offset is valid? +}; + +class AutoScroller final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_ISUPPORTS + + explicit AutoScroller(nsFrameSelection* aFrameSelection) + : mFrameSelection(aFrameSelection), + mPresContext(0), + mPoint(0, 0), + mDelayInMs(30), + mFurtherScrollingAllowed(FurtherScrollingAllowed::kYes) { + MOZ_ASSERT(mFrameSelection); + } + + MOZ_CAN_RUN_SCRIPT nsresult DoAutoScroll(nsIFrame* aFrame, nsPoint aPoint); + + private: + // aPoint is relative to aPresContext's root frame + nsresult ScheduleNextDoAutoScroll(nsPresContext* aPresContext, + nsPoint& aPoint) { + if (NS_WARN_IF(mFurtherScrollingAllowed == FurtherScrollingAllowed::kNo)) { + return NS_ERROR_FAILURE; + } + + mPoint = aPoint; + + // Store the presentation context. The timer will be + // stopped by the selection if the prescontext is destroyed. + mPresContext = aPresContext; + + mContent = PresShell::GetCapturingContent(); + + if (!mTimer) { + mTimer = NS_NewTimer( + mPresContext->Document()->EventTargetFor(TaskCategory::Other)); + + if (!mTimer) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return mTimer->InitWithCallback(this, mDelayInMs, nsITimer::TYPE_ONE_SHOT); + } + + public: + enum class FurtherScrollingAllowed { kYes, kNo }; + + void Stop(const FurtherScrollingAllowed aFurtherScrollingAllowed) { + MOZ_ASSERT((aFurtherScrollingAllowed == FurtherScrollingAllowed::kNo) || + (mFurtherScrollingAllowed == FurtherScrollingAllowed::kYes)); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + mContent = nullptr; + mFurtherScrollingAllowed = aFurtherScrollingAllowed; + } + + void SetDelay(uint32_t aDelayInMs) { mDelayInMs = aDelayInMs; } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Notify(nsITimer* timer) override { + if (mPresContext) { + AutoWeakFrame frame = + mContent ? mPresContext->GetPrimaryFrameFor(mContent) : nullptr; + if (!frame) { + return NS_OK; + } + mContent = nullptr; + + nsPoint pt = mPoint - frame->GetOffsetTo( + mPresContext->PresShell()->GetRootFrame()); + RefPtr frameSelection = mFrameSelection; + frameSelection->HandleDrag(frame, pt); + if (!frame.IsAlive()) { + return NS_OK; + } + + NS_ASSERTION(frame->PresContext() == mPresContext, "document mismatch?"); + DoAutoScroll(frame, pt); + } + return NS_OK; + } + + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("AutoScroller"); + return NS_OK; + } + + protected: + virtual ~AutoScroller() { + if (mTimer) { + mTimer->Cancel(); + } + } + + private: + nsFrameSelection* const mFrameSelection; + nsPresContext* mPresContext; + // relative to mPresContext's root frame + nsPoint mPoint; + nsCOMPtr mTimer; + nsCOMPtr mContent; + uint32_t mDelayInMs; + FurtherScrollingAllowed mFurtherScrollingAllowed; +}; + +NS_IMPL_ISUPPORTS(AutoScroller, nsITimerCallback, nsINamed) + +#ifdef PRINT_RANGE +void printRange(nsRange* aDomRange) { + if (!aDomRange) { + printf("NULL Range\n"); + } + nsINode* startNode = aDomRange->GetStartContainer(); + nsINode* endNode = aDomRange->GetEndContainer(); + int32_t startOffset = aDomRange->StartOffset(); + int32_t endOffset = aDomRange->EndOffset(); + + printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n", + (unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset, + (unsigned long)endNode, (long)endOffset); +} +#endif /* PRINT_RANGE */ + +void Selection::Stringify(nsAString& aResult, FlushFrames aFlushFrames) { + if (aFlushFrames == FlushFrames::Yes) { + // We need FlushType::Frames here to make sure frames have been created for + // the selected content. Use mFrameSelection->GetPresShell() which returns + // null if the Selection has been disconnected (the shell is Destroyed). + RefPtr presShell = + mFrameSelection ? mFrameSelection->GetPresShell() : nullptr; + if (!presShell) { + aResult.Truncate(); + return; + } + presShell->FlushPendingNotifications(FlushType::Frames); + } + + IgnoredErrorResult rv; + ToStringWithFormat(u"text/plain"_ns, nsIDocumentEncoder::SkipInvisibleContent, + 0, aResult, rv); + if (rv.Failed()) { + aResult.Truncate(); + } +} + +void Selection::ToStringWithFormat(const nsAString& aFormatType, + uint32_t aFlags, int32_t aWrapCol, + nsAString& aReturn, ErrorResult& aRv) { + nsCOMPtr encoder = + do_createDocumentEncoder(NS_ConvertUTF16toUTF8(aFormatType).get()); + if (!encoder) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + PresShell* presShell = GetPresShell(); + if (!presShell) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + Document* doc = presShell->GetDocument(); + + // Flags should always include OutputSelectionOnly if we're coming from here: + aFlags |= nsIDocumentEncoder::OutputSelectionOnly; + nsAutoString readstring; + readstring.Assign(aFormatType); + nsresult rv = encoder->Init(doc, readstring, aFlags); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + encoder->SetSelection(this); + if (aWrapCol != 0) encoder->SetWrapColumn(aWrapCol); + + rv = encoder->EncodeToString(aReturn); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +nsresult Selection::SetInterlinePosition(InterlinePosition aInterlinePosition) { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + MOZ_ASSERT(aInterlinePosition != InterlinePosition::Undefined); + + if (!mFrameSelection) { + return NS_ERROR_NOT_INITIALIZED; // Can't do selection + } + + mFrameSelection->SetHint(aInterlinePosition == + InterlinePosition::StartOfNextLine + ? CARET_ASSOCIATE_AFTER + : CARET_ASSOCIATE_BEFORE); + return NS_OK; +} + +Selection::InterlinePosition Selection::GetInterlinePosition() const { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (!mFrameSelection) { + return InterlinePosition::Undefined; + } + return mFrameSelection->GetHint() == CARET_ASSOCIATE_AFTER + ? InterlinePosition::StartOfNextLine + : InterlinePosition::EndOfLine; +} + +void Selection::SetInterlinePositionJS(bool aHintRight, ErrorResult& aRv) { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + aRv = SetInterlinePosition(aHintRight ? InterlinePosition::StartOfNextLine + : InterlinePosition::EndOfLine); +} + +bool Selection::GetInterlinePositionJS(ErrorResult& aRv) const { + const InterlinePosition interlinePosition = GetInterlinePosition(); + if (interlinePosition == InterlinePosition::Undefined) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return false; + } + return interlinePosition == InterlinePosition::StartOfNextLine; +} + +static bool IsEditorNode(const nsINode* aNode) { + if (!aNode) { + return false; + } + + if (aNode->IsEditable()) { + return true; + } + + auto* element = Element::FromNode(aNode); + return element && element->State().HasState(ElementState::READWRITE); +} + +bool Selection::IsEditorSelection() const { + return IsEditorNode(GetFocusNode()); +} + +Nullable Selection::GetCaretBidiLevel( + mozilla::ErrorResult& aRv) const { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return Nullable(); + } + mozilla::intl::BidiEmbeddingLevel caretBidiLevel = + static_cast( + mFrameSelection->GetCaretBidiLevel()); + return (caretBidiLevel & BIDI_LEVEL_UNDEFINED) + ? Nullable() + : Nullable(caretBidiLevel); +} + +void Selection::SetCaretBidiLevel(const Nullable& aCaretBidiLevel, + mozilla::ErrorResult& aRv) { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + if (aCaretBidiLevel.IsNull()) { + mFrameSelection->UndefineCaretBidiLevel(); + } else { + mFrameSelection->SetCaretBidiLevelAndMaybeSchedulePaint( + mozilla::intl::BidiEmbeddingLevel(aCaretBidiLevel.Value())); + } +} + +/** + * Test whether the supplied range points to a single table element. + * Result is one of the TableSelectionMode constants. "None" means + * a table element isn't selected. + */ +// TODO: Figure out TableSelectionMode::Column and TableSelectionMode::AllCells +static nsresult GetTableSelectionMode(const nsRange& aRange, + TableSelectionMode* aTableSelectionType) { + if (!aTableSelectionType) { + return NS_ERROR_NULL_POINTER; + } + + *aTableSelectionType = TableSelectionMode::None; + + nsINode* startNode = aRange.GetStartContainer(); + if (!startNode) { + return NS_ERROR_FAILURE; + } + + nsINode* endNode = aRange.GetEndContainer(); + if (!endNode) { + return NS_ERROR_FAILURE; + } + + // Not a single selected node + if (startNode != endNode) { + return NS_OK; + } + + nsIContent* child = aRange.GetChildAtStartOffset(); + + // Not a single selected node + if (!child || child->GetNextSibling() != aRange.GetChildAtEndOffset()) { + return NS_OK; + } + + if (!startNode->IsHTMLElement()) { + // Implies a check for being an element; if we ever make this work + // for non-HTML, need to keep checking for elements. + return NS_OK; + } + + if (startNode->IsHTMLElement(nsGkAtoms::tr)) { + *aTableSelectionType = TableSelectionMode::Cell; + } else // check to see if we are selecting a table or row (column and all + // cells not done yet) + { + if (child->IsHTMLElement(nsGkAtoms::table)) { + *aTableSelectionType = TableSelectionMode::Table; + } else if (child->IsHTMLElement(nsGkAtoms::tr)) { + *aTableSelectionType = TableSelectionMode::Row; + } + } + + return NS_OK; +} + +nsresult Selection::MaybeAddTableCellRange(nsRange& aRange, + Maybe* aOutIndex) { + if (!aOutIndex) { + return NS_ERROR_NULL_POINTER; + } + + MOZ_ASSERT(aOutIndex->isNothing()); + + if (!mFrameSelection) { + return NS_OK; + } + + // Get if we are adding a cell selection and the row, col of cell if we are + TableSelectionMode tableMode; + nsresult result = GetTableSelectionMode(aRange, &tableMode); + if (NS_FAILED(result)) return result; + + // If not adding a cell range, we are done here + if (tableMode != TableSelectionMode::Cell) { + mFrameSelection->mTableSelection.mMode = tableMode; + // Don't fail if range isn't a selected cell, aDidAddRange tells caller if + // we didn't proceed + return NS_OK; + } + + // Set frame selection mode only if not already set to a table mode + // so we don't lose the select row and column flags (not detected by + // getTableCellLocation) + if (mFrameSelection->mTableSelection.mMode == TableSelectionMode::None) { + mFrameSelection->mTableSelection.mMode = tableMode; + } + + return AddRangesForSelectableNodes(&aRange, aOutIndex, + DispatchSelectstartEvent::Maybe); +} + +Selection::Selection(SelectionType aSelectionType, + nsFrameSelection* aFrameSelection) + : mFrameSelection(aFrameSelection), + mCachedOffsetForFrame(nullptr), + mDirection(eDirNext), + mSelectionType(aSelectionType), + mCustomColors(nullptr), + mSelectionChangeBlockerCount(0), + mUserInitiated(false), + mCalledByJS(false), + mNotifyAutoCopy(false) {} + +Selection::~Selection() { Disconnect(); } + +void Selection::Disconnect() { + RemoveAnchorFocusRange(); + + mStyledRanges.UnregisterSelection(); + + if (mAutoScroller) { + mAutoScroller->Stop(AutoScroller::FurtherScrollingAllowed::kNo); + mAutoScroller = nullptr; + } + + mScrollEvent.Revoke(); + + if (mCachedOffsetForFrame) { + delete mCachedOffsetForFrame; + mCachedOffsetForFrame = nullptr; + } +} + +Document* Selection::GetParentObject() const { + PresShell* presShell = GetPresShell(); + return presShell ? presShell->GetDocument() : nullptr; +} + +DocGroup* Selection::GetDocGroup() const { + PresShell* presShell = GetPresShell(); + if (!presShell) { + return nullptr; + } + Document* doc = presShell->GetDocument(); + return doc ? doc->GetDocGroup() : nullptr; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Selection) + +MOZ_CAN_RUN_SCRIPT_BOUNDARY +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Selection) + // Unlink the selection listeners *before* we do RemoveAllRangesInternal since + // we don't want to notify the listeners during JS GC (they could be + // in JS!). + tmp->mNotifyAutoCopy = false; + if (tmp->mAccessibleCaretEventHub) { + tmp->StopNotifyingAccessibleCaretEventHub(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionChangeEventDispatcher) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionListeners) + MOZ_KnownLive(tmp)->RemoveAllRangesInternal(IgnoreErrors()); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrameSelection) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Selection) + { + uint32_t i, count = tmp->mStyledRanges.Length(); + for (i = 0; i < count; ++i) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyledRanges.mRanges[i].mRange) + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorFocusRange) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameSelection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionChangeEventDispatcher) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionListeners) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +// QueryInterface implementation for Selection +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Selection) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_ADDREF(Selection) +NS_IMPL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE( + Selection, Disconnect()) + +const RangeBoundary& Selection::AnchorRef() const { + if (!mAnchorFocusRange) { + static RangeBoundary sEmpty; + return sEmpty; + } + + if (GetDirection() == eDirNext) { + return mAnchorFocusRange->StartRef(); + } + + return mAnchorFocusRange->EndRef(); +} + +const RangeBoundary& Selection::FocusRef() const { + if (!mAnchorFocusRange) { + static RangeBoundary sEmpty; + return sEmpty; + } + + if (GetDirection() == eDirNext) { + return mAnchorFocusRange->EndRef(); + } + + return mAnchorFocusRange->StartRef(); +} + +void Selection::SetAnchorFocusRange(size_t aIndex) { + if (aIndex >= mStyledRanges.Length()) { + return; + } + // Highlight selections may contain static ranges. + MOZ_ASSERT(mSelectionType != SelectionType::eHighlight); + AbstractRange* anchorFocusRange = mStyledRanges.mRanges[aIndex].mRange; + mAnchorFocusRange = anchorFocusRange->AsDynamicRange(); +} + +static int32_t CompareToRangeStart(const nsINode& aCompareNode, + uint32_t aCompareOffset, + const AbstractRange& aRange) { + MOZ_ASSERT(aRange.GetStartContainer()); + nsINode* start = aRange.GetStartContainer(); + // If the nodes that we're comparing are not in the same document, assume that + // aCompareNode will fall at the end of the ranges. + if (aCompareNode.GetComposedDoc() != start->GetComposedDoc() || + !start->GetComposedDoc()) { + NS_WARNING( + "`CompareToRangeStart` couldn't compare nodes, pretending some order."); + return 1; + } + + // The points are in the same subtree, hence there has to be an order. + return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, start, + aRange.StartOffset()); +} + +static int32_t CompareToRangeEnd(const nsINode& aCompareNode, + uint32_t aCompareOffset, + const AbstractRange& aRange) { + MOZ_ASSERT(aRange.IsPositioned()); + nsINode* end = aRange.GetEndContainer(); + // If the nodes that we're comparing are not in the same document or in the + // same subtree, assume that aCompareNode will fall at the end of the ranges. + if (aCompareNode.GetComposedDoc() != end->GetComposedDoc() || + !end->GetComposedDoc()) { + NS_WARNING( + "`CompareToRangeEnd` couldn't compare nodes, pretending some order."); + return 1; + } + + // The points are in the same subtree, hence there has to be an order. + return *nsContentUtils::ComparePoints(&aCompareNode, aCompareOffset, end, + aRange.EndOffset()); +} + +// static +size_t Selection::StyledRanges::FindInsertionPoint( + const nsTArray* aElementArray, const nsINode& aPointNode, + uint32_t aPointOffset, + int32_t (*aComparator)(const nsINode&, uint32_t, const AbstractRange&)) { + int32_t beginSearch = 0; + int32_t endSearch = aElementArray->Length(); // one beyond what to check + + if (endSearch) { + int32_t center = endSearch - 1; // Check last index, then binary search + do { + const AbstractRange* range = (*aElementArray)[center].mRange; + + int32_t cmp{aComparator(aPointNode, aPointOffset, *range)}; + + if (cmp < 0) { // point < cur + endSearch = center; + } else if (cmp > 0) { // point > cur + beginSearch = center + 1; + } else { // found match, done + beginSearch = center; + break; + } + center = (endSearch - beginSearch) / 2 + beginSearch; + } while (endSearch - beginSearch > 0); + } + + return AssertedCast(beginSearch); +} + +// Selection::SubtractRange +// +// A helper function that subtracts aSubtract from aRange, and adds +// 1 or 2 StyledRange objects representing the remaining non-overlapping +// difference to aOutput. It is assumed that the caller has checked that +// aRange and aSubtract do indeed overlap + +// static +nsresult Selection::StyledRanges::SubtractRange( + StyledRange& aRange, nsRange& aSubtract, nsTArray* aOutput) { + AbstractRange* range = aRange.mRange; + if (NS_WARN_IF(!range->IsPositioned())) { + return NS_ERROR_UNEXPECTED; + } + + if (range->GetStartContainer()->SubtreeRoot() != + aSubtract.GetStartContainer()->SubtreeRoot()) { + // These are ranges for different shadow trees, we can't subtract them in + // any sensible way. + aOutput->InsertElementAt(0, aRange); + return NS_OK; + } + + // First we want to compare to the range start + int32_t cmp{CompareToRangeStart(*range->GetStartContainer(), + range->StartOffset(), aSubtract)}; + + // Also, make a comparison to the range end + int32_t cmp2{CompareToRangeEnd(*range->GetEndContainer(), range->EndOffset(), + aSubtract)}; + + // If the existing range left overlaps the new range (aSubtract) then + // cmp < 0, and cmp2 < 0 + // If it right overlaps the new range then cmp > 0 and cmp2 > 0 + // If it fully contains the new range, then cmp < 0 and cmp2 > 0 + + if (cmp2 > 0) { + // We need to add a new StyledRange to the output, running from + // the end of aSubtract to the end of range + ErrorResult error; + RefPtr postOverlap = + nsRange::Create(aSubtract.EndRef(), range->EndRef(), error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + MOZ_ASSERT(postOverlap); + if (!postOverlap->Collapsed()) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aOutput->InsertElementAt(0, StyledRange(postOverlap)); + (*aOutput)[0].mTextRangeStyle = aRange.mTextRangeStyle; + } + } + + if (cmp < 0) { + // We need to add a new StyledRange to the output, running from + // the start of the range to the start of aSubtract + ErrorResult error; + RefPtr preOverlap = + nsRange::Create(range->StartRef(), aSubtract.StartRef(), error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + MOZ_ASSERT(preOverlap); + if (!preOverlap->Collapsed()) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aOutput->InsertElementAt(0, StyledRange(preOverlap)); + (*aOutput)[0].mTextRangeStyle = aRange.mTextRangeStyle; + } + } + + return NS_OK; +} + +static void UserSelectRangesToAdd(nsRange* aItem, + nsTArray>& aRangesToAdd) { + // We cannot directly call IsEditorSelection() because we may be in an + // inconsistent state during Collapse() (we're cleared already but we haven't + // got a new focus node yet). + if (IsEditorNode(aItem->GetStartContainer()) && + IsEditorNode(aItem->GetEndContainer())) { + // Don't mess with the selection ranges for editing, editor doesn't really + // deal well with multi-range selections. + aRangesToAdd.AppendElement(aItem); + } else { + aItem->ExcludeNonSelectableNodes(&aRangesToAdd); + } +} + +static nsINode* DetermineSelectstartEventTarget( + const bool aSelectionEventsOnTextControlsEnabled, const nsRange& aRange) { + nsINode* target = aRange.GetStartContainer(); + if (aSelectionEventsOnTextControlsEnabled) { + // Get the first element which isn't in a native anonymous subtree + while (target && target->IsInNativeAnonymousSubtree()) { + target = target->GetParent(); + } + } else { + if (target->IsInNativeAnonymousSubtree()) { + // This is a selection under a text control, so don't dispatch the + // event. + target = nullptr; + } + } + return target; +} + +/** + * @return true, iff the default action should be executed. + */ +static bool MaybeDispatchSelectstartEvent( + const nsRange& aRange, const bool aSelectionEventsOnTextControlsEnabled, + Document* aDocument) { + nsCOMPtr selectstartEventTarget = DetermineSelectstartEventTarget( + aSelectionEventsOnTextControlsEnabled, aRange); + + bool executeDefaultAction = true; + + if (selectstartEventTarget) { + nsContentUtils::DispatchTrustedEvent( + aDocument, selectstartEventTarget, u"selectstart"_ns, CanBubble::eYes, + Cancelable::eYes, &executeDefaultAction); + } + + return executeDefaultAction; +} + +// static +bool Selection::IsUserSelectionCollapsed( + const nsRange& aRange, nsTArray>& aTempRangesToAdd) { + MOZ_ASSERT(aTempRangesToAdd.IsEmpty()); + + RefPtr scratchRange = aRange.CloneRange(); + UserSelectRangesToAdd(scratchRange, aTempRangesToAdd); + const bool userSelectionCollapsed = + (aTempRangesToAdd.Length() == 0) || + ((aTempRangesToAdd.Length() == 1) && aTempRangesToAdd[0]->Collapsed()); + + aTempRangesToAdd.ClearAndRetainStorage(); + + return userSelectionCollapsed; +} + +nsresult Selection::AddRangesForUserSelectableNodes( + nsRange* aRange, Maybe* aOutIndex, + const DispatchSelectstartEvent aDispatchSelectstartEvent) { + MOZ_ASSERT(mUserInitiated); + MOZ_ASSERT(aOutIndex); + MOZ_ASSERT(aOutIndex->isNothing()); + + if (!aRange) { + return NS_ERROR_NULL_POINTER; + } + + if (!aRange->IsPositioned()) { + return NS_ERROR_UNEXPECTED; + } + + AutoTArray, 4> rangesToAdd; + if (mStyledRanges.Length()) { + aOutIndex->emplace(mStyledRanges.Length() - 1); + } + + Document* doc = GetDocument(); + + if (aDispatchSelectstartEvent == DispatchSelectstartEvent::Maybe && + mSelectionType == SelectionType::eNormal && IsCollapsed() && + !IsBlockingSelectionChangeEvents()) { + // We consider a selection to be starting if we are currently collapsed, + // and the selection is becoming uncollapsed, and this is caused by a + // user initiated event. + + // First, we generate the ranges to add with a scratch range, which is a + // clone of the original range passed in. We do this seperately, because + // the selectstart event could have caused the world to change, and + // required ranges to be re-generated + + const bool userSelectionCollapsed = + IsUserSelectionCollapsed(*aRange, rangesToAdd); + MOZ_ASSERT(userSelectionCollapsed || nsContentUtils::IsSafeToRunScript()); + if (!userSelectionCollapsed && nsContentUtils::IsSafeToRunScript()) { + // The spec currently doesn't say that we should dispatch this event + // on text controls, so for now we only support doing that under a + // pref, disabled by default. + // See https://github.com/w3c/selection-api/issues/53. + const bool executeDefaultAction = MaybeDispatchSelectstartEvent( + *aRange, + StaticPrefs::dom_select_events_textcontrols_selectstart_enabled(), + doc); + + if (!executeDefaultAction) { + return NS_OK; + } + + // As we potentially dispatched an event to the DOM, something could have + // changed under our feet. Re-generate the rangesToAdd array, and + // ensure that the range we are about to add is still valid. + if (!aRange->IsPositioned()) { + return NS_ERROR_UNEXPECTED; + } + } + } + + // Generate the ranges to add + UserSelectRangesToAdd(aRange, rangesToAdd); + size_t newAnchorFocusIndex = + GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1; + for (size_t i = 0; i < rangesToAdd.Length(); ++i) { + Maybe index; + // `MOZ_KnownLive` needed because of broken static analysis + // (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253#c1). + nsresult rv = mStyledRanges.MaybeAddRangeAndTruncateOverlaps( + MOZ_KnownLive(rangesToAdd[i]), &index); + NS_ENSURE_SUCCESS(rv, rv); + if (i == newAnchorFocusIndex) { + *aOutIndex = index; + rangesToAdd[i]->SetIsGenerated(false); + } else { + rangesToAdd[i]->SetIsGenerated(true); + } + } + return NS_OK; +} + +nsresult Selection::AddRangesForSelectableNodes( + nsRange* aRange, Maybe* aOutIndex, + const DispatchSelectstartEvent aDispatchSelectstartEvent) { + MOZ_ASSERT(aOutIndex); + MOZ_ASSERT(aOutIndex->isNothing()); + + if (!aRange) { + return NS_ERROR_NULL_POINTER; + } + + if (!aRange->IsPositioned()) { + return NS_ERROR_UNEXPECTED; + } + + MOZ_LOG( + sSelectionLog, LogLevel::Debug, + ("%s: selection=%p, type=%i, range=(%p, StartOffset=%u, EndOffset=%u)", + __FUNCTION__, this, static_cast(GetType()), aRange, + aRange->StartOffset(), aRange->EndOffset())); + + if (mUserInitiated) { + return AddRangesForUserSelectableNodes(aRange, aOutIndex, + aDispatchSelectstartEvent); + } + + return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex); +} + +nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps( + nsRange* aRange, Maybe* aOutIndex) { + MOZ_ASSERT(aRange); + MOZ_ASSERT(aRange->IsPositioned()); + MOZ_ASSERT(aOutIndex); + MOZ_ASSERT(aOutIndex->isNothing()); + + // a common case is that we have no ranges yet + if (mRanges.Length() == 0) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mRanges.AppendElement(StyledRange(aRange)); + aRange->RegisterSelection(MOZ_KnownLive(mSelection)); + + aOutIndex->emplace(0u); + return NS_OK; + } + + Maybe maybeStartIndex, maybeEndIndex; + nsresult rv = + GetIndicesForInterval(aRange->GetStartContainer(), aRange->StartOffset(), + aRange->GetEndContainer(), aRange->EndOffset(), + false, maybeStartIndex, maybeEndIndex); + NS_ENSURE_SUCCESS(rv, rv); + + size_t startIndex, endIndex; + if (maybeEndIndex.isNothing()) { + // All ranges start after the given range. We can insert our range at + // position 0, knowing there are no overlaps (handled below) + startIndex = endIndex = 0; + } else if (maybeStartIndex.isNothing()) { + // All ranges end before the given range. We can insert our range at + // the end of the array, knowing there are no overlaps (handled below) + startIndex = endIndex = mRanges.Length(); + } else { + startIndex = *maybeStartIndex; + endIndex = *maybeEndIndex; + } + + // If the range is already contained in mRanges, silently + // succeed + const bool sameRange = HasEqualRangeBoundariesAt(*aRange, startIndex); + if (sameRange) { + aOutIndex->emplace(startIndex); + return NS_OK; + } + + if (startIndex == endIndex) { + // The new range doesn't overlap any existing ranges + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mRanges.InsertElementAt(startIndex, StyledRange(aRange)); + aRange->RegisterSelection(MOZ_KnownLive(mSelection)); + aOutIndex->emplace(startIndex); + return NS_OK; + } + + // We now know that at least 1 existing range overlaps with the range that + // we are trying to add. In fact, the only ranges of interest are those at + // the two end points, startIndex and endIndex - 1 (which may point to the + // same range) as these may partially overlap the new range. Any ranges + // between these indices are fully overlapped by the new range, and so can be + // removed. + AutoTArray overlaps; + overlaps.AppendElement(mRanges[startIndex]); + if (endIndex - 1 != startIndex) { + overlaps.AppendElement(mRanges[endIndex - 1]); + } + + // Remove all the overlapping ranges + for (size_t i = startIndex; i < endIndex; ++i) { + mRanges[i].mRange->UnregisterSelection(mSelection); + } + mRanges.RemoveElementsAt(startIndex, endIndex - startIndex); + + AutoTArray temp; + for (const size_t i : Reversed(IntegerRange(overlaps.Length()))) { + nsresult rv = SubtractRange(overlaps[i], *aRange, &temp); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Insert the new element into our "leftovers" array + // `aRange` is positioned, so it has to have a start container. + size_t insertionPoint{FindInsertionPoint(&temp, *aRange->GetStartContainer(), + aRange->StartOffset(), + CompareToRangeStart)}; + + temp.InsertElementAt(insertionPoint, StyledRange(aRange)); + + // Merge the leftovers back in to mRanges + mRanges.InsertElementsAt(startIndex, temp); + + for (uint32_t i = 0; i < temp.Length(); ++i) { + if (temp[i].mRange->IsDynamicRange()) { + MOZ_KnownLive(temp[i].mRange->AsDynamicRange()) + ->RegisterSelection(MOZ_KnownLive(mSelection)); + // `MOZ_KnownLive` is required because of + // https://bugzilla.mozilla.org/show_bug.cgi?id=1622253. + } + } + + aOutIndex->emplace(startIndex + insertionPoint); + return NS_OK; +} + +nsresult Selection::StyledRanges::RemoveRangeAndUnregisterSelection( + AbstractRange& aRange) { + // Find the range's index & remove it. We could use FindInsertionPoint to + // get O(log n) time, but that requires many expensive DOM comparisons. + // For even several thousand items, this is probably faster because the + // comparisons are so fast. + int32_t idx = -1; + uint32_t i; + for (i = 0; i < mRanges.Length(); i++) { + if (mRanges[i].mRange == &aRange) { + idx = (int32_t)i; + break; + } + } + if (idx < 0) return NS_ERROR_DOM_NOT_FOUND_ERR; + + mRanges.RemoveElementAt(idx); + aRange.UnregisterSelection(mSelection); + + return NS_OK; +} +nsresult Selection::RemoveCollapsedRanges() { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + return mStyledRanges.RemoveCollapsedRanges(); +} + +nsresult Selection::StyledRanges::RemoveCollapsedRanges() { + uint32_t i = 0; + while (i < mRanges.Length()) { + if (mRanges[i].mRange->Collapsed()) { + nsresult rv = RemoveRangeAndUnregisterSelection(*mRanges[i].mRange); + NS_ENSURE_SUCCESS(rv, rv); + } else { + ++i; + } + } + return NS_OK; +} + +void Selection::Clear(nsPresContext* aPresContext) { + RemoveAnchorFocusRange(); + + mStyledRanges.UnregisterSelection(); + for (uint32_t i = 0; i < mStyledRanges.Length(); ++i) { + SelectFrames(aPresContext, *mStyledRanges.mRanges[i].mRange, false); + } + mStyledRanges.Clear(); + + // Reset direction so for more dependable table selection range handling + SetDirection(eDirNext); + + // If this was an ATTENTION selection, change it back to normal now + if (mFrameSelection && mFrameSelection->GetDisplaySelection() == + nsISelectionController::SELECTION_ATTENTION) { + mFrameSelection->SetDisplaySelection(nsISelectionController::SELECTION_ON); + } +} + +bool Selection::StyledRanges::HasEqualRangeBoundariesAt( + const nsRange& aRange, size_t aRangeIndex) const { + if (aRangeIndex < mRanges.Length()) { + const AbstractRange* range = mRanges[aRangeIndex].mRange; + return range->HasEqualBoundaries(aRange); + } + return false; +} + +void Selection::GetRangesForInterval(nsINode& aBeginNode, uint32_t aBeginOffset, + nsINode& aEndNode, uint32_t aEndOffset, + bool aAllowAdjacent, + nsTArray>& aReturn, + mozilla::ErrorResult& aRv) { + AutoTArray results; + nsresult rv = + GetDynamicRangesForIntervalArray(&aBeginNode, aBeginOffset, &aEndNode, + aEndOffset, aAllowAdjacent, &results); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + aReturn.SetLength(results.Length()); + for (size_t i = 0; i < results.Length(); ++i) { + aReturn[i] = results[i]; // AddRefs + } +} + +nsresult Selection::GetAbstractRangesForIntervalArray( + nsINode* aBeginNode, uint32_t aBeginOffset, nsINode* aEndNode, + uint32_t aEndOffset, bool aAllowAdjacent, + nsTArray* aRanges) { + if (NS_WARN_IF(!aBeginNode)) { + return NS_ERROR_UNEXPECTED; + } + + if (NS_WARN_IF(!aEndNode)) { + return NS_ERROR_UNEXPECTED; + } + + aRanges->Clear(); + Maybe maybeStartIndex, maybeEndIndex; + nsresult res = mStyledRanges.GetIndicesForInterval( + aBeginNode, aBeginOffset, aEndNode, aEndOffset, aAllowAdjacent, + maybeStartIndex, maybeEndIndex); + NS_ENSURE_SUCCESS(res, res); + + if (maybeStartIndex.isNothing() || maybeEndIndex.isNothing()) { + return NS_OK; + } + + for (const size_t i : IntegerRange(*maybeStartIndex, *maybeEndIndex)) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aRanges->AppendElement(mStyledRanges.mRanges[i].mRange); + } + + return NS_OK; +} + +nsresult Selection::GetDynamicRangesForIntervalArray( + nsINode* aBeginNode, uint32_t aBeginOffset, nsINode* aEndNode, + uint32_t aEndOffset, bool aAllowAdjacent, nsTArray* aRanges) { + MOZ_ASSERT(mSelectionType != SelectionType::eHighlight); + AutoTArray abstractRanges; + nsresult rv = GetAbstractRangesForIntervalArray( + aBeginNode, aBeginOffset, aEndNode, aEndOffset, aAllowAdjacent, + &abstractRanges); + NS_ENSURE_SUCCESS(rv, rv); + aRanges->Clear(); + aRanges->SetCapacity(abstractRanges.Length()); + for (auto* abstractRange : abstractRanges) { + aRanges->AppendElement(abstractRange->AsDynamicRange()); + } + return NS_OK; +} + +nsresult Selection::StyledRanges::GetIndicesForInterval( + const nsINode* aBeginNode, uint32_t aBeginOffset, const nsINode* aEndNode, + uint32_t aEndOffset, bool aAllowAdjacent, Maybe& aStartIndex, + Maybe& aEndIndex) const { + MOZ_ASSERT(aStartIndex.isNothing()); + MOZ_ASSERT(aEndIndex.isNothing()); + + if (NS_WARN_IF(!aBeginNode)) { + return NS_ERROR_INVALID_POINTER; + } + + if (NS_WARN_IF(!aEndNode)) { + return NS_ERROR_INVALID_POINTER; + } + + if (mRanges.Length() == 0) { + return NS_OK; + } + + const bool intervalIsCollapsed = + aBeginNode == aEndNode && aBeginOffset == aEndOffset; + + // Ranges that end before the given interval and begin after the given + // interval can be discarded + size_t endsBeforeIndex{FindInsertionPoint(&mRanges, *aEndNode, aEndOffset, + &CompareToRangeStart)}; + + if (endsBeforeIndex == 0) { + const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange; + + // If the interval is strictly before the range at index 0, we can optimize + // by returning now - all ranges start after the given interval + if (!endRange->StartRef().Equals(aEndNode, aEndOffset)) { + return NS_OK; + } + + // We now know that the start point of mRanges[0].mRange + // equals the end of the interval. Thus, when aAllowadjacent is true, the + // caller is always interested in this range. However, when excluding + // adjacencies, we must remember to include the range when both it and the + // given interval are collapsed to the same point + if (!aAllowAdjacent && !(endRange->Collapsed() && intervalIsCollapsed)) + return NS_OK; + } + aEndIndex.emplace(endsBeforeIndex); + + size_t beginsAfterIndex{FindInsertionPoint(&mRanges, *aBeginNode, + aBeginOffset, &CompareToRangeEnd)}; + + if (beginsAfterIndex == mRanges.Length()) { + return NS_OK; // optimization: all ranges are strictly before us + } + + if (aAllowAdjacent) { + // At this point, one of the following holds: + // endsBeforeIndex == mRanges.Length(), + // endsBeforeIndex points to a range whose start point does not equal the + // given interval's start point + // endsBeforeIndex points to a range whose start point equals the given + // interval's start point + // In the final case, there can be two such ranges, a collapsed range, and + // an adjacent range (they will appear in mRanges in that + // order). For this final case, we need to increment endsBeforeIndex, until + // one of the first two possibilities hold + while (endsBeforeIndex < mRanges.Length()) { + const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange; + if (!endRange->StartRef().Equals(aEndNode, aEndOffset)) { + break; + } + endsBeforeIndex++; + } + + // Likewise, one of the following holds: + // beginsAfterIndex == 0, + // beginsAfterIndex points to a range whose end point does not equal + // the given interval's end point + // beginsOnOrAfter points to a range whose end point equals the given + // interval's end point + // In the final case, there can be two such ranges, an adjacent range, and + // a collapsed range (they will appear in mRanges in that + // order). For this final case, we only need to take action if both those + // ranges exist, and we are pointing to the collapsed range - we need to + // point to the adjacent range + const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange; + if (beginsAfterIndex > 0 && beginRange->Collapsed() && + beginRange->EndRef().Equals(aBeginNode, aBeginOffset)) { + beginRange = mRanges[beginsAfterIndex - 1].mRange; + if (beginRange->EndRef().Equals(aBeginNode, aBeginOffset)) { + beginsAfterIndex--; + } + } + } else { + // See above for the possibilities at this point. The only case where we + // need to take action is when the range at beginsAfterIndex ends on + // the given interval's start point, but that range isn't collapsed (a + // collapsed range should be included in the returned results). + const AbstractRange* beginRange = mRanges[beginsAfterIndex].mRange; + if (beginRange->EndRef().Equals(aBeginNode, aBeginOffset) && + !beginRange->Collapsed()) { + beginsAfterIndex++; + } + + // Again, see above for the meaning of endsBeforeIndex at this point. + // In particular, endsBeforeIndex may point to a collaped range which + // represents the point at the end of the interval - this range should be + // included + if (endsBeforeIndex < mRanges.Length()) { + const AbstractRange* endRange = mRanges[endsBeforeIndex].mRange; + if (endRange->StartRef().Equals(aEndNode, aEndOffset) && + endRange->Collapsed()) { + endsBeforeIndex++; + } + } + } + + NS_ASSERTION(beginsAfterIndex <= endsBeforeIndex, "Is mRanges not ordered?"); + NS_ENSURE_STATE(beginsAfterIndex <= endsBeforeIndex); + + aStartIndex.emplace(beginsAfterIndex); + aEndIndex = Some(endsBeforeIndex); + return NS_OK; +} + +nsIFrame* Selection::GetPrimaryFrameForAnchorNode() const { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + int32_t frameOffset = 0; + nsCOMPtr content = do_QueryInterface(GetAnchorNode()); + if (content && mFrameSelection) { + return nsFrameSelection::GetFrameForNodeOffset( + content, AnchorOffset(), mFrameSelection->GetHint(), &frameOffset); + } + return nullptr; +} + +nsIFrame* Selection::GetPrimaryFrameForFocusNode(bool aVisual, + int32_t* aOffsetUsed) const { + nsINode* focusNode = GetFocusNode(); + if (!focusNode || !focusNode->IsContent() || !mFrameSelection) { + return nullptr; + } + + nsCOMPtr content = focusNode->AsContent(); + int32_t frameOffset = 0; + if (!aOffsetUsed) { + aOffsetUsed = &frameOffset; + } + + nsIFrame* frame = GetPrimaryOrCaretFrameForNodeOffset(content, FocusOffset(), + aOffsetUsed, aVisual); + if (frame) { + return frame; + } + + // If content is whitespace only, we promote focus node to parent because + // whitespace only node might have no frame. + + if (!content->TextIsOnlyWhitespace()) { + return nullptr; + } + + nsCOMPtr parent = content->GetParent(); + if (NS_WARN_IF(!parent)) { + return nullptr; + } + const Maybe offset = parent->ComputeIndexOf(content); + if (MOZ_UNLIKELY(NS_WARN_IF(offset.isNothing()))) { + return nullptr; + } + return GetPrimaryOrCaretFrameForNodeOffset(parent, *offset, aOffsetUsed, + aVisual); +} + +nsIFrame* Selection::GetPrimaryOrCaretFrameForNodeOffset(nsIContent* aContent, + uint32_t aOffset, + int32_t* aOffsetUsed, + bool aVisual) const { + MOZ_ASSERT(aOffsetUsed); + + if (!mFrameSelection) { + return nullptr; + } + + CaretAssociationHint hint = mFrameSelection->GetHint(); + + if (aVisual) { + mozilla::intl::BidiEmbeddingLevel caretBidiLevel = + mFrameSelection->GetCaretBidiLevel(); + + return nsCaret::GetCaretFrameForNodeOffset( + mFrameSelection, aContent, aOffset, hint, caretBidiLevel, + /* aReturnUnadjustedFrame = */ nullptr, aOffsetUsed); + } + + return nsFrameSelection::GetFrameForNodeOffset(aContent, aOffset, hint, + aOffsetUsed); +} + +void Selection::SelectFramesOf(nsIContent* aContent, bool aSelected) const { + nsIFrame* frame = aContent->GetPrimaryFrame(); + if (!frame) { + return; + } + // The frame could be an SVG text frame, in which case we don't treat it + // as a text frame. + if (frame->IsTextFrame()) { + nsTextFrame* textFrame = static_cast(frame); + textFrame->SelectionStateChanged(0, textFrame->TextFragment()->GetLength(), + aSelected, mSelectionType); + } else { + frame->SelectionStateChanged(); + } +} + +nsresult Selection::SelectFramesOfInclusiveDescendantsOfContent( + PostContentIterator& aPostOrderIter, nsIContent* aContent, + bool aSelected) const { + // If aContent doesn't have children, we should avoid to use the content + // iterator for performance reason. + if (!aContent->HasChildren()) { + SelectFramesOf(aContent, aSelected); + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(aPostOrderIter.Init(aContent)))) { + return NS_ERROR_FAILURE; + } + + for (; !aPostOrderIter.IsDone(); aPostOrderIter.Next()) { + nsINode* node = aPostOrderIter.GetCurrentNode(); + MOZ_ASSERT(node); + nsIContent* innercontent = node->IsContent() ? node->AsContent() : nullptr; + SelectFramesOf(innercontent, aSelected); + } + + return NS_OK; +} + +void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) { + // this method is currently only called in a user-initiated context. + // therefore it is safe to assume that we are not in a Highlight selection + // and we only have to deal with nsRanges (no StaticRanges). + MOZ_ASSERT(mSelectionType != SelectionType::eHighlight); + for (size_t i = 0; i < mStyledRanges.Length(); ++i) { + nsRange* range = mStyledRanges.mRanges[i].mRange->AsDynamicRange(); + MOZ_ASSERT(range->IsInAnySelection()); + SelectFrames(aPresContext, *range, range->IsInAnySelection()); + } +} + +/** + * The idea of this helper method is to select or deselect "top to bottom", + * traversing through the frames + */ +nsresult Selection::SelectFrames(nsPresContext* aPresContext, + AbstractRange& aRange, bool aSelect) const { + if (!mFrameSelection || !aPresContext || !aPresContext->GetPresShell()) { + // nothing to do + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(aRange.IsPositioned()); + + const Document* const document = GetDocument(); + if (MOZ_UNLIKELY(!document || + aRange.GetComposedDocOfContainers() != document)) { + return NS_OK; // Do nothing if the range is now in different document. + } + + if (aRange.IsStaticRange() && !aRange.AsStaticRange()->IsValid()) { + // TODO jjaschke: Actions necessary to unselect invalid static ranges? + return NS_OK; + } + + if (mFrameSelection->IsInTableSelectionMode()) { + const nsIContent* const commonAncestorContent = + nsIContent::FromNodeOrNull(aRange.GetClosestCommonInclusiveAncestor()); + nsIFrame* const frame = commonAncestorContent + ? commonAncestorContent->GetPrimaryFrame() + : aPresContext->PresShell()->GetRootFrame(); + if (frame) { + if (frame->IsTextFrame()) { + MOZ_ASSERT(commonAncestorContent == aRange.GetStartContainer()); + MOZ_ASSERT(commonAncestorContent == aRange.GetEndContainer()); + static_cast(frame)->SelectionStateChanged( + aRange.StartOffset(), aRange.EndOffset(), aSelect, mSelectionType); + } else { + frame->SelectionStateChanged(); + } + } + + return NS_OK; + } + + // Loop through the content iterator for each content node; for each text + // node, call SetSelected on it: + nsIContent* const startContent = + nsIContent::FromNodeOrNull(aRange.GetStartContainer()); + if (MOZ_UNLIKELY(!startContent)) { + // Don't warn, bug 1055722 + // XXX The range can start from a document node and such range can be + // added to Selection with JS. Therefore, even in such cases, + // shouldn't we handle selection in the range? + return NS_ERROR_UNEXPECTED; + } + MOZ_DIAGNOSTIC_ASSERT(startContent->IsInComposedDoc()); + + // We must call first one explicitly + nsINode* const endNode = aRange.GetEndContainer(); + if (NS_WARN_IF(!endNode)) { + // We null-checked start node above, therefore, end node should also be + // non-null here. + return NS_ERROR_UNEXPECTED; + } + const bool isFirstContentTextNode = startContent->IsText(); + if (isFirstContentTextNode) { + if (nsIFrame* const frame = startContent->GetPrimaryFrame()) { + // The frame could be an SVG text frame, in which case we don't treat it + // as a text frame. + if (frame->IsTextFrame()) { + const uint32_t startOffset = aRange.StartOffset(); + const uint32_t endOffset = endNode == startContent + ? aRange.EndOffset() + : startContent->Length(); + static_cast(frame)->SelectionStateChanged( + startOffset, endOffset, aSelect, mSelectionType); + } else { + frame->SelectionStateChanged(); + } + } + } + + // If the range is in a node and the node is a leaf node, we don't need to + // walk the subtree. + if (aRange.Collapsed() || + (startContent == endNode && !startContent->HasChildren())) { + if (!isFirstContentTextNode) { + SelectFramesOf(startContent, aSelect); + } + return NS_OK; + } + + ContentSubtreeIterator subtreeIter; + subtreeIter.Init(&aRange); + if (isFirstContentTextNode && !subtreeIter.IsDone() && + subtreeIter.GetCurrentNode() == startContent) { + subtreeIter.Next(); // first content has already been handled. + } + PostContentIterator postOrderIter; + for (; !subtreeIter.IsDone(); subtreeIter.Next()) { + MOZ_DIAGNOSTIC_ASSERT(subtreeIter.GetCurrentNode()); + if (nsIContent* const content = + nsIContent::FromNodeOrNull(subtreeIter.GetCurrentNode())) { + SelectFramesOfInclusiveDescendantsOfContent(postOrderIter, content, + aSelect); + } + } + + // We must now do the last one if it is not the same as the first + if (endNode == startContent || !endNode->IsText()) { + return NS_OK; + } + + if (nsIFrame* const frame = endNode->AsText()->GetPrimaryFrame()) { + // The frame could be an SVG text frame, in which case we'll ignore it. + if (frame->IsTextFrame()) { + static_cast(frame)->SelectionStateChanged( + 0, aRange.EndOffset(), aSelect, mSelectionType); + } + } + return NS_OK; +} + +// Selection::LookUpSelection +// +// This function is called when a node wants to know where the selection is +// over itself. +// +// Usually, this is called when we already know there is a selection over +// the node in question, and we only need to find the boundaries of it on +// that node. This is when slowCheck is false--a strict test is not needed. +// Other times, the caller has no idea, and wants us to test everything, +// so we are supposed to determine whether there is a selection over the +// node at all. +// +// A previous version of this code used this flag to do less work when +// inclusion was already known (slowCheck=false). However, our tree +// structure allows us to quickly determine ranges overlapping the node, +// so we just ignore the slowCheck flag and do the full test every time. +// +// PERFORMANCE: a common case is that we are doing a fast check with exactly +// one range in the selection. In this case, this function is slower than +// brute force because of the overhead of checking the tree. We can optimize +// this case to make it faster by doing the same thing the previous version +// of this function did in the case of 1 range. This would also mean that +// the aSlowCheck flag would have meaning again. + +UniquePtr Selection::LookUpSelection( + nsIContent* aContent, uint32_t aContentOffset, uint32_t aContentLength, + UniquePtr aDetailsHead, SelectionType aSelectionType, + bool aSlowCheck) { + if (!aContent) { + return aDetailsHead; + } + + // it is common to have no ranges, to optimize that + if (mStyledRanges.Length() == 0) { + return aDetailsHead; + } + + nsTArray overlappingRanges; + nsresult rv = GetAbstractRangesForIntervalArray( + aContent, aContentOffset, aContent, aContentOffset + aContentLength, + false, &overlappingRanges); + if (NS_FAILED(rv)) { + return aDetailsHead; + } + + if (overlappingRanges.Length() == 0) { + return aDetailsHead; + } + + UniquePtr detailsHead = std::move(aDetailsHead); + + for (size_t i = 0; i < overlappingRanges.Length(); i++) { + AbstractRange* range = overlappingRanges[i]; + if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) { + continue; + } + nsINode* startNode = range->GetStartContainer(); + nsINode* endNode = range->GetEndContainer(); + uint32_t startOffset = range->StartOffset(); + uint32_t endOffset = range->EndOffset(); + + Maybe start, end; + if (startNode == aContent && endNode == aContent) { + if (startOffset < (aContentOffset + aContentLength) && + endOffset > aContentOffset) { + // this range is totally inside the requested content range + start.emplace( + startOffset >= aContentOffset ? startOffset - aContentOffset : 0u); + end.emplace(std::min(aContentLength, endOffset - aContentOffset)); + } + // otherwise, range is inside the requested node, but does not intersect + // the requested content range, so ignore it + } else if (startNode == aContent) { + if (startOffset < (aContentOffset + aContentLength)) { + // the beginning of the range is inside the requested node, but the + // end is outside, select everything from there to the end + start.emplace( + startOffset >= aContentOffset ? startOffset - aContentOffset : 0u); + end.emplace(aContentLength); + } + } else if (endNode == aContent) { + if (endOffset > aContentOffset) { + // the end of the range is inside the requested node, but the beginning + // is outside, select everything from the beginning to there + start.emplace(0u); + end.emplace(std::min(aContentLength, endOffset - aContentOffset)); + } + } else { + // this range does not begin or end in the requested node, but since + // GetRangesForInterval returned this range, we know it overlaps. + // Therefore, this node is enclosed in the range, and we select all + // of it. + start.emplace(0u); + end.emplace(aContentLength); + } + if (start.isNothing()) { + continue; // the ranges do not overlap the input range + } + + auto newHead = MakeUnique(); + + newHead->mNext = std::move(detailsHead); + newHead->mStart = AssertedCast(*start); + newHead->mEnd = AssertedCast(*end); + newHead->mSelectionType = aSelectionType; + newHead->mHighlightName = mHighlightName; + StyledRange* rd = mStyledRanges.FindRangeData(range); + if (rd) { + newHead->mTextRangeStyle = rd->mTextRangeStyle; + } + detailsHead = std::move(newHead); + } + return detailsHead; +} + +NS_IMETHODIMP +Selection::Repaint(nsPresContext* aPresContext) { + int32_t arrCount = (int32_t)mStyledRanges.Length(); + + if (arrCount < 1) return NS_OK; + + int32_t i; + + for (i = 0; i < arrCount; i++) { + MOZ_ASSERT(mStyledRanges.mRanges[i].mRange); + nsresult rv = + SelectFrames(aPresContext, *mStyledRanges.mRanges[i].mRange, true); + + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +void Selection::SetCanCacheFrameOffset(bool aCanCacheFrameOffset) { + if (!mCachedOffsetForFrame) { + mCachedOffsetForFrame = new CachedOffsetForFrame; + } + + mCachedOffsetForFrame->mCanCacheFrameOffset = aCanCacheFrameOffset; + + // clean up cached frame when turn off cache + // fix bug 207936 + if (!aCanCacheFrameOffset) { + mCachedOffsetForFrame->mLastCaretFrame = nullptr; + } +} + +nsresult Selection::GetCachedFrameOffset(nsIFrame* aFrame, int32_t inOffset, + nsPoint& aPoint) { + if (!mCachedOffsetForFrame) { + mCachedOffsetForFrame = new CachedOffsetForFrame; + } + + nsresult rv = NS_OK; + if (mCachedOffsetForFrame->mCanCacheFrameOffset && + mCachedOffsetForFrame->mLastCaretFrame && + (aFrame == mCachedOffsetForFrame->mLastCaretFrame) && + (inOffset == mCachedOffsetForFrame->mLastContentOffset)) { + // get cached frame offset + aPoint = mCachedOffsetForFrame->mCachedFrameOffset; + } else { + // Recalculate frame offset and cache it. Don't cache a frame offset if + // GetPointFromOffset fails, though. + rv = aFrame->GetPointFromOffset(inOffset, &aPoint); + if (NS_SUCCEEDED(rv) && mCachedOffsetForFrame->mCanCacheFrameOffset) { + mCachedOffsetForFrame->mCachedFrameOffset = aPoint; + mCachedOffsetForFrame->mLastCaretFrame = aFrame; + mCachedOffsetForFrame->mLastContentOffset = inOffset; + } + } + + return rv; +} + +nsIContent* Selection::GetAncestorLimiter() const { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (mFrameSelection) { + return mFrameSelection->GetAncestorLimiter(); + } + return nullptr; +} + +void Selection::SetAncestorLimiter(nsIContent* aLimiter) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aLimiter", aLimiter); + LogStackForSelectionAPI(); + } + + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (mFrameSelection) { + RefPtr frameSelection = mFrameSelection; + frameSelection->SetAncestorLimiter(aLimiter); + } +} + +void Selection::StyledRanges::UnregisterSelection() { + uint32_t count = mRanges.Length(); + for (uint32_t i = 0; i < count; ++i) { + mRanges[i].mRange->UnregisterSelection(mSelection); + } +} + +void Selection::StyledRanges::Clear() { mRanges.Clear(); } + +StyledRange* Selection::StyledRanges::FindRangeData(AbstractRange* aRange) { + NS_ENSURE_TRUE(aRange, nullptr); + for (uint32_t i = 0; i < mRanges.Length(); i++) { + if (mRanges[i].mRange == aRange) { + return &mRanges[i]; + } + } + return nullptr; +} + +Selection::StyledRanges::Elements::size_type Selection::StyledRanges::Length() + const { + return mRanges.Length(); +} + +nsresult Selection::SetTextRangeStyle(nsRange* aRange, + const TextRangeStyle& aTextRangeStyle) { + NS_ENSURE_ARG_POINTER(aRange); + StyledRange* rd = mStyledRanges.FindRangeData(aRange); + if (rd) { + rd->mTextRangeStyle = aTextRangeStyle; + } + return NS_OK; +} + +nsresult Selection::StartAutoScrollTimer(nsIFrame* aFrame, + const nsPoint& aPoint, + uint32_t aDelayInMs) { + MOZ_ASSERT(aFrame, "Need a frame"); + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (!mFrameSelection) { + return NS_OK; // nothing to do + } + + if (!mAutoScroller) { + mAutoScroller = new AutoScroller(mFrameSelection); + } + + mAutoScroller->SetDelay(aDelayInMs); + + RefPtr autoScroller{mAutoScroller}; + return autoScroller->DoAutoScroll(aFrame, aPoint); +} + +nsresult Selection::StopAutoScrollTimer() { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (mAutoScroller) { + mAutoScroller->Stop(AutoScroller::FurtherScrollingAllowed::kYes); + } + + return NS_OK; +} + +nsresult AutoScroller::DoAutoScroll(nsIFrame* aFrame, nsPoint aPoint) { + MOZ_ASSERT(aFrame, "Need a frame"); + + Stop(FurtherScrollingAllowed::kYes); + + nsPresContext* presContext = aFrame->PresContext(); + RefPtr presShell = presContext->PresShell(); + nsRootPresContext* rootPC = presContext->GetRootPresContext(); + if (!rootPC) { + return NS_OK; + } + nsIFrame* rootmostFrame = rootPC->PresShell()->GetRootFrame(); + AutoWeakFrame weakRootFrame(rootmostFrame); + AutoWeakFrame weakFrame(aFrame); + // Get the point relative to the root most frame because the scroll we are + // about to do will change the coordinates of aFrame. + nsPoint globalPoint = aPoint + aFrame->GetOffsetToCrossDoc(rootmostFrame); + + bool done = false; + bool didScroll; + while (true) { + didScroll = presShell->ScrollFrameIntoView( + aFrame, Some(nsRect(aPoint, nsSize())), ScrollAxis(), ScrollAxis(), + ScrollFlags::None); + if (!weakFrame || !weakRootFrame) { + return NS_OK; + } + if (!didScroll && !done) { + // If aPoint is at the very edge of the root, then try to scroll anyway, + // once. + nsRect rootRect = rootmostFrame->GetRect(); + nscoord onePx = AppUnitsPerCSSPixel(); + nscoord scrollAmount = 10 * onePx; + if (std::abs(rootRect.x - globalPoint.x) <= onePx) { + aPoint.x -= scrollAmount; + } else if (std::abs(rootRect.XMost() - globalPoint.x) <= onePx) { + aPoint.x += scrollAmount; + } else if (std::abs(rootRect.y - globalPoint.y) <= onePx) { + aPoint.y -= scrollAmount; + } else if (std::abs(rootRect.YMost() - globalPoint.y) <= onePx) { + aPoint.y += scrollAmount; + } else { + break; + } + done = true; + continue; + } + break; + } + + // Start the AutoScroll timer if necessary. + // `ScrollFrameRectIntoView` above may have run script and this may have + // forbidden to continue scrolling. + if (didScroll && mFurtherScrollingAllowed == FurtherScrollingAllowed::kYes) { + nsPoint presContextPoint = + globalPoint - + presShell->GetRootFrame()->GetOffsetToCrossDoc(rootmostFrame); + ScheduleNextDoAutoScroll(presContext, presContextPoint); + } + + return NS_OK; +} + +void Selection::RemoveAllRanges(ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + RemoveAllRangesInternal(aRv); +} + +void Selection::RemoveAllRangesInternal(ErrorResult& aRv) { + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + RefPtr presContext = GetPresContext(); + Clear(presContext); + + // Turn off signal for table selection + RefPtr frameSelection = mFrameSelection; + frameSelection->ClearTableCellSelection(); + + RefPtr kungFuDeathGrip{this}; + // Be aware, this instance may be destroyed after this call. + NotifySelectionListeners(); +} + +void Selection::AddRangeJS(nsRange& aRange, ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aRange", aRange); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + RefPtr document(GetDocument()); + AddRangeAndSelectFramesAndNotifyListenersInternal(aRange, document, aRv); +} + +void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aRange", aRange); + LogStackForSelectionAPI(); + } + + RefPtr document(GetDocument()); + return AddRangeAndSelectFramesAndNotifyListenersInternal(aRange, document, + aRv); +} + +void Selection::AddRangeAndSelectFramesAndNotifyListenersInternal( + nsRange& aRange, Document* aDocument, ErrorResult& aRv) { + RefPtr range = &aRange; + if (aRange.IsInAnySelection()) { + if (aRange.IsInSelection(*this)) { + // If we already have the range, we don't need to handle this except + // setting the interline position. + if (mSelectionType == SelectionType::eNormal) { + SetInterlinePosition(InterlinePosition::StartOfNextLine); + } + return; + } + if (mSelectionType != SelectionType::eNormal && + mSelectionType != SelectionType::eHighlight) { + range = aRange.CloneRange(); + } + } + + nsINode* rangeRoot = range->GetRoot(); + if (aDocument != rangeRoot && + (!rangeRoot || aDocument != rangeRoot->GetComposedDoc())) { + // http://w3c.github.io/selection-api/#dom-selection-addrange + // "... if the root of the range's boundary points are the document + // associated with context object. Otherwise, this method must do nothing." + return; + } + + // MaybeAddTableCellRange might flush frame and `NotifySelectionListeners` + // below might destruct `this`. + RefPtr kungFuDeathGrip(this); + + // This inserts a table cell range in proper document order + // and returns NS_OK if range doesn't contain just one table cell + Maybe maybeRangeIndex; + nsresult result = MaybeAddTableCellRange(*range, &maybeRangeIndex); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + + if (maybeRangeIndex.isNothing()) { + result = AddRangesForSelectableNodes(range, &maybeRangeIndex, + DispatchSelectstartEvent::Maybe); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + if (maybeRangeIndex.isNothing()) { + return; + } + } + + MOZ_ASSERT(*maybeRangeIndex < mStyledRanges.Length()); + + SetAnchorFocusRange(*maybeRangeIndex); + + // Make sure the caret appears on the next line, if at a newline + if (mSelectionType == SelectionType::eNormal) { + SetInterlinePosition(InterlinePosition::StartOfNextLine); + } + + if (!mFrameSelection) { + return; // nothing to do + } + + RefPtr presContext = GetPresContext(); + SelectFrames(presContext, *range, true); + + // Be aware, this instance may be destroyed after this call. + NotifySelectionListeners(); +} + +void Selection::AddHighlightRangeAndSelectFramesAndNotifyListeners( + AbstractRange& aRange) { + MOZ_ASSERT(mSelectionType == SelectionType::eHighlight); + + mStyledRanges.mRanges.AppendElement(StyledRange{&aRange}); + aRange.RegisterSelection(*this); + + if (!mFrameSelection) { + return; // nothing to do + } + + RefPtr presContext = GetPresContext(); + SelectFrames(presContext, aRange, true); + + // Be aware, this instance may be destroyed after this call. + NotifySelectionListeners(); +} + +// Selection::RemoveRangeAndUnselectFramesAndNotifyListeners +// +// Removes the given range from the selection. The tricky part is updating +// the flags on the frames that indicate whether they have a selection or +// not. There could be several selection ranges on the frame, and clearing +// the bit would cause the selection to not be drawn, even when there is +// another range on the frame (bug 346185). +// +// We therefore find any ranges that intersect the same nodes as the range +// being removed, and cause them to set the selected bits back on their +// selected frames after we've cleared the bit from ours. + +void Selection::RemoveRangeAndUnselectFramesAndNotifyListeners( + AbstractRange& aRange, ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aRange", aRange); + LogStackForSelectionAPI(); + } + + nsresult rv = mStyledRanges.RemoveRangeAndUnregisterSelection(aRange); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + nsINode* beginNode = aRange.GetStartContainer(); + nsINode* endNode = aRange.GetEndContainer(); + + if (!beginNode || !endNode) { + // Detached range; nothing else to do here. + return; + } + + // find out the length of the end node, so we can select all of it + uint32_t beginOffset, endOffset; + if (endNode->IsText()) { + // Get the length of the text. We can't just use the offset because + // another range could be touching this text node but not intersect our + // range. + beginOffset = 0; + endOffset = endNode->AsText()->TextLength(); + } else { + // For non-text nodes, the given offsets should be sufficient. + beginOffset = aRange.StartOffset(); + endOffset = aRange.EndOffset(); + } + + // clear the selected bit from the removed range's frames + RefPtr presContext = GetPresContext(); + SelectFrames(presContext, aRange, false); + + // add back the selected bit for each range touching our nodes + nsTArray affectedRanges; + rv = GetAbstractRangesForIntervalArray(beginNode, beginOffset, endNode, + endOffset, true, &affectedRanges); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + for (uint32_t i = 0; i < affectedRanges.Length(); i++) { + MOZ_ASSERT(affectedRanges[i]); + SelectFrames(presContext, *affectedRanges[i], true); + } + + if (&aRange == mAnchorFocusRange) { + const size_t rangeCount = mStyledRanges.Length(); + if (rangeCount) { + SetAnchorFocusRange(rangeCount - 1); + } else { + RemoveAnchorFocusRange(); + } + + // When the selection is user-created it makes sense to scroll the range + // into view. The spell-check selection, however, is created and destroyed + // in the background. We don't want to scroll in this case or the view + // might appear to be moving randomly (bug 337871). + if (mSelectionType != SelectionType::eSpellCheck && rangeCount) { + ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION); + } + } + + if (!mFrameSelection) return; // nothing to do + + RefPtr kungFuDeathGrip{this}; + // Be aware, this instance may be destroyed after this call. + NotifySelectionListeners(); +} + +/* + * Collapse sets the whole selection to be one point. + */ +void Selection::CollapseJS(nsINode* aContainer, uint32_t aOffset, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aContainer", aContainer, "aOffset", + aOffset); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + if (!aContainer) { + RemoveAllRangesInternal(aRv); + return; + } + CollapseInternal(InLimiter::eNo, RawRangeBoundary(aContainer, aOffset), aRv); +} + +void Selection::CollapseInLimiter(const RawRangeBoundary& aPoint, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aPoint", aPoint); + LogStackForSelectionAPI(); + } + + CollapseInternal(InLimiter::eYes, aPoint, aRv); +} + +void Selection::CollapseInternal(InLimiter aInLimiter, + const RawRangeBoundary& aPoint, + ErrorResult& aRv) { + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return; + } + + if (!aPoint.IsSet()) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (aPoint.Container()->NodeType() == nsINode::DOCUMENT_TYPE_NODE) { + aRv.ThrowInvalidNodeTypeError(kNoDocumentTypeNodeError); + return; + } + + // RawRangeBoundary::IsSetAndValid() checks if the point actually refers + // a child of the container when IsSet() is true. If its offset hasn't been + // computed yet, this just checks it with its mRef. So, we can avoid + // computing offset here. + if (!aPoint.IsSetAndValid()) { + aRv.ThrowIndexSizeError("The offset is out of range."); + return; + } + + if (!HasSameRootOrSameComposedDoc(*aPoint.Container())) { + // Return with no error + return; + } + + RefPtr frameSelection = mFrameSelection; + frameSelection->InvalidateDesiredCaretPos(); + if (aInLimiter == InLimiter::eYes && + !frameSelection->IsValidSelectionPoint(aPoint.Container())) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + nsresult result; + + RefPtr presContext = GetPresContext(); + if (!presContext || + presContext->Document() != aPoint.Container()->OwnerDoc()) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Delete all of the current ranges + Clear(presContext); + + // Turn off signal for table selection + frameSelection->ClearTableCellSelection(); + + // Hack to display the caret on the right line (bug 1237236). + if (frameSelection->GetHint() != CARET_ASSOCIATE_AFTER && + aPoint.Container()->IsContent()) { + int32_t frameOffset; + nsTextFrame* f = do_QueryFrame(nsCaret::GetFrameAndOffset( + this, aPoint.Container(), + *aPoint.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets), + &frameOffset)); + if (f && f->IsAtEndOfLine() && f->HasSignificantTerminalNewline()) { + // RawRangeBounary::Offset() causes computing offset if it's not been + // done yet. However, it's called only when the container is a text + // node. In such case, offset has always been set since it cannot have + // any children. So, this doesn't cause computing offset with expensive + // method, nsINode::ComputeIndexOf(). + if ((aPoint.Container()->AsContent() == f->GetContent() && + f->GetContentEnd() == + static_cast(*aPoint.Offset( + RawRangeBoundary::OffsetFilter::kValidOffsets))) || + (aPoint.Container() == f->GetContent()->GetParentNode() && + f->GetContent() == aPoint.GetPreviousSiblingOfChildAtOffset())) { + frameSelection->SetHint(CARET_ASSOCIATE_AFTER); + } + } + } + + RefPtr range = nsRange::Create(aPoint.Container()); + result = range->CollapseTo(aPoint); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + +#ifdef DEBUG_SELECTION + nsCOMPtr content = do_QueryInterface(aPoint.Container()); + nsCOMPtr doc = do_QueryInterface(aPoint.Container()); + printf("Sel. Collapse to %p %s %d\n", container.get(), + content ? nsAtomCString(content->NodeInfo()->NameAtom()).get() + : (doc ? "DOCUMENT" : "???"), + aPoint.Offset()); +#endif + + Maybe maybeRangeIndex; + result = AddRangesForSelectableNodes(range, &maybeRangeIndex, + DispatchSelectstartEvent::Maybe); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + SetAnchorFocusRange(0); + SelectFrames(presContext, *range, true); + + RefPtr kungFuDeathGrip{this}; + // Be aware, this instance may be destroyed after this call. + NotifySelectionListeners(); +} + +/* + * Sets the whole selection to be one point + * at the start of the current selection + */ +void Selection::CollapseToStartJS(ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + CollapseToStart(aRv); +} + +void Selection::CollapseToStart(ErrorResult& aRv) { + if (!mCalledByJS && NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + if (RangeCount() == 0) { + aRv.ThrowInvalidStateError(kNoRangeExistsError); + return; + } + + // Get the first range + const AbstractRange* firstRange = mStyledRanges.mRanges[0].mRange; + if (!firstRange) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mFrameSelection) { + mFrameSelection->AddChangeReasons( + nsISelectionListener::COLLAPSETOSTART_REASON); + } + nsINode* container = firstRange->GetStartContainer(); + if (!container) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + CollapseInternal(InLimiter::eNo, + RawRangeBoundary(container, firstRange->StartOffset()), aRv); +} + +/* + * Sets the whole selection to be one point + * at the end of the current selection + */ +void Selection::CollapseToEndJS(ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + CollapseToEnd(aRv); +} + +void Selection::CollapseToEnd(ErrorResult& aRv) { + if (!mCalledByJS && NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + uint32_t cnt = RangeCount(); + if (cnt == 0) { + aRv.ThrowInvalidStateError(kNoRangeExistsError); + return; + } + + // Get the last range + const AbstractRange* lastRange = mStyledRanges.mRanges[cnt - 1].mRange; + if (!lastRange) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mFrameSelection) { + mFrameSelection->AddChangeReasons( + nsISelectionListener::COLLAPSETOEND_REASON); + } + nsINode* container = lastRange->GetEndContainer(); + if (!container) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + CollapseInternal(InLimiter::eNo, + RawRangeBoundary(container, lastRange->EndOffset()), aRv); +} + +void Selection::GetType(nsAString& aOutType) const { + if (!RangeCount()) { + aOutType.AssignLiteral("None"); + } else if (IsCollapsed()) { + aOutType.AssignLiteral("Caret"); + } else { + aOutType.AssignLiteral("Range"); + } +} + +nsRange* Selection::GetRangeAt(uint32_t aIndex, ErrorResult& aRv) { + nsRange* range = GetRangeAt(aIndex); + if (!range) { + aRv.ThrowIndexSizeError(nsPrintfCString("%u is out of range", aIndex)); + return nullptr; + } + + return range; +} + +AbstractRange* Selection::GetAbstractRangeAt(uint32_t aIndex) const { + StyledRange empty(nullptr); + return mStyledRanges.mRanges.SafeElementAt(aIndex, empty).mRange; +} + +nsRange* Selection::GetRangeAt(uint32_t aIndex) const { + // This method per IDL spec returns a dynamic range. + // Therefore, it must be ensured that it is only called + // for a selection which contains dynamic ranges exclusively. + // Highlight Selections are allowed to contain StaticRanges, + // therefore this method must not be called. + MOZ_ASSERT(mSelectionType != SelectionType::eHighlight); + AbstractRange* abstractRange = GetAbstractRangeAt(aIndex); + if (!abstractRange) { + return nullptr; + } + return abstractRange->AsDynamicRange(); +} + +nsresult Selection::SetAnchorFocusToRange(nsRange* aRange) { + NS_ENSURE_STATE(mAnchorFocusRange); + + const DispatchSelectstartEvent dispatchSelectstartEvent = + IsCollapsed() ? DispatchSelectstartEvent::Maybe + : DispatchSelectstartEvent::No; + + nsresult rv = + mStyledRanges.RemoveRangeAndUnregisterSelection(*mAnchorFocusRange); + if (NS_FAILED(rv)) { + return rv; + } + + Maybe maybeOutIndex; + rv = AddRangesForSelectableNodes(aRange, &maybeOutIndex, + dispatchSelectstartEvent); + if (NS_FAILED(rv)) { + return rv; + } + if (maybeOutIndex.isSome()) { + SetAnchorFocusRange(*maybeOutIndex); + } else { + RemoveAnchorFocusRange(); + } + + return NS_OK; +} + +void Selection::ReplaceAnchorFocusRange(nsRange* aRange) { + NS_ENSURE_TRUE_VOID(mAnchorFocusRange); + RefPtr presContext = GetPresContext(); + if (presContext) { + SelectFrames(presContext, *mAnchorFocusRange, false); + SetAnchorFocusToRange(aRange); + SelectFrames(presContext, *mAnchorFocusRange, true); + } +} + +void Selection::AdjustAnchorFocusForMultiRange(nsDirection aDirection) { + if (aDirection == mDirection) { + return; + } + SetDirection(aDirection); + + if (RangeCount() <= 1) { + return; + } + + nsRange* firstRange = GetRangeAt(0); + nsRange* lastRange = GetRangeAt(RangeCount() - 1); + + if (mDirection == eDirPrevious) { + firstRange->SetIsGenerated(false); + lastRange->SetIsGenerated(true); + SetAnchorFocusRange(0); + } else { // aDir == eDirNext + firstRange->SetIsGenerated(true); + lastRange->SetIsGenerated(false); + SetAnchorFocusRange(RangeCount() - 1); + } +} + +/* + * Extend extends the selection away from the anchor. + * We don't need to know the direction, because we always change the focus. + */ +void Selection::ExtendJS(nsINode& aContainer, uint32_t aOffset, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aContainer", &aContainer, "aOffset", + aOffset); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + Extend(aContainer, aOffset, aRv); +} + +nsresult Selection::Extend(nsINode* aContainer, uint32_t aOffset) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aContainer", aContainer, "aOffset", + aOffset); + LogStackForSelectionAPI(); + } + + if (!aContainer) { + return NS_ERROR_INVALID_ARG; + } + + ErrorResult result; + Extend(*aContainer, aOffset, result); + return result.StealNSResult(); +} + +void Selection::Extend(nsINode& aContainer, uint32_t aOffset, + ErrorResult& aRv) { + /* + Notes which might come in handy for extend: + + We can tell the direction of the selection by asking for the anchors + selection if the begin is less than the end then we know the selection is to + the "right", else it is a backwards selection. Notation: a = anchor, 1 = old + cursor, 2 = new cursor. + + if (a <= 1 && 1 <=2) a,1,2 or (a1,2) + if (a < 2 && 1 > 2) a,2,1 + if (1 < a && a <2) 1,a,2 + if (a > 2 && 2 >1) 1,2,a + if (2 < a && a <1) 2,a,1 + if (a > 1 && 1 >2) 2,1,a + then execute + a 1 2 select from 1 to 2 + a 2 1 deselect from 2 to 1 + 1 a 2 deselect from 1 to a select from a to 2 + 1 2 a deselect from 1 to 2 + 2 1 a = continue selection from 2 to 1 + */ + + // First, find the range containing the old focus point: + if (!mAnchorFocusRange) { + aRv.ThrowInvalidStateError(kNoRangeExistsError); + return; + } + + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return; + } + + if (!HasSameRootOrSameComposedDoc(aContainer)) { + // Return with no error + return; + } + + nsresult res; + if (!mFrameSelection->IsValidSelectionPoint(&aContainer)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr presContext = GetPresContext(); + if (!presContext || presContext->Document() != aContainer.OwnerDoc()) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + +#ifdef DEBUG_SELECTION + nsDirection oldDirection = GetDirection(); +#endif + nsINode* anchorNode = GetAnchorNode(); + nsINode* focusNode = GetFocusNode(); + const uint32_t anchorOffset = AnchorOffset(); + const uint32_t focusOffset = FocusOffset(); + + RefPtr range = mAnchorFocusRange->CloneRange(); + + nsINode* startNode = range->GetStartContainer(); + nsINode* endNode = range->GetEndContainer(); + const uint32_t startOffset = range->StartOffset(); + const uint32_t endOffset = range->EndOffset(); + + bool shouldClearRange = false; + const Maybe anchorOldFocusOrder = nsContentUtils::ComparePoints( + anchorNode, anchorOffset, focusNode, focusOffset); + shouldClearRange |= !anchorOldFocusOrder; + const Maybe oldFocusNewFocusOrder = nsContentUtils::ComparePoints( + focusNode, focusOffset, &aContainer, aOffset); + shouldClearRange |= !oldFocusNewFocusOrder; + const Maybe anchorNewFocusOrder = nsContentUtils::ComparePoints( + anchorNode, anchorOffset, &aContainer, aOffset); + shouldClearRange |= !anchorNewFocusOrder; + + // If the points are disconnected, the range will be collapsed below, + // resulting in a range that selects nothing. + if (shouldClearRange) { + // Repaint the current range with the selection removed. + SelectFrames(presContext, *range, false); + + res = range->CollapseTo(&aContainer, aOffset); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } else { + RefPtr difRange = nsRange::Create(&aContainer); + if ((*anchorOldFocusOrder == 0 && *anchorNewFocusOrder < 0) || + (*anchorOldFocusOrder <= 0 && + *oldFocusNewFocusOrder < 0)) { // a1,2 a,1,2 + // select from 1 to 2 unless they are collapsed + range->SetEnd(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + SetDirection(eDirNext); + res = difRange->SetStartAndEnd( + focusNode, focusOffset, range->GetEndContainer(), range->EndOffset()); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + SelectFrames(presContext, *difRange, true); + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } else if (*anchorOldFocusOrder == 0 && + *anchorNewFocusOrder > 0) { // 2, a1 + // select from 2 to 1a + SetDirection(eDirPrevious); + range->SetStart(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + SelectFrames(presContext, *range, true); + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } else if (*anchorNewFocusOrder <= 0 && + *oldFocusNewFocusOrder >= 0) { // a,2,1 or a2,1 or a,21 or a21 + // deselect from 2 to 1 + res = difRange->SetStartAndEnd(&aContainer, aOffset, focusNode, + focusOffset); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + + range->SetEnd(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + SelectFrames(presContext, *difRange, false); // deselect now + difRange->SetEnd(range->GetEndContainer(), range->EndOffset()); + SelectFrames(presContext, *difRange, true); // must reselect last node + // maybe more + } else if (*anchorOldFocusOrder >= 0 && + *anchorNewFocusOrder <= 0) { // 1,a,2 or 1a,2 or 1,a2 or 1a2 + if (GetDirection() == eDirPrevious) { + res = range->SetStart(endNode, endOffset); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + SetDirection(eDirNext); + range->SetEnd(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + if (focusNode != anchorNode || + focusOffset != anchorOffset) { // if collapsed diff dont do anything + res = difRange->SetStart(focusNode, focusOffset); + nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset); + if (NS_FAILED(tmp)) { + res = tmp; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + // deselect from 1 to a + SelectFrames(presContext, *difRange, false); + } else { + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + // select from a to 2 + SelectFrames(presContext, *range, true); + } else if (*oldFocusNewFocusOrder <= 0 && + *anchorNewFocusOrder >= 0) { // 1,2,a or 12,a or 1,2a or 12a + // deselect from 1 to 2 + res = difRange->SetStartAndEnd(focusNode, focusOffset, &aContainer, + aOffset); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + SetDirection(eDirPrevious); + range->SetStart(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + SelectFrames(presContext, *difRange, false); + difRange->SetStart(range->GetStartContainer(), range->StartOffset()); + SelectFrames(presContext, *difRange, true); // must reselect last node + } else if (*anchorNewFocusOrder >= 0 && + *anchorOldFocusOrder <= 0) { // 2,a,1 or 2a,1 or 2,a1 or 2a1 + if (GetDirection() == eDirNext) { + range->SetEnd(startNode, startOffset); + } + SetDirection(eDirPrevious); + range->SetStart(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + // deselect from a to 1 + if (focusNode != anchorNode || + focusOffset != anchorOffset) { // if collapsed diff dont do anything + res = difRange->SetStartAndEnd(anchorNode, anchorOffset, focusNode, + focusOffset); + nsresult tmp = SetAnchorFocusToRange(range); + if (NS_FAILED(tmp)) { + res = tmp; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + SelectFrames(presContext, *difRange, false); + } else { + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + // select from 2 to a + SelectFrames(presContext, *range, true); + } else if (*oldFocusNewFocusOrder >= 0 && + *anchorOldFocusOrder >= 0) { // 2,1,a or 21,a or 2,1a or 21a + // select from 2 to 1 + range->SetStart(aContainer, aOffset, aRv); + if (aRv.Failed()) { + return; + } + SetDirection(eDirPrevious); + res = difRange->SetStartAndEnd(range->GetStartContainer(), + range->StartOffset(), focusNode, + focusOffset); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + + SelectFrames(presContext, *difRange, true); + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + } + + if (mStyledRanges.Length() > 1) { + SelectFramesInAllRanges(presContext); + } + + DEBUG_OUT_RANGE(range); +#ifdef DEBUG_SELECTION + if (GetDirection() != oldDirection) { + printf(" direction changed to %s\n", + GetDirection() == eDirNext ? "eDirNext" : "eDirPrevious"); + } + nsCOMPtr content = do_QueryInterface(&aContainer); + printf("Sel. Extend to %p %s %d\n", content.get(), + nsAtomCString(content->NodeInfo()->NameAtom()).get(), aOffset); +#endif + + RefPtr kungFuDeathGrip{this}; + // Be aware, this instance may be destroyed after this call. + NotifySelectionListeners(); +} + +void Selection::SelectAllChildrenJS(nsINode& aNode, ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aNode", &aNode); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + SelectAllChildren(aNode, aRv); +} + +void Selection::SelectAllChildren(nsINode& aNode, ErrorResult& aRv) { + if (!mCalledByJS && NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aNode", &aNode); + LogStackForSelectionAPI(); + } + + if (aNode.NodeType() == nsINode::DOCUMENT_TYPE_NODE) { + aRv.ThrowInvalidNodeTypeError(kNoDocumentTypeNodeError); + return; + } + + if (!HasSameRootOrSameComposedDoc(aNode)) { + // Return with no error + return; + } + + if (mFrameSelection) { + mFrameSelection->AddChangeReasons(nsISelectionListener::SELECTALL_REASON); + } + + // Chrome moves focus when aNode is outside of active editing host. + // So, we don't need to respect the limiter with this method. + SetStartAndEndInternal(InLimiter::eNo, RawRangeBoundary(&aNode, 0u), + RawRangeBoundary(&aNode, aNode.GetChildCount()), + eDirNext, aRv); +} + +bool Selection::ContainsNode(nsINode& aNode, bool aAllowPartial, + ErrorResult& aRv) { + nsresult rv; + if (mStyledRanges.Length() == 0) { + return false; + } + + // XXXbz this duplicates the GetNodeLength code in nsRange.cpp + uint32_t nodeLength; + auto* nodeAsCharData = CharacterData::FromNode(aNode); + if (nodeAsCharData) { + nodeLength = nodeAsCharData->TextLength(); + } else { + nodeLength = aNode.GetChildCount(); + } + + nsTArray overlappingRanges; + rv = GetAbstractRangesForIntervalArray(&aNode, 0, &aNode, nodeLength, false, + &overlappingRanges); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + if (overlappingRanges.Length() == 0) return false; // no ranges overlap + + // if the caller said partial intersections are OK, we're done + if (aAllowPartial) { + return true; + } + + // text nodes always count as inside + if (nodeAsCharData) { + return true; + } + + // The caller wants to know if the node is entirely within the given range, + // so we have to check all intersecting ranges. + for (uint32_t i = 0; i < overlappingRanges.Length(); i++) { + bool nodeStartsBeforeRange, nodeEndsAfterRange; + if (NS_SUCCEEDED(RangeUtils::CompareNodeToRange( + &aNode, overlappingRanges[i], &nodeStartsBeforeRange, + &nodeEndsAfterRange))) { + if (!nodeStartsBeforeRange && !nodeEndsAfterRange) { + return true; + } + } + } + return false; +} + +class PointInRectChecker : public mozilla::RectCallback { + public: + explicit PointInRectChecker(const nsPoint& aPoint) + : mPoint(aPoint), mMatchFound(false) {} + + void AddRect(const nsRect& aRect) override { + mMatchFound = mMatchFound || aRect.Contains(mPoint); + } + + bool MatchFound() { return mMatchFound; } + + private: + nsPoint mPoint; + bool mMatchFound; +}; + +bool Selection::ContainsPoint(const nsPoint& aPoint) { + if (IsCollapsed()) { + return false; + } + PointInRectChecker checker(aPoint); + const uint32_t rangeCount = RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(RangeCount() == rangeCount); + nsRange* range = GetRangeAt(i); + MOZ_ASSERT(range); + nsRange::CollectClientRectsAndText( + &checker, nullptr, range, range->GetStartContainer(), + range->StartOffset(), range->GetEndContainer(), range->EndOffset(), + true, false); + if (checker.MatchFound()) { + return true; + } + } + return false; +} + +void Selection::MaybeNotifyAccessibleCaretEventHub(PresShell* aPresShell) { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + if (!mAccessibleCaretEventHub && aPresShell) { + mAccessibleCaretEventHub = aPresShell->GetAccessibleCaretEventHub(); + } +} + +void Selection::StopNotifyingAccessibleCaretEventHub() { + MOZ_ASSERT(mSelectionType == SelectionType::eNormal); + + mAccessibleCaretEventHub = nullptr; +} + +nsPresContext* Selection::GetPresContext() const { + PresShell* presShell = GetPresShell(); + return presShell ? presShell->GetPresContext() : nullptr; +} + +PresShell* Selection::GetPresShell() const { + if (!mFrameSelection) { + return nullptr; // nothing to do + } + return mFrameSelection->GetPresShell(); +} + +Document* Selection::GetDocument() const { + PresShell* presShell = GetPresShell(); + return presShell ? presShell->GetDocument() : nullptr; +} + +nsIFrame* Selection::GetSelectionAnchorGeometry(SelectionRegion aRegion, + nsRect* aRect) { + if (!mFrameSelection) return nullptr; // nothing to do + + NS_ENSURE_TRUE(aRect, nullptr); + + aRect->SetRect(0, 0, 0, 0); + + switch (aRegion) { + case nsISelectionController::SELECTION_ANCHOR_REGION: + case nsISelectionController::SELECTION_FOCUS_REGION: + return GetSelectionEndPointGeometry(aRegion, aRect); + case nsISelectionController::SELECTION_WHOLE_SELECTION: + break; + default: + return nullptr; + } + + NS_ASSERTION(aRegion == nsISelectionController::SELECTION_WHOLE_SELECTION, + "should only be SELECTION_WHOLE_SELECTION here"); + + nsRect anchorRect; + nsIFrame* anchorFrame = GetSelectionEndPointGeometry( + nsISelectionController::SELECTION_ANCHOR_REGION, &anchorRect); + if (!anchorFrame) return nullptr; + + nsRect focusRect; + nsIFrame* focusFrame = GetSelectionEndPointGeometry( + nsISelectionController::SELECTION_FOCUS_REGION, &focusRect); + if (!focusFrame) return nullptr; + + NS_ASSERTION(anchorFrame->PresContext() == focusFrame->PresContext(), + "points of selection in different documents?"); + // make focusRect relative to anchorFrame + focusRect += focusFrame->GetOffsetTo(anchorFrame); + + *aRect = anchorRect.UnionEdges(focusRect); + return anchorFrame; +} + +nsIFrame* Selection::GetSelectionEndPointGeometry(SelectionRegion aRegion, + nsRect* aRect) { + if (!mFrameSelection) return nullptr; // nothing to do + + NS_ENSURE_TRUE(aRect, nullptr); + + aRect->SetRect(0, 0, 0, 0); + + nsINode* node = nullptr; + uint32_t nodeOffset = 0; + nsIFrame* frame = nullptr; + + switch (aRegion) { + case nsISelectionController::SELECTION_ANCHOR_REGION: + node = GetAnchorNode(); + nodeOffset = AnchorOffset(); + break; + case nsISelectionController::SELECTION_FOCUS_REGION: + node = GetFocusNode(); + nodeOffset = FocusOffset(); + break; + default: + return nullptr; + } + + if (!node) return nullptr; + + nsCOMPtr content = do_QueryInterface(node); + NS_ENSURE_TRUE(content.get(), nullptr); + int32_t frameOffset = 0; + frame = nsFrameSelection::GetFrameForNodeOffset( + content, nodeOffset, mFrameSelection->GetHint(), &frameOffset); + if (!frame) return nullptr; + + nsFrameSelection::AdjustFrameForLineStart(frame, frameOffset); + + // Figure out what node type we have, then get the + // appropriate rect for it's nodeOffset. + bool isText = node->IsText(); + + nsPoint pt(0, 0); + if (isText) { + nsIFrame* childFrame = nullptr; + frameOffset = 0; + nsresult rv = frame->GetChildFrameContainingOffset( + nodeOffset, mFrameSelection->GetHint(), &frameOffset, &childFrame); + if (NS_FAILED(rv)) return nullptr; + if (!childFrame) return nullptr; + + frame = childFrame; + + // Get the x coordinate of the offset into the text frame. + rv = GetCachedFrameOffset(frame, nodeOffset, pt); + if (NS_FAILED(rv)) return nullptr; + } + + // Return the rect relative to the frame, with zero width. + if (isText) { + aRect->x = pt.x; + } else if (mFrameSelection->GetHint() == CARET_ASSOCIATE_BEFORE) { + // It's the frame's right edge we're interested in. + aRect->x = frame->GetRect().Width(); + } + aRect->SetHeight(frame->GetRect().Height()); + + return frame; +} + +NS_IMETHODIMP +Selection::ScrollSelectionIntoViewEvent::Run() { + if (!mSelection) return NS_OK; // event revoked + + int32_t flags = Selection::SCROLL_DO_FLUSH | Selection::SCROLL_SYNCHRONOUS; + + const RefPtr selection{mSelection}; + selection->mScrollEvent.Forget(); + selection->ScrollIntoView(mRegion, mVerticalScroll, mHorizontalScroll, + mFlags | flags); + return NS_OK; +} + +nsresult Selection::PostScrollSelectionIntoViewEvent(SelectionRegion aRegion, + int32_t aFlags, + ScrollAxis aVertical, + ScrollAxis aHorizontal) { + // If we've already posted an event, revoke it and place a new one at the + // end of the queue to make sure that any new pending reflow events are + // processed before we scroll. This will insure that we scroll to the + // correct place on screen. + mScrollEvent.Revoke(); + nsPresContext* presContext = GetPresContext(); + NS_ENSURE_STATE(presContext); + nsRefreshDriver* refreshDriver = presContext->RefreshDriver(); + NS_ENSURE_STATE(refreshDriver); + + mScrollEvent = new ScrollSelectionIntoViewEvent(this, aRegion, aVertical, + aHorizontal, aFlags); + refreshDriver->AddEarlyRunner(mScrollEvent.get()); + return NS_OK; +} + +void Selection::ScrollIntoView(int16_t aRegion, bool aIsSynchronous, + int16_t aVPercent, int16_t aHPercent, + ErrorResult& aRv) { + int32_t flags = aIsSynchronous ? Selection::SCROLL_SYNCHRONOUS : 0; + // -1 means nearest in this API. + const auto v = + aVPercent == -1 ? WhereToScroll::Nearest : WhereToScroll(aVPercent); + const auto h = + aHPercent == -1 ? WhereToScroll::Nearest : WhereToScroll(aHPercent); + nsresult rv = ScrollIntoView(aRegion, ScrollAxis(v), ScrollAxis(h), flags); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +nsresult Selection::ScrollIntoView(SelectionRegion aRegion, + ScrollAxis aVertical, ScrollAxis aHorizontal, + int32_t aFlags) { + if (!mFrameSelection) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr presShell = mFrameSelection->GetPresShell(); + if (!presShell || !presShell->GetDocument()) { + return NS_OK; + } + + if (mFrameSelection->IsBatching()) { + return NS_OK; + } + + if (!(aFlags & Selection::SCROLL_SYNCHRONOUS)) + return PostScrollSelectionIntoViewEvent(aRegion, aFlags, aVertical, + aHorizontal); + + // From this point on, the presShell may get destroyed by the calls below, so + // hold on to it using a strong reference to ensure the safety of the + // accesses to frame pointers in the callees. + RefPtr kungFuDeathGrip(presShell); + + // Now that text frame character offsets are always valid (though not + // necessarily correct), the worst that will happen if we don't flush here + // is that some callers might scroll to the wrong place. Those should + // either manually flush if they're in a safe position for it or use the + // async version of this method. + if (aFlags & Selection::SCROLL_DO_FLUSH) { + presShell->GetDocument()->FlushPendingNotifications(FlushType::Layout); + + // Reget the presshell, since it might have been Destroy'ed. + presShell = mFrameSelection ? mFrameSelection->GetPresShell() : nullptr; + if (!presShell) { + return NS_OK; + } + } + + // + // Scroll the selection region into view. + // + + nsRect rect; + nsIFrame* frame = GetSelectionAnchorGeometry(aRegion, &rect); + if (!frame) return NS_ERROR_FAILURE; + + // Scroll vertically to get the caret into view, but only if the container + // is perceived to be scrollable in that direction (i.e. there is a visible + // vertical scrollbar or the scroll range is at least one device pixel) + aVertical.mOnlyIfPerceivedScrollableDirection = true; + + auto scrollFlags = ScrollFlags::None; + if (aFlags & Selection::SCROLL_FIRST_ANCESTOR_ONLY) { + scrollFlags |= ScrollFlags::ScrollFirstAncestorOnly; + } + if (aFlags & Selection::SCROLL_OVERFLOW_HIDDEN) { + scrollFlags |= ScrollFlags::ScrollOverflowHidden; + } + + presShell->ScrollFrameIntoView(frame, Some(rect), aVertical, aHorizontal, + scrollFlags); + return NS_OK; +} + +void Selection::AddSelectionListener(nsISelectionListener* aNewListener) { + MOZ_ASSERT(aNewListener); + mSelectionListeners.AppendElement(aNewListener); // AddRefs +} + +void Selection::RemoveSelectionListener( + nsISelectionListener* aListenerToRemove) { + mSelectionListeners.RemoveElement(aListenerToRemove); // Releases +} + +Element* Selection::StyledRanges::GetCommonEditingHost() const { + Element* editingHost = nullptr; + for (const StyledRange& rangeData : mRanges) { + const AbstractRange* range = rangeData.mRange; + MOZ_ASSERT(range); + nsINode* commonAncestorNode = range->GetClosestCommonInclusiveAncestor(); + if (!commonAncestorNode || !commonAncestorNode->IsContent()) { + return nullptr; + } + nsIContent* commonAncestor = commonAncestorNode->AsContent(); + Element* foundEditingHost = commonAncestor->GetEditingHost(); + // Even when common ancestor is a non-editable element in a contenteditable + // element, we don't need to move focus to the contenteditable element + // because Chromium doesn't set focus to it. + if (!foundEditingHost) { + return nullptr; + } + if (!editingHost) { + editingHost = foundEditingHost; + continue; + } + if (editingHost == foundEditingHost) { + continue; + } + if (foundEditingHost->IsInclusiveDescendantOf(editingHost)) { + continue; + } + if (editingHost->IsInclusiveDescendantOf(foundEditingHost)) { + editingHost = foundEditingHost; + continue; + } + // editingHost and foundEditingHost are not a descendant of the other. + // So, there is no common editing host. + return nullptr; + } + return editingHost; +} + +void Selection::StyledRanges::MaybeFocusCommonEditingHost( + PresShell* aPresShell) const { + if (!aPresShell) { + return; + } + + nsPresContext* presContext = aPresShell->GetPresContext(); + if (!presContext) { + return; + } + + Document* document = aPresShell->GetDocument(); + if (!document) { + return; + } + + nsPIDOMWindowOuter* window = document->GetWindow(); + // If the document is in design mode or doesn't have contenteditable + // element, we don't need to move focus. + if (window && !document->IsInDesignMode() && + nsContentUtils::GetHTMLEditor(presContext)) { + RefPtr newEditingHost = GetCommonEditingHost(); + RefPtr fm = nsFocusManager::GetFocusManager(); + nsCOMPtr focusedWindow; + nsIContent* focusedContent = nsFocusManager::GetFocusedDescendant( + window, nsFocusManager::eOnlyCurrentWindow, + getter_AddRefs(focusedWindow)); + nsCOMPtr focusedElement = do_QueryInterface(focusedContent); + // When all selected ranges are in an editing host, it should take focus. + // But otherwise, we shouldn't move focus since Chromium doesn't move + // focus but only selection range is updated. + if (newEditingHost && newEditingHost != focusedElement) { + MOZ_ASSERT(!newEditingHost->IsInNativeAnonymousSubtree()); + // Note that don't steal focus from focused window if the window doesn't + // have focus. Additionally, although when an element gets focus, we + // usually scroll to the element, but in this case, we shouldn't do it + // because Chrome does not do so. + fm->SetFocus(newEditingHost, nsIFocusManager::FLAG_NOSWITCHFRAME | + nsIFocusManager::FLAG_NOSCROLL); + } + } +} + +void Selection::NotifySelectionListeners(bool aCalledByJS) { + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = aCalledByJS; + NotifySelectionListeners(); +} + +void Selection::NotifySelectionListeners() { + if (!mFrameSelection) { + return; // nothing to do + } + + MOZ_LOG(sSelectionLog, LogLevel::Debug, + ("%s: selection=%p", __FUNCTION__, this)); + + // Our internal code should not move focus with using this class while + // this moves focus nor from selection listeners. + AutoRestore calledByJSRestorer(mCalledByJS); + mCalledByJS = false; + + // When normal selection is changed by Selection API, we need to move focus + // if common ancestor of all ranges are in an editing host. Note that we + // don't need to move focus *to* the other focusable node because other + // browsers don't do it either. + if (mSelectionType == SelectionType::eNormal && + calledByJSRestorer.SavedValue()) { + RefPtr presShell = GetPresShell(); + mStyledRanges.MaybeFocusCommonEditingHost(presShell); + } + + RefPtr frameSelection = mFrameSelection; + + // This flag will be set to true if a selection by double click is detected. + // As soon as the selection is modified, it needs to be set to false. + frameSelection->SetIsDoubleClickSelection(false); + + if (frameSelection->IsBatching()) { + frameSelection->SetChangesDuringBatchingFlag(); + return; + } + if (mSelectionListeners.IsEmpty() && !mNotifyAutoCopy && + !mAccessibleCaretEventHub && !mSelectionChangeEventDispatcher) { + // If there are no selection listeners, we're done! + return; + } + + nsCOMPtr doc; + if (PresShell* presShell = GetPresShell()) { + doc = presShell->GetDocument(); + presShell->ScheduleContentRelevancyUpdate(ContentRelevancyReason::Selected); + } + + // We've notified all selection listeners even when some of them are removed + // (and may be destroyed) during notifying one of them. Therefore, we should + // copy all listeners to the local variable first. + const CopyableAutoTArray, 5> + selectionListeners = mSelectionListeners; + + int16_t reason = frameSelection->PopChangeReasons(); + if (calledByJSRestorer.SavedValue()) { + reason |= nsISelectionListener::JS_REASON; + } + + int32_t amount = static_cast(frameSelection->GetCaretMoveAmount()); + + if (mNotifyAutoCopy) { + AutoCopyListener::OnSelectionChange(doc, *this, reason); + } + + if (mAccessibleCaretEventHub) { + RefPtr hub(mAccessibleCaretEventHub); + hub->OnSelectionChange(doc, this, reason); + } + + if (mSelectionChangeEventDispatcher) { + RefPtr dispatcher( + mSelectionChangeEventDispatcher); + dispatcher->OnSelectionChange(doc, this, reason); + } + + for (const auto& listener : selectionListeners) { + // MOZ_KnownLive because 'selectionListeners' is guaranteed to + // keep it alive. + // + // This can go away once + // https://bugzilla.mozilla.org/show_bug.cgi?id=1620312 is fixed. + MOZ_KnownLive(listener)->NotifySelectionChanged(doc, this, reason, amount); + } +} + +void Selection::StartBatchChanges(const char* aDetails) { + if (mFrameSelection) { + RefPtr frameSelection = mFrameSelection; + frameSelection->StartBatchChanges(aDetails); + } +} + +void Selection::EndBatchChanges(const char* aDetails, int16_t aReasons) { + if (mFrameSelection) { + RefPtr frameSelection = mFrameSelection; + frameSelection->EndBatchChanges(aDetails, aReasons); + } +} + +void Selection::AddSelectionChangeBlocker() { mSelectionChangeBlockerCount++; } + +void Selection::RemoveSelectionChangeBlocker() { + MOZ_ASSERT(mSelectionChangeBlockerCount > 0, + "mSelectionChangeBlockerCount has an invalid value - " + "maybe you have a mismatched RemoveSelectionChangeBlocker?"); + mSelectionChangeBlockerCount--; +} + +bool Selection::IsBlockingSelectionChangeEvents() const { + return mSelectionChangeBlockerCount > 0; +} + +void Selection::DeleteFromDocument(ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__); + LogStackForSelectionAPI(); + } + + if (mSelectionType != SelectionType::eNormal) { + return; // Nothing to do. + } + + // If we're already collapsed, then we do nothing (bug 719503). + if (IsCollapsed()) { + return; + } + + for (uint32_t rangeIdx = 0; rangeIdx < RangeCount(); ++rangeIdx) { + RefPtr range = GetRangeAt(rangeIdx); + range->DeleteContents(aRv); + if (aRv.Failed()) { + return; + } + } + + // Collapse to the new location. + // If we deleted one character, then we move back one element. + // FIXME We don't know how to do this past frame boundaries yet. + if (AnchorOffset() > 0) { + RefPtr anchor = GetAnchorNode(); + CollapseInLimiter(anchor, AnchorOffset()); + } +#ifdef DEBUG + else { + printf("Don't know how to set selection back past frame boundary\n"); + } +#endif +} + +void Selection::Modify(const nsAString& aAlter, const nsAString& aDirection, + const nsAString& aGranularity, ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aAlter", aAlter, "aDirection", + aDirection, "aGranularity", aGranularity); + LogStackForSelectionAPI(); + } + + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + if (!GetAnchorFocusRange() || !GetFocusNode()) { + return; + } + + if (!aAlter.LowerCaseEqualsLiteral("move") && + !aAlter.LowerCaseEqualsLiteral("extend")) { + aRv.ThrowSyntaxError( + R"(The first argument must be one of: "move" or "extend")"); + return; + } + + if (!aDirection.LowerCaseEqualsLiteral("forward") && + !aDirection.LowerCaseEqualsLiteral("backward") && + !aDirection.LowerCaseEqualsLiteral("left") && + !aDirection.LowerCaseEqualsLiteral("right")) { + aRv.ThrowSyntaxError( + R"(The direction argument must be one of: "forward", "backward", "left", or "right")"); + return; + } + + // Make sure the layout is up to date as we access bidi information below. + if (RefPtr doc = GetDocument()) { + doc->FlushPendingNotifications(FlushType::Layout); + } + + // Line moves are always visual. + bool visual = aDirection.LowerCaseEqualsLiteral("left") || + aDirection.LowerCaseEqualsLiteral("right") || + aGranularity.LowerCaseEqualsLiteral("line"); + + bool forward = aDirection.LowerCaseEqualsLiteral("forward") || + aDirection.LowerCaseEqualsLiteral("right"); + + bool extend = aAlter.LowerCaseEqualsLiteral("extend"); + + nsSelectionAmount amount; + if (aGranularity.LowerCaseEqualsLiteral("character")) { + amount = eSelectCluster; + } else if (aGranularity.LowerCaseEqualsLiteral("word")) { + amount = eSelectWordNoSpace; + } else if (aGranularity.LowerCaseEqualsLiteral("line")) { + amount = eSelectLine; + } else if (aGranularity.LowerCaseEqualsLiteral("lineboundary")) { + amount = forward ? eSelectEndLine : eSelectBeginLine; + } else if (aGranularity.LowerCaseEqualsLiteral("sentence") || + aGranularity.LowerCaseEqualsLiteral("sentenceboundary") || + aGranularity.LowerCaseEqualsLiteral("paragraph") || + aGranularity.LowerCaseEqualsLiteral("paragraphboundary") || + aGranularity.LowerCaseEqualsLiteral("documentboundary")) { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } else { + aRv.ThrowSyntaxError( + R"(The granularity argument must be one of: "character", "word", "line", or "lineboundary")"); + return; + } + + // If the anchor doesn't equal the focus and we try to move without first + // collapsing the selection, MoveCaret will collapse the selection and quit. + // To avoid this, we need to collapse the selection first. + nsresult rv = NS_OK; + if (!extend) { + RefPtr focusNode = GetFocusNode(); + // We should have checked earlier that there was a focus node. + if (!focusNode) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + uint32_t focusOffset = FocusOffset(); + CollapseInLimiter(focusNode, focusOffset); + } + + // If the paragraph direction of the focused frame is right-to-left, + // we may have to swap the direction of movement. + if (nsIFrame* frame = GetPrimaryFrameForFocusNode(visual)) { + mozilla::intl::BidiDirection paraDir = + nsBidiPresUtils::ParagraphDirection(frame); + + if (paraDir == mozilla::intl::BidiDirection::RTL && visual) { + if (amount == eSelectBeginLine) { + amount = eSelectEndLine; + forward = !forward; + } else if (amount == eSelectEndLine) { + amount = eSelectBeginLine; + forward = !forward; + } + } + } + + // MoveCaret will return an error if it can't move in the specified + // direction, but we just ignore this error unless it's a line move, in which + // case we call nsISelectionController::CompleteMove to move the cursor to + // the beginning/end of the line. + RefPtr frameSelection = mFrameSelection; + rv = frameSelection->MoveCaret( + forward ? eDirNext : eDirPrevious, extend, amount, + visual ? nsFrameSelection::eVisual : nsFrameSelection::eLogical); + + if (aGranularity.LowerCaseEqualsLiteral("line") && NS_FAILED(rv)) { + RefPtr presShell = frameSelection->GetPresShell(); + if (!presShell) { + return; + } + presShell->CompleteMove(forward, extend); + } +} + +void Selection::SetBaseAndExtentJS(nsINode& aAnchorNode, uint32_t aAnchorOffset, + nsINode& aFocusNode, uint32_t aFocusOffset, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aAnchorNode", aAnchorNode, + "aAnchorOffset", aAnchorOffset, "aFocusNode", aFocusNode, + "aFocusOffset", aFocusOffset); + LogStackForSelectionAPI(); + } + + AutoRestore calledFromJSRestorer(mCalledByJS); + mCalledByJS = true; + SetBaseAndExtent(aAnchorNode, aAnchorOffset, aFocusNode, aFocusOffset, aRv); +} + +void Selection::SetBaseAndExtent(nsINode& aAnchorNode, uint32_t aAnchorOffset, + nsINode& aFocusNode, uint32_t aFocusOffset, + ErrorResult& aRv) { + if (aAnchorOffset > aAnchorNode.Length()) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "The anchor offset value %u is out of range", aAnchorOffset)); + return; + } + if (aFocusOffset > aFocusNode.Length()) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "The focus offset value %u is out of range", aFocusOffset)); + return; + } + + SetBaseAndExtent(RawRangeBoundary{&aAnchorNode, aAnchorOffset}, + RawRangeBoundary{&aFocusNode, aFocusOffset}, aRv); +} + +void Selection::SetBaseAndExtent(const RawRangeBoundary& aAnchorRef, + const RawRangeBoundary& aFocusRef, + ErrorResult& aRv) { + if (!mCalledByJS && NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aAnchorRef", aAnchorRef, "aFocusRef", + aFocusRef); + LogStackForSelectionAPI(); + } + + SetBaseAndExtentInternal(InLimiter::eNo, aAnchorRef, aFocusRef, aRv); +} + +void Selection::SetBaseAndExtentInLimiter(const RawRangeBoundary& aAnchorRef, + const RawRangeBoundary& aFocusRef, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aAnchorRef", aAnchorRef, "aFocusRef", + aFocusRef); + LogStackForSelectionAPI(); + } + + SetBaseAndExtentInternal(InLimiter::eYes, aAnchorRef, aFocusRef, aRv); +} + +void Selection::SetBaseAndExtentInternal(InLimiter aInLimiter, + const RawRangeBoundary& aAnchorRef, + const RawRangeBoundary& aFocusRef, + ErrorResult& aRv) { + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + if (NS_WARN_IF(!aAnchorRef.IsSet()) || NS_WARN_IF(!aFocusRef.IsSet())) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (!HasSameRootOrSameComposedDoc(*aAnchorRef.Container()) || + !HasSameRootOrSameComposedDoc(*aFocusRef.Container())) { + // Return with no error + return; + } + + // Prevent "selectionchange" event temporarily because it should be fired + // after we set the direction. + // XXX If they are disconnected, shouldn't we return error before allocating + // new nsRange instance? + SelectionBatcher batch(this, __FUNCTION__); + const Maybe order = + nsContentUtils::ComparePoints(aAnchorRef, aFocusRef); + if (order && (*order <= 0)) { + SetStartAndEndInternal(aInLimiter, aAnchorRef, aFocusRef, eDirNext, aRv); + return; + } + + // If there's no `order`, the range will be collapsed, unless another error is + // detected before. + SetStartAndEndInternal(aInLimiter, aFocusRef, aAnchorRef, eDirPrevious, aRv); +} + +void Selection::SetStartAndEndInLimiter(const RawRangeBoundary& aStartRef, + const RawRangeBoundary& aEndRef, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aStartRef", aStartRef, "aEndRef", + aEndRef); + LogStackForSelectionAPI(); + } + + SetStartAndEndInternal(InLimiter::eYes, aStartRef, aEndRef, eDirNext, aRv); +} + +Result Selection::SetStartAndEndInLimiter( + nsINode& aStartContainer, uint32_t aStartOffset, nsINode& aEndContainer, + uint32_t aEndOffset, nsDirection aDirection, int16_t aReason) { + MOZ_ASSERT(aDirection == eDirPrevious || aDirection == eDirNext); + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aStartContainer", aStartContainer, + "aStartOffset", aStartOffset, "aEndContainer", + aEndContainer, "aEndOffset", aEndOffset, "nsDirection", + aDirection, "aReason", aReason); + LogStackForSelectionAPI(); + } + + if (mFrameSelection) { + mFrameSelection->AddChangeReasons(aReason); + } + + ErrorResult error; + SetStartAndEndInternal( + InLimiter::eYes, RawRangeBoundary(&aStartContainer, aStartOffset), + RawRangeBoundary(&aEndContainer, aEndOffset), aDirection, error); + MOZ_TRY(error.StealNSResult()); + return Ok(); +} + +void Selection::SetStartAndEnd(const RawRangeBoundary& aStartRef, + const RawRangeBoundary& aEndRef, + ErrorResult& aRv) { + if (NeedsToLogSelectionAPI(*this)) { + LogSelectionAPI(this, __FUNCTION__, "aStartRef", aStartRef, "aEndRef", + aEndRef); + LogStackForSelectionAPI(); + } + + SetStartAndEndInternal(InLimiter::eNo, aStartRef, aEndRef, eDirNext, aRv); +} + +void Selection::SetStartAndEndInternal(InLimiter aInLimiter, + const RawRangeBoundary& aStartRef, + const RawRangeBoundary& aEndRef, + nsDirection aDirection, + ErrorResult& aRv) { + if (NS_WARN_IF(!aStartRef.IsSet()) || NS_WARN_IF(!aEndRef.IsSet())) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // Don't fire "selectionchange" event until everything done. + SelectionBatcher batch(this, __FUNCTION__); + + if (aInLimiter == InLimiter::eYes) { + if (!mFrameSelection || + !mFrameSelection->IsValidSelectionPoint(aStartRef.Container())) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + if (aStartRef.Container() != aEndRef.Container() && + !mFrameSelection->IsValidSelectionPoint(aEndRef.Container())) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + + RefPtr newRange = nsRange::Create(aStartRef, aEndRef, aRv); + if (aRv.Failed()) { + return; + } + + RemoveAllRangesInternal(aRv); + if (aRv.Failed()) { + return; + } + + RefPtr document(GetDocument()); + AddRangeAndSelectFramesAndNotifyListenersInternal(*newRange, document, aRv); + if (aRv.Failed()) { + return; + } + + // Adding a range may set 2 or more ranges if there are non-selectable + // contents only when this change is caused by a user operation. Therefore, + // we need to select frames with the result in such case. + if (mUserInitiated) { + RefPtr presContext = GetPresContext(); + if (mStyledRanges.Length() > 1 && presContext) { + SelectFramesInAllRanges(presContext); + } + } + + SetDirection(aDirection); +} + +/** SelectionLanguageChange modifies the cursor Bidi level after a change in + * keyboard direction + * @param aLangRTL is true if the new language is right-to-left or false if the + * new language is left-to-right + */ +nsresult Selection::SelectionLanguageChange(bool aLangRTL) { + if (!mFrameSelection) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr frameSelection = mFrameSelection; + + // if the direction of the language hasn't changed, nothing to do + mozilla::intl::BidiEmbeddingLevel kbdBidiLevel = + aLangRTL ? mozilla::intl::BidiEmbeddingLevel::RTL() + : mozilla::intl::BidiEmbeddingLevel::LTR(); + if (kbdBidiLevel == frameSelection->mKbdBidiLevel) { + return NS_OK; + } + + frameSelection->mKbdBidiLevel = kbdBidiLevel; + + nsIFrame* focusFrame = GetPrimaryFrameForFocusNode(false); + if (!focusFrame) { + return NS_ERROR_FAILURE; + } + + auto [frameStart, frameEnd] = focusFrame->GetOffsets(); + RefPtr context = GetPresContext(); + mozilla::intl::BidiEmbeddingLevel levelBefore, levelAfter; + if (!context) { + return NS_ERROR_FAILURE; + } + + mozilla::intl::BidiEmbeddingLevel level = focusFrame->GetEmbeddingLevel(); + int32_t focusOffset = static_cast(FocusOffset()); + if ((focusOffset != frameStart) && (focusOffset != frameEnd)) + // the cursor is not at a frame boundary, so the level of both the + // characters (logically) before and after the cursor is equal to the frame + // level + levelBefore = levelAfter = level; + else { + // the cursor is at a frame boundary, so use GetPrevNextBidiLevels to find + // the level of the characters before and after the cursor + nsCOMPtr focusContent = do_QueryInterface(GetFocusNode()); + nsPrevNextBidiLevels levels = + frameSelection->GetPrevNextBidiLevels(focusContent, focusOffset, false); + + levelBefore = levels.mLevelBefore; + levelAfter = levels.mLevelAfter; + } + + if (levelBefore.IsSameDirection(levelAfter)) { + // if cursor is between two characters with the same orientation, changing + // the keyboard language must toggle the cursor level between the level of + // the character with the lowest level (if the new language corresponds to + // the orientation of that character) and this level plus 1 (if the new + // language corresponds to the opposite orientation) + if ((level != levelBefore) && (level != levelAfter)) { + level = std::min(levelBefore, levelAfter); + } + if (level.IsSameDirection(kbdBidiLevel)) { + frameSelection->SetCaretBidiLevelAndMaybeSchedulePaint(level); + } else { + frameSelection->SetCaretBidiLevelAndMaybeSchedulePaint( + mozilla::intl::BidiEmbeddingLevel(level + 1)); + } + } else { + // if cursor is between characters with opposite orientations, changing the + // keyboard language must change the cursor level to that of the adjacent + // character with the orientation corresponding to the new language. + if (levelBefore.IsSameDirection(kbdBidiLevel)) { + frameSelection->SetCaretBidiLevelAndMaybeSchedulePaint(levelBefore); + } else { + frameSelection->SetCaretBidiLevelAndMaybeSchedulePaint(levelAfter); + } + } + + // The caret might have moved, so invalidate the desired position + // for future usages of up-arrow or down-arrow + frameSelection->InvalidateDesiredCaretPos(); + + return NS_OK; +} + +void Selection::SetColors(const nsAString& aForegroundColor, + const nsAString& aBackgroundColor, + const nsAString& aAltForegroundColor, + const nsAString& aAltBackgroundColor, + ErrorResult& aRv) { + if (mSelectionType != SelectionType::eFind) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + mCustomColors.reset(new SelectionCustomColors); + + constexpr auto currentColorStr = u"currentColor"_ns; + constexpr auto transparentStr = u"transparent"_ns; + + if (!aForegroundColor.Equals(currentColorStr)) { + nscolor foregroundColor; + nsAttrValue aForegroundColorValue; + aForegroundColorValue.ParseColor(aForegroundColor); + if (!aForegroundColorValue.GetColorValue(foregroundColor)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + mCustomColors->mForegroundColor = Some(foregroundColor); + } else { + mCustomColors->mForegroundColor = Nothing(); + } + + if (!aBackgroundColor.Equals(transparentStr)) { + nscolor backgroundColor; + nsAttrValue aBackgroundColorValue; + aBackgroundColorValue.ParseColor(aBackgroundColor); + if (!aBackgroundColorValue.GetColorValue(backgroundColor)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + mCustomColors->mBackgroundColor = Some(backgroundColor); + } else { + mCustomColors->mBackgroundColor = Nothing(); + } + + if (!aAltForegroundColor.Equals(currentColorStr)) { + nscolor altForegroundColor; + nsAttrValue aAltForegroundColorValue; + aAltForegroundColorValue.ParseColor(aAltForegroundColor); + if (!aAltForegroundColorValue.GetColorValue(altForegroundColor)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + mCustomColors->mAltForegroundColor = Some(altForegroundColor); + } else { + mCustomColors->mAltForegroundColor = Nothing(); + } + + if (!aAltBackgroundColor.Equals(transparentStr)) { + nscolor altBackgroundColor; + nsAttrValue aAltBackgroundColorValue; + aAltBackgroundColorValue.ParseColor(aAltBackgroundColor); + if (!aAltBackgroundColorValue.GetColorValue(altBackgroundColor)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + mCustomColors->mAltBackgroundColor = Some(altBackgroundColor); + } else { + mCustomColors->mAltBackgroundColor = Nothing(); + } +} + +void Selection::ResetColors() { mCustomColors = nullptr; } + +void Selection::SetHighlightName(const nsAtom* aHighlightName) { + MOZ_ASSERT(mSelectionType == SelectionType::eHighlight); + mHighlightName = aHighlightName; +} + +JSObject* Selection::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return mozilla::dom::Selection_Binding::Wrap(aCx, this, aGivenProto); +} + +// AutoHideSelectionChanges +AutoHideSelectionChanges::AutoHideSelectionChanges( + const nsFrameSelection* aFrame) + : AutoHideSelectionChanges( + aFrame ? aFrame->GetSelection(SelectionType::eNormal) : nullptr) {} + +bool Selection::HasSameRootOrSameComposedDoc(const nsINode& aNode) { + nsINode* root = aNode.SubtreeRoot(); + Document* doc = GetDocument(); + return doc == root || (root && doc == root->GetComposedDoc()); +} -- cgit v1.2.3