Bug 739396, part 1 - Split a range (that are to be added to the Selection as a result of a user action) into multiple ranges that excludes any non-selectable (-moz-user-select:none) sub-trees.

This commit is contained in:
Mats Palmgren 2014-09-10 17:07:36 +00:00
parent d0973c7c50
commit 501e985685
6 changed files with 231 additions and 10 deletions

View File

@ -3039,3 +3039,95 @@ nsRange::Constructor(const GlobalObject& aGlobal,
return window->GetDoc()->CreateRange(aRv); return window->GetDoc()->CreateRange(aRv);
} }
void
nsRange::ExcludeNonSelectableNodes(nsTArray<nsRefPtr<nsRange>>* aOutRanges)
{
MOZ_ASSERT(mIsPositioned);
MOZ_ASSERT(mEndParent);
MOZ_ASSERT(mStartParent);
nsRange* range = this;
nsRefPtr<nsRange> newRange;
while (range) {
nsCOMPtr<nsIContentIterator> iter = NS_NewPreContentIterator();
nsresult rv = iter->Init(range);
if (NS_FAILED(rv)) {
return;
}
bool added = false;
bool seenSelectable = false;
nsIContent* firstNonSelectableContent = nullptr;
while (true) {
ErrorResult err;
nsINode* node = iter->GetCurrentNode();
iter->Next();
bool selectable = true;
nsIContent* content =
node && node->IsContent() ? node->AsContent() : nullptr;
if (content) {
nsIFrame* frame = content->GetPrimaryFrame();
for (nsIContent* p = content; !frame && (p = p->GetParent()); ) {
frame = p->GetPrimaryFrame();
}
if (frame) {
frame->IsSelectable(&selectable, nullptr);
}
}
if (!selectable) {
if (!firstNonSelectableContent) {
firstNonSelectableContent = content;
}
if (iter->IsDone() && seenSelectable) {
// The tail end of the initial range is non-selectable - truncate the
// current range before the first non-selectable node.
range->SetEndBefore(*firstNonSelectableContent, err);
}
} else if (firstNonSelectableContent) {
if (range == this && !seenSelectable) {
// This is the initial range and all its nodes until now are
// non-selectable so just trim them from the start.
range->SetStartBefore(*node, err);
if (err.Failed()) {
return;
}
break; // restart the same range with a new iterator
} else {
// Save the end point before truncating the range.
nsINode* endParent = range->mEndParent;
int32_t endOffset = range->mEndOffset;
// Truncate the current range before the first non-selectable node.
range->SetEndBefore(*firstNonSelectableContent, err);
// Store it in the result (strong ref) - do this before creating
// a new range in |newRange| below so we don't drop the last ref
// to the range created in the previous iteration.
if (!added && !err.Failed()) {
aOutRanges->AppendElement(range);
}
// Create a new range for the remainder.
rv = CreateRange(node, 0, endParent, endOffset,
getter_AddRefs(newRange));
if (NS_FAILED(rv) || newRange->Collapsed()) {
newRange = nullptr;
}
range = newRange;
break; // create a new iterator for the new range, if any
}
} else {
seenSelectable = true;
if (!added) {
added = true;
aOutRanges->AppendElement(range);
}
}
if (iter->IsDone()) {
return;
}
}
}
}

View File

@ -264,6 +264,19 @@ public:
nsINode* aEndParent, int32_t aEndOffset, nsINode* aEndParent, int32_t aEndOffset,
bool aClampToEdge, bool aFlushLayout); bool aClampToEdge, bool aFlushLayout);
/**
* Scan this range for -moz-user-select:none nodes and split it up into
* multiple ranges to exclude those nodes. The resulting ranges are put
* in aOutRanges. If no -moz-user-select:none node is found in the range
* then |this| is unmodified and is the only range in aOutRanges.
* Otherwise, |this| will be modified so that it ends before the first
* -moz-user-select:none node and additional ranges may also be created.
* If all nodes in the range are -moz-user-select:none then aOutRanges
* will be empty.
* @param aOutRanges the resulting set of ranges
*/
void ExcludeNonSelectableNodes(nsTArray<nsRefPtr<nsRange>>* aOutRanges);
typedef nsTHashtable<nsPtrHashKey<nsRange> > RangeHashTable; typedef nsTHashtable<nsPtrHashKey<nsRange> > RangeHashTable;
protected: protected:
void RegisterCommonAncestor(nsINode* aNode); void RegisterCommonAncestor(nsINode* aNode);

