/* -*- 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_SCRIPT_OBJECTS 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::Constructor(const mozilla::dom::GlobalObject& aGlobal, mozilla::dom::IntersectionCallback& aCb, mozilla::ErrorResult& aRv) { return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv); } already_AddRefed DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal, mozilla::dom::IntersectionCallback& aCb, const mozilla::dom::IntersectionObserverInit& aOptions, mozilla::ErrorResult& aRv) { nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); if (!window) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr 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& thresholds = aOptions.mThreshold.GetAsDoubleSequence(); observer->mThresholds.SetCapacity(thresholds.Length()); for (const auto& thresh : thresholds) { if (thresh < 0.0 || thresh > 1.0) { aRv.ThrowTypeError(); return nullptr; } observer->mThresholds.AppendElement(thresh); } observer->mThresholds.Sort(); } else { double thresh = aOptions.mThreshold.GetAsDouble(); if (thresh < 0.0 || thresh > 1.0) { aRv.ThrowTypeError(); 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& 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.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>& 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 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 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 (size_t i = 0; i < mObservationTargets.Length(); ++i) { Element* target = mObservationTargets.ElementAt(i); nsIFrame* targetFrame = target->GetPrimaryFrame(); nsRect targetRect; Maybe 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 = targetFrame->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& aRootRect, const nsRect& aTargetRect, const Maybe& aIntersectionRect, double aIntersectionRatio) { RefPtr rootBounds; if (aRootRect.isSome()) { rootBounds = new DOMRect(this); rootBounds->SetLayoutRect(aRootRect.value()); } RefPtr boundingClientRect = new DOMRect(this); boundingClientRect->SetLayoutRect(aTargetRect); RefPtr intersectionRect = new DOMRect(this); if (aIntersectionRect.isSome()) { intersectionRect->SetLayoutRect(aIntersectionRect.value()); } RefPtr 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> entries; if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) { for (size_t i = 0; i < mQueuedEntries.Length(); ++i) { RefPtr next = mQueuedEntries[i]; *entries.AppendElement(mozilla::fallible) = next; } } mQueuedEntries.Clear(); mCallback->Call(this, entries, *this); } } // namespace dom } // namespace mozilla