mirror of
https://github.com/roytam1/UXP.git
synced 2026-05-26 22:48:47 +00:00
78b6f639b2
targetFrame is modified during the intersection computation loop, so it's not the viewport you want if there are scrollframes around. This bug triggers when IntersectionObservers are used on frames that wrap. Follow-up for #249.
528 lines
17 KiB
C++
528 lines
17 KiB
C++
/* -*- 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/. */
|
|
|
|
#include "DOMIntersectionObserver.h"
|
|
#include "nsCSSParser.h"
|
|
#include "nsCSSPropertyID.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsContentUtils.h"
|
|
#include "nsLayoutUtils.h"
|
|
|
|
namespace mozilla {
|
|
namespace dom {
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
|
|
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
|
|
mRootBounds, mBoundingClientRect,
|
|
mIntersectionRect, mTarget)
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
|
|
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
|
tmp->Disconnect();
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
already_AddRefed<DOMIntersectionObserver>
|
|
DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
|
|
mozilla::dom::IntersectionCallback& aCb,
|
|
mozilla::ErrorResult& aRv)
|
|
{
|
|
return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
|
|
}
|
|
|
|
already_AddRefed<DOMIntersectionObserver>
|
|
DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
|
|
mozilla::dom::IntersectionCallback& aCb,
|
|
const mozilla::dom::IntersectionObserverInit& aOptions,
|
|
mozilla::ErrorResult& aRv)
|
|
{
|
|
nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
|
|
if (!window) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
RefPtr<DOMIntersectionObserver> observer =
|
|
new DOMIntersectionObserver(window.forget(), aCb);
|
|
|
|
observer->mRoot = aOptions.mRoot;
|
|
|
|
if (!observer->SetRootMargin(aOptions.mRootMargin)) {
|
|
aRv.ThrowDOMException(NS_ERROR_DOM_SYNTAX_ERR,
|
|
NS_LITERAL_CSTRING("rootMargin must be specified in pixels or percent."));
|
|
return nullptr;
|
|
}
|
|
|
|
if (aOptions.mThreshold.IsDoubleSequence()) {
|
|
const mozilla::dom::Sequence<double>& thresholds = aOptions.mThreshold.GetAsDoubleSequence();
|
|
observer->mThresholds.SetCapacity(thresholds.Length());
|
|
for (const auto& thresh : thresholds) {
|
|
if (thresh < 0.0 || thresh > 1.0) {
|
|
aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
|
|
return nullptr;
|
|
}
|
|
observer->mThresholds.AppendElement(thresh);
|
|
}
|
|
observer->mThresholds.Sort();
|
|
} else {
|
|
double thresh = aOptions.mThreshold.GetAsDouble();
|
|
if (thresh < 0.0 || thresh > 1.0) {
|
|
aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
|
|
return nullptr;
|
|
}
|
|
observer->mThresholds.AppendElement(thresh);
|
|
}
|
|
|
|
return observer.forget();
|
|
}
|
|
|
|
bool
|
|
DOMIntersectionObserver::SetRootMargin(const nsAString& aString)
|
|
{
|
|
// By not passing a CSS Loader object we make sure we don't parse in quirks
|
|
// mode so that pixel/percent and unit-less values will be differentiated.
|
|
nsCSSParser parser(nullptr);
|
|
nsCSSValue value;
|
|
if (!parser.ParseMarginString(aString, nullptr, 0, value, true)) {
|
|
return false;
|
|
}
|
|
|
|
mRootMargin = value.GetRectValue();
|
|
|
|
for (uint32_t i = 0; i < ArrayLength(nsCSSRect::sides); ++i) {
|
|
nsCSSValue value = mRootMargin.*nsCSSRect::sides[i];
|
|
if (!(value.IsPixelLengthUnit() || value.IsPercentLengthUnit())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal)
|
|
{
|
|
mRootMargin.AppendToString(eCSSProperty_DOM, aRetVal, nsCSSValue::eNormalized);
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal)
|
|
{
|
|
aRetVal = mThresholds;
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::Observe(Element& aTarget)
|
|
{
|
|
if (mObservationTargets.Contains(&aTarget)) {
|
|
return;
|
|
}
|
|
aTarget.RegisterIntersectionObserver(this);
|
|
mObservationTargets.AppendElement(&aTarget);
|
|
Connect();
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::Unobserve(Element& aTarget)
|
|
{
|
|
if (!mObservationTargets.Contains(&aTarget)) {
|
|
// You're not on the list, buddy!
|
|
return;
|
|
}
|
|
|
|
if (mObservationTargets.Length() == 1) {
|
|
Disconnect();
|
|
return;
|
|
}
|
|
|
|
mObservationTargets.RemoveElement(&aTarget);
|
|
aTarget.UnregisterIntersectionObserver(this);
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::UnlinkTarget(Element& aTarget)
|
|
{
|
|
mObservationTargets.RemoveElement(&aTarget);
|
|
if (mObservationTargets.Length() == 0) {
|
|
Disconnect();
|
|
}
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::Connect()
|
|
{
|
|
if (mConnected) {
|
|
return;
|
|
}
|
|
|
|
mConnected = true;
|
|
if (mDocument) {
|
|
mDocument->AddIntersectionObserver(this);
|
|
}
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::Disconnect()
|
|
{
|
|
if (!mConnected) {
|
|
return;
|
|
}
|
|
|
|
mConnected = false;
|
|
|
|
for (size_t i = 0; i < mObservationTargets.Length(); ++i) {
|
|
Element* target = mObservationTargets.ElementAt(i);
|
|
target->UnregisterIntersectionObserver(this);
|
|
}
|
|
mObservationTargets.Clear();
|
|
if (mDocument) {
|
|
mDocument->RemoveIntersectionObserver(this);
|
|
}
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal)
|
|
{
|
|
aRetVal.SwapElements(mQueuedEntries);
|
|
mQueuedEntries.Clear();
|
|
}
|
|
|
|
static bool
|
|
CheckSimilarOrigin(nsINode* aNode1, nsINode* aNode2)
|
|
{
|
|
nsIPrincipal* principal1 = aNode1->NodePrincipal();
|
|
nsIPrincipal* principal2 = aNode2->NodePrincipal();
|
|
nsAutoCString baseDomain1;
|
|
nsAutoCString baseDomain2;
|
|
|
|
nsresult rv = principal1->GetBaseDomain(baseDomain1);
|
|
if (NS_FAILED(rv)) {
|
|
return principal1 == principal2;
|
|
}
|
|
|
|
rv = principal2->GetBaseDomain(baseDomain2);
|
|
if (NS_FAILED(rv)) {
|
|
return principal1 == principal2;
|
|
}
|
|
|
|
return baseDomain1 == baseDomain2;
|
|
}
|
|
|
|
static Maybe<nsRect>
|
|
EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect)
|
|
{
|
|
nscoord left = std::max(aRect.x, aOtherRect.x);
|
|
nscoord top = std::max(aRect.y, aOtherRect.y);
|
|
nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
|
|
nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
|
|
if (left > right || top > bottom) {
|
|
return Nothing();
|
|
}
|
|
return Some(nsRect(left, top, right - left, bottom - top));
|
|
}
|
|
|
|
enum class BrowsingContextInfo {
|
|
SimilarOriginBrowsingContext,
|
|
DifferentOriginBrowsingContext,
|
|
UnknownBrowsingContext
|
|
};
|
|
|
|
void
|
|
DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
|
|
{
|
|
Element* root = nullptr;
|
|
nsIFrame* rootFrame = nullptr;
|
|
nsRect rootRect;
|
|
|
|
if (mRoot) {
|
|
root = mRoot;
|
|
rootFrame = root->GetPrimaryFrame();
|
|
if (rootFrame) {
|
|
nsRect rootRectRelativeToRootFrame;
|
|
if (rootFrame->GetType() == nsGkAtoms::scrollFrame) {
|
|
// rootRectRelativeToRootFrame should be the content rect of rootFrame, not including the scrollbars.
|
|
nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
|
|
rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
|
|
} else {
|
|
// rootRectRelativeToRootFrame should be the border rect of rootFrame.
|
|
rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
|
|
}
|
|
nsIFrame* containingBlock =
|
|
nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
|
|
rootRect =
|
|
nsLayoutUtils::TransformFrameRectToAncestor(rootFrame,
|
|
rootRectRelativeToRootFrame,
|
|
containingBlock);
|
|
}
|
|
} else {
|
|
nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
|
|
if (presShell) {
|
|
rootFrame = presShell->GetRootScrollFrame();
|
|
if (rootFrame) {
|
|
nsPresContext* presContext = rootFrame->PresContext();
|
|
while (!presContext->IsRootContentDocument()) {
|
|
// Walk up the tree
|
|
presContext = presContext->GetParentPresContext();
|
|
if (!presContext) {
|
|
break;
|
|
}
|
|
nsIFrame* rootScrollFrame = presContext->PresShell()->GetRootScrollFrame();
|
|
if (rootScrollFrame) {
|
|
rootFrame = rootScrollFrame;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
root = rootFrame->GetContent()->AsElement();
|
|
nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
|
|
// If we end up with a null root frame for some reason, we'll proceed
|
|
// with an empty root intersection rect.
|
|
if (scrollFrame) {
|
|
rootRect = scrollFrame->GetScrollPortRect();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
nsMargin rootMargin;
|
|
NS_FOR_CSS_SIDES(side) {
|
|
nscoord basis = side == NS_SIDE_TOP || side == NS_SIDE_BOTTOM ?
|
|
rootRect.height : rootRect.width;
|
|
nsCSSValue value = mRootMargin.*nsCSSRect::sides[side];
|
|
nsStyleCoord coord;
|
|
if (value.IsPixelLengthUnit()) {
|
|
coord.SetCoordValue(value.GetPixelLength());
|
|
} else if (value.IsPercentLengthUnit()) {
|
|
coord.SetPercentValue(value.GetPercentValue());
|
|
} else {
|
|
MOZ_ASSERT_UNREACHABLE("invalid length unit");
|
|
}
|
|
rootMargin.Side(side) = nsLayoutUtils::ComputeCBDependentValue(basis, coord);
|
|
}
|
|
|
|
for (size_t i = 0; i < mObservationTargets.Length(); ++i) {
|
|
Element* target = mObservationTargets.ElementAt(i);
|
|
nsIFrame* targetFrame = target->GetPrimaryFrame();
|
|
nsIFrame* originalTargetFrame = targetFrame;
|
|
nsRect targetRect;
|
|
Maybe<nsRect> intersectionRect;
|
|
bool isSameDoc = root && root->GetComposedDoc() == target->GetComposedDoc();
|
|
|
|
if (rootFrame && targetFrame) {
|
|
// If mRoot is set we are testing intersection with a container element
|
|
// instead of the implicit root.
|
|
if (mRoot) {
|
|
// Skip further processing of this target if it is not in the same
|
|
// Document as the intersection root, e.g. if root is an element of
|
|
// the main document and target an element from an embedded iframe.
|
|
if (!isSameDoc) {
|
|
continue;
|
|
}
|
|
// Skip further processing of this target if is not a descendant of the
|
|
// intersection root in the containing block chain. E.g. this would be
|
|
// the case if the target is in a position:absolute element whose
|
|
// containing block is an ancestor of root.
|
|
if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
|
|
targetFrame,
|
|
nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
|
|
nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
|
|
);
|
|
intersectionRect = Some(targetFrame->GetRectRelativeToSelf());
|
|
|
|
nsIFrame* containerFrame = nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
|
|
while (containerFrame && containerFrame != rootFrame) {
|
|
if (containerFrame->GetType() == nsGkAtoms::scrollFrame) {
|
|
nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
|
|
nsRect subFrameRect = scrollFrame->GetScrollPortRect();
|
|
nsRect intersectionRectRelativeToContainer =
|
|
nsLayoutUtils::TransformFrameRectToAncestor(targetFrame,
|
|
intersectionRect.value(),
|
|
containerFrame);
|
|
intersectionRect = EdgeInclusiveIntersection(intersectionRectRelativeToContainer,
|
|
subFrameRect);
|
|
if (!intersectionRect) {
|
|
break;
|
|
}
|
|
targetFrame = containerFrame;
|
|
}
|
|
|
|
// TODO: Apply clip-path.
|
|
|
|
containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
|
|
}
|
|
}
|
|
|
|
nsRect rootIntersectionRect;
|
|
BrowsingContextInfo isInSimilarOriginBrowsingContext =
|
|
BrowsingContextInfo::UnknownBrowsingContext;
|
|
|
|
if (rootFrame && targetFrame) {
|
|
rootIntersectionRect = rootRect;
|
|
}
|
|
|
|
if (root && target) {
|
|
isInSimilarOriginBrowsingContext = CheckSimilarOrigin(root, target) ?
|
|
BrowsingContextInfo::SimilarOriginBrowsingContext :
|
|
BrowsingContextInfo::DifferentOriginBrowsingContext;
|
|
}
|
|
|
|
if (isInSimilarOriginBrowsingContext ==
|
|
BrowsingContextInfo::SimilarOriginBrowsingContext) {
|
|
rootIntersectionRect.Inflate(rootMargin);
|
|
}
|
|
|
|
if (intersectionRect.isSome()) {
|
|
nsRect intersectionRectRelativeToRoot =
|
|
nsLayoutUtils::TransformFrameRectToAncestor(
|
|
targetFrame,
|
|
intersectionRect.value(),
|
|
nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
|
|
);
|
|
intersectionRect = EdgeInclusiveIntersection(
|
|
intersectionRectRelativeToRoot,
|
|
rootIntersectionRect
|
|
);
|
|
if (intersectionRect.isSome() && !isSameDoc) {
|
|
nsRect rect = intersectionRect.value();
|
|
nsPresContext* presContext = originalTargetFrame->PresContext();
|
|
nsLayoutUtils::TransformRect(rootFrame,
|
|
presContext->PresShell()->GetRootScrollFrame(), rect);
|
|
intersectionRect = Some(rect);
|
|
}
|
|
}
|
|
|
|
int64_t targetArea =
|
|
(int64_t) targetRect.Width() * (int64_t) targetRect.Height();
|
|
int64_t intersectionArea = !intersectionRect ? 0 :
|
|
(int64_t) intersectionRect->Width() *
|
|
(int64_t) intersectionRect->Height();
|
|
|
|
double intersectionRatio;
|
|
if (targetArea > 0.0) {
|
|
intersectionRatio = (double) intersectionArea / (double) targetArea;
|
|
} else {
|
|
intersectionRatio = intersectionRect.isSome() ? 1.0 : 0.0;
|
|
}
|
|
|
|
int32_t threshold = -1;
|
|
if (intersectionRatio > 0.0) {
|
|
if (intersectionRatio >= 1.0) {
|
|
intersectionRatio = 1.0;
|
|
threshold = (int32_t)mThresholds.Length();
|
|
} else {
|
|
for (size_t k = 0; k < mThresholds.Length(); ++k) {
|
|
if (mThresholds[k] <= intersectionRatio) {
|
|
threshold = (int32_t)k + 1;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (intersectionRect.isSome()) {
|
|
threshold = 0;
|
|
}
|
|
|
|
if (target->UpdateIntersectionObservation(this, threshold)) {
|
|
QueueIntersectionObserverEntry(
|
|
target, time,
|
|
isInSimilarOriginBrowsingContext ==
|
|
BrowsingContextInfo::DifferentOriginBrowsingContext ?
|
|
Nothing() : Some(rootIntersectionRect),
|
|
targetRect, intersectionRect, intersectionRatio
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::QueueIntersectionObserverEntry(Element* aTarget,
|
|
DOMHighResTimeStamp time,
|
|
const Maybe<nsRect>& aRootRect,
|
|
const nsRect& aTargetRect,
|
|
const Maybe<nsRect>& aIntersectionRect,
|
|
double aIntersectionRatio)
|
|
{
|
|
RefPtr<DOMRect> rootBounds;
|
|
if (aRootRect.isSome()) {
|
|
rootBounds = new DOMRect(this);
|
|
rootBounds->SetLayoutRect(aRootRect.value());
|
|
}
|
|
RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
|
|
boundingClientRect->SetLayoutRect(aTargetRect);
|
|
RefPtr<DOMRect> intersectionRect = new DOMRect(this);
|
|
if (aIntersectionRect.isSome()) {
|
|
intersectionRect->SetLayoutRect(aIntersectionRect.value());
|
|
}
|
|
RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
|
|
this,
|
|
time,
|
|
rootBounds.forget(),
|
|
boundingClientRect.forget(),
|
|
intersectionRect.forget(),
|
|
aIntersectionRect.isSome(),
|
|
aTarget, aIntersectionRatio);
|
|
mQueuedEntries.AppendElement(entry.forget());
|
|
}
|
|
|
|
void
|
|
DOMIntersectionObserver::Notify()
|
|
{
|
|
if (!mQueuedEntries.Length()) {
|
|
return;
|
|
}
|
|
mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>> entries;
|
|
if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
|
|
for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
|
|
RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
|
|
*entries.AppendElement(mozilla::fallible) = next;
|
|
}
|
|
}
|
|
mQueuedEntries.Clear();
|
|
mCallback->Call(this, entries, *this);
|
|
}
|
|
|
|
|
|
} // namespace dom
|
|
} // namespace mozilla
|