summaryrefslogtreecommitdiffstats
path: root/layout/generic/nsFrameSelection.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/generic/nsFrameSelection.cpp3378
1 files changed, 3378 insertions, 0 deletions
diff --git a/layout/generic/nsFrameSelection.cpp b/layout/generic/nsFrameSelection.cpp
new file mode 100644
index 0000000000..8cc3a66ee1
--- /dev/null
+++ b/layout/generic/nsFrameSelection.cpp
@@ -0,0 +1,3378 @@
+/* -*- 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 nsFrameSelection
+ */
+
+#include "nsFrameSelection.h"
+
+#include "mozilla/intl/BidiEmbeddingLevel.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/IntegerRange.h"
+#include "mozilla/Logging.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScrollTypes.h"
+#include "mozilla/StaticAnalysisFunctions.h"
+#include "mozilla/StaticPrefs_bidi.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/Unused.h"
+
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsString.h"
+#include "nsISelectionListener.h"
+#include "nsContentCID.h"
+#include "nsDeviceContext.h"
+#include "nsIContent.h"
+#include "nsRange.h"
+#include "nsITableCellLayout.h"
+#include "nsTArray.h"
+#include "nsTableWrapperFrame.h"
+#include "nsTableCellFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsCCUncollectableMarker.h"
+#include "nsTextFragment.h"
+#include <algorithm>
+#include "nsContentUtils.h"
+#include "nsCSSFrameConstructor.h"
+
+#include "nsGkAtoms.h"
+#include "nsIFrameTraversal.h"
+#include "nsLayoutUtils.h"
+#include "nsLayoutCID.h"
+#include "nsBidiPresUtils.h"
+static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID);
+#include "nsTextFrame.h"
+
+#include "nsThreadUtils.h"
+#include "mozilla/Preferences.h"
+
+#include "mozilla/PresShell.h"
+#include "nsPresContext.h"
+#include "nsCaret.h"
+
+#include "mozilla/MouseEvents.h"
+#include "mozilla/TextEvents.h"
+
+// notifications
+#include "mozilla/dom/Document.h"
+
+#include "nsISelectionController.h" //for the enums
+#include "nsCopySupport.h"
+#include "nsIClipboard.h"
+#include "nsIFrameInlines.h"
+
+#include "nsError.h"
+#include "mozilla/AutoCopyListener.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Highlight.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/ShadowRoot.h"
+#include "mozilla/dom/StaticRange.h"
+#include "mozilla/dom/Text.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/SelectionBinding.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/Telemetry.h"
+
+#include "nsFocusManager.h"
+#include "nsPIDOMWindow.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static LazyLogModule sFrameSelectionLog("FrameSelection");
+
+// #define DEBUG_TABLE 1
+
+/**
+ * Add cells to the selection inside of the given cells range.
+ *
+ * @param aTable [in] HTML table element
+ * @param aStartRowIndex [in] row index where the cells range starts
+ * @param aStartColumnIndex [in] column index where the cells range starts
+ * @param aEndRowIndex [in] row index where the cells range ends
+ * @param aEndColumnIndex [in] column index where the cells range ends
+ */
+static nsresult AddCellsToSelection(const nsIContent* aTableContent,
+ int32_t aStartRowIndex,
+ int32_t aStartColumnIndex,
+ int32_t aEndRowIndex,
+ int32_t aEndColumnIndex,
+ Selection& aNormalSelection);
+
+static nsAtom* GetTag(nsINode* aNode);
+
+static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode);
+MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange(
+ nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection);
+static nsresult SelectCellElement(nsIContent* aCellElement,
+ Selection& aNormalSelection);
+
+#ifdef XP_MACOSX
+static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel);
+#endif // XP_MACOSX
+
+#ifdef PRINT_RANGE
+static void printRange(nsRange* aDomRange);
+# define DEBUG_OUT_RANGE(x) printRange(x)
+#else
+# define DEBUG_OUT_RANGE(x)
+#endif // PRINT_RANGE
+
+/******************************************************************************
+ * mozilla::PeekOffsetStruct
+ ******************************************************************************/
+
+// #define DEBUG_SELECTION // uncomment for printf describing every collapse and
+// extend. #define DEBUG_NAVIGATION
+
+// #define DEBUG_TABLE_SELECTION 1
+
+namespace mozilla {
+
+PeekOffsetStruct::PeekOffsetStruct(nsSelectionAmount aAmount,
+ nsDirection aDirection, int32_t aStartOffset,
+ nsPoint aDesiredCaretPos,
+ const PeekOffsetOptions aOptions,
+ EWordMovementType aWordMovementType)
+ : mAmount(aAmount),
+ mDirection(aDirection),
+ mStartOffset(aStartOffset),
+ mDesiredCaretPos(aDesiredCaretPos),
+ mWordMovementType(aWordMovementType),
+ mOptions(aOptions),
+ mResultContent(),
+ mResultFrame(nullptr),
+ mContentOffset(0),
+ mAttach(CARET_ASSOCIATE_BEFORE) {}
+
+} // namespace mozilla
+
+// Array which contains index of each SelecionType in Selection::mDOMSelections.
+// For avoiding using if nor switch to retrieve the index, this needs to have
+// -1 for SelectionTypes which won't be created its Selection instance.
+static const int8_t kIndexOfSelections[] = {
+ -1, // SelectionType::eInvalid
+ -1, // SelectionType::eNone
+ 0, // SelectionType::eNormal
+ 1, // SelectionType::eSpellCheck
+ 2, // SelectionType::eIMERawClause
+ 3, // SelectionType::eIMESelectedRawClause
+ 4, // SelectionType::eIMEConvertedClause
+ 5, // SelectionType::eIMESelectedClause
+ 6, // SelectionType::eAccessibility
+ 7, // SelectionType::eFind
+ 8, // SelectionType::eURLSecondary
+ 9, // SelectionType::eURLStrikeout
+ -1, // SelectionType::eHighlight
+};
+
+inline int8_t GetIndexFromSelectionType(SelectionType aSelectionType) {
+ // The enum value of eInvalid is -1 and the others are sequential value
+ // starting from 0. Therefore, |SelectionType + 1| is the index of
+ // kIndexOfSelections.
+ return kIndexOfSelections[static_cast<int8_t>(aSelectionType) + 1];
+}
+
+/*
+The limiter is used specifically for the text areas and textfields
+In that case it is the DIV tag that is anonymously created for the text
+areas/fields. Text nodes and BR nodes fall beneath it. In the case of a
+BR node the limiter will be the parent and the offset will point before or
+after the BR node. In the case of the text node the parent content is
+the text node itself and the offset will be the exact character position.
+The offset is not important to check for validity. Simply look at the
+passed in content. If it equals the limiter then the selection point is valid.
+If its parent it the limiter then the point is also valid. In the case of
+NO limiter all points are valid since you are in a topmost iframe. (browser
+or composer)
+*/
+bool nsFrameSelection::IsValidSelectionPoint(nsINode* aNode) const {
+ if (!aNode) {
+ return false;
+ }
+
+ nsIContent* limiter = GetLimiter();
+ if (limiter && limiter != aNode && limiter != aNode->GetParent()) {
+ // if newfocus == the limiter. that's ok. but if not there and not parent
+ // bad
+ return false; // not in the right content. tLimiter said so
+ }
+
+ limiter = GetAncestorLimiter();
+ return !limiter || aNode->IsInclusiveDescendantOf(limiter);
+}
+
+namespace mozilla {
+struct MOZ_RAII AutoPrepareFocusRange {
+ AutoPrepareFocusRange(Selection* aSelection,
+ const bool aMultiRangeSelection) {
+ MOZ_ASSERT(aSelection);
+ MOZ_ASSERT(aSelection->GetType() == SelectionType::eNormal);
+
+ if (aSelection->mStyledRanges.mRanges.Length() <= 1) {
+ return;
+ }
+
+ if (aSelection->mFrameSelection->IsUserSelectionReason()) {
+ mUserSelect.emplace(aSelection);
+ }
+
+ nsTArray<StyledRange>& ranges = aSelection->mStyledRanges.mRanges;
+ if (!aSelection->mUserInitiated || aMultiRangeSelection) {
+ // Scripted command or the user is starting a new explicit multi-range
+ // selection.
+ for (StyledRange& entry : ranges) {
+ MOZ_ASSERT(entry.mRange->IsDynamicRange());
+ entry.mRange->AsDynamicRange()->SetIsGenerated(false);
+ }
+ return;
+ }
+
+ if (!IsAnchorRelativeOperation(
+ aSelection->mFrameSelection->mSelectionChangeReasons)) {
+ return;
+ }
+
+ // This operation is against the anchor but our current mAnchorFocusRange
+ // represents the focus in a multi-range selection. The anchor from a user
+ // perspective is the most distant generated range on the opposite side.
+ // Find that range and make it the mAnchorFocusRange.
+ nsRange* const newAnchorFocusRange =
+ FindGeneratedRangeMostDistantFromAnchor(*aSelection);
+
+ if (!newAnchorFocusRange) {
+ // There are no generated ranges - that's fine.
+ return;
+ }
+
+ // Setup the new mAnchorFocusRange and mark the old one as generated.
+ if (aSelection->mAnchorFocusRange) {
+ aSelection->mAnchorFocusRange->SetIsGenerated(true);
+ }
+
+ newAnchorFocusRange->SetIsGenerated(false);
+ aSelection->mAnchorFocusRange = newAnchorFocusRange;
+
+ RemoveGeneratedRanges(*aSelection);
+
+ if (aSelection->mFrameSelection) {
+ aSelection->mFrameSelection->InvalidateDesiredCaretPos();
+ }
+ }
+
+ private:
+ static nsRange* FindGeneratedRangeMostDistantFromAnchor(
+ const Selection& aSelection) {
+ const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
+ const size_t len = ranges.Length();
+ nsRange* result{nullptr};
+ if (aSelection.GetDirection() == eDirNext) {
+ for (size_t i = 0; i < len; ++i) {
+ // This function is only called for selections with type == eNormal.
+ // (see MOZ_ASSERT in constructor).
+ // Therefore, all ranges must be dynamic.
+ if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
+ result = ranges[i].mRange->AsDynamicRange();
+ break;
+ }
+ }
+ } else {
+ size_t i = len;
+ while (i--) {
+ if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
+ result = ranges[i].mRange->AsDynamicRange();
+ break;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ static void RemoveGeneratedRanges(Selection& aSelection) {
+ RefPtr<nsPresContext> presContext = aSelection.GetPresContext();
+ nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
+ size_t i = ranges.Length();
+ while (i--) {
+ // This function is only called for selections with type == eNormal.
+ // (see MOZ_ASSERT in constructor).
+ // Therefore, all ranges must be dynamic.
+ if (!ranges[i].mRange->IsDynamicRange()) {
+ continue;
+ }
+ nsRange* range = ranges[i].mRange->AsDynamicRange();
+ if (range->IsGenerated()) {
+ range->UnregisterSelection(aSelection);
+ aSelection.SelectFrames(presContext, *range, false);
+ ranges.RemoveElementAt(i);
+ }
+ }
+ }
+
+ /**
+ * @aParam aSelectionChangeReasons can be multiple of the reasons defined in
+ nsISelectionListener.idl.
+ */
+ static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) {
+ return aSelectionChangeReasons &
+ (nsISelectionListener::DRAG_REASON |
+ nsISelectionListener::MOUSEDOWN_REASON |
+ nsISelectionListener::MOUSEUP_REASON |
+ nsISelectionListener::COLLAPSETOSTART_REASON);
+ }
+
+ Maybe<Selection::AutoUserInitiated> mUserSelect;
+};
+
+} // namespace mozilla
+
+////////////BEGIN nsFrameSelection methods
+
+template Result<RefPtr<nsRange>, nsresult>
+nsFrameSelection::CreateRangeExtendedToSomewhere(
+ nsDirection aDirection, const nsSelectionAmount aAmount,
+ CaretMovementStyle aMovementStyle);
+template Result<RefPtr<StaticRange>, nsresult>
+nsFrameSelection::CreateRangeExtendedToSomewhere(
+ nsDirection aDirection, const nsSelectionAmount aAmount,
+ CaretMovementStyle aMovementStyle);
+
+nsFrameSelection::nsFrameSelection(PresShell* aPresShell, nsIContent* aLimiter,
+ const bool aAccessibleCaretEnabled) {
+ for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
+ mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this);
+ }
+
+#ifdef XP_MACOSX
+ // On macOS, cache the current selection to send to service menu of macOS.
+ bool enableAutoCopy = true;
+ AutoCopyListener::Init(nsIClipboard::kSelectionCache);
+#else // #ifdef XP_MACOSX
+ // Check to see if the auto-copy pref is enabled and make the normal
+ // Selection notifies auto-copy listener of its changes.
+ bool enableAutoCopy = AutoCopyListener::IsPrefEnabled();
+ if (enableAutoCopy) {
+ AutoCopyListener::Init(nsIClipboard::kSelectionClipboard);
+ }
+#endif // #ifdef XP_MACOSX #else
+
+ if (enableAutoCopy) {
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (mDomSelections[index]) {
+ mDomSelections[index]->NotifyAutoCopy();
+ }
+ }
+
+ mPresShell = aPresShell;
+ mDragState = false;
+ mLimiters.mLimiter = aLimiter;
+
+ // This should only ever be initialized on the main thread, so we are OK here.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+
+ mAccessibleCaretEnabled = aAccessibleCaretEnabled;
+ if (mAccessibleCaretEnabled) {
+ mDomSelections[index]->MaybeNotifyAccessibleCaretEventHub(aPresShell);
+ }
+
+ if (mDomSelections[index]) {
+ mDomSelections[index]->EnableSelectionChangeEvent();
+ }
+}
+
+nsFrameSelection::~nsFrameSelection() = default;
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection)
+ for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
+ tmp->mDomSelections[i] = nullptr;
+ }
+ tmp->mHighlightSelections.Clear();
+
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(
+ mTableSelection.mClosestInclusiveTableCellAncestor)
+ tmp->mTableSelection.mMode = TableSelectionMode::None;
+ tmp->mTableSelection.mDragSelectingCells = false;
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mLimiter)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection)
+ if (tmp->mPresShell && tmp->mPresShell->GetDocument() &&
+ nsCCUncollectableMarker::InGeneration(
+ cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) {
+ return NS_SUCCESS_INTERRUPTED_TRAVERSE;
+ }
+ for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i])
+ }
+
+ for (const auto& value : tmp->mHighlightSelections) {
+ CycleCollectionNoteChild(cb, value.second().get(),
+ "mHighlightSelections[]");
+ }
+
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
+ mTableSelection.mClosestInclusiveTableCellAncestor)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mLimiter)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+bool nsFrameSelection::Caret::IsVisualMovement(
+ bool aContinueSelection, CaretMovementStyle aMovementStyle) const {
+ int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style();
+ return aMovementStyle == eVisual ||
+ (aMovementStyle == eUsePrefStyle &&
+ (movementFlag == 1 || (movementFlag == 2 && !aContinueSelection)));
+}
+
+// Get the x (or y, in vertical writing mode) position requested
+// by the Key Handling for line-up/down
+nsresult nsFrameSelection::DesiredCaretPos::FetchPos(
+ nsPoint& aDesiredCaretPos, const PresShell& aPresShell,
+ Selection& aNormalSelection) const {
+ MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal);
+
+ if (mIsSet) {
+ aDesiredCaretPos = mValue;
+ return NS_OK;
+ }
+
+ RefPtr<nsCaret> caret = aPresShell.GetCaret();
+ if (!caret) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ caret->SetSelection(&aNormalSelection);
+
+ nsRect coord;
+ nsIFrame* caretFrame = caret->GetGeometry(&coord);
+ if (!caretFrame) {
+ return NS_ERROR_FAILURE;
+ }
+ nsPoint viewOffset(0, 0);
+ nsView* view = nullptr;
+ caretFrame->GetOffsetFromView(viewOffset, &view);
+ if (view) {
+ coord += viewOffset;
+ }
+ aDesiredCaretPos = coord.TopLeft();
+ return NS_OK;
+}
+
+void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to
+ // mDesiredCaretPos.mValue;
+ // you must get another.
+{
+ mDesiredCaretPos.Invalidate();
+}
+
+void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; }
+
+void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) {
+ mValue = aPos;
+ mIsSet = true;
+}
+
+nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(
+ nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame,
+ nsPoint& aRetPoint) const {
+ //
+ // The whole point of this method is to return a frame and point that
+ // that lie within the same valid subtree as the anchor node's frame,
+ // for use with the method GetContentAndOffsetsFromPoint().
+ //
+ // A valid subtree is defined to be one where all the content nodes in
+ // the tree have a valid parent-child relationship.
+ //
+ // If the anchor frame and aFrame are in the same subtree, aFrame will
+ // be returned in aRetFrame. If they are in different subtrees, we
+ // return the frame for the root of the subtree.
+ //
+
+ if (!aFrame || !aRetFrame) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ *aRetFrame = aFrame;
+ aRetPoint = aPoint;
+
+ //
+ // Get the frame and content for the selection's anchor point!
+ //
+
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (!mDomSelections[index]) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsCOMPtr<nsIContent> anchorContent =
+ do_QueryInterface(mDomSelections[index]->GetAnchorNode());
+ if (!anchorContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ //
+ // Now find the root of the subtree containing the anchor's content.
+ //
+
+ NS_ENSURE_STATE(mPresShell);
+ RefPtr<PresShell> presShell = mPresShell;
+ nsIContent* anchorRoot = anchorContent->GetSelectionRootContent(presShell);
+ NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED);
+
+ //
+ // Now find the root of the subtree containing aFrame's content.
+ //
+
+ nsCOMPtr<nsIContent> content = aFrame->GetContent();
+
+ if (content) {
+ nsIContent* contentRoot = content->GetSelectionRootContent(presShell);
+ NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED);
+
+ if (anchorRoot == contentRoot) {
+ // If the aFrame's content isn't the capturing content, it should be
+ // a descendant. At this time, we can return simply.
+ nsIContent* capturedContent = PresShell::GetCapturingContent();
+ if (capturedContent != content) {
+ return NS_OK;
+ }
+
+ // Find the frame under the mouse cursor with the root frame.
+ // At this time, don't use the anchor's frame because it may not have
+ // fixed positioned frames.
+ nsIFrame* rootFrame = presShell->GetRootFrame();
+ nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame);
+ nsIFrame* cursorFrame =
+ nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot);
+
+ // If the mouse cursor in on a frame which is descendant of same
+ // selection root, we can expand the selection to the frame.
+ if (cursorFrame && cursorFrame->PresShell() == presShell) {
+ nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent();
+ NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE);
+ nsIContent* cursorContentRoot =
+ cursorContent->GetSelectionRootContent(presShell);
+ NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED);
+ if (cursorContentRoot == anchorRoot) {
+ *aRetFrame = cursorFrame;
+ aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame);
+ return NS_OK;
+ }
+ }
+ // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse
+ // cursor is out of the window), we should use the frame of the anchor
+ // root.
+ }
+ }
+
+ //
+ // When we can't find a frame which is under the mouse cursor and has a same
+ // selection root as the anchor node's, we should return the selection root
+ // frame.
+ //
+
+ *aRetFrame = anchorRoot->GetPrimaryFrame();
+
+ if (!*aRetFrame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ //
+ // Now make sure that aRetPoint is converted to the same coordinate
+ // system used by aRetFrame.
+ //
+
+ aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame);
+
+ return NS_OK;
+}
+
+void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint(
+ mozilla::intl::BidiEmbeddingLevel aLevel) {
+ // If the current level is undefined, we have just inserted new text.
+ // In this case, we don't want to reset the keyboard language
+ mCaret.mBidiLevel = aLevel;
+
+ RefPtr<nsCaret> caret;
+ if (mPresShell && (caret = mPresShell->GetCaret())) {
+ caret->SchedulePaint();
+ }
+}
+
+mozilla::intl::BidiEmbeddingLevel nsFrameSelection::GetCaretBidiLevel() const {
+ return mCaret.mBidiLevel;
+}
+
+void nsFrameSelection::UndefineCaretBidiLevel() {
+ mCaret.mBidiLevel = mozilla::intl::BidiEmbeddingLevel(mCaret.mBidiLevel |
+ BIDI_LEVEL_UNDEFINED);
+}
+
+#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 */
+
+static nsAtom* GetTag(nsINode* aNode) {
+ nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
+ if (!content) {
+ MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()");
+ return nullptr;
+ }
+
+ return content->NodeInfo()->NameAtom();
+}
+
+/**
+ * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor.
+ */
+static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) {
+ if (!aDomNode) return nullptr;
+ nsINode* current = aDomNode;
+ // Start with current node and look for a table cell
+ while (current) {
+ nsAtom* tag = GetTag(current);
+ if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) return current;
+ current = current->GetParent();
+ }
+ return nullptr;
+}
+
+static nsDirection GetCaretDirection(const nsIFrame& aFrame,
+ nsDirection aDirection,
+ bool aVisualMovement) {
+ const mozilla::intl::BidiDirection paragraphDirection =
+ nsBidiPresUtils::ParagraphDirection(&aFrame);
+ return (aVisualMovement &&
+ paragraphDirection == mozilla::intl::BidiDirection::RTL)
+ ? nsDirection(1 - aDirection)
+ : aDirection;
+}
+
+nsresult nsFrameSelection::MoveCaret(nsDirection aDirection,
+ bool aContinueSelection,
+ const nsSelectionAmount aAmount,
+ CaretMovementStyle aMovementStyle) {
+ NS_ENSURE_STATE(mPresShell);
+ // Flush out layout, since we need it to be up to date to do caret
+ // positioning.
+ OwningNonNull<PresShell> presShell(*mPresShell);
+ presShell->FlushPendingNotifications(FlushType::Layout);
+
+ if (!mPresShell) {
+ return NS_OK;
+ }
+
+ nsPresContext* context = mPresShell->GetPresContext();
+ if (!context) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ const RefPtr<Selection> sel = mDomSelections[index];
+ if (!sel) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE;
+ if (sel->IsEditorSelection()) {
+ // If caret moves in editor, it should cause scrolling even if it's in
+ // overflow: hidden;.
+ scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN;
+ }
+
+ const bool doCollapse = [&] {
+ if (sel->IsCollapsed() || aContinueSelection) {
+ return false;
+ }
+ if (aAmount > eSelectLine) {
+ return false;
+ }
+ int32_t caretStyle = StaticPrefs::layout_selection_caret_style();
+ return caretStyle == 2 || (caretStyle == 0 && aAmount != eSelectLine);
+ }();
+
+ if (doCollapse) {
+ if (aDirection == eDirPrevious) {
+ SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON);
+ mCaret.mHint = CARET_ASSOCIATE_AFTER;
+ } else {
+ SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON);
+ mCaret.mHint = CARET_ASSOCIATE_BEFORE;
+ }
+ } else {
+ SetChangeReasons(nsISelectionListener::KEYPRESS_REASON);
+ }
+
+ mCaretMoveAmount = aAmount;
+
+ AutoPrepareFocusRange prep(sel, false);
+
+ // we must keep this around and revalidate it when its just UP/DOWN
+ nsPoint desiredPos(0, 0);
+
+ if (aAmount == eSelectLine) {
+ nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel);
+ if (NS_FAILED(result)) {
+ return result;
+ }
+ mDesiredCaretPos.Set(desiredPos);
+ }
+
+ bool visualMovement =
+ mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
+ nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(visualMovement);
+ if (!frame) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Result<bool, nsresult> isIntraLineCaretMove = IsIntraLineCaretMove(aAmount);
+ nsDirection direction{aDirection};
+ if (isIntraLineCaretMove.isErr()) {
+ return isIntraLineCaretMove.unwrapErr();
+ }
+ if (isIntraLineCaretMove.inspect()) {
+ // Forget old caret position for moving caret to different line since
+ // caret position may be changed.
+ mDesiredCaretPos.Invalidate();
+ direction = GetCaretDirection(*frame, aDirection, visualMovement);
+ }
+
+ if (doCollapse) {
+ const nsRange* anchorFocusRange = sel->GetAnchorFocusRange();
+ if (anchorFocusRange) {
+ RefPtr<nsINode> node;
+ uint32_t offset;
+ if (visualMovement && nsBidiPresUtils::IsReversedDirectionFrame(frame)) {
+ direction = nsDirection(1 - direction);
+ }
+ if (direction == eDirPrevious) {
+ node = anchorFocusRange->GetStartContainer();
+ offset = anchorFocusRange->StartOffset();
+ } else {
+ node = anchorFocusRange->GetEndContainer();
+ offset = anchorFocusRange->EndOffset();
+ }
+ sel->CollapseInLimiter(node, offset);
+ }
+ sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
+ ScrollAxis(), ScrollAxis(), scrollFlags);
+ return NS_OK;
+ }
+
+ CaretAssociateHint tHint(mCaret.mHint); // temporary variable so we dont set
+ // mCaret.mHint until it is necessary
+
+ Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
+ direction, aContinueSelection, aAmount, aMovementStyle, desiredPos);
+ nsresult rv;
+ if (result.isOk() && result.inspect().mResultContent) {
+ const PeekOffsetStruct& pos = result.inspect();
+ nsIFrame* theFrame;
+ int32_t currentOffset, frameStart, frameEnd;
+
+ if (aAmount <= eSelectWordNoSpace) {
+ // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does
+ // not set pos.mAttachForward, so determine the hint here based on the
+ // result frame and offset: If we're at the end of a text frame, set the
+ // hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed
+ // at the end of this frame, not at the beginning of the next one.
+ theFrame = pos.mResultFrame;
+ std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
+ currentOffset = pos.mContentOffset;
+ if (frameEnd == currentOffset && !(frameStart == 0 && frameEnd == 0))
+ tHint = CARET_ASSOCIATE_BEFORE;
+ else
+ tHint = CARET_ASSOCIATE_AFTER;
+ } else {
+ // For up/down and home/end, pos.mResultFrame might not be set correctly,
+ // or not at all. In these cases, get the frame based on the content and
+ // hint returned by PeekOffset().
+ tHint = pos.mAttach;
+ theFrame = GetFrameForNodeOffset(pos.mResultContent, pos.mContentOffset,
+ tHint, &currentOffset);
+ if (!theFrame) return NS_ERROR_FAILURE;
+
+ std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
+ }
+
+ if (context->BidiEnabled()) {
+ switch (aAmount) {
+ case eSelectBeginLine:
+ case eSelectEndLine: {
+ // In Bidi contexts, PeekOffset calculates pos.mContentOffset
+ // differently depending on whether the movement is visual or logical.
+ // For visual movement, pos.mContentOffset depends on the direction-
+ // ality of the first/last frame on the line (theFrame), and the caret
+ // directionality must correspond.
+ FrameBidiData bidiData = theFrame->GetBidiData();
+ SetCaretBidiLevelAndMaybeSchedulePaint(
+ visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel);
+ break;
+ }
+ default:
+ // If the current position is not a frame boundary, it's enough just
+ // to take the Bidi level of the current frame
+ if ((pos.mContentOffset != frameStart &&
+ pos.mContentOffset != frameEnd) ||
+ eSelectLine == aAmount) {
+ SetCaretBidiLevelAndMaybeSchedulePaint(
+ theFrame->GetEmbeddingLevel());
+ } else {
+ BidiLevelFromMove(mPresShell, pos.mResultContent,
+ pos.mContentOffset, aAmount, tHint);
+ }
+ }
+ }
+ // "pos" is on the stack, so pos.mResultContent has stack lifetime, so using
+ // MOZ_KnownLive is ok.
+ const FocusMode focusMode = aContinueSelection
+ ? FocusMode::kExtendSelection
+ : FocusMode::kCollapseToNewPoint;
+ rv = TakeFocus(MOZ_KnownLive(*pos.mResultContent), pos.mContentOffset,
+ pos.mContentOffset, tHint, focusMode);
+ } else if (aAmount <= eSelectWordNoSpace && direction == eDirNext &&
+ !aContinueSelection) {
+ // Collapse selection if PeekOffset failed, we either
+ // 1. bumped into the BRFrame, bug 207623
+ // 2. had select-all in a text input (DIV range), bug 352759.
+ bool isBRFrame = frame->IsBrFrame();
+ RefPtr<nsINode> node = sel->GetFocusNode();
+ sel->CollapseInLimiter(node, sel->FocusOffset());
+ // Note: 'frame' might be dead here.
+ if (!isBRFrame) {
+ mCaret.mHint = CARET_ASSOCIATE_BEFORE; // We're now at the end of the
+ // frame to the left.
+ }
+ rv = NS_OK;
+ } else {
+ rv = result.isErr() ? result.unwrapErr() : NS_OK;
+ }
+ if (NS_SUCCEEDED(rv)) {
+ rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
+ ScrollAxis(), ScrollAxis(), scrollFlags);
+ }
+
+ return rv;
+}
+
+Result<PeekOffsetStruct, nsresult> nsFrameSelection::PeekOffsetForCaretMove(
+ nsDirection aDirection, bool aContinueSelection,
+ const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle,
+ const nsPoint& aDesiredCaretPos) const {
+ Selection* selection =
+ mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
+ if (!selection) {
+ return Err(NS_ERROR_NULL_POINTER);
+ }
+
+ const bool visualMovement =
+ mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
+
+ int32_t offsetused = 0;
+ nsIFrame* frame =
+ selection->GetPrimaryFrameForFocusNode(visualMovement, &offsetused);
+ if (!frame) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // set data using mLimiters.mLimiter to stop on scroll views. If we have a
+ // limiter then we stop peeking when we hit scrollable views. If no limiter
+ // then just let it go ahead
+ PeekOffsetOptions options{PeekOffsetOption::JumpLines,
+ PeekOffsetOption::IsKeyboardSelect};
+ if (mLimiters.mLimiter) {
+ options += PeekOffsetOption::ScrollViewStop;
+ }
+ if (visualMovement) {
+ options += PeekOffsetOption::Visual;
+ }
+ if (aContinueSelection) {
+ options += PeekOffsetOption::Extend;
+ }
+ if (selection->IsEditorSelection()) {
+ options += PeekOffsetOption::ForceEditableRegion;
+ }
+ PeekOffsetStruct pos(aAmount, aDirection, offsetused, aDesiredCaretPos,
+ options);
+ nsresult rv = frame->PeekOffset(&pos);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+ return pos;
+}
+
+nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
+ nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const {
+ return GetPrevNextBidiLevels(aNode, aContentOffset, mCaret.mHint, aJumpLines);
+}
+
+// static
+nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
+ nsIContent* aNode, uint32_t aContentOffset, CaretAssociateHint aHint,
+ bool aJumpLines) {
+ // Get the level of the frames on each side
+ nsIFrame* currentFrame;
+ int32_t currentOffset;
+ nsDirection direction;
+
+ nsPrevNextBidiLevels levels{};
+ levels.SetData(nullptr, nullptr, mozilla::intl::BidiEmbeddingLevel::LTR(),
+ mozilla::intl::BidiEmbeddingLevel::LTR());
+
+ currentFrame = GetFrameForNodeOffset(
+ aNode, static_cast<int32_t>(aContentOffset), aHint, &currentOffset);
+ if (!currentFrame) {
+ return levels;
+ }
+
+ auto [frameStart, frameEnd] = currentFrame->GetOffsets();
+
+ if (0 == frameStart && 0 == frameEnd) {
+ direction = eDirPrevious;
+ } else if (frameStart == currentOffset) {
+ direction = eDirPrevious;
+ } else if (frameEnd == currentOffset) {
+ direction = eDirNext;
+ } else {
+ // we are neither at the beginning nor at the end of the frame, so we have
+ // no worries
+ mozilla::intl::BidiEmbeddingLevel currentLevel =
+ currentFrame->GetEmbeddingLevel();
+ levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel);
+ return levels;
+ }
+
+ PeekOffsetOptions peekOffsetOptions{PeekOffsetOption::ScrollViewStop};
+ if (aJumpLines) {
+ peekOffsetOptions += PeekOffsetOption::JumpLines;
+ }
+ nsIFrame* newFrame =
+ currentFrame->GetFrameFromDirection(direction, peekOffsetOptions).mFrame;
+
+ FrameBidiData currentBidi = currentFrame->GetBidiData();
+ mozilla::intl::BidiEmbeddingLevel currentLevel = currentBidi.embeddingLevel;
+ mozilla::intl::BidiEmbeddingLevel newLevel =
+ newFrame ? newFrame->GetEmbeddingLevel() : currentBidi.baseLevel;
+
+ // If not jumping lines, disregard br frames, since they might be positioned
+ // incorrectly.
+ // XXX This could be removed once bug 339786 is fixed.
+ if (!aJumpLines) {
+ if (currentFrame->IsBrFrame()) {
+ currentFrame = nullptr;
+ currentLevel = currentBidi.baseLevel;
+ }
+ if (newFrame && newFrame->IsBrFrame()) {
+ newFrame = nullptr;
+ newLevel = currentBidi.baseLevel;
+ }
+ }
+
+ if (direction == eDirNext)
+ levels.SetData(currentFrame, newFrame, currentLevel, newLevel);
+ else
+ levels.SetData(newFrame, currentFrame, newLevel, currentLevel);
+
+ return levels;
+}
+
+nsresult nsFrameSelection::GetFrameFromLevel(
+ nsIFrame* aFrameIn, nsDirection aDirection,
+ mozilla::intl::BidiEmbeddingLevel aBidiLevel, nsIFrame** aFrameOut) const {
+ NS_ENSURE_STATE(mPresShell);
+ mozilla::intl::BidiEmbeddingLevel foundLevel =
+ mozilla::intl::BidiEmbeddingLevel::LTR();
+ nsIFrame* foundFrame = aFrameIn;
+
+ nsCOMPtr<nsIFrameEnumerator> frameTraversal;
+ nsresult result;
+ nsCOMPtr<nsIFrameTraversal> trav(
+ do_CreateInstance(kFrameTraversalCID, &result));
+ if (NS_FAILED(result)) return result;
+
+ result =
+ trav->NewFrameTraversal(getter_AddRefs(frameTraversal),
+ mPresShell->GetPresContext(), aFrameIn, eLeaf,
+ false, // aVisual
+ false, // aLockInScrollView
+ false, // aFollowOOFs
+ false // aSkipPopupChecks
+ );
+ if (NS_FAILED(result)) return result;
+
+ do {
+ *aFrameOut = foundFrame;
+ foundFrame = frameTraversal->Traverse(aDirection == eDirNext);
+ if (!foundFrame) return NS_ERROR_FAILURE;
+ foundLevel = foundFrame->GetEmbeddingLevel();
+
+ } while (foundLevel > aBidiLevel);
+
+ return NS_OK;
+}
+
+nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) {
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (!mDomSelections[index]) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ mMaintainedRange.MaintainAnchorFocusRange(*mDomSelections[index], aAmount);
+
+ return NS_OK;
+}
+
+void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell,
+ nsIContent* aNode,
+ uint32_t aContentOffset,
+ nsSelectionAmount aAmount,
+ CaretAssociateHint aHint) {
+ switch (aAmount) {
+ // Movement within the line: the new cursor Bidi level is the level of the
+ // last character moved over
+ case eSelectCharacter:
+ case eSelectCluster:
+ case eSelectWord:
+ case eSelectWordNoSpace:
+ case eSelectBeginLine:
+ case eSelectEndLine:
+ case eSelectNoAmount: {
+ nsPrevNextBidiLevels levels =
+ GetPrevNextBidiLevels(aNode, aContentOffset, aHint, false);
+
+ SetCaretBidiLevelAndMaybeSchedulePaint(aHint == CARET_ASSOCIATE_BEFORE
+ ? levels.mLevelBefore
+ : levels.mLevelAfter);
+ break;
+ }
+ /*
+ // Up and Down: the new cursor Bidi level is the smaller of the two
+ surrounding characters case eSelectLine: case eSelectParagraph:
+ GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame,
+ &secondFrame, &firstLevel, &secondLevel);
+ aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel,
+ secondLevel)); break;
+ */
+
+ default:
+ UndefineCaretBidiLevel();
+ }
+}
+
+void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode,
+ uint32_t aContentOffset) {
+ nsIFrame* clickInFrame = nullptr;
+ int32_t OffsetNotUsed;
+
+ clickInFrame = GetFrameForNodeOffset(aNode, aContentOffset, mCaret.mHint,
+ &OffsetNotUsed);
+ if (!clickInFrame) return;
+
+ SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel());
+}
+
+void nsFrameSelection::MaintainedRange::AdjustNormalSelection(
+ const nsIContent* aContent, const int32_t aOffset,
+ Selection& aNormalSelection) const {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ if (!mRange || !aContent) {
+ return;
+ }
+
+ nsINode* rangeStartNode = mRange->GetStartContainer();
+ nsINode* rangeEndNode = mRange->GetEndContainer();
+ const uint32_t rangeStartOffset = mRange->StartOffset();
+ const uint32_t rangeEndOffset = mRange->EndOffset();
+
+ NS_ASSERTION(aOffset >= 0, "aOffset should not be negative");
+ const Maybe<int32_t> relToStart =
+ nsContentUtils::ComparePoints_AllowNegativeOffsets(
+ rangeStartNode, rangeStartOffset, aContent, aOffset);
+ if (NS_WARN_IF(!relToStart)) {
+ // Potentially handle this properly when Selection across Shadow DOM
+ // boundary is implemented
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
+ return;
+ }
+
+ const Maybe<int32_t> relToEnd =
+ nsContentUtils::ComparePoints_AllowNegativeOffsets(
+ rangeEndNode, rangeEndOffset, aContent, aOffset);
+ if (NS_WARN_IF(!relToEnd)) {
+ // Potentially handle this properly when Selection across Shadow DOM
+ // boundary is implemented
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
+ return;
+ }
+
+ // If aContent/aOffset is inside (or at the edge of) the maintained
+ // selection, or if it is on the "anchor" side of the maintained selection,
+ // we need to do something.
+ if ((*relToStart <= 0 && *relToEnd >= 0) ||
+ (*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) ||
+ (*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) {
+ // Set the current range to the maintained range.
+ aNormalSelection.ReplaceAnchorFocusRange(mRange);
+ // Set the direction of the selection so that the anchor will be on the
+ // far side of the maintained selection, relative to aContent/aOffset.
+ aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext);
+ }
+}
+
+void nsFrameSelection::MaintainedRange::AdjustContentOffsets(
+ nsIFrame::ContentOffsets& aOffsets, const bool aScrollViewStop) const {
+ // Adjust offsets according to maintained amount
+ if (mRange && mAmount != eSelectNoAmount) {
+ nsINode* rangenode = mRange->GetStartContainer();
+ int32_t rangeOffset = mRange->StartOffset();
+ const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints(
+ rangenode, rangeOffset, aOffsets.content, aOffsets.offset);
+ if (NS_WARN_IF(!relativePosition)) {
+ // Potentially handle this properly when Selection across Shadow DOM
+ // boundary is implemented
+ // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
+ return;
+ }
+
+ nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext;
+ nsSelectionAmount amount = mAmount;
+ if (amount == eSelectBeginLine && direction == eDirNext) {
+ amount = eSelectEndLine;
+ }
+
+ int32_t offset;
+ nsIFrame* frame = GetFrameForNodeOffset(aOffsets.content, aOffsets.offset,
+ CARET_ASSOCIATE_AFTER, &offset);
+
+ PeekOffsetOptions peekOffsetOptions{};
+ if (aScrollViewStop) {
+ peekOffsetOptions += PeekOffsetOption::ScrollViewStop;
+ }
+ if (frame && amount == eSelectWord && direction == eDirPrevious) {
+ // To avoid selecting the previous word when at start of word,
+ // first move one character forward.
+ PeekOffsetStruct charPos(eSelectCharacter, eDirNext, offset,
+ nsPoint(0, 0), peekOffsetOptions);
+ if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) {
+ frame = charPos.mResultFrame;
+ offset = charPos.mContentOffset;
+ }
+ }
+
+ PeekOffsetStruct pos(amount, direction, offset, nsPoint(0, 0),
+ peekOffsetOptions);
+ if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) {
+ aOffsets.content = pos.mResultContent;
+ aOffsets.offset = pos.mContentOffset;
+ }
+ }
+}
+
+void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange(
+ const Selection& aNormalSelection, const nsSelectionAmount aAmount) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ mAmount = aAmount;
+
+ const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange();
+ if (anchorFocusRange && aAmount != eSelectNoAmount) {
+ mRange = anchorFocusRange->CloneRange();
+ return;
+ }
+
+ mRange = nullptr;
+}
+
+nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus,
+ uint32_t aContentOffset,
+ uint32_t aContentEndOffset,
+ const FocusMode aFocusMode,
+ CaretAssociateHint aHint) {
+ if (!aNewFocus) return NS_ERROR_INVALID_ARG;
+
+ if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) {
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
+ ("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i",
+ __FUNCTION__,
+ mDomSelections[index] ? mDomSelections[index].get() : nullptr,
+ aNewFocus, aContentOffset, aContentEndOffset,
+ static_cast<int>(aFocusMode)));
+ }
+
+ mDesiredCaretPos.Invalidate();
+
+ if (aFocusMode != FocusMode::kExtendSelection) {
+ mMaintainedRange.mRange = nullptr;
+ if (!IsValidSelectionPoint(aNewFocus)) {
+ mLimiters.mAncestorLimiter = nullptr;
+ }
+ }
+
+ // Don't take focus when dragging off of a table
+ if (!mTableSelection.mDragSelectingCells) {
+ BidiLevelFromClick(aNewFocus, aContentOffset);
+ SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON +
+ nsISelectionListener::DRAG_REASON);
+
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ RefPtr<Selection> selection = mDomSelections[index];
+ MOZ_ASSERT(selection);
+
+ if (aFocusMode == FocusMode::kExtendSelection) {
+ mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset,
+ *selection);
+ }
+
+ AutoPrepareFocusRange prep(selection,
+ aFocusMode == FocusMode::kMultiRangeSelection);
+ return TakeFocus(*aNewFocus, aContentOffset, aContentEndOffset, aHint,
+ aFocusMode);
+ }
+
+ return NS_OK;
+}
+
+void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) {
+ if (!aFrame || !mPresShell) {
+ return;
+ }
+
+ nsresult result;
+ nsIFrame* newFrame = 0;
+ nsPoint newPoint;
+
+ result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame,
+ newPoint);
+ if (NS_FAILED(result)) return;
+ if (!newFrame) return;
+
+ nsIFrame::ContentOffsets offsets =
+ newFrame->GetContentOffsetsFromPoint(newPoint);
+ if (!offsets.content) return;
+
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ RefPtr<Selection> selection = mDomSelections[index];
+ if (newFrame->IsSelected() && selection) {
+ // `MOZ_KnownLive` required because of
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1636889.
+ mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content),
+ offsets.offset, *selection);
+ }
+
+ const bool scrollViewStop = mLimiters.mLimiter != nullptr;
+ mMaintainedRange.AdjustContentOffsets(offsets, scrollViewStop);
+
+ // TODO: no click has happened, rename `HandleClick`.
+ HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset,
+ offsets.offset, FocusMode::kExtendSelection, offsets.associate);
+}
+
+nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame,
+ const nsPoint& aPoint,
+ uint32_t aDelay) {
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (!mDomSelections[index]) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<Selection> selection = mDomSelections[index];
+ return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay);
+}
+
+void nsFrameSelection::StopAutoScrollTimer() {
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (!mDomSelections[index]) {
+ return;
+ }
+
+ mDomSelections[index]->StopAutoScrollTimer();
+}
+
+// static
+nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell(
+ nsPresContext* aContext, nsIContent* aContent) {
+ if (!aContext) {
+ return nullptr;
+ }
+
+ RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext);
+ if (!htmlEditor) {
+ return nullptr;
+ }
+
+ nsINode* inclusiveTableCellAncestor =
+ GetClosestInclusiveTableCellAncestor(aContent);
+ if (!inclusiveTableCellAncestor) {
+ return nullptr;
+ }
+
+ const Element* editingHost = htmlEditor->ComputeEditingHost();
+ if (!editingHost) {
+ return nullptr;
+ }
+
+ const bool editableCell =
+ inclusiveTableCellAncestor->IsInclusiveDescendantOf(editingHost);
+ return editableCell ? inclusiveTableCellAncestor : nullptr;
+}
+
+namespace {
+struct ParentAndOffset {
+ explicit ParentAndOffset(const nsINode& aNode)
+ : mParent{aNode.GetParent()},
+ mOffset{mParent ? mParent->ComputeIndexOf_Deprecated(&aNode) : 0} {}
+
+ nsINode* mParent;
+
+ // 0, if there's no parent.
+ int32_t mOffset;
+};
+
+} // namespace
+/**
+hard to go from nodes to frames, easy the other way!
+ */
+nsresult nsFrameSelection::TakeFocus(nsIContent& aNewFocus,
+ uint32_t aContentOffset,
+ uint32_t aContentEndOffset,
+ CaretAssociateHint aHint,
+ const FocusMode aFocusMode) {
+ NS_ENSURE_STATE(mPresShell);
+
+ if (!IsValidSelectionPoint(&aNewFocus)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose,
+ ("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i",
+ __FUNCTION__, &aNewFocus, aContentOffset, aContentEndOffset,
+ static_cast<int>(aHint), static_cast<int>(aFocusMode)));
+
+ mPresShell->FrameSelectionWillTakeFocus(*this);
+
+ // Clear all table selection data
+ mTableSelection.mMode = TableSelectionMode::None;
+ mTableSelection.mDragSelectingCells = false;
+ mTableSelection.mStartSelectedCell = nullptr;
+ mTableSelection.mEndSelectedCell = nullptr;
+ mTableSelection.mAppendStartSelectedCell = nullptr;
+ mCaret.mHint = aHint;
+
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
+
+ Maybe<Selection::AutoUserInitiated> userSelect;
+ if (IsUserSelectionReason()) {
+ userSelect.emplace(mDomSelections[index]);
+ }
+
+ // traverse through document and unselect crap here
+ switch (aFocusMode) {
+ case FocusMode::kCollapseToNewPoint:
+ [[fallthrough]];
+ case FocusMode::kMultiRangeSelection: {
+ // single click? setting cursor down
+ const Batching saveBatching =
+ mBatching; // hack to use the collapse code.
+ mBatching.mCounter = 1;
+
+ RefPtr<Selection> selection = mDomSelections[index];
+
+ if (aFocusMode == FocusMode::kMultiRangeSelection) {
+ // Remove existing collapsed ranges as there's no point in having
+ // non-anchor/focus collapsed ranges.
+ selection->RemoveCollapsedRanges();
+
+ ErrorResult error;
+ RefPtr<nsRange> newRange = nsRange::Create(
+ &aNewFocus, aContentOffset, &aNewFocus, aContentOffset, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+ MOZ_ASSERT(newRange);
+ selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange,
+ IgnoreErrors());
+ } else {
+ bool oldDesiredPosSet =
+ mDesiredCaretPos.mIsSet; // need to keep old desired
+ // position if it was set.
+ selection->CollapseInLimiter(&aNewFocus, aContentOffset);
+ mDesiredCaretPos.mIsSet =
+ oldDesiredPosSet; // now reset desired pos back.
+ }
+
+ mBatching = saveBatching;
+
+ if (aContentEndOffset != aContentOffset) {
+ selection->Extend(&aNewFocus, aContentEndOffset);
+ }
+
+ // find out if we are inside a table. if so, find out which one and which
+ // cell once we do that, the next time we get a takefocus, check the
+ // parent tree. if we are no longer inside same table ,cell then switch to
+ // table selection mode. BUT only do this in an editor
+
+ NS_ENSURE_STATE(mPresShell);
+ RefPtr<nsPresContext> context = mPresShell->GetPresContext();
+ mTableSelection.mClosestInclusiveTableCellAncestor = nullptr;
+ if (nsINode* inclusiveTableCellAncestor =
+ TableSelection::IsContentInActivelyEditableTableCell(
+ context, &aNewFocus)) {
+ mTableSelection.mClosestInclusiveTableCellAncestor =
+ inclusiveTableCellAncestor;
+ MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
+ ("%s: Collapsing into new cell", __FUNCTION__));
+ }
+
+ break;
+ }
+ case FocusMode::kExtendSelection: {
+ // Now update the range list:
+ nsINode* inclusiveTableCellAncestor =
+ GetClosestInclusiveTableCellAncestor(&aNewFocus);
+ if (mTableSelection.mClosestInclusiveTableCellAncestor &&
+ inclusiveTableCellAncestor &&
+ inclusiveTableCellAncestor !=
+ mTableSelection
+ .mClosestInclusiveTableCellAncestor) // switch to cell
+ // selection mode
+ {
+ MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
+ ("%s: moving into new cell", __FUNCTION__));
+
+ WidgetMouseEvent event(false, eVoidEvent, nullptr,
+ WidgetMouseEvent::eReal);
+
+ // Start selecting in the cell we were in before
+ ParentAndOffset parentAndOffset{
+ *mTableSelection.mClosestInclusiveTableCellAncestor};
+ if (parentAndOffset.mParent) {
+ const nsresult result = HandleTableSelection(
+ parentAndOffset.mParent, parentAndOffset.mOffset,
+ TableSelectionMode::Cell, &event);
+ if (NS_WARN_IF(NS_FAILED(result))) {
+ return result;
+ }
+ }
+
+ // Find the parent of this new cell and extend selection to it
+ parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor};
+
+ // XXXX We need to REALLY get the current key shift state
+ // (we'd need to add event listener -- let's not bother for now)
+ event.mModifiers &= ~MODIFIER_SHIFT; // aContinueSelection;
+ if (parentAndOffset.mParent) {
+ mTableSelection.mClosestInclusiveTableCellAncestor =
+ inclusiveTableCellAncestor;
+ // Continue selection into next cell
+ const nsresult result = HandleTableSelection(
+ parentAndOffset.mParent, parentAndOffset.mOffset,
+ TableSelectionMode::Cell, &event);
+ if (NS_WARN_IF(NS_FAILED(result))) {
+ return result;
+ }
+ }
+ } else {
+ RefPtr<Selection> selection = mDomSelections[index];
+ // XXXX Problem: Shift+click in browser is appending text selection to
+ // selected table!!!
+ // is this the place to erase selected cells ?????
+ uint32_t offset =
+ (selection->GetDirection() == eDirNext &&
+ aContentEndOffset > aContentOffset) // didn't go far enough
+ ? aContentEndOffset // this will only redraw the diff
+ : aContentOffset;
+ selection->Extend(&aNewFocus, offset);
+ }
+ break;
+ }
+ }
+
+ // Don't notify selection listeners if batching is on:
+ if (IsBatching()) {
+ return NS_OK;
+ }
+
+ // Be aware, the Selection instance may be destroyed after this call.
+ return NotifySelectionListeners(SelectionType::eNormal);
+}
+
+UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection(
+ nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength,
+ bool aSlowCheck) const {
+ if (!aContent || !mPresShell) {
+ return nullptr;
+ }
+
+ // TODO: Layout should use `uint32_t` for handling offset in DOM nodes
+ // (for example: bug 1735262)
+ MOZ_ASSERT(aContentOffset >= 0);
+ MOZ_ASSERT(aContentLength >= 0);
+ if (MOZ_UNLIKELY(aContentOffset < 0) || MOZ_UNLIKELY(aContentLength < 0)) {
+ return nullptr;
+ }
+
+ UniquePtr<SelectionDetails> details;
+
+ for (size_t j = 0; j < ArrayLength(mDomSelections); j++) {
+ if (mDomSelections[j]) {
+ details = mDomSelections[j]->LookUpSelection(
+ aContent, static_cast<uint32_t>(aContentOffset),
+ static_cast<uint32_t>(aContentLength), std::move(details),
+ kPresentSelectionTypes[j], aSlowCheck);
+ }
+ }
+
+ // This may seem counter intuitive at first. Highlight selections need to be
+ // iterated from back to front:
+ //
+ // - `mHighlightSelections` is ordered by insertion, i.e. if two or more
+ // highlights overlap, the latest must take precedence.
+ // - however, the `LookupSelection()` algorithm reverses the order by setting
+ // the current `details` as `mNext`.
+ for (const auto& iter : Reversed(mHighlightSelections)) {
+ details = iter.second()->LookUpSelection(
+ aContent, static_cast<uint32_t>(aContentOffset),
+ static_cast<uint32_t>(aContentLength), std::move(details),
+ SelectionType::eHighlight, aSlowCheck);
+ }
+
+ return details;
+}
+
+void nsFrameSelection::SetDragState(bool aState) {
+ if (mDragState == aState) return;
+
+ mDragState = aState;
+
+ if (!mDragState) {
+ mTableSelection.mDragSelectingCells = false;
+ // Notify that reason is mouse up.
+ SetChangeReasons(nsISelectionListener::MOUSEUP_REASON);
+
+ // flag is set to false in `NotifySelectionListeners`.
+ // since this function call is part of click event, this would immediately
+ // reset the flag, rendering it useless.
+ AutoRestore<bool> restoreIsDoubleClickSelectionFlag(
+ mIsDoubleClickSelection);
+ // Be aware, the Selection instance may be destroyed after this call.
+ NotifySelectionListeners(SelectionType::eNormal);
+ }
+}
+
+Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const {
+ int8_t index = GetIndexFromSelectionType(aSelectionType);
+ if (index < 0) return nullptr;
+
+ return mDomSelections[index];
+}
+
+void nsFrameSelection::AddHighlightSelection(
+ const nsAtom* aHighlightName, const mozilla::dom::Highlight& aHighlight) {
+ RefPtr<Selection> selection =
+ aHighlight.CreateHighlightSelection(aHighlightName, this);
+ if (auto iter =
+ std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
+ [&aHighlightName](auto const& aElm) {
+ return aElm.first() == aHighlightName;
+ });
+ iter != mHighlightSelections.end()) {
+ iter->second() = std::move(selection);
+ } else {
+ mHighlightSelections.AppendElement(
+ CompactPair<RefPtr<const nsAtom>, RefPtr<Selection>>(
+ aHighlightName, std::move(selection)));
+ }
+}
+
+void nsFrameSelection::RemoveHighlightSelection(const nsAtom* aHighlightName) {
+ if (auto iter =
+ std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
+ [&aHighlightName](auto const& aElm) {
+ return aElm.first() == aHighlightName;
+ });
+ iter != mHighlightSelections.end()) {
+ RefPtr<Selection> selection = iter->second();
+ selection->RemoveAllRanges(IgnoreErrors());
+ mHighlightSelections.RemoveElementAt(iter);
+ }
+}
+
+void nsFrameSelection::AddHighlightSelectionRange(
+ const nsAtom* aHighlightName, const mozilla::dom::Highlight& aHighlight,
+ mozilla::dom::AbstractRange& aRange) {
+ if (auto iter =
+ std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
+ [&aHighlightName](auto const& aElm) {
+ return aElm.first() == aHighlightName;
+ });
+ iter != mHighlightSelections.end()) {
+ RefPtr<Selection> selection = iter->second();
+ selection->AddHighlightRangeAndSelectFramesAndNotifyListeners(aRange);
+ } else {
+ // if the selection does not exist yet, add all of its ranges and exit.
+ RefPtr<Selection> selection =
+ aHighlight.CreateHighlightSelection(aHighlightName, this);
+ mHighlightSelections.AppendElement(
+ CompactPair<RefPtr<const nsAtom>, RefPtr<Selection>>(
+ aHighlightName, std::move(selection)));
+ }
+}
+
+void nsFrameSelection::RemoveHighlightSelectionRange(
+ const nsAtom* aHighlightName, mozilla::dom::AbstractRange& aRange) {
+ if (auto iter =
+ std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
+ [&aHighlightName](auto const& aElm) {
+ return aElm.first() == aHighlightName;
+ });
+ iter != mHighlightSelections.end()) {
+ // NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
+ RefPtr<Selection> selection = iter->second();
+ selection->RemoveRangeAndUnselectFramesAndNotifyListeners(aRange,
+ IgnoreErrors());
+ }
+}
+
+nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType,
+ SelectionRegion aRegion,
+ int16_t aFlags) const {
+ int8_t index = GetIndexFromSelectionType(aSelectionType);
+ if (index < 0) return NS_ERROR_INVALID_ARG;
+
+ if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
+
+ ScrollAxis verticalScroll = ScrollAxis();
+ int32_t flags = Selection::SCROLL_DO_FLUSH;
+ if (aFlags & nsISelectionController::SCROLL_SYNCHRONOUS) {
+ flags |= Selection::SCROLL_SYNCHRONOUS;
+ } else if (aFlags & nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY) {
+ flags |= Selection::SCROLL_FIRST_ANCESTOR_ONLY;
+ }
+ if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) {
+ flags |= Selection::SCROLL_OVERFLOW_HIDDEN;
+ }
+ if (aFlags & nsISelectionController::SCROLL_CENTER_VERTICALLY) {
+ verticalScroll =
+ ScrollAxis(WhereToScroll::Center, WhenToScroll::IfNotFullyVisible);
+ }
+ if (aFlags & nsISelectionController::SCROLL_FOR_CARET_MOVE) {
+ flags |= Selection::SCROLL_FOR_CARET_MOVE;
+ }
+
+ // After ScrollSelectionIntoView(), the pending notifications might be
+ // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
+ RefPtr<Selection> sel = mDomSelections[index];
+ return sel->ScrollIntoView(aRegion, verticalScroll, ScrollAxis(), flags);
+}
+
+nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) {
+ int8_t index = GetIndexFromSelectionType(aSelectionType);
+ if (index < 0) return NS_ERROR_INVALID_ARG;
+ if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
+ NS_ENSURE_STATE(mPresShell);
+
+// On macOS, update the selection cache to the new active selection
+// aka the current selection.
+#ifdef XP_MACOSX
+ // Check that we're in the an active window and, if this is Web content,
+ // in the frontmost tab.
+ Document* doc = mPresShell->GetDocument();
+ if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) {
+ UpdateSelectionCacheOnRepaintSelection(mDomSelections[index]);
+ }
+#endif
+ return mDomSelections[index]->Repaint(mPresShell->GetPresContext());
+}
+
+static bool IsDisplayContents(const nsIContent* aContent) {
+ return aContent->IsElement() && aContent->AsElement()->IsDisplayContents();
+}
+
+bool nsFrameSelection::AdjustFrameForLineStart(nsIFrame*& aFrame,
+ int32_t& aFrameOffset) {
+ if (!aFrame->HasSignificantTerminalNewline()) {
+ return false;
+ }
+
+ auto [start, end] = aFrame->GetOffsets();
+ if (aFrameOffset != end) {
+ return false;
+ }
+
+ nsIFrame* nextSibling = aFrame->GetNextSibling();
+ if (!nextSibling) {
+ return false;
+ }
+
+ aFrame = nextSibling;
+ std::tie(start, end) = aFrame->GetOffsets();
+ aFrameOffset = start;
+ return true;
+}
+
+// static
+nsIFrame* nsFrameSelection::GetFrameForNodeOffset(nsIContent* aNode,
+ int32_t aOffset,
+ CaretAssociateHint aHint,
+ int32_t* aReturnOffset) {
+ if (!aNode || !aReturnOffset) return nullptr;
+
+ if (aOffset < 0) return nullptr;
+
+ if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) {
+ return nullptr;
+ }
+
+ nsIFrame* returnFrame = nullptr;
+ nsCOMPtr<nsIContent> theNode;
+
+ while (true) {
+ *aReturnOffset = aOffset;
+
+ theNode = aNode;
+
+ if (aNode->IsElement()) {
+ int32_t childIndex = 0;
+ int32_t numChildren = theNode->GetChildCount();
+
+ if (aHint == CARET_ASSOCIATE_BEFORE) {
+ if (aOffset > 0) {
+ childIndex = aOffset - 1;
+ } else {
+ childIndex = aOffset;
+ }
+ } else {
+ NS_ASSERTION(aHint == CARET_ASSOCIATE_AFTER, "unknown direction");
+ if (aOffset >= numChildren) {
+ if (numChildren > 0) {
+ childIndex = numChildren - 1;
+ } else {
+ childIndex = 0;
+ }
+ } else {
+ childIndex = aOffset;
+ }
+ }
+
+ if (childIndex > 0 || numChildren > 0) {
+ nsCOMPtr<nsIContent> childNode =
+ theNode->GetChildAt_Deprecated(childIndex);
+
+ if (!childNode) {
+ break;
+ }
+
+ theNode = childNode;
+ }
+
+ // Now that we have the child node, check if it too
+ // can contain children. If so, descend into child.
+ if (theNode->IsElement() && theNode->GetChildCount() &&
+ !theNode->HasIndependentSelection()) {
+ aNode = theNode;
+ aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0;
+ continue;
+ } else {
+ // Check to see if theNode is a text node. If it is, translate
+ // aOffset into an offset into the text node.
+
+ RefPtr<Text> textNode = theNode->GetAsText();
+ if (textNode) {
+ if (theNode->GetPrimaryFrame()) {
+ if (aOffset > childIndex) {
+ uint32_t textLength = textNode->Length();
+
+ *aReturnOffset = (int32_t)textLength;
+ } else {
+ *aReturnOffset = 0;
+ }
+ } else {
+ int32_t numChildren = aNode->GetChildCount();
+ int32_t newChildIndex = aHint == CARET_ASSOCIATE_BEFORE
+ ? childIndex - 1
+ : childIndex + 1;
+
+ if (newChildIndex >= 0 && newChildIndex < numChildren) {
+ nsCOMPtr<nsIContent> newChildNode =
+ aNode->GetChildAt_Deprecated(newChildIndex);
+ if (!newChildNode) {
+ return nullptr;
+ }
+
+ aNode = newChildNode;
+ aOffset =
+ aHint == CARET_ASSOCIATE_BEFORE ? aNode->GetChildCount() : 0;
+ continue;
+ } else {
+ // newChildIndex is illegal which means we're at first or last
+ // child. Just use original node to get the frame.
+ theNode = aNode;
+ }
+ }
+ }
+ }
+ }
+
+ // If the node is a ShadowRoot, the frame needs to be adjusted,
+ // because a ShadowRoot does not get a frame. Its children are rendered
+ // as children of the host.
+ if (ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) {
+ theNode = shadow->GetHost();
+ }
+
+ returnFrame = theNode->GetPrimaryFrame();
+ if (!returnFrame) {
+ if (aHint == CARET_ASSOCIATE_BEFORE) {
+ if (aOffset > 0) {
+ --aOffset;
+ continue;
+ } else {
+ break;
+ }
+ } else {
+ int32_t end = theNode->GetChildCount();
+ if (aOffset < end) {
+ ++aOffset;
+ continue;
+ } else {
+ break;
+ }
+ }
+ }
+
+ break;
+ } // end while
+
+ if (!returnFrame) return nullptr;
+
+ // If we ended up here and were asked to position the caret after a visible
+ // break, let's return the frame on the next line instead if it exists.
+ if (aOffset > 0 && (uint32_t)aOffset >= aNode->Length() &&
+ theNode == aNode->GetLastChild()) {
+ nsIFrame* newFrame;
+ nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame);
+ if (newFrame) {
+ returnFrame = newFrame;
+ *aReturnOffset = 0;
+ }
+ }
+
+ // find the child frame containing the offset we want
+ returnFrame->GetChildFrameContainingOffset(
+ *aReturnOffset, aHint == CARET_ASSOCIATE_AFTER, &aOffset, &returnFrame);
+ return returnFrame;
+}
+
+nsIFrame* nsFrameSelection::GetFrameToPageSelect() const {
+ if (NS_WARN_IF(!mPresShell)) {
+ return nullptr;
+ }
+
+ nsIFrame* rootFrameToSelect;
+ if (mLimiters.mLimiter) {
+ rootFrameToSelect = mLimiters.mLimiter->GetPrimaryFrame();
+ if (NS_WARN_IF(!rootFrameToSelect)) {
+ return nullptr;
+ }
+ } else if (mLimiters.mAncestorLimiter) {
+ rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame();
+ if (NS_WARN_IF(!rootFrameToSelect)) {
+ return nullptr;
+ }
+ } else {
+ rootFrameToSelect = mPresShell->GetRootScrollFrame();
+ if (NS_WARN_IF(!rootFrameToSelect)) {
+ return nullptr;
+ }
+ }
+
+ nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling();
+ if (contentToSelect) {
+ // If there is selected content, look for nearest and vertical scrollable
+ // parent under the root frame.
+ for (nsIFrame* frame = contentToSelect->GetPrimaryFrame();
+ frame && frame != rootFrameToSelect; frame = frame->GetParent()) {
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(frame);
+ if (!scrollableFrame) {
+ continue;
+ }
+ ScrollStyles scrollStyles = scrollableFrame->GetScrollStyles();
+ if (scrollStyles.mVertical == StyleOverflow::Hidden) {
+ continue;
+ }
+ layers::ScrollDirections directions =
+ scrollableFrame->GetAvailableScrollingDirections();
+ if (directions.contains(layers::ScrollDirection::eVertical)) {
+ // If there is sub scrollable frame, let's use its page size to select.
+ return frame;
+ }
+ }
+ }
+ // Otherwise, i.e., there is no scrollable frame or only the root frame is
+ // scrollable, let's return the root frame because Shift + PageUp/PageDown
+ // should expand the selection in the root content even if it's not
+ // scrollable.
+ return rootFrameToSelect;
+}
+
+nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend,
+ nsIFrame* aFrame,
+ SelectionIntoView aSelectionIntoView) {
+ MOZ_ASSERT(aFrame);
+
+ // expected behavior for PageMove is to scroll AND move the caret
+ // and remain relative position of the caret in view. see Bug 4302.
+
+ // Get the scrollable frame. If aFrame is not scrollable, this is nullptr.
+ nsIScrollableFrame* scrollableFrame = aFrame->GetScrollTargetFrame();
+ // Get the scrolled frame. If aFrame is not scrollable, this is aFrame
+ // itself.
+ nsIFrame* scrolledFrame =
+ scrollableFrame ? scrollableFrame->GetScrolledFrame() : aFrame;
+ if (!scrolledFrame) {
+ return NS_OK;
+ }
+
+ // find out where the caret is.
+ // we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I
+ // havent seen that behavior in other windows applications yet.
+ RefPtr<Selection> selection = GetSelection(SelectionType::eNormal);
+ if (!selection) {
+ return NS_OK;
+ }
+
+ nsRect caretPos;
+ nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos);
+ if (!caretFrame) {
+ return NS_OK;
+ }
+
+ // If the scrolled frame is outside of current selection limiter,
+ // we need to scroll the frame but keep moving selection in the limiter.
+ nsIFrame* frameToClick = scrolledFrame;
+ if (!IsValidSelectionPoint(scrolledFrame->GetContent())) {
+ frameToClick = GetFrameToPageSelect();
+ if (NS_WARN_IF(!frameToClick)) {
+ return NS_OK;
+ }
+ }
+
+ if (scrollableFrame) {
+ // If there is a scrollable frame, adjust pseudo-click position with page
+ // scroll amount.
+ // XXX This may scroll more than one page if ScrollSelectionIntoView is
+ // called later because caret may not fully visible. E.g., if
+ // clicking line will be visible only half height with scrolling
+ // the frame, ScrollSelectionIntoView additionally scrolls to show
+ // the caret entirely.
+ if (aForward) {
+ caretPos.y += scrollableFrame->GetPageScrollAmount().height;
+ } else {
+ caretPos.y -= scrollableFrame->GetPageScrollAmount().height;
+ }
+ } else {
+ // Otherwise, adjust pseudo-click position with the frame size.
+ if (aForward) {
+ caretPos.y += frameToClick->GetSize().height;
+ } else {
+ caretPos.y -= frameToClick->GetSize().height;
+ }
+ }
+
+ caretPos += caretFrame->GetOffsetTo(frameToClick);
+
+ // get a content at desired location
+ nsPoint desiredPoint;
+ desiredPoint.x = caretPos.x;
+ desiredPoint.y = caretPos.y + caretPos.height / 2;
+ nsIFrame::ContentOffsets offsets =
+ frameToClick->GetContentOffsetsFromPoint(desiredPoint);
+
+ if (!offsets.content) {
+ // XXX Do we need to handle ScrollSelectionIntoView in this case?
+ return NS_OK;
+ }
+
+ // First, place the caret.
+ bool selectionChanged;
+ {
+ // We don't want any script to run until we check whether selection is
+ // modified by HandleClick.
+ SelectionBatcher ensureNoSelectionChangeNotifications(selection,
+ __FUNCTION__);
+
+ RangeBoundary oldAnchor = selection->AnchorRef();
+ RangeBoundary oldFocus = selection->FocusRef();
+
+ const FocusMode focusMode =
+ aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint;
+ HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
+ offsets.offset, offsets.offset, focusMode,
+ CARET_ASSOCIATE_AFTER);
+
+ selectionChanged = selection->AnchorRef() != oldAnchor ||
+ selection->FocusRef() != oldFocus;
+ }
+
+ bool doScrollSelectionIntoView = !(
+ aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged);
+
+ // Then, scroll the given frame one page.
+ if (scrollableFrame) {
+ // If we'll call ScrollSelectionIntoView later and selection wasn't
+ // changed and we scroll outside of selection limiter, we shouldn't use
+ // smooth scroll here because nsIScrollableFrame uses normal runnable,
+ // but ScrollSelectionIntoView uses early runner and it cancels the
+ // pending smooth scroll. Therefore, if we used smooth scroll in such
+ // case, ScrollSelectionIntoView would scroll to show caret instead of
+ // page scroll of an element outside selection limiter.
+ ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged &&
+ scrolledFrame != frameToClick
+ ? ScrollMode::Instant
+ : ScrollMode::Smooth;
+ scrollableFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
+ ScrollUnit::PAGES, scrollMode);
+ }
+
+ // Finally, scroll selection into view if requested.
+ if (!doScrollSelectionIntoView) {
+ return NS_OK;
+ }
+ return ScrollSelectionIntoView(
+ SelectionType::eNormal, nsISelectionController::SELECTION_FOCUS_REGION,
+ nsISelectionController::SCROLL_SYNCHRONOUS |
+ nsISelectionController::SCROLL_FOR_CARET_MOVE);
+}
+
+nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount,
+ bool aExtend) {
+ NS_ENSURE_STATE(mPresShell);
+ // Flush out layout, since we need it to be up to date to do caret
+ // positioning.
+ OwningNonNull<PresShell> presShell(*mPresShell);
+ presShell->FlushPendingNotifications(FlushType::Layout);
+
+ if (!mPresShell) {
+ return NS_OK;
+ }
+
+ // Check that parameters are safe
+ if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsPresContext* context = mPresShell->GetPresContext();
+ if (!context) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ RefPtr<Selection> sel = mDomSelections[index];
+ if (!sel) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Map the abstract movement amounts (0-1) to direction-specific
+ // selection units.
+ static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord};
+ static const nsSelectionAmount blockPrevAmount[] = {eSelectLine,
+ eSelectBeginLine};
+ static const nsSelectionAmount blockNextAmount[] = {eSelectLine,
+ eSelectEndLine};
+
+ struct PhysicalToLogicalMapping {
+ nsDirection direction;
+ const nsSelectionAmount* amounts;
+ };
+ static const PhysicalToLogicalMapping verticalLR[4] = {
+ {eDirPrevious, blockPrevAmount}, // left
+ {eDirNext, blockNextAmount}, // right
+ {eDirPrevious, inlineAmount}, // up
+ {eDirNext, inlineAmount} // down
+ };
+ static const PhysicalToLogicalMapping verticalRL[4] = {
+ {eDirNext, blockNextAmount},
+ {eDirPrevious, blockPrevAmount},
+ {eDirPrevious, inlineAmount},
+ {eDirNext, inlineAmount}};
+ static const PhysicalToLogicalMapping horizontal[4] = {
+ {eDirPrevious, inlineAmount},
+ {eDirNext, inlineAmount},
+ {eDirPrevious, blockPrevAmount},
+ {eDirNext, blockNextAmount}};
+
+ WritingMode wm;
+ nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(true);
+ if (frame) {
+ if (!frame->Style()->IsTextCombined()) {
+ wm = frame->GetWritingMode();
+ } else {
+ // Using different direction for horizontal-in-vertical would
+ // make it hard to navigate via keyboard. Inherit the moving
+ // direction from its parent.
+ MOZ_ASSERT(frame->IsTextFrame());
+ wm = frame->GetParent()->GetWritingMode();
+ MOZ_ASSERT(wm.IsVertical(),
+ "Text combined "
+ "can only appear in vertical text");
+ }
+ }
+
+ const PhysicalToLogicalMapping& mapping =
+ wm.IsVertical()
+ ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection]
+ : horizontal[aDirection];
+
+ nsresult rv =
+ MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount], eVisual);
+ if (NS_FAILED(rv)) {
+ // If we tried to do a line move, but couldn't move in the given direction,
+ // then we'll "promote" this to a line-edge move instead.
+ if (mapping.amounts[aAmount] == eSelectLine) {
+ rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount + 1],
+ eVisual);
+ }
+ // And if it was a next-word move that failed (which can happen when
+ // eat_space_to_next_word is true, see bug 1153237), then just move forward
+ // to the line-edge.
+ else if (mapping.amounts[aAmount] == eSelectWord &&
+ mapping.direction == eDirNext) {
+ rv = MoveCaret(eDirNext, aExtend, eSelectEndLine, eVisual);
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) {
+ return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectCluster,
+ eUsePrefStyle);
+}
+
+nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) {
+ return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectWord,
+ eUsePrefStyle);
+}
+
+nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) {
+ return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectLine,
+ eUsePrefStyle);
+}
+
+nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) {
+ if (aForward) {
+ return MoveCaret(eDirNext, aExtend, eSelectEndLine, eLogical);
+ } else {
+ return MoveCaret(eDirPrevious, aExtend, eSelectBeginLine, eLogical);
+ }
+}
+
+template <typename RangeType>
+Result<RefPtr<RangeType>, nsresult>
+nsFrameSelection::CreateRangeExtendedToSomewhere(
+ nsDirection aDirection, const nsSelectionAmount aAmount,
+ CaretMovementStyle aMovementStyle) {
+ MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious);
+ MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
+ aAmount == eSelectWord || aAmount == eSelectBeginLine ||
+ aAmount == eSelectEndLine);
+ MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual ||
+ aMovementStyle == eUsePrefStyle);
+
+ if (!mPresShell) {
+ return Err(NS_ERROR_UNEXPECTED);
+ }
+ OwningNonNull<PresShell> presShell(*mPresShell);
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (!mPresShell) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Selection* selection =
+ mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
+ if (!selection || selection->RangeCount() != 1) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ RefPtr<const nsRange> firstRange = selection->GetRangeAt(0);
+ if (!firstRange || !firstRange->IsPositioned()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
+ aDirection, true, aAmount, aMovementStyle, nsPoint(0, 0));
+ if (result.isErr()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const PeekOffsetStruct& pos = result.inspect();
+ RefPtr<RangeType> range;
+ if (NS_WARN_IF(!pos.mResultContent)) {
+ return range;
+ }
+ if (aDirection == eDirPrevious) {
+ range = RangeType::Create(
+ RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
+ firstRange->EndRef(), IgnoreErrors());
+ } else {
+ range = RangeType::Create(
+ firstRange->StartRef(),
+ RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
+ IgnoreErrors());
+ }
+ return range;
+}
+
+//////////END FRAMESELECTION
+
+LazyLogModule gBatchLog("SelectionBatch");
+
+void nsFrameSelection::StartBatchChanges(const char* aRequesterFuncName) {
+ MOZ_LOG(gBatchLog, LogLevel::Info,
+ ("%p%snsFrameSelection::StartBatchChanges(%s)", this,
+ std::string((mBatching.mCounter + 1) * 2, ' ').c_str(),
+ aRequesterFuncName));
+ mBatching.mCounter++;
+}
+
+void nsFrameSelection::EndBatchChanges(const char* aRequesterFuncName,
+ int16_t aReasons) {
+ MOZ_LOG(gBatchLog, LogLevel::Info,
+ ("%p%snsFrameSelection::EndBatchChanges (%s, %s)", this,
+ std::string(mBatching.mCounter * 2, ' ').c_str(), aRequesterFuncName,
+ SelectionChangeReasonsToCString(aReasons).get()));
+ MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter");
+ mBatching.mCounter--;
+
+ if (mBatching.mCounter == 0 && mBatching.mChangesDuringBatching) {
+ AddChangeReasons(aReasons);
+ mCaretMoveAmount = eSelectNoAmount;
+ mBatching.mChangesDuringBatching = false;
+ // Be aware, the Selection instance may be destroyed after this call.
+ NotifySelectionListeners(SelectionType::eNormal);
+ }
+}
+
+nsresult nsFrameSelection::NotifySelectionListeners(
+ SelectionType aSelectionType) {
+ int8_t index = GetIndexFromSelectionType(aSelectionType);
+ if (index >= 0 && mDomSelections[index]) {
+ RefPtr<Selection> selection = mDomSelections[index];
+ selection->NotifySelectionListeners();
+ mCaretMoveAmount = eSelectNoAmount;
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+// Start of Table Selection methods
+
+static bool IsCell(nsIContent* aContent) {
+ return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
+}
+
+// static
+nsITableCellLayout* nsFrameSelection::GetCellLayout(
+ const nsIContent* aCellContent) {
+ nsITableCellLayout* cellLayoutObject =
+ do_QueryFrame(aCellContent->GetPrimaryFrame());
+ return cellLayoutObject;
+}
+
+nsresult nsFrameSelection::ClearNormalSelection() {
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ RefPtr<Selection> selection = mDomSelections[index];
+ if (!selection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ ErrorResult err;
+ selection->RemoveAllRanges(err);
+ return err.StealNSResult();
+}
+
+static nsIContent* GetFirstSelectedContent(const nsRange* aRange) {
+ if (!aRange) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!");
+ MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent");
+
+ return aRange->GetChildAtStartOffset();
+}
+
+// Table selection support.
+// TODO: Separate table methods into a separate nsITableSelection interface
+nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent,
+ int32_t aContentOffset,
+ TableSelectionMode aTarget,
+ WidgetMouseEvent* aMouseEvent) {
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ RefPtr<Selection> selection = mDomSelections[index];
+ if (!selection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return mTableSelection.HandleSelection(aParentContent, aContentOffset,
+ aTarget, aMouseEvent, mDragState,
+ *selection);
+}
+
+nsresult nsFrameSelection::TableSelection::HandleSelection(
+ nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget,
+ WidgetMouseEvent* aMouseEvent, bool aDragState,
+ Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER);
+
+ if (aDragState && mDragSelectingCells &&
+ aTarget == TableSelectionMode::Table) {
+ // We were selecting cells and user drags mouse in table border or inbetween
+ // cells,
+ // just do nothing
+ return NS_OK;
+ }
+
+ RefPtr<nsIContent> childContent =
+ aParentContent->GetChildAt_Deprecated(aContentOffset);
+
+ // When doing table selection, always set the direction to next so
+ // we can be sure that anchorNode's offset always points to the
+ // selected cell
+ aNormalSelection.SetDirection(eDirNext);
+
+ // Stack-class to wrap all table selection changes in
+ // BeginBatchChanges() / EndBatchChanges()
+ SelectionBatcher selectionBatcher(&aNormalSelection, __FUNCTION__);
+
+ if (aDragState && mDragSelectingCells) {
+ return HandleDragSelecting(aTarget, childContent, aMouseEvent,
+ aNormalSelection);
+ }
+
+ return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent,
+ aContentOffset, aMouseEvent, aNormalSelection);
+}
+
+class nsFrameSelection::TableSelection::RowAndColumnRelation {
+ public:
+ static Result<RowAndColumnRelation, nsresult> Create(
+ const nsIContent* aFirst, const nsIContent* aSecond) {
+ RowAndColumnRelation result;
+
+ nsresult errorResult =
+ GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn);
+ if (NS_FAILED(errorResult)) {
+ return Err(errorResult);
+ }
+
+ errorResult =
+ GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn);
+ if (NS_FAILED(errorResult)) {
+ return Err(errorResult);
+ }
+
+ return result;
+ }
+
+ bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; }
+
+ bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; }
+
+ private:
+ RowAndColumnRelation() = default;
+
+ struct RowAndColumn {
+ int32_t mRow = 0;
+ int32_t mColumn = 0;
+ };
+
+ RowAndColumn mFirst;
+ RowAndColumn mSecond;
+};
+
+nsresult nsFrameSelection::TableSelection::HandleDragSelecting(
+ TableSelectionMode aTarget, nsIContent* aChildContent,
+ const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
+ // We are drag-selecting
+ if (aTarget != TableSelectionMode::Table) {
+ // If dragging in the same cell as last event, do nothing
+ if (mEndSelectedCell == aChildContent) {
+ return NS_OK;
+ }
+
+#ifdef DEBUG_TABLE_SELECTION
+ printf(
+ " mStartSelectedCell = %p, "
+ "mEndSelectedCell = %p, aChildContent = %p "
+ "\n",
+ mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent);
+#endif
+ // aTarget can be any "cell mode",
+ // so we can easily drag-select rows and columns
+ // Once we are in row or column mode,
+ // we can drift into any cell to stay in that mode
+ // even if aTarget = TableSelectionMode::Cell
+
+ if (mMode == TableSelectionMode::Row ||
+ mMode == TableSelectionMode::Column) {
+ if (mEndSelectedCell) {
+ Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
+ RowAndColumnRelation::Create(mEndSelectedCell, aChildContent);
+
+ if (rowAndColumnRelation.isErr()) {
+ return rowAndColumnRelation.unwrapErr();
+ }
+
+ if ((mMode == TableSelectionMode::Row &&
+ rowAndColumnRelation.inspect().IsSameRow()) ||
+ (mMode == TableSelectionMode::Column &&
+ rowAndColumnRelation.inspect().IsSameColumn())) {
+ return NS_OK;
+ }
+ }
+#ifdef DEBUG_TABLE_SELECTION
+ printf(" Dragged into a new column or row\n");
+#endif
+ // Continue dragging row or column selection
+
+ return SelectRowOrColumn(aChildContent, aNormalSelection);
+ }
+ if (mMode == TableSelectionMode::Cell) {
+#ifdef DEBUG_TABLE_SELECTION
+ printf("HandleTableSelection: Dragged into a new cell\n");
+#endif
+ // Trick for quick selection of rows and columns
+ // Hold down shift, then start selecting in one direction
+ // If next cell dragged into is in same row, select entire row,
+ // if next cell is in same column, select entire column
+ if (mStartSelectedCell && aMouseEvent->IsShift()) {
+ Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
+ RowAndColumnRelation::Create(mStartSelectedCell, aChildContent);
+ if (rowAndColumnRelation.isErr()) {
+ return rowAndColumnRelation.unwrapErr();
+ }
+
+ if (rowAndColumnRelation.inspect().IsSameRow() ||
+ rowAndColumnRelation.inspect().IsSameColumn()) {
+ // Force new selection block
+ mStartSelectedCell = nullptr;
+ aNormalSelection.RemoveAllRanges(IgnoreErrors());
+
+ if (rowAndColumnRelation.inspect().IsSameRow()) {
+ mMode = TableSelectionMode::Row;
+ } else {
+ mMode = TableSelectionMode::Column;
+ }
+
+ return SelectRowOrColumn(aChildContent, aNormalSelection);
+ }
+ }
+
+ // Reselect block of cells to new end location
+ return SelectBlockOfCells(mStartSelectedCell, aChildContent,
+ aNormalSelection);
+ }
+ }
+ // Do nothing if dragging in table, but outside a cell
+ return NS_OK;
+}
+
+nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown(
+ TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent,
+ nsINode* aParentContent, int32_t aContentOffset,
+ const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
+ nsresult result = NS_OK;
+ // Not dragging -- mouse event is down or up
+ if (aDragState) {
+#ifdef DEBUG_TABLE_SELECTION
+ printf("HandleTableSelection: Mouse down event\n");
+#endif
+ // Clear cell we stored in mouse-down
+ mUnselectCellOnMouseUp = nullptr;
+
+ if (aTarget == TableSelectionMode::Cell) {
+ bool isSelected = false;
+
+ // Check if we have other selected cells
+ nsIContent* previousCellNode =
+ GetFirstSelectedContent(GetFirstCellRange(aNormalSelection));
+ if (previousCellNode) {
+ // We have at least 1 other selected cell
+
+ // Check if new cell is already selected
+ nsIFrame* cellFrame = aChildContent->GetPrimaryFrame();
+ if (!cellFrame) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ isSelected = cellFrame->IsSelected();
+ } else {
+ // No cells selected -- remove non-cell selection
+ aNormalSelection.RemoveAllRanges(IgnoreErrors());
+ }
+ mDragSelectingCells = true; // Signal to start drag-cell-selection
+ mMode = aTarget;
+ // Set start for new drag-selection block (not appended)
+ mStartSelectedCell = aChildContent;
+ // The initial block end is same as the start
+ mEndSelectedCell = aChildContent;
+
+ if (isSelected) {
+ // Remember this cell to (possibly) unselect it on mouseup
+ mUnselectCellOnMouseUp = aChildContent;
+#ifdef DEBUG_TABLE_SELECTION
+ printf(
+ "HandleTableSelection: Saving "
+ "mUnselectCellOnMouseUp\n");
+#endif
+ } else {
+ // Select an unselected cell
+ // but first remove existing selection if not in same table
+ if (previousCellNode &&
+ !IsInSameTable(previousCellNode, aChildContent)) {
+ aNormalSelection.RemoveAllRanges(IgnoreErrors());
+ // Reset selection mode that is cleared in RemoveAllRanges
+ mMode = aTarget;
+ }
+
+ return ::SelectCellElement(aChildContent, aNormalSelection);
+ }
+
+ return NS_OK;
+ }
+ if (aTarget == TableSelectionMode::Table) {
+ // TODO: We currently select entire table when clicked between cells,
+ // should we restrict to only around border?
+ // *** How do we get location data for cell and click?
+ mDragSelectingCells = false;
+ mStartSelectedCell = nullptr;
+ mEndSelectedCell = nullptr;
+
+ // Remove existing selection and select the table
+ aNormalSelection.RemoveAllRanges(IgnoreErrors());
+ return CreateAndAddRange(aParentContent, aContentOffset,
+ aNormalSelection);
+ }
+ if (aTarget == TableSelectionMode::Row ||
+ aTarget == TableSelectionMode::Column) {
+#ifdef DEBUG_TABLE_SELECTION
+ printf("aTarget == %d\n", aTarget);
+#endif
+
+ // Start drag-selecting mode so multiple rows/cols can be selected
+ // Note: Currently, nsIFrame::GetDataForTableSelection
+ // will never call us for row or column selection on mouse down
+ mDragSelectingCells = true;
+
+ // Force new selection block
+ mStartSelectedCell = nullptr;
+ aNormalSelection.RemoveAllRanges(IgnoreErrors());
+ // Always do this AFTER RemoveAllRanges
+ mMode = aTarget;
+
+ return SelectRowOrColumn(aChildContent, aNormalSelection);
+ }
+ } else {
+#ifdef DEBUG_TABLE_SELECTION
+ printf(
+ "HandleTableSelection: Mouse UP event. "
+ "mDragSelectingCells=%d, "
+ "mStartSelectedCell=%p\n",
+ mDragSelectingCells, mStartSelectedCell.get());
+#endif
+ // First check if we are extending a block selection
+ const uint32_t rangeCount = aNormalSelection.RangeCount();
+
+ if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell &&
+ mAppendStartSelectedCell != aChildContent) {
+ // Shift key is down: append a block selection
+ mDragSelectingCells = false;
+
+ return SelectBlockOfCells(mAppendStartSelectedCell, aChildContent,
+ aNormalSelection);
+ }
+
+ if (mDragSelectingCells) {
+ mAppendStartSelectedCell = mStartSelectedCell;
+ }
+
+ mDragSelectingCells = false;
+ mStartSelectedCell = nullptr;
+ mEndSelectedCell = nullptr;
+
+ // Any other mouseup actions require that Ctrl or Cmd key is pressed
+ // else stop table selection mode
+ bool doMouseUpAction = false;
+#ifdef XP_MACOSX
+ doMouseUpAction = aMouseEvent->IsMeta();
+#else
+ doMouseUpAction = aMouseEvent->IsControl();
+#endif
+ if (!doMouseUpAction) {
+#ifdef DEBUG_TABLE_SELECTION
+ printf(
+ "HandleTableSelection: Ending cell selection on mouseup: "
+ "mAppendStartSelectedCell=%p\n",
+ mAppendStartSelectedCell.get());
+#endif
+ return NS_OK;
+ }
+ // Unselect a cell only if it wasn't
+ // just selected on mousedown
+ if (aChildContent == mUnselectCellOnMouseUp) {
+ // Scan ranges to find the cell to unselect (the selection range to
+ // remove)
+ // XXXbz it's really weird that this lives outside the loop, so once we
+ // find one we keep looking at it even if we find no more cells...
+ nsINode* previousCellParent = nullptr;
+#ifdef DEBUG_TABLE_SELECTION
+ printf(
+ "HandleTableSelection: Unselecting "
+ "mUnselectCellOnMouseUp; "
+ "rangeCount=%d\n",
+ rangeCount);
+#endif
+ for (const uint32_t i : IntegerRange(rangeCount)) {
+ MOZ_ASSERT(aNormalSelection.RangeCount() == rangeCount);
+ // Strong reference, because sometimes we want to remove
+ // this range, and then we might be the only owner.
+ RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i);
+ if (MOZ_UNLIKELY(!range)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ nsINode* container = range->GetStartContainer();
+ if (!container) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ int32_t offset = range->StartOffset();
+ // Be sure previous selection is a table cell
+ nsIContent* child = range->GetChildAtStartOffset();
+ if (child && IsCell(child)) {
+ previousCellParent = container;
+ }
+
+ // We're done if we didn't find parent of a previously-selected cell
+ if (!previousCellParent) {
+ break;
+ }
+
+ if (previousCellParent == aParentContent && offset == aContentOffset) {
+ // Cell is already selected
+ if (rangeCount == 1) {
+#ifdef DEBUG_TABLE_SELECTION
+ printf("HandleTableSelection: Unselecting single selected cell\n");
+#endif
+ // This was the only cell selected.
+ // Collapse to "normal" selection inside the cell
+ mStartSelectedCell = nullptr;
+ mEndSelectedCell = nullptr;
+ mAppendStartSelectedCell = nullptr;
+ // TODO: We need a "Collapse to just before deepest child" routine
+ // Even better, should we collapse to just after the LAST deepest
+ // child
+ // (i.e., at the end of the cell's contents)?
+ return aNormalSelection.CollapseInLimiter(aChildContent, 0);
+ }
+#ifdef DEBUG_TABLE_SELECTION
+ printf(
+ "HandleTableSelection: Removing cell from multi-cell "
+ "selection\n");
+#endif
+ // Unselecting the start of previous block
+ // XXX What do we use now!
+ if (aChildContent == mAppendStartSelectedCell) {
+ mAppendStartSelectedCell = nullptr;
+ }
+
+ // Deselect cell by removing its range from selection
+ ErrorResult err;
+ aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
+ *range, err);
+ return err.StealNSResult();
+ }
+ }
+ mUnselectCellOnMouseUp = nullptr;
+ }
+ }
+ return result;
+}
+
+nsresult nsFrameSelection::TableSelection::SelectBlockOfCells(
+ nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) {
+ NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER);
+ NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER);
+ mEndSelectedCell = aEndCell;
+
+ nsresult result = NS_OK;
+
+ // If new end cell is in a different table, do nothing
+ const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell);
+ if (!table) {
+ return NS_OK;
+ }
+
+ // Get starting and ending cells' location in the cellmap
+ int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
+ result = GetCellIndexes(aStartCell, startRowIndex, startColIndex);
+ if (NS_FAILED(result)) return result;
+ result = GetCellIndexes(aEndCell, endRowIndex, endColIndex);
+ if (NS_FAILED(result)) return result;
+
+ if (mDragSelectingCells) {
+ // Drag selecting: remove selected cells outside of new block limits
+ // TODO: `UnselectCells`'s return value shouldn't be ignored.
+ UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex,
+ true, aNormalSelection);
+ }
+
+ // Note that we select block in the direction of user's mouse dragging,
+ // which means start cell may be after the end cell in either row or column
+ return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex,
+ endColIndex, aNormalSelection);
+}
+
+nsresult nsFrameSelection::TableSelection::UnselectCells(
+ const nsIContent* aTableContent, int32_t aStartRowIndex,
+ int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex,
+ bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ nsTableWrapperFrame* tableFrame =
+ do_QueryFrame(aTableContent->GetPrimaryFrame());
+ if (!tableFrame) return NS_ERROR_FAILURE;
+
+ int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex);
+ int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex);
+ int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex);
+ int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex);
+
+ // Strong reference because we sometimes remove the range
+ RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection);
+ nsIContent* cellNode = GetFirstSelectedContent(range);
+ MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
+
+ int32_t curRowIndex, curColIndex;
+ while (cellNode) {
+ nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex);
+ if (NS_FAILED(result)) return result;
+
+#ifdef DEBUG_TABLE_SELECTION
+ if (!range) printf("RemoveCellsToSelection -- range is null\n");
+#endif
+
+ if (range) {
+ if (aRemoveOutsideOfCellRange) {
+ if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex ||
+ curColIndex < minColIndex || curColIndex > maxColIndex) {
+ aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
+ *range, IgnoreErrors());
+ // Since we've removed the range, decrement pointer to next range
+ mSelectedCellIndex--;
+ }
+
+ } else {
+ // Remove cell from selection if it belongs to the given cells range or
+ // it is spanned onto the cells range.
+ nsTableCellFrame* cellFrame =
+ tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
+
+ uint32_t origRowIndex = cellFrame->RowIndex();
+ uint32_t origColIndex = cellFrame->ColIndex();
+ uint32_t actualRowSpan =
+ tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
+ uint32_t actualColSpan =
+ tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
+ if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) &&
+ maxRowIndex >= 0 &&
+ origRowIndex + actualRowSpan - 1 >=
+ static_cast<uint32_t>(minRowIndex) &&
+ origColIndex <= static_cast<uint32_t>(maxColIndex) &&
+ maxColIndex >= 0 &&
+ origColIndex + actualColSpan - 1 >=
+ static_cast<uint32_t>(minColIndex)) {
+ aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
+ *range, IgnoreErrors());
+ // Since we've removed the range, decrement pointer to next range
+ mSelectedCellIndex--;
+ }
+ }
+ }
+
+ range = GetNextCellRange(aNormalSelection);
+ cellNode = GetFirstSelectedContent(range);
+ MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
+ }
+
+ return NS_OK;
+}
+
+nsresult SelectCellElement(nsIContent* aCellElement,
+ Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ nsIContent* parent = aCellElement->GetParent();
+
+ // Get child offset
+ const int32_t offset = parent->ComputeIndexOf_Deprecated(aCellElement);
+
+ return CreateAndAddRange(parent, offset, aNormalSelection);
+}
+
+static nsresult AddCellsToSelection(const nsIContent* aTableContent,
+ int32_t aStartRowIndex,
+ int32_t aStartColumnIndex,
+ int32_t aEndRowIndex,
+ int32_t aEndColumnIndex,
+ Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ nsTableWrapperFrame* tableFrame =
+ do_QueryFrame(aTableContent->GetPrimaryFrame());
+ if (!tableFrame) { // Check that |table| is a table.
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult result = NS_OK;
+ uint32_t row = aStartRowIndex;
+ while (true) {
+ uint32_t col = aStartColumnIndex;
+ while (true) {
+ nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
+
+ // Skip cells that are spanned from previous locations or are already
+ // selected
+ if (cellFrame) {
+ uint32_t origRow = cellFrame->RowIndex();
+ uint32_t origCol = cellFrame->ColIndex();
+ if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
+ result = SelectCellElement(cellFrame->GetContent(), aNormalSelection);
+ if (NS_FAILED(result)) {
+ return result;
+ }
+ }
+ }
+ // Done when we reach end column
+ if (col == static_cast<uint32_t>(aEndColumnIndex)) {
+ break;
+ }
+
+ if (aStartColumnIndex < aEndColumnIndex) {
+ col++;
+ } else {
+ col--;
+ }
+ }
+ if (row == static_cast<uint32_t>(aEndRowIndex)) {
+ break;
+ }
+
+ if (aStartRowIndex < aEndRowIndex) {
+ row++;
+ } else {
+ row--;
+ }
+ }
+ return result;
+}
+
+nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable,
+ int32_t aStartRowIndex,
+ int32_t aStartColumnIndex,
+ int32_t aEndRowIndex,
+ int32_t aEndColumnIndex) {
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
+ if (!selection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return mTableSelection.UnselectCells(aTable, aStartRowIndex,
+ aStartColumnIndex, aEndRowIndex,
+ aEndColumnIndex, false, *selection);
+}
+
+nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable,
+ int32_t aStartRowIndex,
+ int32_t aStartColumnIndex,
+ int32_t aEndRowIndex,
+ int32_t aEndColumnIndex) {
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
+ if (!selection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return mTableSelection.UnselectCells(aTable, aStartRowIndex,
+ aStartColumnIndex, aEndRowIndex,
+ aEndColumnIndex, true, *selection);
+}
+
+Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult>
+nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn(
+ const nsIContent& aCellContent) const {
+ const nsIContent* table = GetParentTable(&aCellContent);
+ if (!table) {
+ return Err(NS_ERROR_NULL_POINTER);
+ }
+
+ // Get table and cell layout interfaces to access
+ // cell data based on cellmap location
+ // Frames are not ref counted, so don't use an nsCOMPtr
+ nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame());
+ if (!tableFrame) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent);
+ if (!cellLayout) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ // Get location of target cell:
+ int32_t rowIndex, colIndex;
+ nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex);
+ if (NS_FAILED(result)) {
+ return Err(result);
+ }
+
+ // Be sure we start at proper beginning
+ // (This allows us to select row or col given ANY cell!)
+ if (mMode == TableSelectionMode::Row) {
+ colIndex = 0;
+ }
+ if (mMode == TableSelectionMode::Column) {
+ rowIndex = 0;
+ }
+
+ FirstAndLastCell firstAndLastCell;
+ while (true) {
+ // Loop through all cells in column or row to find first and last
+ nsCOMPtr<nsIContent> curCellContent =
+ tableFrame->GetCellAt(rowIndex, colIndex);
+ if (!curCellContent) {
+ break;
+ }
+
+ if (!firstAndLastCell.mFirst) {
+ firstAndLastCell.mFirst = curCellContent;
+ }
+
+ firstAndLastCell.mLast = std::move(curCellContent);
+
+ // Move to next cell in cellmap, skipping spanned locations
+ if (mMode == TableSelectionMode::Row) {
+ colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
+ } else {
+ rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
+ }
+ }
+ return firstAndLastCell;
+}
+
+nsresult nsFrameSelection::TableSelection::SelectRowOrColumn(
+ nsIContent* aCellContent, Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ if (!aCellContent) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ Result<FirstAndLastCell, nsresult> firstAndLastCell =
+ FindFirstAndLastCellOfRowOrColumn(*aCellContent);
+ if (firstAndLastCell.isErr()) {
+ return firstAndLastCell.unwrapErr();
+ }
+
+ // Use SelectBlockOfCells:
+ // This will replace existing selection,
+ // but allow unselecting by dragging out of selected region
+ if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) {
+ nsresult rv{NS_OK};
+
+ if (!mStartSelectedCell) {
+ // We are starting a new block, so select the first cell
+ rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst,
+ aNormalSelection);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mStartSelectedCell = firstAndLastCell.inspect().mFirst;
+ }
+
+ rv = SelectBlockOfCells(mStartSelectedCell,
+ firstAndLastCell.inspect().mLast, aNormalSelection);
+
+ // This gets set to the cell at end of row/col,
+ // but we need it to be the cell under cursor
+ mEndSelectedCell = aCellContent;
+ return rv;
+ }
+
+#if 0
+// This is a more efficient strategy that appends row to current selection,
+// but doesn't allow dragging OFF of an existing selection to unselect!
+ do {
+ // Loop through all cells in column or row
+ result = tableLayout->GetCellDataAt(rowIndex, colIndex,
+ getter_AddRefs(cellElement),
+ curRowIndex, curColIndex,
+ rowSpan, colSpan,
+ actualRowSpan, actualColSpan,
+ isSelected);
+ if (NS_FAILED(result)) return result;
+ // We're done when cell is not found
+ if (!cellElement) break;
+
+
+ // Check spans else we infinitely loop
+ NS_ASSERTION(actualColSpan, "actualColSpan is 0!");
+ NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!");
+
+ // Skip cells that are already selected or span from outside our region
+ if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex)
+ {
+ result = SelectCellElement(cellElement);
+ if (NS_FAILED(result)) return result;
+ }
+ // Move to next row or column in cellmap, skipping spanned locations
+ if (mMode == TableSelectionMode::Row)
+ colIndex += actualColSpan;
+ else
+ rowIndex += actualRowSpan;
+ }
+ while (cellElement);
+#endif
+
+ return NS_OK;
+}
+
+// static
+nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) {
+ if (!aRange) return nullptr;
+
+ nsIContent* childContent = aRange->GetChildAtStartOffset();
+ if (!childContent) return nullptr;
+ // Don't return node if not a cell
+ if (!IsCell(childContent)) return nullptr;
+
+ return childContent;
+}
+
+nsRange* nsFrameSelection::TableSelection::GetFirstCellRange(
+ const mozilla::dom::Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ nsRange* firstRange = aNormalSelection.GetRangeAt(0);
+ if (!GetFirstCellNodeInRange(firstRange)) {
+ return nullptr;
+ }
+
+ // Setup for next cell
+ mSelectedCellIndex = 1;
+
+ return firstRange;
+}
+
+nsRange* nsFrameSelection::TableSelection::GetNextCellRange(
+ const mozilla::dom::Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ nsRange* range =
+ aNormalSelection.GetRangeAt(AssertedCast<uint32_t>(mSelectedCellIndex));
+
+ // Get first node in next range of selection - test if it's a cell
+ if (!GetFirstCellNodeInRange(range)) {
+ return nullptr;
+ }
+
+ // Setup for next cell
+ mSelectedCellIndex++;
+
+ return range;
+}
+
+// static
+nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell,
+ int32_t& aRowIndex,
+ int32_t& aColIndex) {
+ if (!aCell) return NS_ERROR_NULL_POINTER;
+
+ aColIndex = 0; // initialize out params
+ aRowIndex = 0;
+
+ nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell);
+ if (!cellLayoutObject) return NS_ERROR_FAILURE;
+ return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex);
+}
+
+// static
+nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1,
+ const nsIContent* aContent2) {
+ if (!aContent1 || !aContent2) return nullptr;
+
+ nsIContent* tableNode1 = GetParentTable(aContent1);
+ nsIContent* tableNode2 = GetParentTable(aContent2);
+
+ // Must be in the same table. Note that we want to return false for
+ // the test if both tables are null.
+ return (tableNode1 == tableNode2) ? tableNode1 : nullptr;
+}
+
+// static
+nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) {
+ if (!aCell) {
+ return nullptr;
+ }
+
+ for (nsIContent* parent = aCell->GetParent(); parent;
+ parent = parent->GetParent()) {
+ if (parent->IsHTMLElement(nsGkAtoms::table)) {
+ return parent;
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) {
+ const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ const RefPtr<Selection> selection = mDomSelections[index];
+ if (!selection) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return ::SelectCellElement(aCellElement, *selection);
+}
+
+nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset,
+ Selection& aNormalSelection) {
+ MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
+
+ if (!aContainer) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ // Set range around child at given offset
+ ErrorResult error;
+ RefPtr<nsRange> range =
+ nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error);
+ if (NS_WARN_IF(error.Failed())) {
+ return error.StealNSResult();
+ }
+ MOZ_ASSERT(range);
+
+ ErrorResult err;
+ aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err);
+ return err.StealNSResult();
+}
+
+// End of Table Selection
+
+void nsFrameSelection::SetAncestorLimiter(nsIContent* aLimiter) {
+ if (mLimiters.mAncestorLimiter != aLimiter) {
+ mLimiters.mAncestorLimiter = aLimiter;
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ if (!mDomSelections[index]) return;
+
+ if (!IsValidSelectionPoint(mDomSelections[index]->GetFocusNode())) {
+ ClearNormalSelection();
+ if (mLimiters.mAncestorLimiter) {
+ SetChangeReasons(nsISelectionListener::NO_REASON);
+ nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter);
+ const nsresult rv = TakeFocus(*limiter, 0, 0, CARET_ASSOCIATE_BEFORE,
+ FocusMode::kCollapseToNewPoint);
+ Unused << NS_WARN_IF(NS_FAILED(rv));
+ // TODO: in case of failure, propagate it to the callers.
+ }
+ }
+ }
+}
+
+void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) {
+ if (aMouseEvent) {
+ mDelayedMouseEvent.mIsValid = true;
+ mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift();
+ mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount;
+ } else {
+ mDelayedMouseEvent.mIsValid = false;
+ }
+}
+
+void nsFrameSelection::DisconnectFromPresShell() {
+ if (mAccessibleCaretEnabled) {
+ int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
+ mDomSelections[index]->StopNotifyingAccessibleCaretEventHub();
+ }
+
+ StopAutoScrollTimer();
+ for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
+ mDomSelections[i]->Clear(nullptr);
+ }
+ mPresShell = nullptr;
+}
+
+#ifdef XP_MACOSX
+/**
+ * See Bug 1288453.
+ *
+ * Update the selection cache on repaint to handle when a pre-existing
+ * selection becomes active aka the current selection.
+ *
+ * 1. Change the current selection by click n dragging another selection.
+ * - Make a selection on content page. Make a selection in a text editor.
+ * - You can click n drag the content selection to make it active again.
+ * 2. Change the current selection when switching to a tab with a selection.
+ * - Make selection in tab.
+ * - Switching tabs will make its respective selection active.
+ *
+ * Therefore, we only update the selection cache on a repaint
+ * if the current selection being repainted is not an empty selection.
+ *
+ * If the current selection is empty. The current selection cache
+ * would be cleared by AutoCopyListener::OnSelectionChange().
+ */
+static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) {
+ PresShell* presShell = aSel->GetPresShell();
+ if (!presShell) {
+ return NS_OK;
+ }
+ nsCOMPtr<Document> aDoc = presShell->GetDocument();
+
+ if (aDoc && aSel && !aSel->IsCollapsed()) {
+ return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
+ aSel, aDoc, nsIClipboard::kSelectionCache, false);
+ }
+
+ return NS_OK;
+}
+#endif // XP_MACOSX
+
+// mozilla::AutoCopyListener
+
+int16_t AutoCopyListener::sClipboardID = -1;
+
+/*
+ * What we do now:
+ * On every selection change, we copy to the clipboard anew, creating a
+ * HTML buffer, a transferable, an nsISupportsString and
+ * a huge mess every time. This is basically what
+ * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
+ * selection into the clipboard for Edit->Copy.
+ *
+ * What we should do, to make our end of the deal faster:
+ * Create a singleton transferable with our own magic converter. When selection
+ * changes (use a quick cache to detect ``real'' changes), we put the new
+ * Selection in the transferable. Our magic converter will take care of
+ * transferable->whatever-other-format when the time comes to actually
+ * hand over the clipboard contents.
+ *
+ * Other issues:
+ * - which X clipboard should we populate?
+ * - should we use a different one than Edit->Copy, so that inadvertant
+ * selections (or simple clicks, which currently cause a selection
+ * notification, regardless of if they're in the document which currently has
+ * selection!) don't lose the contents of the ``application''? Or should we
+ * just put some intelligence in the ``is this a real selection?'' code to
+ * protect our selection against clicks in other documents that don't create
+ * selections?
+ * - maybe we should just never clear the X clipboard? That would make this
+ * problem just go away, which is very tempting.
+ *
+ * On macOS,
+ * nsIClipboard::kSelectionCache is the flag for current selection cache.
+ * Set the current selection cache on the parent process in
+ * widget cocoa nsClipboard whenever selection changes.
+ */
+
+// static
+void AutoCopyListener::OnSelectionChange(Document* aDocument,
+ Selection& aSelection,
+ int16_t aReason) {
+ MOZ_ASSERT(IsValidClipboardID(sClipboardID));
+
+ // For now, we should prevent any updates caused by a call of Selection API.
+ // We should allow this in some cases later, though. See the valid usage in
+ // bug 1567160.
+ if (aReason & nsISelectionListener::JS_REASON) {
+ return;
+ }
+
+ if (sClipboardID == nsIClipboard::kSelectionCache) {
+ // Do nothing if this isn't in the active window and,
+ // in the case of Web content, in the frontmost tab.
+ if (!aDocument || !IsInActiveTab(aDocument)) {
+ return;
+ }
+ }
+
+ static const int16_t kResasonsToHandle =
+ nsISelectionListener::MOUSEUP_REASON |
+ nsISelectionListener::SELECTALL_REASON |
+ nsISelectionListener::KEYPRESS_REASON;
+ if (!(aReason & kResasonsToHandle)) {
+ return; // Don't care if we are still dragging.
+ }
+
+ if (!aDocument || aSelection.IsCollapsed()) {
+#ifdef DEBUG_CLIPBOARD
+ fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
+#endif
+ if (sClipboardID != nsIClipboard::kSelectionCache) {
+ // XXX Should we clear X clipboard?
+ return;
+ }
+
+ // If on macOS, clear the current selection transferable cached
+ // on the parent process (nsClipboard) when the selection is empty.
+ DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsCopySupport::ClearSelectionCache() failed");
+ return;
+ }
+
+ DebugOnly<nsresult> rv =
+ nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
+ &aSelection, aDocument, sClipboardID, false);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");
+}