зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1810403: Allow `nsRange`s to be in multiple `Selection`s. r=masayuki
The Custom Highlight API allows a use case where a `Range` of a `Highlight` is also used as `Selection`. Due to the decision to use the `Selection` mechanism to display `Highlight`s, a `Range` can be part of several `Selection`s. Since the `Range` has a pointer to its associated `Selection` to notify about changes, this must be adapted to allow several `Selections`. As a tradeoff of performance and memory usage, the `Selection`s are stored as `mozilla::LinkedList`. A helper class `mozilla::SelectionListWrapper` was implemented to allow `Selection`s to be in multiple of these lists and without having to be derived from `LinkedListElement<T>`. To simplify usage of the list, the use case "does this range belong to Selection x?" is wrapped into the convenience method`IsInSelection(Selection&)`; The method previously named like this was renamed to `IsInAnySelection()` to be named more precisely. Registering and unregistering of the closest common inclusive ancestor of the `Range` is done when the first `Selection` is registered and the last `Selection` is unregistered. Differential Revision: https://phabricator.services.mozilla.com/D169597
This commit is contained in:
Родитель
5f6910e75e
Коммит
2243494a74
|
@ -966,11 +966,10 @@ nsresult Selection::AddRangesForUserSelectableNodes(
|
||||||
GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1;
|
GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1;
|
||||||
for (size_t i = 0; i < rangesToAdd.Length(); ++i) {
|
for (size_t i = 0; i < rangesToAdd.Length(); ++i) {
|
||||||
Maybe<size_t> index;
|
Maybe<size_t> index;
|
||||||
const RefPtr<Selection> selection{this};
|
|
||||||
// `MOZ_KnownLive` needed because of broken static analysis
|
// `MOZ_KnownLive` needed because of broken static analysis
|
||||||
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253#c1).
|
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1622253#c1).
|
||||||
nsresult rv = mStyledRanges.MaybeAddRangeAndTruncateOverlaps(
|
nsresult rv = mStyledRanges.MaybeAddRangeAndTruncateOverlaps(
|
||||||
MOZ_KnownLive(rangesToAdd[i]), &index, *selection);
|
MOZ_KnownLive(rangesToAdd[i]), &index);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
if (i == newAnchorFocusIndex) {
|
if (i == newAnchorFocusIndex) {
|
||||||
*aOutIndex = index;
|
*aOutIndex = index;
|
||||||
|
@ -1007,13 +1006,11 @@ nsresult Selection::AddRangesForSelectableNodes(
|
||||||
aDispatchSelectstartEvent);
|
aDispatchSelectstartEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RefPtr<Selection> selection{this};
|
return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex);
|
||||||
return mStyledRanges.MaybeAddRangeAndTruncateOverlaps(aRange, aOutIndex,
|
|
||||||
*selection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
|
nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
|
||||||
nsRange* aRange, Maybe<size_t>* aOutIndex, Selection& aSelection) {
|
nsRange* aRange, Maybe<size_t>* aOutIndex) {
|
||||||
MOZ_ASSERT(aRange);
|
MOZ_ASSERT(aRange);
|
||||||
MOZ_ASSERT(aRange->IsPositioned());
|
MOZ_ASSERT(aRange->IsPositioned());
|
||||||
MOZ_ASSERT(aOutIndex);
|
MOZ_ASSERT(aOutIndex);
|
||||||
|
@ -1024,7 +1021,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
|
||||||
// XXX(Bug 1631371) Check if this should use a fallible operation as it
|
// XXX(Bug 1631371) Check if this should use a fallible operation as it
|
||||||
// pretended earlier.
|
// pretended earlier.
|
||||||
mRanges.AppendElement(StyledRange(aRange));
|
mRanges.AppendElement(StyledRange(aRange));
|
||||||
aRange->RegisterSelection(aSelection);
|
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
|
||||||
|
|
||||||
aOutIndex->emplace(0u);
|
aOutIndex->emplace(0u);
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
|
@ -1064,7 +1061,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
|
||||||
// XXX(Bug 1631371) Check if this should use a fallible operation as it
|
// XXX(Bug 1631371) Check if this should use a fallible operation as it
|
||||||
// pretended earlier.
|
// pretended earlier.
|
||||||
mRanges.InsertElementAt(startIndex, StyledRange(aRange));
|
mRanges.InsertElementAt(startIndex, StyledRange(aRange));
|
||||||
aRange->RegisterSelection(aSelection);
|
aRange->RegisterSelection(MOZ_KnownLive(mSelection));
|
||||||
aOutIndex->emplace(startIndex);
|
aOutIndex->emplace(startIndex);
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
@ -1083,7 +1080,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
|
||||||
|
|
||||||
// Remove all the overlapping ranges
|
// Remove all the overlapping ranges
|
||||||
for (size_t i = startIndex; i < endIndex; ++i) {
|
for (size_t i = startIndex; i < endIndex; ++i) {
|
||||||
mRanges[i].mRange->UnregisterSelection();
|
mRanges[i].mRange->UnregisterSelection(mSelection);
|
||||||
}
|
}
|
||||||
mRanges.RemoveElementsAt(startIndex, endIndex - startIndex);
|
mRanges.RemoveElementsAt(startIndex, endIndex - startIndex);
|
||||||
|
|
||||||
|
@ -1105,7 +1102,7 @@ nsresult Selection::StyledRanges::MaybeAddRangeAndTruncateOverlaps(
|
||||||
mRanges.InsertElementsAt(startIndex, temp);
|
mRanges.InsertElementsAt(startIndex, temp);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < temp.Length(); ++i) {
|
for (uint32_t i = 0; i < temp.Length(); ++i) {
|
||||||
MOZ_KnownLive(temp[i].mRange)->RegisterSelection(aSelection);
|
MOZ_KnownLive(temp[i].mRange)->RegisterSelection(MOZ_KnownLive(mSelection));
|
||||||
// `MOZ_KnownLive` is required because of
|
// `MOZ_KnownLive` is required because of
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1622253.
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1622253.
|
||||||
}
|
}
|
||||||
|
@ -1131,7 +1128,7 @@ nsresult Selection::StyledRanges::RemoveRangeAndUnregisterSelection(
|
||||||
if (idx < 0) return NS_ERROR_DOM_NOT_FOUND_ERR;
|
if (idx < 0) return NS_ERROR_DOM_NOT_FOUND_ERR;
|
||||||
|
|
||||||
mRanges.RemoveElementAt(idx);
|
mRanges.RemoveElementAt(idx);
|
||||||
aRange.UnregisterSelection();
|
aRange.UnregisterSelection(mSelection);
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
nsresult Selection::RemoveCollapsedRanges() {
|
nsresult Selection::RemoveCollapsedRanges() {
|
||||||
|
@ -1470,8 +1467,8 @@ nsresult Selection::SelectFramesOfInclusiveDescendantsOfContent(
|
||||||
void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) {
|
void Selection::SelectFramesInAllRanges(nsPresContext* aPresContext) {
|
||||||
for (size_t i = 0; i < mStyledRanges.Length(); ++i) {
|
for (size_t i = 0; i < mStyledRanges.Length(); ++i) {
|
||||||
nsRange* range = mStyledRanges.mRanges[i].mRange;
|
nsRange* range = mStyledRanges.mRanges[i].mRange;
|
||||||
MOZ_ASSERT(range->IsInSelection());
|
MOZ_ASSERT(range->IsInAnySelection());
|
||||||
SelectFrames(aPresContext, range, range->IsInSelection());
|
SelectFrames(aPresContext, range, range->IsInAnySelection());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1789,7 +1786,7 @@ void Selection::SetAncestorLimiter(nsIContent* aLimiter) {
|
||||||
void Selection::StyledRanges::UnregisterSelection() {
|
void Selection::StyledRanges::UnregisterSelection() {
|
||||||
uint32_t count = mRanges.Length();
|
uint32_t count = mRanges.Length();
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
mRanges[i].mRange->UnregisterSelection();
|
mRanges[i].mRange->UnregisterSelection(mSelection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1946,19 +1943,16 @@ void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange,
|
||||||
void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange,
|
void Selection::AddRangeAndSelectFramesAndNotifyListeners(nsRange& aRange,
|
||||||
Document* aDocument,
|
Document* aDocument,
|
||||||
ErrorResult& aRv) {
|
ErrorResult& aRv) {
|
||||||
// If the given range is part of another Selection, we need to clone the
|
RefPtr<nsRange> range = &aRange;
|
||||||
// range first.
|
if (aRange.IsInAnySelection()) {
|
||||||
RefPtr<nsRange> range;
|
if (aRange.IsInSelection(*this)) {
|
||||||
if (aRange.IsInSelection()) {
|
|
||||||
// If we already have the range, we don't need to handle this.
|
// If we already have the range, we don't need to handle this.
|
||||||
if (aRange.GetSelection() == this) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Because of performance reason, when there is a cached range, let's use
|
if (mSelectionType != SelectionType::eNormal &&
|
||||||
// it. Otherwise, clone the range.
|
mSelectionType != SelectionType::eHighlight) {
|
||||||
range = aRange.CloneRange();
|
range = aRange.CloneRange();
|
||||||
} else {
|
}
|
||||||
range = &aRange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nsINode* rangeRoot = range->GetRoot();
|
nsINode* rangeRoot = range->GetRoot();
|
||||||
|
|
|
@ -814,6 +814,7 @@ class Selection final : public nsSupportsWeakReference,
|
||||||
void Disconnect();
|
void Disconnect();
|
||||||
|
|
||||||
struct StyledRanges {
|
struct StyledRanges {
|
||||||
|
explicit StyledRanges(Selection& aSelection) : mSelection(aSelection) {}
|
||||||
void Clear();
|
void Clear();
|
||||||
|
|
||||||
StyledRange* FindRangeData(nsRange* aRange);
|
StyledRange* FindRangeData(nsRange* aRange);
|
||||||
|
@ -871,8 +872,8 @@ class Selection final : public nsSupportsWeakReference,
|
||||||
* it. Hence it'll always be in [0, mRanges.Length()).
|
* it. Hence it'll always be in [0, mRanges.Length()).
|
||||||
* This is nothing only when the method returns an error.
|
* This is nothing only when the method returns an error.
|
||||||
*/
|
*/
|
||||||
MOZ_CAN_RUN_SCRIPT nsresult MaybeAddRangeAndTruncateOverlaps(
|
MOZ_CAN_RUN_SCRIPT nsresult
|
||||||
nsRange* aRange, Maybe<size_t>* aOutIndex, Selection& aSelection);
|
MaybeAddRangeAndTruncateOverlaps(nsRange* aRange, Maybe<size_t>* aOutIndex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GetCommonEditingHost() returns common editing host of all
|
* GetCommonEditingHost() returns common editing host of all
|
||||||
|
@ -929,9 +930,11 @@ class Selection final : public nsSupportsWeakReference,
|
||||||
// a possible solution, allowing the calculation of the overlap interval in
|
// a possible solution, allowing the calculation of the overlap interval in
|
||||||
// O(log n) time, though this would require rebalancing and other overhead.
|
// O(log n) time, though this would require rebalancing and other overhead.
|
||||||
Elements mRanges;
|
Elements mRanges;
|
||||||
|
|
||||||
|
Selection& mSelection;
|
||||||
};
|
};
|
||||||
|
|
||||||
StyledRanges mStyledRanges;
|
StyledRanges mStyledRanges{*this};
|
||||||
|
|
||||||
RefPtr<nsRange> mAnchorFocusRange;
|
RefPtr<nsRange> mAnchorFocusRange;
|
||||||
RefPtr<nsFrameSelection> mFrameSelection;
|
RefPtr<nsFrameSelection> mFrameSelection;
|
||||||
|
|
|
@ -343,7 +343,6 @@ bool nsINode::IsSelected(const uint32_t aStartOffset,
|
||||||
|
|
||||||
// Collect the selection objects for potential ranges.
|
// Collect the selection objects for potential ranges.
|
||||||
nsTHashSet<Selection*> ancestorSelections;
|
nsTHashSet<Selection*> ancestorSelections;
|
||||||
Selection* prevSelection = nullptr;
|
|
||||||
for (; n; n = GetClosestCommonInclusiveAncestorForRangeInSelection(
|
for (; n; n = GetClosestCommonInclusiveAncestorForRangeInSelection(
|
||||||
n->GetParentNode())) {
|
n->GetParentNode())) {
|
||||||
const LinkedList<nsRange>* ranges =
|
const LinkedList<nsRange>* ranges =
|
||||||
|
@ -352,14 +351,12 @@ bool nsINode::IsSelected(const uint32_t aStartOffset,
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const nsRange* range : *ranges) {
|
for (const nsRange* range : *ranges) {
|
||||||
MOZ_ASSERT(range->IsInSelection(),
|
MOZ_ASSERT(range->IsInAnySelection(),
|
||||||
"Why is this range registeed with a node?");
|
"Why is this range registered with a node?");
|
||||||
// Looks like that IsInSelection() assert fails sometimes...
|
// Looks like that IsInSelection() assert fails sometimes...
|
||||||
if (range->IsInSelection()) {
|
if (range->IsInAnySelection()) {
|
||||||
Selection* selection = range->GetSelection();
|
for (const auto* selectionWrapper : range->GetSelections()) {
|
||||||
if (prevSelection != selection) {
|
ancestorSelections.Insert(selectionWrapper->Get());
|
||||||
prevSelection = selection;
|
|
||||||
ancestorSelections.Insert(selection);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,11 @@
|
||||||
using namespace mozilla;
|
using namespace mozilla;
|
||||||
using namespace mozilla::dom;
|
using namespace mozilla::dom;
|
||||||
|
|
||||||
|
SelectionListWrapper::SelectionListWrapper(Selection* aSelection)
|
||||||
|
: mSelection(aSelection) {}
|
||||||
|
NS_IMPL_CYCLE_COLLECTION(SelectionListWrapper)
|
||||||
|
Selection* SelectionListWrapper::Get() const { return mSelection; }
|
||||||
|
|
||||||
template already_AddRefed<nsRange> nsRange::Create(
|
template already_AddRefed<nsRange> nsRange::Create(
|
||||||
const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary,
|
const RangeBoundary& aStartBoundary, const RangeBoundary& aEndBoundary,
|
||||||
ErrorResult& aRv);
|
ErrorResult& aRv);
|
||||||
|
@ -129,7 +134,7 @@ static void InvalidateAllFrames(nsINode* aNode) {
|
||||||
nsTArray<RefPtr<nsRange>>* nsRange::sCachedRanges = nullptr;
|
nsTArray<RefPtr<nsRange>>* nsRange::sCachedRanges = nullptr;
|
||||||
|
|
||||||
nsRange::~nsRange() {
|
nsRange::~nsRange() {
|
||||||
NS_ASSERTION(!IsInSelection(), "deleting nsRange that is in use");
|
NS_ASSERTION(!IsInAnySelection(), "deleting nsRange that is in use");
|
||||||
|
|
||||||
// we want the side effects (releases and list removals)
|
// we want the side effects (releases and list removals)
|
||||||
DoSetRange(RawRangeBoundary(), RawRangeBoundary(), nullptr);
|
DoSetRange(RawRangeBoundary(), RawRangeBoundary(), nullptr);
|
||||||
|
@ -141,7 +146,8 @@ nsRange::nsRange(nsINode* aNode)
|
||||||
mNextStartRef(nullptr),
|
mNextStartRef(nullptr),
|
||||||
mNextEndRef(nullptr) {
|
mNextEndRef(nullptr) {
|
||||||
// printf("Size of nsRange: %zu\n", sizeof(nsRange));
|
// printf("Size of nsRange: %zu\n", sizeof(nsRange));
|
||||||
static_assert(sizeof(nsRange) <= 208,
|
|
||||||
|
static_assert(sizeof(nsRange) <= 216,
|
||||||
"nsRange size shouldn't be increased as far as possible");
|
"nsRange size shouldn't be increased as far as possible");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +194,7 @@ NS_INTERFACE_MAP_END_INHERITING(AbstractRange)
|
||||||
NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange)
|
NS_IMPL_CYCLE_COLLECTION_CLASS(nsRange)
|
||||||
|
|
||||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange)
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsRange, AbstractRange)
|
||||||
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelections);
|
||||||
// We _could_ just rely on Reset() to
|
// We _could_ just rely on Reset() to
|
||||||
// UnregisterClosestCommonInclusiveAncestor(), but it wouldn't know we're
|
// UnregisterClosestCommonInclusiveAncestor(), but it wouldn't know we're
|
||||||
// calling it from Unlink and so would do more work than it really needs to.
|
// calling it from Unlink and so would do more work than it really needs to.
|
||||||
|
@ -204,6 +211,7 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||||
|
|
||||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange)
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsRange, AbstractRange)
|
||||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
|
||||||
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelections)
|
||||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||||
|
|
||||||
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange)
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsRange, AbstractRange)
|
||||||
|
@ -259,7 +267,8 @@ static void UnmarkDescendants(nsINode* aNode) {
|
||||||
void nsRange::RegisterClosestCommonInclusiveAncestor(nsINode* aNode) {
|
void nsRange::RegisterClosestCommonInclusiveAncestor(nsINode* aNode) {
|
||||||
MOZ_ASSERT(aNode, "bad arg");
|
MOZ_ASSERT(aNode, "bad arg");
|
||||||
|
|
||||||
MOZ_DIAGNOSTIC_ASSERT(IsInSelection(), "registering range not in selection");
|
MOZ_DIAGNOSTIC_ASSERT(IsInAnySelection(),
|
||||||
|
"registering range not in selection");
|
||||||
|
|
||||||
mRegisteredClosestCommonInclusiveAncestor = aNode;
|
mRegisteredClosestCommonInclusiveAncestor = aNode;
|
||||||
|
|
||||||
|
@ -437,7 +446,7 @@ void nsRange::CharacterDataChanged(nsIContent* aContent,
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCommonAncestor =
|
bool isCommonAncestor =
|
||||||
IsInSelection() && mStart.Container() == mEnd.Container();
|
IsInAnySelection() && mStart.Container() == mEnd.Container();
|
||||||
if (isCommonAncestor) {
|
if (isCommonAncestor) {
|
||||||
UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false);
|
UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false);
|
||||||
RegisterClosestCommonInclusiveAncestor(newStart.Container());
|
RegisterClosestCommonInclusiveAncestor(newStart.Container());
|
||||||
|
@ -489,7 +498,7 @@ void nsRange::CharacterDataChanged(nsIContent* aContent,
|
||||||
newEnd = {aInfo.mDetails->mNextSibling, newEndOffset};
|
newEnd = {aInfo.mDetails->mNextSibling, newEndOffset};
|
||||||
|
|
||||||
bool isCommonAncestor =
|
bool isCommonAncestor =
|
||||||
IsInSelection() && mStart.Container() == mEnd.Container();
|
IsInAnySelection() && mStart.Container() == mEnd.Container();
|
||||||
if (isCommonAncestor && !newStart.Container()) {
|
if (isCommonAncestor && !newStart.Container()) {
|
||||||
// The split occurs inside the range.
|
// The split occurs inside the range.
|
||||||
UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false);
|
UnregisterClosestCommonInclusiveAncestor(mStart.Container(), false);
|
||||||
|
@ -557,7 +566,7 @@ void nsRange::ContentAppended(nsIContent* aFirstNewContent) {
|
||||||
|
|
||||||
nsINode* container = aFirstNewContent->GetParentNode();
|
nsINode* container = aFirstNewContent->GetParentNode();
|
||||||
MOZ_ASSERT(container);
|
MOZ_ASSERT(container);
|
||||||
if (container->IsMaybeSelected() && IsInSelection()) {
|
if (container->IsMaybeSelected() && IsInAnySelection()) {
|
||||||
nsINode* child = aFirstNewContent;
|
nsINode* child = aFirstNewContent;
|
||||||
while (child) {
|
while (child) {
|
||||||
if (!child
|
if (!child
|
||||||
|
@ -840,19 +849,61 @@ bool nsRange::IntersectsNode(nsINode& aNode, ErrorResult& aRv) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helper class that creates a local copy of `nsRange::mSelections`.
|
||||||
|
*
|
||||||
|
* This class uses the RAII principle to create a local copy of
|
||||||
|
* `nsRange::mSelections`, which is safely iterable while modifications may
|
||||||
|
* occur on the original.
|
||||||
|
* When going out of scope, the local copy is being deleted.
|
||||||
|
*/
|
||||||
|
class MOZ_RAII SelectionListLocalCopy final {
|
||||||
|
public:
|
||||||
|
explicit SelectionListLocalCopy(
|
||||||
|
mozilla::LinkedList<RefPtr<SelectionListWrapper>>& aSelectionList) {
|
||||||
|
for (const auto* elem : aSelectionList) {
|
||||||
|
mSelectionList.insertBack(new SelectionListWrapper(elem->Get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mozilla::LinkedList<RefPtr<SelectionListWrapper>>& Get() {
|
||||||
|
return mSelectionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
~SelectionListLocalCopy() { mSelectionList.clear(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
mozilla::LinkedList<RefPtr<SelectionListWrapper>> mSelectionList;
|
||||||
|
};
|
||||||
|
|
||||||
void nsRange::NotifySelectionListenersAfterRangeSet() {
|
void nsRange::NotifySelectionListenersAfterRangeSet() {
|
||||||
if (mSelection) {
|
if (!mSelections.isEmpty()) {
|
||||||
// Our internal code should not move focus with using this instance while
|
// Our internal code should not move focus with using this instance while
|
||||||
// it's calling Selection::NotifySelectionListeners() which may move focus
|
// it's calling Selection::NotifySelectionListeners() which may move focus
|
||||||
// or calls selection listeners. So, let's set mCalledByJS to false here
|
// or calls selection listeners. So, let's set mCalledByJS to false here
|
||||||
// since non-*JS() methods don't set it to false.
|
// since non-*JS() methods don't set it to false.
|
||||||
AutoCalledByJSRestore calledByJSRestorer(*this);
|
AutoCalledByJSRestore calledByJSRestorer(*this);
|
||||||
mCalledByJS = false;
|
mCalledByJS = false;
|
||||||
// Be aware, this range may be modified or stop being a range for selection
|
|
||||||
// after this call. Additionally, the selection instance may have gone.
|
// Notify all Selections. This may modify the range,
|
||||||
RefPtr<Selection> selection = mSelection.get();
|
// remove it from the selection, or the selection itself may have gone after
|
||||||
|
// the call. Also, new selections may be added.
|
||||||
|
// To ensure that listeners are notified for all *current* selections,
|
||||||
|
// create a copy of the list of selections and use that for iterating. This
|
||||||
|
// way selections can be added or removed safely during iteration.
|
||||||
|
// To save allocation cost, the copy is only created if there is more than
|
||||||
|
// one Selection present (which will barely ever be the case).
|
||||||
|
if (mSelections.getFirst() != mSelections.getLast()) {
|
||||||
|
SelectionListLocalCopy copiedSelections{mSelections};
|
||||||
|
for (const auto* selectionWrapper : copiedSelections.Get()) {
|
||||||
|
RefPtr<Selection> selection = selectionWrapper->Get();
|
||||||
selection->NotifySelectionListeners(calledByJSRestorer.SavedValue());
|
selection->NotifySelectionListeners(calledByJSRestorer.SavedValue());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
RefPtr<Selection> selection = mSelections.getFirst()->Get();
|
||||||
|
selection->NotifySelectionListeners(calledByJSRestorer.SavedValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************
|
/******************************************************
|
||||||
|
@ -929,7 +980,7 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
|
||||||
bool checkCommonAncestor =
|
bool checkCommonAncestor =
|
||||||
(mStart.Container() != aStartBoundary.Container() ||
|
(mStart.Container() != aStartBoundary.Container() ||
|
||||||
mEnd.Container() != aEndBoundary.Container()) &&
|
mEnd.Container() != aEndBoundary.Container()) &&
|
||||||
IsInSelection() && !aNotInsertedYet;
|
IsInAnySelection() && !aNotInsertedYet;
|
||||||
|
|
||||||
// GetClosestCommonInclusiveAncestor is unreliable while we're unlinking
|
// GetClosestCommonInclusiveAncestor is unreliable while we're unlinking
|
||||||
// (could return null if our start/end have already been unlinked), so make
|
// (could return null if our start/end have already been unlinked), so make
|
||||||
|
@ -948,7 +999,7 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
|
||||||
RegisterClosestCommonInclusiveAncestor(newCommonAncestor);
|
RegisterClosestCommonInclusiveAncestor(newCommonAncestor);
|
||||||
} else {
|
} else {
|
||||||
MOZ_DIAGNOSTIC_ASSERT(!mIsPositioned, "unexpected disconnected nodes");
|
MOZ_DIAGNOSTIC_ASSERT(!mIsPositioned, "unexpected disconnected nodes");
|
||||||
mSelection = nullptr;
|
mSelections.clear();
|
||||||
MOZ_DIAGNOSTIC_ASSERT(
|
MOZ_DIAGNOSTIC_ASSERT(
|
||||||
!mRegisteredClosestCommonInclusiveAncestor,
|
!mRegisteredClosestCommonInclusiveAncestor,
|
||||||
"How can we have a registered common ancestor when we "
|
"How can we have a registered common ancestor when we "
|
||||||
|
@ -970,42 +1021,48 @@ void nsRange::DoSetRange(const RangeBoundaryBase<SPT, SRT>& aStartBoundary,
|
||||||
// the world could be observed by a selection listener while the range was in
|
// the world could be observed by a selection listener while the range was in
|
||||||
// an invalid state. So we run it off of a script runner to ensure it runs
|
// an invalid state. So we run it off of a script runner to ensure it runs
|
||||||
// after the mutation observers have finished running.
|
// after the mutation observers have finished running.
|
||||||
if (mSelection) {
|
if (!mSelections.isEmpty()) {
|
||||||
nsContentUtils::AddScriptRunner(
|
nsContentUtils::AddScriptRunner(
|
||||||
NewRunnableMethod("NotifySelectionListenersAfterRangeSet", this,
|
NewRunnableMethod("NotifySelectionListenersAfterRangeSet", this,
|
||||||
&nsRange::NotifySelectionListenersAfterRangeSet));
|
&nsRange::NotifySelectionListenersAfterRangeSet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void nsRange::RegisterSelection(Selection& aSelection) {
|
bool nsRange::IsInSelection(const Selection& aSelection) const {
|
||||||
// A range can belong to at most one Selection instance.
|
for (const auto* selectionWrapper : mSelections) {
|
||||||
MOZ_ASSERT(!mSelection);
|
if (selectionWrapper->Get() == &aSelection) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (mSelection == &aSelection) {
|
void nsRange::RegisterSelection(Selection& aSelection) {
|
||||||
|
if (IsInSelection(aSelection)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
bool isFirstSelection = mSelections.isEmpty();
|
||||||
// Extra step in case our parent failed to ensure the above precondition.
|
mSelections.insertBack(new SelectionListWrapper(&aSelection));
|
||||||
if (mSelection) {
|
if (isFirstSelection && !mRegisteredClosestCommonInclusiveAncestor) {
|
||||||
const RefPtr<nsRange> range{this};
|
|
||||||
const RefPtr<Selection> selection{mSelection};
|
|
||||||
selection->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
|
|
||||||
IgnoreErrors());
|
|
||||||
}
|
|
||||||
|
|
||||||
mSelection = &aSelection;
|
|
||||||
|
|
||||||
nsINode* commonAncestor = GetClosestCommonInclusiveAncestor();
|
nsINode* commonAncestor = GetClosestCommonInclusiveAncestor();
|
||||||
MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes");
|
MOZ_ASSERT(commonAncestor, "unexpected disconnected nodes");
|
||||||
RegisterClosestCommonInclusiveAncestor(commonAncestor);
|
RegisterClosestCommonInclusiveAncestor(commonAncestor);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Selection* nsRange::GetSelection() const { return mSelection; }
|
const mozilla::LinkedList<RefPtr<mozilla::SelectionListWrapper>>&
|
||||||
|
nsRange::GetSelections() const {
|
||||||
|
return mSelections;
|
||||||
|
}
|
||||||
|
|
||||||
void nsRange::UnregisterSelection() {
|
void nsRange::UnregisterSelection(Selection& aSelection) {
|
||||||
mSelection = nullptr;
|
for (auto* selectionWrapper : mSelections) {
|
||||||
|
if (selectionWrapper->Get() == &aSelection) {
|
||||||
if (mRegisteredClosestCommonInclusiveAncestor) {
|
selectionWrapper->remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mSelections.isEmpty() && mRegisteredClosestCommonInclusiveAncestor) {
|
||||||
UnregisterClosestCommonInclusiveAncestor(
|
UnregisterClosestCommonInclusiveAncestor(
|
||||||
mRegisteredClosestCommonInclusiveAncestor, false);
|
mRegisteredClosestCommonInclusiveAncestor, false);
|
||||||
MOZ_DIAGNOSTIC_ASSERT(
|
MOZ_DIAGNOSTIC_ASSERT(
|
||||||
|
@ -2948,7 +3005,7 @@ nsresult nsRange::GetUsedFontFaces(nsLayoutUtils::UsedFontFaceList& aResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
nsINode* nsRange::GetRegisteredClosestCommonInclusiveAncestor() {
|
nsINode* nsRange::GetRegisteredClosestCommonInclusiveAncestor() {
|
||||||
MOZ_ASSERT(IsInSelection(),
|
MOZ_ASSERT(IsInAnySelection(),
|
||||||
"GetRegisteredClosestCommonInclusiveAncestor only valid for range "
|
"GetRegisteredClosestCommonInclusiveAncestor only valid for range "
|
||||||
"in selection");
|
"in selection");
|
||||||
MOZ_ASSERT(mRegisteredClosestCommonInclusiveAncestor);
|
MOZ_ASSERT(mRegisteredClosestCommonInclusiveAncestor);
|
||||||
|
@ -2970,7 +3027,7 @@ nsRange::AutoInvalidateSelection::~AutoInvalidateSelection() {
|
||||||
// with selections, ranges, etc. But if it still is, we should check whether
|
// with selections, ranges, etc. But if it still is, we should check whether
|
||||||
// we have a different common ancestor now, and if so invalidate its subtree
|
// we have a different common ancestor now, and if so invalidate its subtree
|
||||||
// so it paints the selection it's in now.
|
// so it paints the selection it's in now.
|
||||||
if (mRange->IsInSelection()) {
|
if (mRange->IsInAnySelection()) {
|
||||||
nsINode* commonAncestor =
|
nsINode* commonAncestor =
|
||||||
mRange->GetRegisteredClosestCommonInclusiveAncestor();
|
mRange->GetRegisteredClosestCommonInclusiveAncestor();
|
||||||
// XXXbz can commonAncestor really be null here? I wouldn't think so! If
|
// XXXbz can commonAncestor really be null here? I wouldn't think so! If
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#include "mozilla/ErrorResult.h"
|
#include "mozilla/ErrorResult.h"
|
||||||
#include "mozilla/LinkedList.h"
|
#include "mozilla/LinkedList.h"
|
||||||
#include "mozilla/RangeBoundary.h"
|
#include "mozilla/RangeBoundary.h"
|
||||||
|
#include "mozilla/RefPtr.h"
|
||||||
#include "mozilla/WeakPtr.h"
|
#include "mozilla/WeakPtr.h"
|
||||||
|
|
||||||
namespace mozilla {
|
namespace mozilla {
|
||||||
|
@ -33,6 +34,26 @@ class DOMRectList;
|
||||||
class InspectorFontFace;
|
class InspectorFontFace;
|
||||||
class Selection;
|
class Selection;
|
||||||
} // namespace dom
|
} // namespace dom
|
||||||
|
/**
|
||||||
|
* @brief Wrapper class to allow storing a |Selection| in a |LinkedList|.
|
||||||
|
*
|
||||||
|
* This helper allows an |nsRange| to store all |Selection|s associated with it
|
||||||
|
* in a |mozilla::LinkedList|.
|
||||||
|
*/
|
||||||
|
class SelectionListWrapper
|
||||||
|
: public LinkedListElement<RefPtr<SelectionListWrapper>> {
|
||||||
|
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(SelectionListWrapper)
|
||||||
|
NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(SelectionListWrapper)
|
||||||
|
public:
|
||||||
|
explicit SelectionListWrapper(dom::Selection* aSelection);
|
||||||
|
|
||||||
|
/// Returns the stored |Selection|.
|
||||||
|
dom::Selection* Get() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
~SelectionListWrapper() = default;
|
||||||
|
WeakPtr<dom::Selection> mSelection;
|
||||||
|
};
|
||||||
} // namespace mozilla
|
} // namespace mozilla
|
||||||
|
|
||||||
class nsRange final : public mozilla::dom::AbstractRange,
|
class nsRange final : public mozilla::dom::AbstractRange,
|
||||||
|
@ -92,20 +113,26 @@ class nsRange final : public mozilla::dom::AbstractRange,
|
||||||
nsINode* GetRoot() const { return mRoot; }
|
nsINode* GetRoot() const { return mRoot; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true iff this range is part of a Selection object
|
* Return true if this range is part of a Selection object
|
||||||
* and isn't detached.
|
* and isn't detached.
|
||||||
*/
|
*/
|
||||||
bool IsInSelection() const { return !!mSelection; }
|
bool IsInAnySelection() const { return !mSelections.isEmpty(); }
|
||||||
|
|
||||||
MOZ_CAN_RUN_SCRIPT void RegisterSelection(
|
MOZ_CAN_RUN_SCRIPT void RegisterSelection(
|
||||||
mozilla::dom::Selection& aSelection);
|
mozilla::dom::Selection& aSelection);
|
||||||
|
|
||||||
void UnregisterSelection();
|
void UnregisterSelection(mozilla::dom::Selection& aSelection);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns pointer to a Selection if the range is associated with a Selection.
|
* Returns a list of all Selections the range is associated with.
|
||||||
*/
|
*/
|
||||||
mozilla::dom::Selection* GetSelection() const;
|
const mozilla::LinkedList<RefPtr<mozilla::SelectionListWrapper>>&
|
||||||
|
GetSelections() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this range is in |aSelection|.
|
||||||
|
*/
|
||||||
|
bool IsInSelection(const mozilla::dom::Selection& aSelection) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if this range was generated.
|
* Return true if this range was generated.
|
||||||
|
@ -415,7 +442,7 @@ class nsRange final : public mozilla::dom::AbstractRange,
|
||||||
|
|
||||||
struct MOZ_STACK_CLASS AutoInvalidateSelection {
|
struct MOZ_STACK_CLASS AutoInvalidateSelection {
|
||||||
explicit AutoInvalidateSelection(nsRange* aRange) : mRange(aRange) {
|
explicit AutoInvalidateSelection(nsRange* aRange) : mRange(aRange) {
|
||||||
if (!mRange->IsInSelection() || sIsNested) {
|
if (!mRange->IsInAnySelection() || sIsNested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sIsNested = true;
|
sIsNested = true;
|
||||||
|
@ -432,16 +459,18 @@ class nsRange final : public mozilla::dom::AbstractRange,
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
bool IsCleared() const {
|
bool IsCleared() const {
|
||||||
return !mRoot && !mRegisteredClosestCommonInclusiveAncestor &&
|
return !mRoot && !mRegisteredClosestCommonInclusiveAncestor &&
|
||||||
!mSelection && !mNextStartRef && !mNextEndRef;
|
mSelections.isEmpty() && !mNextStartRef && !mNextEndRef;
|
||||||
}
|
}
|
||||||
#endif // #ifdef DEBUG
|
#endif // #ifdef DEBUG
|
||||||
|
|
||||||
nsCOMPtr<nsINode> mRoot;
|
nsCOMPtr<nsINode> mRoot;
|
||||||
// mRegisteredClosestCommonInclusiveAncestor is only non-null when the range
|
// mRegisteredClosestCommonInclusiveAncestor is only non-null when the range
|
||||||
// IsInSelection(). It's kept alive via mStart/mEnd,
|
// IsInAnySelection(). It's kept alive via mStart/mEnd,
|
||||||
// because we update it any time those could become disconnected from it.
|
// because we update it any time those could become disconnected from it.
|
||||||
nsINode* MOZ_NON_OWNING_REF mRegisteredClosestCommonInclusiveAncestor;
|
nsINode* MOZ_NON_OWNING_REF mRegisteredClosestCommonInclusiveAncestor;
|
||||||
mozilla::WeakPtr<mozilla::dom::Selection> mSelection;
|
|
||||||
|
// A Range can be part of multiple |Selection|s. This is a very rare use case.
|
||||||
|
mozilla::LinkedList<RefPtr<mozilla::SelectionListWrapper>> mSelections;
|
||||||
|
|
||||||
// These raw pointers are used to remember a child that is about
|
// These raw pointers are used to remember a child that is about
|
||||||
// to be inserted between a CharacterData call and a subsequent
|
// to be inserted between a CharacterData call and a subsequent
|
||||||
|
|
|
@ -429,7 +429,7 @@ AutoRangeArray::ShrinkRangesIfStartFromOrEndAfterAtomicContent(
|
||||||
|
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
for (auto& range : mRanges) {
|
for (auto& range : mRanges) {
|
||||||
MOZ_ASSERT(!range->IsInSelection(),
|
MOZ_ASSERT(!range->IsInAnySelection(),
|
||||||
"Changing range in selection may cause running script");
|
"Changing range in selection may cause running script");
|
||||||
Result<bool, nsresult> result =
|
Result<bool, nsresult> result =
|
||||||
WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
|
WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
|
||||||
|
@ -939,7 +939,7 @@ AutoRangeArray::SplitTextAtEndBoundariesAndInlineAncestorsAtBothBoundaries(
|
||||||
|
|
||||||
// Correct the range.
|
// Correct the range.
|
||||||
// The new end parent becomes the parent node of the text.
|
// The new end parent becomes the parent node of the text.
|
||||||
MOZ_ASSERT(!range->IsInSelection());
|
MOZ_ASSERT(!range->IsInAnySelection());
|
||||||
range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
|
range->SetEnd(unwrappedSplitAtEndResult.AtNextContent<EditorRawDOMPoint>()
|
||||||
.ToRawRangeBoundary(),
|
.ToRawRangeBoundary(),
|
||||||
ignoredError);
|
ignoredError);
|
||||||
|
|
|
@ -4298,7 +4298,7 @@ WSRunScanner::ShrinkRangeIfStartsFromOrEndsAfterAtomicContent(
|
||||||
const HTMLEditor& aHTMLEditor, nsRange& aRange,
|
const HTMLEditor& aHTMLEditor, nsRange& aRange,
|
||||||
const Element* aEditingHost) {
|
const Element* aEditingHost) {
|
||||||
MOZ_ASSERT(aRange.IsPositioned());
|
MOZ_ASSERT(aRange.IsPositioned());
|
||||||
MOZ_ASSERT(!aRange.IsInSelection(),
|
MOZ_ASSERT(!aRange.IsInAnySelection(),
|
||||||
"Changing range in selection may cause running script");
|
"Changing range in selection may cause running script");
|
||||||
|
|
||||||
if (NS_WARN_IF(!aRange.GetStartContainer()) ||
|
if (NS_WARN_IF(!aRange.GetStartContainer()) ||
|
||||||
|
|
|
@ -1834,11 +1834,12 @@ void mozInlineSpellChecker::UpdateRangesForMisspelledWords(
|
||||||
const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
|
const size_t indexOfOldRangeToKeep = aOldRangesForSomeWords.IndexOf(
|
||||||
nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
|
nodeOffsetRange, 0, CompareRangeAndNodeOffsetRange{});
|
||||||
if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
|
if (indexOfOldRangeToKeep != aOldRangesForSomeWords.NoIndex &&
|
||||||
aOldRangesForSomeWords[indexOfOldRangeToKeep]->GetSelection() ==
|
aOldRangesForSomeWords[indexOfOldRangeToKeep]->IsInSelection(
|
||||||
&aSpellCheckerSelection /** TODO: warn in case the old range doesn't
|
aSpellCheckerSelection)) {
|
||||||
|
/** TODO: warn in case the old range doesn't
|
||||||
belong to the selection. This is not critical,
|
belong to the selection. This is not critical,
|
||||||
because other code can always remove them
|
because other code can always remove them
|
||||||
before the actual spellchecking happens. */) {
|
before the actual spellchecking happens. */
|
||||||
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
|
MOZ_LOG(sInlineSpellCheckerLog, LogLevel::Verbose,
|
||||||
("%s: reusing old range.", __FUNCTION__));
|
("%s: reusing old range.", __FUNCTION__));
|
||||||
|
|
||||||
|
|
|
@ -305,7 +305,7 @@ struct MOZ_RAII AutoPrepareFocusRange {
|
||||||
while (i--) {
|
while (i--) {
|
||||||
nsRange* range = ranges[i].mRange;
|
nsRange* range = ranges[i].mRange;
|
||||||
if (range->IsGenerated()) {
|
if (range->IsGenerated()) {
|
||||||
range->UnregisterSelection();
|
range->UnregisterSelection(aSelection);
|
||||||
aSelection.SelectFrames(presContext, range, false);
|
aSelection.SelectFrames(presContext, range, false);
|
||||||
ranges.RemoveElementAt(i);
|
ranges.RemoveElementAt(i);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
prefs: [dom.customHighlightAPI.enabled:true]
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<script src=/resources/testharness.js></script>
|
||||||
|
<script src=/resources/testharnessreport.js></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<span>One two</span>
|
||||||
|
<script>
|
||||||
|
promise_test(async function (t) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
window.onload = resolve;
|
||||||
|
})
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(document.body, 0);
|
||||||
|
range.setEnd(document.body, 1);
|
||||||
|
const highlight = new Highlight(range);
|
||||||
|
CSS.highlights.set("foo", highlight);
|
||||||
|
document.getSelection().addRange(range);
|
||||||
|
|
||||||
|
const highlightRange = highlight.entries().next().value[0];
|
||||||
|
const selectionRange = document.getSelection().getRangeAt(0);
|
||||||
|
assert_equals(
|
||||||
|
highlightRange,
|
||||||
|
selectionRange,
|
||||||
|
"The same range must be present in the highlight and the Selection."
|
||||||
|
);
|
||||||
|
}, "Range is shared between a custom highlight and the document's Selection.");
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Загрузка…
Ссылка в новой задаче