summaryrefslogtreecommitdiffstats
path: root/dom/base/Selection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base/Selection.cpp')
-rw-r--r--dom/base/Selection.cpp4191
1 files changed, 4191 insertions, 0 deletions
diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp
new file mode 100644
index 0000000000..a56e460cbf
--- /dev/null
+++ b/dom/base/Selection.cpp
@@ -0,0 +1,4191 @@
+/* -*- 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/CaretAssociationHint.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/Bidi.h"
+#include "mozilla/intl/BidiEmbeddingLevel.h"
+#include "mozilla/Logging.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/RangeBoundary.h"
+#include "mozilla/RangeUtils.h"
+#include "mozilla/SelectionMovementUtils.h"
+#include "mozilla/StackWalk.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Try.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 <algorithm>
+#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 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(GetMainThreadSerialEventTarget());
+ 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<nsFrameSelection> 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<nsITimer> mTimer;
+ nsCOMPtr<nsIContent> 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> 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<nsIDocumentEncoder> 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
+ ? CaretAssociationHint::After
+ : CaretAssociationHint::Before);
+ return NS_OK;
+}
+
+Selection::InterlinePosition Selection::GetInterlinePosition() const {
+ MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
+
+ if (!mFrameSelection) {
+ return InterlinePosition::Undefined;
+ }
+ return mFrameSelection->GetHint() == CaretAssociationHint::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<int16_t> Selection::GetCaretBidiLevel(
+ mozilla::ErrorResult& aRv) const {
+ MOZ_ASSERT(mSelectionType == SelectionType::eNormal);
+
+ if (!mFrameSelection) {
+ aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+ return Nullable<int16_t>();
+ }
+ mozilla::intl::BidiEmbeddingLevel caretBidiLevel =
+ static_cast<mozilla::intl::BidiEmbeddingLevel>(
+ mFrameSelection->GetCaretBidiLevel());
+ return (caretBidiLevel & BIDI_LEVEL_UNDEFINED)
+ ? Nullable<int16_t>()
+ : Nullable<int16_t>(caretBidiLevel);
+}
+
+void Selection::SetCaretBidiLevel(const Nullable<int16_t>& 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<size_t>* 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(mHighlightData.mHighlight)
+ 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)
+ }
+ count = tmp->mStyledRanges.mInvalidStaticRanges.Length();
+ for (i = 0; i < count; ++i) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
+ mStyledRanges.mInvalidStaticRanges[i].mRange);
+ }
+ }
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorFocusRange)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameSelection)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHighlightData.mHighlight)
+ 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_CYCLE_COLLECTING_ADDREF(Selection)
+NS_IMPL_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<StyledRange>* 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<size_t>(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<StyledRange>* 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<nsRange> 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<nsRange> 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<RefPtr<nsRange>>& 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<nsINode> 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<RefPtr<nsRange>>& aTempRangesToAdd) {
+ MOZ_ASSERT(aTempRangesToAdd.IsEmpty());
+
+ RefPtr<nsRange> 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<size_t>* 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<RefPtr<nsRange>, 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<size_t> 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<size_t>* 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<int>(GetType()), aRange,
+ aRange->StartOffset(), aRange->EndOffset()));
+
+ if (mUserInitiated) {
+ return AddRangesForUserSelectableNodes(aRange, aOutIndex,
+ aDispatchSelectstartEvent);
+ }
+
+ return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex);
+}
+
+nsresult Selection::StyledRanges::AddRangeAndIgnoreOverlaps(
+ AbstractRange* aRange) {
+ MOZ_ASSERT(aRange);
+ MOZ_ASSERT(aRange->IsPositioned());
+ MOZ_ASSERT(mSelection.mSelectionType == SelectionType::eHighlight);
+ if (aRange->IsStaticRange() && !aRange->AsStaticRange()->IsValid()) {
+ mInvalidStaticRanges.AppendElement(StyledRange(aRange));
+ aRange->RegisterSelection(MOZ_KnownLive(mSelection));
+ return NS_OK;
+ }
+
+ // a common case is that we have no ranges yet
+ if (mRanges.Length() == 0) {
+ mRanges.AppendElement(StyledRange(aRange));
+ aRange->RegisterSelection(MOZ_KnownLive(mSelection));
+ return NS_OK;
+ }
+
+ Maybe<size_t> maybeStartIndex, maybeEndIndex;
+ nsresult rv =
+ GetIndicesForInterval(aRange->GetStartContainer(), aRange->StartOffset(),
+ aRange->GetEndContainer(), aRange->EndOffset(),
+ false, maybeStartIndex, maybeEndIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ size_t startIndex(0);
+ if (maybeEndIndex.isNothing()) {
+ // All ranges start after the given range. We can insert our range at
+ // position 0.
+ startIndex = 0;
+ } else if (maybeStartIndex.isNothing()) {
+ // All ranges end before the given range. We can insert our range at
+ // the end of the array.
+ startIndex = mRanges.Length();
+ } else {
+ startIndex = *maybeStartIndex;
+ }
+
+ mRanges.InsertElementAt(startIndex, StyledRange(aRange));
+ aRange->RegisterSelection(MOZ_KnownLive(mSelection));
+ return NS_OK;
+}
+
+nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
+ nsRange* aRange, Maybe<size_t>* 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<size_t> 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<StyledRange, 2> 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<StyledRange, 3> 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 AbstractRange& 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<RefPtr<nsRange>>& aReturn,
+ mozilla::ErrorResult& aRv) {
+ AutoTArray<nsRange*, 2> 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<AbstractRange*>* aRanges) {
+ if (NS_WARN_IF(!aBeginNode)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (NS_WARN_IF(!aEndNode)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ aRanges->Clear();
+ Maybe<size_t> 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<nsRange*>* aRanges) {
+ MOZ_ASSERT(mSelectionType != SelectionType::eHighlight);
+ AutoTArray<AbstractRange*, 2> 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;
+}
+
+void Selection::StyledRanges::ReorderRangesIfNecessary() {
+ const Document* doc = mSelection.GetDocument();
+ if (!doc) {
+ return;
+ }
+ if (mRanges.Length() < 2 && mInvalidStaticRanges.IsEmpty()) {
+ // There is nothing to be reordered.
+ return;
+ }
+ const int32_t currentDocumentGeneration = doc->GetGeneration();
+ const bool domMutationHasHappened =
+ currentDocumentGeneration != mDocumentGeneration;
+ if (domMutationHasHappened) {
+ // After a DOM mutation, invalid static ranges might have become valid and
+ // valid static ranges might have become invalid.
+ StyledRangeArray invalidStaticRanges;
+ for (StyledRangeArray::const_iterator iter = mRanges.begin();
+ iter != mRanges.end();) {
+ const AbstractRange* range = iter->mRange;
+ if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) {
+ invalidStaticRanges.AppendElement(*iter);
+ iter = mRanges.RemoveElementAt(iter);
+ } else {
+ ++iter;
+ }
+ }
+ for (StyledRangeArray::const_iterator iter = mInvalidStaticRanges.begin();
+ iter != mInvalidStaticRanges.end();) {
+ MOZ_ASSERT(iter->mRange->IsStaticRange());
+ if (iter->mRange->AsStaticRange()->IsValid()) {
+ mRanges.AppendElement(*iter);
+ iter = mInvalidStaticRanges.RemoveElementAt(iter);
+ } else {
+ ++iter;
+ }
+ }
+ mInvalidStaticRanges.AppendElements(std::move(invalidStaticRanges));
+ }
+ if (domMutationHasHappened || mRangesMightHaveChanged) {
+ mRanges.Sort([](const StyledRange& a, const StyledRange& b) -> int {
+ return CompareToRangeStart(*a.mRange->GetStartContainer(),
+ a.mRange->StartOffset(), *b.mRange);
+ });
+ mDocumentGeneration = currentDocumentGeneration;
+ mRangesMightHaveChanged = false;
+ }
+}
+
+nsresult Selection::StyledRanges::GetIndicesForInterval(
+ const nsINode* aBeginNode, uint32_t aBeginOffset, const nsINode* aEndNode,
+ uint32_t aEndOffset, bool aAllowAdjacent, Maybe<size_t>& aStartIndex,
+ Maybe<size_t>& aEndIndex) {
+ 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;
+ }
+
+ ReorderRangesIfNecessary();
+
+ 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);
+
+ nsCOMPtr<nsIContent> content = do_QueryInterface(GetAnchorNode());
+ if (content && mFrameSelection) {
+ return SelectionMovementUtils::GetFrameForNodeOffset(
+ content, AnchorOffset(), mFrameSelection->GetHint());
+ }
+ return nullptr;
+}
+
+PrimaryFrameData Selection::GetPrimaryFrameForCaretAtFocusNode(
+ bool aVisual) const {
+ nsIContent* content = nsIContent::FromNodeOrNull(GetFocusNode());
+ if (!content || !mFrameSelection || !mFrameSelection->GetPresShell()) {
+ return {};
+ }
+
+ MOZ_ASSERT(mFrameSelection->GetPresShell()->GetDocument() ==
+ content->GetComposedDoc());
+
+ CaretAssociationHint hint = mFrameSelection->GetHint();
+ intl::BidiEmbeddingLevel caretBidiLevel =
+ mFrameSelection->GetCaretBidiLevel();
+ return SelectionMovementUtils::GetPrimaryFrameForCaret(
+ content, FocusOffset(), aVisual, hint, caretBidiLevel);
+}
+
+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<nsTextFrame*>(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<nsTextFrame*>(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<nsTextFrame*>(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<nsTextFrame*>(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<SelectionDetails> Selection::LookUpSelection(
+ nsIContent* aContent, uint32_t aContentOffset, uint32_t aContentLength,
+ UniquePtr<SelectionDetails> 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<AbstractRange*> overlappingRanges;
+ nsresult rv = GetAbstractRangesForIntervalArray(
+ aContent, aContentOffset, aContent, aContentOffset + aContentLength,
+ false, &overlappingRanges);
+ if (NS_FAILED(rv)) {
+ return aDetailsHead;
+ }
+
+ if (overlappingRanges.Length() == 0) {
+ return aDetailsHead;
+ }
+
+ UniquePtr<SelectionDetails> 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<uint32_t> 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<SelectionDetails>();
+
+ newHead->mNext = std::move(detailsHead);
+ newHead->mStart = AssertedCast<int32_t>(*start);
+ newHead->mEnd = AssertedCast<int32_t>(*end);
+ newHead->mSelectionType = aSelectionType;
+ newHead->mHighlightData = mHighlightData;
+ 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<nsFrameSelection> 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();
+ mInvalidStaticRanges.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::StyledRangeArray::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> 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> 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<nsPresContext> presContext = GetPresContext();
+ Clear(presContext);
+
+ // Turn off signal for table selection
+ RefPtr<nsFrameSelection> frameSelection = mFrameSelection;
+ frameSelection->ClearTableCellSelection();
+
+ RefPtr<Selection> 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<bool> calledFromJSRestorer(mCalledByJS);
+ mCalledByJS = true;
+ RefPtr<Document> document(GetDocument());
+ AddRangeAndSelectFramesAndNotifyListenersInternal(aRange, document, aRv);
+}
+
+void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange,
+ ErrorResult& aRv) {
+ if (NeedsToLogSelectionAPI(*this)) {
+ LogSelectionAPI(this, __FUNCTION__, "aRange", aRange);
+ LogStackForSelectionAPI();
+ }
+
+ RefPtr<Document> document(GetDocument());
+ return AddRangeAndSelectFramesAndNotifyListenersInternal(aRange, document,
+ aRv);
+}
+
+void Selection::AddRangeAndSelectFramesAndNotifyListenersInternal(
+ nsRange& aRange, Document* aDocument, ErrorResult& aRv) {
+ RefPtr<nsRange> 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<Selection> 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<size_t> 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<nsPresContext> presContext = GetPresContext();
+ SelectFrames(presContext, *range, true);
+
+ // Be aware, this instance may be destroyed after this call.
+ NotifySelectionListeners();
+ // Range order is guaranteed after adding a range.
+ // Therefore, this flag can be reset to avoid
+ // another unnecessary and costly reordering.
+ mStyledRanges.mRangesMightHaveChanged = false;
+}
+
+void Selection::AddHighlightRangeAndSelectFramesAndNotifyListeners(
+ AbstractRange& aRange) {
+ MOZ_ASSERT(mSelectionType == SelectionType::eHighlight);
+ nsresult rv = mStyledRanges.AddRangeAndIgnoreOverlaps(&aRange);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ if (!mFrameSelection) {
+ return; // nothing to do
+ }
+
+ RefPtr<nsPresContext> presContext = GetPresContext();
+ SelectFrames(presContext, aRange, true);
+
+ // Be aware, this instance may be destroyed after this call.
+ RefPtr<Selection> kungFuDeathGrip(this);
+ NotifySelectionListeners();
+ // Range order is guaranteed after adding a range.
+ // Therefore, this flag can be reset to avoid
+ // another unnecessary and costly reordering.
+ mStyledRanges.mRangesMightHaveChanged = false;
+}
+
+// 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<nsPresContext> presContext = GetPresContext();
+ SelectFrames(presContext, aRange, false);
+
+ // add back the selected bit for each range touching our nodes
+ nsTArray<AbstractRange*> 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<Selection> 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<bool> 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<nsFrameSelection> frameSelection = mFrameSelection;
+ frameSelection->InvalidateDesiredCaretPos();
+ if (aInLimiter == InLimiter::eYes &&
+ !frameSelection->IsValidSelectionPoint(aPoint.Container())) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ nsresult result;
+
+ RefPtr<nsPresContext> 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() == CaretAssociationHint::Before &&
+ 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<int32_t>(*aPoint.Offset(
+ RawRangeBoundary::OffsetFilter::kValidOffsets))) ||
+ (aPoint.Container() == f->GetContent()->GetParentNode() &&
+ f->GetContent() == aPoint.GetPreviousSiblingOfChildAtOffset())) {
+ frameSelection->SetHint(CaretAssociationHint::After);
+ }
+ }
+ }
+
+ RefPtr<nsRange> range = nsRange::Create(aPoint.Container());
+ result = range->CollapseTo(aPoint);
+ if (NS_FAILED(result)) {
+ aRv.Throw(result);
+ return;
+ }
+
+#ifdef DEBUG_SELECTION
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aPoint.Container());
+ nsCOMPtr<Document> 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<size_t> maybeRangeIndex;
+ result = AddRangesForSelectableNodes(range, &maybeRangeIndex,
+ DispatchSelectstartEvent::Maybe);
+ if (NS_FAILED(result)) {
+ aRv.Throw(result);
+ return;
+ }
+ SetAnchorFocusRange(0);
+ SelectFrames(presContext, *range, true);
+
+ RefPtr<Selection> 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<bool> 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<bool> 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<size_t> 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<nsPresContext> 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<bool> 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<nsPresContext> 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<nsRange> 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<int32_t> anchorOldFocusOrder = nsContentUtils::ComparePoints(
+ anchorNode, anchorOffset, focusNode, focusOffset);
+ shouldClearRange |= !anchorOldFocusOrder;
+ const Maybe<int32_t> oldFocusNewFocusOrder = nsContentUtils::ComparePoints(
+ focusNode, focusOffset, &aContainer, aOffset);
+ shouldClearRange |= !oldFocusNewFocusOrder;
+ const Maybe<int32_t> 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<nsRange> 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<nsIContent> content = do_QueryInterface(&aContainer);
+ printf("Sel. Extend to %p %s %d\n", content.get(),
+ nsAtomCString(content->NodeInfo()->NameAtom()).get(), aOffset);
+#endif
+
+ RefPtr<Selection> 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<bool> 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<AbstractRange*> 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<nsIContent> content = do_QueryInterface(node);
+ NS_ENSURE_TRUE(content.get(), nullptr);
+ uint32_t frameOffset = 0;
+ frame = SelectionMovementUtils::GetFrameForNodeOffset(
+ content, nodeOffset, mFrameSelection->GetHint(), &frameOffset);
+ if (!frame) return nullptr;
+
+ SelectionMovementUtils::AdjustFrameForLineStart(frame, frameOffset);
+
+ // Figure out what node type we have, then get the
+ // appropriate rect for its nodeOffset.
+ bool isText = node->IsText();
+
+ nsPoint pt(0, 0);
+ if (isText) {
+ nsIFrame* childFrame = nullptr;
+ int32_t frameOffset = 0;
+ nsresult rv = frame->GetChildFrameContainingOffset(
+ nodeOffset, mFrameSelection->GetHint() == CaretAssociationHint::After,
+ &frameOffset, &childFrame);
+ if (NS_FAILED(rv)) return nullptr;
+ if (!childFrame) return nullptr;
+
+ frame = childFrame;
+
+ // Get the coordinates 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 inline-size. The
+ // inline-position is either 'pt' (if we're a text node) or otherwise just
+ // the physical "end" edge of the frame (which we express as the frame's own
+ // width or height, since the returned position is relative to the frame).
+ // The block position and size are set so as to fill the frame in that axis.
+ // (i.e. block-position of 0, and block-size matching the frame's own block
+ // size).
+ const WritingMode wm = frame->GetWritingMode();
+ // Helper to determine the inline-axis position for the aRect outparam.
+ auto GetInlinePosition = [&]() {
+ if (isText) {
+ return wm.IsVertical() ? pt.y : pt.x;
+ }
+ // Return the frame's physical end edge of its inline axis, relative to the
+ // frame. That's just its height or width.
+ // TODO(dholbert): This seems to work, but perhaps we really want the
+ // inline-end edge (rather than physical end of inline axis)? (i.e. if we
+ // have direction:rtl, maybe this code would want to return 0 instead of
+ // height/width?)
+ return frame->ISize(wm);
+ };
+
+ // Set the inline position and block-size. Leave inline size and block
+ // position set to 0, as discussed above.
+ if (wm.IsVertical()) {
+ aRect->y = GetInlinePosition();
+ aRect->SetWidth(frame->BSize(wm));
+ } else {
+ aRect->x = GetInlinePosition();
+ aRect->SetHeight(frame->BSize(wm));
+ }
+
+ 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> 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> 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<PresShell> 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<Element> newEditingHost = GetCommonEditingHost();
+ RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
+ nsCOMPtr<nsPIDOMWindowOuter> focusedWindow;
+ nsIContent* focusedContent = nsFocusManager::GetFocusedDescendant(
+ window, nsFocusManager::eOnlyCurrentWindow,
+ getter_AddRefs(focusedWindow));
+ nsCOMPtr<Element> 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<bool> calledFromJSRestorer(mCalledByJS);
+ mCalledByJS = aCalledByJS;
+ NotifySelectionListeners();
+}
+
+void Selection::NotifySelectionListeners() {
+ if (!mFrameSelection) {
+ return; // nothing to do
+ }
+
+ MOZ_LOG(sSelectionLog, LogLevel::Debug,
+ ("%s: selection=%p", __FUNCTION__, this));
+
+ mStyledRanges.mRangesMightHaveChanged = true;
+
+ // Our internal code should not move focus with using this class while
+ // this moves focus nor from selection listeners.
+ AutoRestore<bool> 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> presShell = GetPresShell();
+ mStyledRanges.MaybeFocusCommonEditingHost(presShell);
+ }
+
+ RefPtr<nsFrameSelection> 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<Document> 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<nsCOMPtr<nsISelectionListener>, 5>
+ selectionListeners = mSelectionListeners;
+
+ int16_t reason = frameSelection->PopChangeReasons();
+ if (calledByJSRestorer.SavedValue()) {
+ reason |= nsISelectionListener::JS_REASON;
+ }
+
+ int32_t amount = static_cast<int32_t>(frameSelection->GetCaretMoveAmount());
+
+ if (mNotifyAutoCopy) {
+ AutoCopyListener::OnSelectionChange(doc, *this, reason);
+ }
+
+ if (mAccessibleCaretEventHub) {
+ RefPtr<AccessibleCaretEventHub> hub(mAccessibleCaretEventHub);
+ hub->OnSelectionChange(doc, this, reason);
+ }
+
+ if (mSelectionChangeEventDispatcher) {
+ RefPtr<SelectionChangeEventDispatcher> 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<nsFrameSelection> frameSelection = mFrameSelection;
+ frameSelection->StartBatchChanges(aDetails);
+ }
+}
+
+void Selection::EndBatchChanges(const char* aDetails, int16_t aReasons) {
+ if (mFrameSelection) {
+ RefPtr<nsFrameSelection> 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;
+ }
+
+ // nsRange::DeleteContents() may run script, let's store all ranges first.
+ AutoTArray<RefPtr<nsRange>, 1> ranges;
+ MOZ_ASSERT(RangeCount() == mStyledRanges.mRanges.Length());
+ ranges.SetCapacity(RangeCount());
+ for (uint32_t index : IntegerRange(RangeCount())) {
+ ranges.AppendElement(mStyledRanges.mRanges[index].mRange->AsDynamicRange());
+ }
+ for (const auto& range : ranges) {
+ MOZ_KnownLive(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<nsINode> 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<Document> 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<nsINode> 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.
+ const PrimaryFrameData frameForFocus =
+ GetPrimaryFrameForCaretAtFocusNode(visual);
+ if (frameForFocus.mFrame) {
+ if (visual) {
+ // FYI: This was done during a call of GetPrimaryFrameForCaretAtFocusNode.
+ // Therefore, this may not be intended by the original author.
+ mFrameSelection->SetHint(frameForFocus.mHint);
+ }
+ mozilla::intl::BidiDirection paraDir =
+ nsBidiPresUtils::ParagraphDirection(frameForFocus.mFrame);
+
+ 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<nsFrameSelection> frameSelection = mFrameSelection;
+ rv = frameSelection->MoveCaret(
+ forward ? eDirNext : eDirPrevious, extend, amount,
+ visual ? nsFrameSelection::eVisual : nsFrameSelection::eLogical);
+
+ if (aGranularity.LowerCaseEqualsLiteral("line") && NS_FAILED(rv)) {
+ RefPtr<PresShell> 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<bool> 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<int32_t> 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<Ok, nsresult> 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<nsRange> newRange = nsRange::Create(aStartRef, aEndRef, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RemoveAllRangesInternal(aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+
+ RefPtr<Document> 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<nsPresContext> 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<nsFrameSelection> 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;
+
+ PrimaryFrameData focusFrameData = GetPrimaryFrameForCaretAtFocusNode(false);
+ if (!focusFrameData.mFrame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ auto [frameStart, frameEnd] = focusFrameData.mFrame->GetOffsets();
+ RefPtr<nsPresContext> context = GetPresContext();
+ mozilla::intl::BidiEmbeddingLevel levelBefore, levelAfter;
+ if (!context) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mozilla::intl::BidiEmbeddingLevel level =
+ focusFrameData.mFrame->GetEmbeddingLevel();
+ int32_t focusOffset = static_cast<int32_t>(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<nsIContent> 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::SetHighlightSelectionData(
+ HighlightSelectionData aHighlightSelectionData) {
+ MOZ_ASSERT(mSelectionType == SelectionType::eHighlight);
+ mHighlightData = std::move(aHighlightSelectionData);
+}
+
+JSObject* Selection::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> 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());
+}