View File

@ -2620,6 +2620,7 @@ NS_IMETHODIMP nsDocumentViewer::SelectAll()
rv = selection->RemoveAllRanges(); rv = selection->RemoveAllRanges();
if (NS_FAILED(rv)) return rv; if (NS_FAILED(rv)) return rv;
mozilla::dom::Selection::AutoApplyUserSelectStyle userSelection(selection);
rv = selection->SelectAllChildren(bodyNode); rv = selection->SelectAllChildren(bodyNode);
return rv; return rv;
} }

View File

@ -9,12 +9,13 @@
#include "nsIWeakReference.h" #include "nsIWeakReference.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/TextRange.h"
#include "nsISelection.h" #include "nsISelection.h"
#include "nsISelectionController.h" #include "nsISelectionController.h"
#include "nsISelectionPrivate.h" #include "nsISelectionPrivate.h"
#include "nsRange.h" #include "nsRange.h"
#include "nsThreadUtils.h" #include "nsThreadUtils.h"
#include "mozilla/TextRange.h"
#include "nsWrapperCache.h" #include "nsWrapperCache.h"
struct CachedOffsetForFrame; struct CachedOffsetForFrame;
@ -100,8 +101,14 @@ public:
int32_t aFlags = 0); int32_t aFlags = 0);
nsresult SubtractRange(RangeData* aRange, nsRange* aSubtract, nsresult SubtractRange(RangeData* aRange, nsRange* aSubtract,
nsTArray<RangeData>* aOutput); nsTArray<RangeData>* aOutput);
nsresult AddItem(nsRange *aRange, int32_t* aOutIndex); /**
nsresult RemoveItem(nsRange *aRange); * AddItem adds aRange to this Selection. If mApplyUserSelectStyle is true,
* then aRange is first scanned for -moz-user-select:none nodes and split up
* into multiple ranges to exclude those before adding the resulting ranges
* to this Selection.
*/
nsresult AddItem(nsRange* aRange, int32_t* aOutIndex);
nsresult RemoveItem(nsRange* aRange);
nsresult RemoveCollapsedRanges(); nsresult RemoveCollapsedRanges();
nsresult Clear(nsPresContext* aPresContext); nsresult Clear(nsPresContext* aPresContext);
nsresult Collapse(nsINode* aParentNode, int32_t aOffset); nsresult Collapse(nsINode* aParentNode, int32_t aOffset);
@ -205,6 +212,19 @@ public:
nsresult NotifySelectionListeners(); nsresult NotifySelectionListeners();
friend struct AutoApplyUserSelectStyle;
struct MOZ_STACK_CLASS AutoApplyUserSelectStyle
{
AutoApplyUserSelectStyle(Selection* aSelection
MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
: mSavedValue(aSelection->mApplyUserSelectStyle)
{
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
aSelection->mApplyUserSelectStyle = true;
}
AutoRestore<bool> mSavedValue;
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
};
private: private:
class ScrollSelectionIntoViewEvent; class ScrollSelectionIntoViewEvent;
@ -258,6 +278,11 @@ private:
int32_t* aStartIndex, int32_t* aEndIndex); int32_t* aStartIndex, int32_t* aEndIndex);
RangeData* FindRangeData(nsIDOMRange* aRange); RangeData* FindRangeData(nsIDOMRange* aRange);
/**
* Helper method for AddItem.
*/
nsresult AddItemInternal(nsRange* aRange, int32_t* aOutIndex);
// These are the ranges inside this selection. They are kept sorted in order // These are the ranges inside this selection. They are kept sorted in order
// of DOM start position. // of DOM start position.
// //
@ -281,6 +306,11 @@ private:
CachedOffsetForFrame *mCachedOffsetForFrame; CachedOffsetForFrame *mCachedOffsetForFrame;
nsDirection mDirection; nsDirection mDirection;
SelectionType mType; SelectionType mType;
/**
* True if the current selection operation was initiated by user action.
* It determines whether we exclude -moz-user-select:none nodes or not.
*/
bool mApplyUserSelectStyle;
}; };
} // namespace dom } // namespace dom

