gecko-dev/dom/base/DOMIntersectionObserver.cpp
2016-11-02 00:25:10 -07:00

462 lines
15 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
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
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_SCRIPT_OBJECTS
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
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.PutEntry(&aTarget);
Connect();
}
void
DOMIntersectionObserver::Unobserve(Element& aTarget)
{
if (!mObservationTargets.Contains(&aTarget)) {
return;
}
if (mObservationTargets.Count() == 1) {
Disconnect();
return;
}
aTarget.UnregisterIntersectionObserver(this);
mObservationTargets.RemoveEntry(&aTarget);
}
void
DOMIntersectionObserver::Connect()
{
if (mConnected) {
return;
}
nsIDocument* document = mOwner->GetExtantDoc();
document->AddIntersectionObserver(this);
mConnected = true;
}
void
DOMIntersectionObserver::Disconnect()
{
if (!mConnected) {
return;
}
for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
Element* target = iter.Get()->GetKey();
target->UnregisterIntersectionObserver(this);
}
mObservationTargets.Clear();
nsIDocument* document = mOwner->GetExtantDoc();
document->RemoveIntersectionObserver(this);
mConnected = false;
}
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));
}
void
DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
{
Element* root = nullptr;
nsIFrame* rootFrame = nullptr;
nsRect rootRect;
if (mRoot) {
root = mRoot;
rootFrame = root->GetPrimaryFrame();
if (rootFrame) {
if (rootFrame->GetType() == nsGkAtoms::scrollFrame) {
nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
rootFrame,
rootFrame->GetContentRectRelativeToSelf(),
scrollFrame->GetScrolledFrame());
} else {
rootRect = nsLayoutUtils::GetAllInFlowRectsUnion(rootFrame,
nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
}
}
} else {
nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
if (presShell) {
rootFrame = presShell->GetRootScrollFrame();
if (rootFrame) {
nsPresContext* presContext = rootFrame->PresContext();
while (!presContext->IsRootContentDocument()) {
presContext = rootFrame->PresContext()->GetParentPresContext();
rootFrame = presContext->PresShell()->GetRootScrollFrame();
}
root = rootFrame->GetContent()->AsElement();
nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
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 (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
Element* target = iter.Get()->GetKey();
nsIFrame* targetFrame = target->GetPrimaryFrame();
nsRect targetRect;
Maybe<nsRect> intersectionRect;
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 (target->GetComposedDoc() != root->GetComposedDoc()) {
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->GetVisualOverflowRect());
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 = rootRect;
bool isInSimilarOriginBrowsingContext = rootFrame && targetFrame &&
CheckSimilarOrigin(root, target);
if (isInSimilarOriginBrowsingContext) {
rootIntersectionRect.Inflate(rootMargin);
}
if (intersectionRect.isSome()) {
nsRect intersectionRectRelativeToRoot =
nsLayoutUtils::TransformFrameRectToAncestor(
targetFrame,
intersectionRect.value(),
nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
);
intersectionRect = EdgeInclusiveIntersection(
intersectionRectRelativeToRoot,
rootIntersectionRect
);
if (intersectionRect.isSome()) {
intersectionRect = Some(nsLayoutUtils::TransformFrameRectToAncestor(
nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
intersectionRect.value(),
targetFrame->PresContext()->PresShell()->GetRootScrollFrame()
));
}
}
double targetArea = targetRect.width * targetRect.height;
double intersectionArea = !intersectionRect ?
0 : intersectionRect->width * intersectionRect->height;
double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
size_t threshold = -1;
if (intersectionRatio > 0.0) {
if (intersectionRatio >= 1.0) {
intersectionRatio = 1.0;
threshold = mThresholds.Length();
} else {
for (size_t k = 0; k < mThresholds.Length(); ++k) {
if (mThresholds[k] <= intersectionRatio) {
threshold = k + 1;
} else {
break;
}
}
}
} else if (intersectionRect.isSome()) {
threshold = 0;
}
if (target->UpdateIntersectionObservation(this, threshold)) {
QueueIntersectionObserverEntry(
target, time,
isInSimilarOriginBrowsingContext ? Some(rootIntersectionRect) : Nothing(),
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(),
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 (uint32_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