summaryrefslogtreecommitdiffstats
path: root/dom/base/DOMIntersectionObserver.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/base/DOMIntersectionObserver.cpp')
-rw-r--r--dom/base/DOMIntersectionObserver.cpp471
1 files changed, 471 insertions, 0 deletions
diff --git a/dom/base/DOMIntersectionObserver.cpp b/dom/base/DOMIntersectionObserver.cpp
new file mode 100644
index 000000000..169c3fe7a
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.cpp
@@ -0,0 +1,471 @@
+/* -*- 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 (UnlinkTarget(aTarget)) {
+ aTarget.UnregisterIntersectionObserver(this);
+ }
+}
+
+bool
+DOMIntersectionObserver::UnlinkTarget(Element& aTarget)
+{
+ if (!mObservationTargets.Contains(&aTarget)) {
+ return false;
+ }
+ if (mObservationTargets.Count() == 1) {
+ Disconnect();
+ return false;
+ }
+ mObservationTargets.RemoveEntry(&aTarget);
+ return true;
+}
+
+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();
+ if (mOwner) {
+ 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