View File

@ -12,6 +12,7 @@
#include "nsIFrame.h" #include "nsIFrame.h"
#include "nsIContent.h" #include "nsIContent.h"
#include "nsISelectionController.h" #include "nsISelectionController.h"
#include "nsISelectionListener.h"
#include "nsITableCellLayout.h" #include "nsITableCellLayout.h"
#include "nsIDOMElement.h" #include "nsIDOMElement.h"
#include "WordMovementType.h" #include "WordMovementType.h"
@ -613,9 +614,18 @@ private:
int16_t PopReason() int16_t PopReason()
{ {
int16_t retval = mSelectionChangeReason; int16_t retval = mSelectionChangeReason;
mSelectionChangeReason = 0; mSelectionChangeReason = nsISelectionListener::NO_REASON;
return retval; return retval;
} }
bool IsUserSelectionReason() const
{
return (mSelectionChangeReason &
(nsISelectionListener::DRAG_REASON |
nsISelectionListener::MOUSEDOWN_REASON |
nsISelectionListener::MOUSEUP_REASON |
nsISelectionListener::KEYPRESS_REASON)) !=
nsISelectionListener::NO_REASON;
}
friend class mozilla::dom::Selection; friend class mozilla::dom::Selection;
#ifdef DEBUG #ifdef DEBUG

View File

@ -1548,6 +1548,11 @@ nsFrameSelection::TakeFocus(nsIContent* aNewFocus,
if (!mDomSelections[index]) if (!mDomSelections[index])
return NS_ERROR_NULL_POINTER; return NS_ERROR_NULL_POINTER;
Maybe<Selection::AutoApplyUserSelectStyle> userSelect;
if (IsUserSelectionReason()) {
userSelect.emplace(mDomSelections[index]);
}
//traverse through document and unselect crap here //traverse through document and unselect crap here
if (!aContinueSelection) {//single click? setting cursor down if (!aContinueSelection) {//single click? setting cursor down
uint32_t batching = mBatching;//hack to use the collapse code. uint32_t batching = mBatching;//hack to use the collapse code.
@ -3103,6 +3108,7 @@ Selection::Selection()
: mCachedOffsetForFrame(nullptr) : mCachedOffsetForFrame(nullptr)
, mDirection(eDirNext) , mDirection(eDirNext)
, mType(nsISelectionController::SELECTION_NORMAL) , mType(nsISelectionController::SELECTION_NORMAL)
, mApplyUserSelectStyle(false)
{ {
SetIsDOMBinding(); SetIsDOMBinding();
} }
@ -3112,6 +3118,7 @@ Selection::Selection(nsFrameSelection* aList)
, mCachedOffsetForFrame(nullptr) , mCachedOffsetForFrame(nullptr)
, mDirection(eDirNext) , mDirection(eDirNext)
, mType(nsISelectionController::SELECTION_NORMAL) , mType(nsISelectionController::SELECTION_NORMAL)
, mApplyUserSelectStyle(false)
{ {
SetIsDOMBinding(); SetIsDOMBinding();
} }
@ -3454,6 +3461,25 @@ Selection::AddItem(nsRange* aItem, int32_t* aOutIndex)
NS_ASSERTION(aOutIndex, "aOutIndex can't be null"); NS_ASSERTION(aOutIndex, "aOutIndex can't be null");
if (mApplyUserSelectStyle) {
nsAutoTArray<nsRefPtr<nsRange>, 4> rangesToAdd;
aItem->ExcludeNonSelectableNodes(&rangesToAdd);
for (size_t i = 0; i < rangesToAdd.Length(); ++i) {
nsresult rv = AddItemInternal(rangesToAdd[i], aOutIndex);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
return AddItemInternal(aItem, aOutIndex);
}
nsresult
Selection::AddItemInternal(nsRange* aItem, int32_t* aOutIndex)
{
MOZ_ASSERT(aItem);
MOZ_ASSERT(aItem->IsPositioned());
MOZ_ASSERT(aOutIndex);
*aOutIndex = -1; *aOutIndex = -1;
// a common case is that we have no ranges yet // a common case is that we have no ranges yet
@ -4443,8 +4469,7 @@ Selection::AddRange(nsRange& aRange, ErrorResult& aRv)
return; return;
} }
if (!didAddRange) if (!didAddRange) {
{
result = AddItem(&aRange, &rangeIndex); result = AddItem(&aRange, &rangeIndex);
if (NS_FAILED(result)) { if (NS_FAILED(result)) {
aRv.Throw(result); aRv.Throw(result);
@ -4452,7 +4477,10 @@ Selection::AddRange(nsRange& aRange, ErrorResult& aRv)
} }
} }
NS_ASSERTION(rangeIndex >= 0, "Range index not returned"); if (rangeIndex < 0) {
return;
}
setAnchorFocusRange(rangeIndex); setAnchorFocusRange(rangeIndex);
// Make sure the caret appears on the next line, if at a newline // Make sure the caret appears on the next line, if at a newline
@ -4921,7 +4949,48 @@ Selection::Extend(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv)
return; return;
} }
//mFrameSelection->InvalidateDesiredX(); nsDirection dir = GetDirection();
// If aParentNode is inside a range in a multi-range selection we need
// to remove the ranges that follows in the selection direction and
// make that range the mAnchorFocusRange.
if (mRanges.Length() > 1) {
for (size_t i = 0; i < mRanges.Length(); ++i) {
nsRange* range = mRanges[i].mRange;
bool disconnected1 = false;
bool disconnected2 = false;
const bool isBeforeStart =
nsContentUtils::ComparePoints(range->GetStartParent(),
range->StartOffset(),
&aParentNode, aOffset,
&disconnected1) > 0;
const bool isAfterEnd =
nsContentUtils::ComparePoints(range->GetEndParent(),
range->EndOffset(),
&aParentNode, aOffset,
&disconnected2) < 0;
if (!isBeforeStart && !isAfterEnd && !disconnected1 && !disconnected2) {
// aParentNode/aOffset is inside 'range'.
mAnchorFocusRange = range;
if (dir == eDirNext) {
for (size_t j = i + 1; j < mRanges.Length(); ++j) {
nsRange* r = mRanges[j].mRange;
r->SetInSelection(false);
selectFrames(presContext, r, false);
}
mRanges.TruncateLength(i + 1);
} else {
for (size_t j = 0; j < i; ++j) {
nsRange* r = mRanges[j].mRange;
r->SetInSelection(false);
selectFrames(presContext, r, false);
}
mRanges.RemoveElementsAt(0, i);
}
break;
}
}
}
nsINode* anchorNode = GetAnchorNode(); nsINode* anchorNode = GetAnchorNode();
nsINode* focusNode = GetFocusNode(); nsINode* focusNode = GetFocusNode();
@ -4935,8 +5004,6 @@ Selection::Extend(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv)
int32_t startOffset = range->StartOffset(); int32_t startOffset = range->StartOffset();
int32_t endOffset = range->EndOffset(); int32_t endOffset = range->EndOffset();
nsDirection dir = GetDirection();
//compare anchor to old cursor. //compare anchor to old cursor.
// We pass |disconnected| to the following ComparePoints calls in order // We pass |disconnected| to the following ComparePoints calls in order
@ -5162,6 +5229,14 @@ Selection::Extend(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv)
} }
} }
if (mRanges.Length() > 1) {
for (size_t i = 0; i < mRanges.Length(); ++i) {
nsRange* range = mRanges[i].mRange;
MOZ_ASSERT(range->IsInSelection());
selectFrames(presContext, range, range->IsInSelection());
}
}
DEBUG_OUT_RANGE(range); DEBUG_OUT_RANGE(range);
#ifdef DEBUG_SELECTION #ifdef DEBUG_SELECTION
if (eDirNext == mDirection) if (eDirNext == mDirection)