1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "DOMIntersectionObserver.h"
8 #include "nsCSSPropertyID.h"
10 #include "nsContentUtils.h"
11 #include "nsLayoutUtils.h"
12 #include "mozilla/PresShell.h"
13 #include "mozilla/ServoBindings.h"
18 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry
)
19 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
20 NS_INTERFACE_MAP_ENTRY(nsISupports
)
23 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry
)
24 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry
)
26 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry
, mOwner
,
27 mRootBounds
, mBoundingClientRect
,
28 mIntersectionRect
, mTarget
)
30 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver
)
31 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
32 NS_INTERFACE_MAP_ENTRY(nsISupports
)
33 NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver
)
36 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver
)
37 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver
)
39 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver
)
41 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver
)
42 NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
43 NS_IMPL_CYCLE_COLLECTION_TRACE_END
45 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver
)
46 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
48 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner
)
49 NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument
)
50 NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback
)
51 NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot
)
52 NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries
)
53 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
55 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver
)
56 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner
)
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument
)
58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback
)
59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot
)
60 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries
)
61 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
63 already_AddRefed
<DOMIntersectionObserver
> DOMIntersectionObserver::Constructor(
64 const mozilla::dom::GlobalObject
& aGlobal
,
65 mozilla::dom::IntersectionCallback
& aCb
, mozilla::ErrorResult
& aRv
) {
66 return Constructor(aGlobal
, aCb
, IntersectionObserverInit(), aRv
);
69 already_AddRefed
<DOMIntersectionObserver
> DOMIntersectionObserver::Constructor(
70 const mozilla::dom::GlobalObject
& aGlobal
,
71 mozilla::dom::IntersectionCallback
& aCb
,
72 const mozilla::dom::IntersectionObserverInit
& aOptions
,
73 mozilla::ErrorResult
& aRv
) {
74 nsCOMPtr
<nsPIDOMWindowInner
> window
=
75 do_QueryInterface(aGlobal
.GetAsSupports());
77 aRv
.Throw(NS_ERROR_FAILURE
);
80 RefPtr
<DOMIntersectionObserver
> observer
=
81 new DOMIntersectionObserver(window
.forget(), aCb
);
83 observer
->mRoot
= aOptions
.mRoot
;
85 if (!observer
->SetRootMargin(aOptions
.mRootMargin
)) {
86 aRv
.ThrowDOMException(
87 NS_ERROR_DOM_SYNTAX_ERR
,
89 "rootMargin must be specified in pixels or percent."));
93 if (aOptions
.mThreshold
.IsDoubleSequence()) {
94 const mozilla::dom::Sequence
<double>& thresholds
=
95 aOptions
.mThreshold
.GetAsDoubleSequence();
96 observer
->mThresholds
.SetCapacity(thresholds
.Length());
97 for (const auto& thresh
: thresholds
) {
98 if (thresh
< 0.0 || thresh
> 1.0) {
99 aRv
.ThrowTypeError
<dom::MSG_THRESHOLD_RANGE_ERROR
>();
102 observer
->mThresholds
.AppendElement(thresh
);
104 observer
->mThresholds
.Sort();
106 double thresh
= aOptions
.mThreshold
.GetAsDouble();
107 if (thresh
< 0.0 || thresh
> 1.0) {
108 aRv
.ThrowTypeError
<dom::MSG_THRESHOLD_RANGE_ERROR
>();
111 observer
->mThresholds
.AppendElement(thresh
);
114 return observer
.forget();
117 bool DOMIntersectionObserver::SetRootMargin(const nsAString
& aString
) {
118 return Servo_IntersectionObserverRootMargin_Parse(&aString
, &mRootMargin
);
121 void DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString
& aRetVal
) {
122 nsString
& retVal
= aRetVal
;
123 Servo_IntersectionObserverRootMargin_ToString(&mRootMargin
, &retVal
);
126 void DOMIntersectionObserver::GetThresholds(nsTArray
<double>& aRetVal
) {
127 aRetVal
= mThresholds
;
130 void DOMIntersectionObserver::Observe(Element
& aTarget
) {
131 if (mObservationTargets
.Contains(&aTarget
)) {
134 aTarget
.RegisterIntersectionObserver(this);
135 mObservationTargets
.AppendElement(&aTarget
);
139 void DOMIntersectionObserver::Unobserve(Element
& aTarget
) {
140 if (!mObservationTargets
.Contains(&aTarget
)) {
144 if (mObservationTargets
.Length() == 1) {
149 mObservationTargets
.RemoveElement(&aTarget
);
150 aTarget
.UnregisterIntersectionObserver(this);
153 void DOMIntersectionObserver::UnlinkTarget(Element
& aTarget
) {
154 mObservationTargets
.RemoveElement(&aTarget
);
155 if (mObservationTargets
.Length() == 0) {
160 void DOMIntersectionObserver::Connect() {
167 mDocument
->AddIntersectionObserver(this);
171 void DOMIntersectionObserver::Disconnect() {
177 for (size_t i
= 0; i
< mObservationTargets
.Length(); ++i
) {
178 Element
* target
= mObservationTargets
.ElementAt(i
);
179 target
->UnregisterIntersectionObserver(this);
181 mObservationTargets
.Clear();
183 mDocument
->RemoveIntersectionObserver(this);
187 void DOMIntersectionObserver::TakeRecords(
188 nsTArray
<RefPtr
<DOMIntersectionObserverEntry
>>& aRetVal
) {
189 aRetVal
.SwapElements(mQueuedEntries
);
190 mQueuedEntries
.Clear();
193 static Maybe
<nsRect
> EdgeInclusiveIntersection(const nsRect
& aRect
,
194 const nsRect
& aOtherRect
) {
195 nscoord left
= std::max(aRect
.x
, aOtherRect
.x
);
196 nscoord top
= std::max(aRect
.y
, aOtherRect
.y
);
197 nscoord right
= std::min(aRect
.XMost(), aOtherRect
.XMost());
198 nscoord bottom
= std::min(aRect
.YMost(), aOtherRect
.YMost());
199 if (left
> right
|| top
> bottom
) {
202 return Some(nsRect(left
, top
, right
- left
, bottom
- top
));
205 enum class BrowsingContextOrigin
{ Similar
, Different
, Unknown
};
207 // FIXME(emilio): The whole concept of "units of related similar-origin browsing
208 // contexts" is gone, but this is still in the spec, see
209 // https://github.com/w3c/IntersectionObserver/issues/161
210 static BrowsingContextOrigin
SimilarOrigin(const Element
& aTarget
,
211 const Element
* aRoot
) {
213 return BrowsingContextOrigin::Unknown
;
215 nsIPrincipal
* principal1
= aTarget
.NodePrincipal();
216 nsIPrincipal
* principal2
= aRoot
->NodePrincipal();
218 if (principal1
== principal2
) {
219 return BrowsingContextOrigin::Similar
;
222 nsAutoCString baseDomain1
;
223 nsAutoCString baseDomain2
;
224 if (NS_FAILED(principal1
->GetBaseDomain(baseDomain1
)) ||
225 NS_FAILED(principal2
->GetBaseDomain(baseDomain2
))) {
226 return BrowsingContextOrigin::Different
;
229 return baseDomain1
== baseDomain2
? BrowsingContextOrigin::Similar
230 : BrowsingContextOrigin::Different
;
233 void DOMIntersectionObserver::Update(Document
* aDocument
,
234 DOMHighResTimeStamp time
) {
236 nsIFrame
* rootFrame
= nullptr;
237 Element
* root
= mRoot
;
239 if ((rootFrame
= mRoot
->GetPrimaryFrame())) {
240 nsRect rootRectRelativeToRootFrame
;
241 if (rootFrame
->IsScrollFrame()) {
242 // rootRectRelativeToRootFrame should be the content rect of rootFrame,
243 // not including the scrollbars.
244 nsIScrollableFrame
* scrollFrame
= do_QueryFrame(rootFrame
);
245 rootRectRelativeToRootFrame
= scrollFrame
->GetScrollPortRect();
247 // rootRectRelativeToRootFrame should be the border rect of rootFrame.
248 rootRectRelativeToRootFrame
= rootFrame
->GetRectRelativeToSelf();
250 nsIFrame
* containingBlock
=
251 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame
);
252 rootRect
= nsLayoutUtils::TransformFrameRectToAncestor(
253 rootFrame
, rootRectRelativeToRootFrame
, containingBlock
);
255 } else if (PresShell
* presShell
= aDocument
->GetPresShell()) {
256 // FIXME(emilio): This shouldn't probably go through the presShell and just
257 // through the document tree.
258 rootFrame
= presShell
->GetRootScrollFrame();
260 nsPresContext
* presContext
= rootFrame
->PresContext();
261 while (!presContext
->IsRootContentDocument()) {
262 presContext
= presContext
->GetParentPresContext();
266 nsIFrame
* rootScrollFrame
=
267 presContext
->PresShell()->GetRootScrollFrame();
268 if (rootScrollFrame
) {
269 rootFrame
= rootScrollFrame
;
274 root
= rootFrame
->GetContent()->AsElement();
275 nsIScrollableFrame
* scrollFrame
= do_QueryFrame(rootFrame
);
276 rootRect
= scrollFrame
->GetScrollPortRect();
281 NS_FOR_CSS_SIDES(side
) {
282 nscoord basis
= side
== eSideTop
|| side
== eSideBottom
? rootRect
.Height()
284 rootMargin
.Side(side
) =
285 nsLayoutUtils::ComputeCBDependentValue(basis
, mRootMargin
.Get(side
));
288 for (Element
* target
: mObservationTargets
) {
289 nsIFrame
* targetFrame
= target
->GetPrimaryFrame();
290 nsIFrame
* originalTargetFrame
= targetFrame
;
292 Maybe
<nsRect
> intersectionRect
;
293 bool isSameDoc
= root
&& root
->GetComposedDoc() == target
->GetComposedDoc();
295 if (rootFrame
&& targetFrame
) {
296 // If mRoot is set we are testing intersection with a container element
297 // instead of the implicit root.
299 // Skip further processing of this target if it is not in the same
300 // Document as the intersection root, e.g. if root is an element of
301 // the main document and target an element from an embedded iframe.
305 // Skip further processing of this target if is not a descendant of the
306 // intersection root in the containing block chain. E.g. this would be
307 // the case if the target is in a position:absolute element whose
308 // containing block is an ancestor of root.
309 if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame
, targetFrame
)) {
314 targetRect
= nsLayoutUtils::GetAllInFlowRectsUnion(
316 nsLayoutUtils::GetContainingBlockForClientRect(targetFrame
),
317 nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
);
318 intersectionRect
= Some(targetFrame
->GetRectRelativeToSelf());
320 nsIFrame
* containerFrame
=
321 nsLayoutUtils::GetCrossDocParentFrame(targetFrame
);
322 while (containerFrame
&& containerFrame
!= rootFrame
) {
323 if (containerFrame
->IsScrollFrame()) {
324 nsIScrollableFrame
* scrollFrame
= do_QueryFrame(containerFrame
);
325 nsRect subFrameRect
= scrollFrame
->GetScrollPortRect();
326 nsRect intersectionRectRelativeToContainer
=
327 nsLayoutUtils::TransformFrameRectToAncestor(
328 targetFrame
, intersectionRect
.value(), containerFrame
);
329 intersectionRect
= EdgeInclusiveIntersection(
330 intersectionRectRelativeToContainer
, subFrameRect
);
331 if (!intersectionRect
) {
334 targetFrame
= containerFrame
;
337 // TODO: Apply clip-path.
339 containerFrame
= nsLayoutUtils::GetCrossDocParentFrame(containerFrame
);
343 nsRect rootIntersectionRect
;
344 if (rootFrame
&& targetFrame
) {
345 // FIXME(emilio): Why only if there are frames?
346 rootIntersectionRect
= rootRect
;
349 BrowsingContextOrigin origin
= SimilarOrigin(*target
, root
);
350 if (origin
== BrowsingContextOrigin::Similar
) {
351 rootIntersectionRect
.Inflate(rootMargin
);
354 if (intersectionRect
.isSome()) {
355 nsRect intersectionRectRelativeToRoot
=
356 nsLayoutUtils::TransformFrameRectToAncestor(
357 targetFrame
, intersectionRect
.value(),
358 nsLayoutUtils::GetContainingBlockForClientRect(rootFrame
));
359 intersectionRect
= EdgeInclusiveIntersection(
360 intersectionRectRelativeToRoot
, rootIntersectionRect
);
361 if (intersectionRect
.isSome() && !isSameDoc
) {
362 nsRect rect
= intersectionRect
.value();
363 nsPresContext
* presContext
= originalTargetFrame
->PresContext();
364 nsIFrame
* rootScrollFrame
=
365 presContext
->PresShell()->GetRootScrollFrame();
366 if (rootScrollFrame
) {
367 nsLayoutUtils::TransformRect(rootFrame
, rootScrollFrame
, rect
);
369 intersectionRect
= Some(rect
);
374 (int64_t)targetRect
.Width() * (int64_t)targetRect
.Height();
375 int64_t intersectionArea
= !intersectionRect
377 : (int64_t)intersectionRect
->Width() *
378 (int64_t)intersectionRect
->Height();
380 double intersectionRatio
;
381 if (targetArea
> 0.0) {
383 std::min((double)intersectionArea
/ (double)targetArea
, 1.0);
385 intersectionRatio
= intersectionRect
.isSome() ? 1.0 : 0.0;
388 int32_t threshold
= -1;
389 if (intersectionRect
.isSome()) {
390 // Spec: "Let thresholdIndex be the index of the first entry in
391 // observer.thresholds whose value is greater than intersectionRatio."
392 threshold
= mThresholds
.IndexOfFirstElementGt(intersectionRatio
);
393 if (threshold
== 0) {
394 // Per the spec, we should leave threshold at 0 and distinguish between
395 // "less than all thresholds and intersecting" and "not intersecting"
396 // (queuing observer entries as both cases come to pass). However,
397 // neither Chrome nor the WPT tests expect this behavior, so treat these
403 if (target
->UpdateIntersectionObservation(this, threshold
)) {
404 QueueIntersectionObserverEntry(target
, time
,
405 origin
== BrowsingContextOrigin::Different
407 : Some(rootIntersectionRect
),
408 targetRect
, intersectionRect
,
414 void DOMIntersectionObserver::QueueIntersectionObserverEntry(
415 Element
* aTarget
, DOMHighResTimeStamp time
, const Maybe
<nsRect
>& aRootRect
,
416 const nsRect
& aTargetRect
, const Maybe
<nsRect
>& aIntersectionRect
,
417 double aIntersectionRatio
) {
418 RefPtr
<DOMRect
> rootBounds
;
419 if (aRootRect
.isSome()) {
420 rootBounds
= new DOMRect(this);
421 rootBounds
->SetLayoutRect(aRootRect
.value());
423 RefPtr
<DOMRect
> boundingClientRect
= new DOMRect(this);
424 boundingClientRect
->SetLayoutRect(aTargetRect
);
425 RefPtr
<DOMRect
> intersectionRect
= new DOMRect(this);
426 if (aIntersectionRect
.isSome()) {
427 intersectionRect
->SetLayoutRect(aIntersectionRect
.value());
429 RefPtr
<DOMIntersectionObserverEntry
> entry
= new DOMIntersectionObserverEntry(
430 this, time
, rootBounds
.forget(), boundingClientRect
.forget(),
431 intersectionRect
.forget(), aIntersectionRect
.isSome(), aTarget
,
433 mQueuedEntries
.AppendElement(entry
.forget());
436 void DOMIntersectionObserver::Notify() {
437 if (!mQueuedEntries
.Length()) {
440 mozilla::dom::Sequence
<mozilla::OwningNonNull
<DOMIntersectionObserverEntry
>>
442 if (entries
.SetCapacity(mQueuedEntries
.Length(), mozilla::fallible
)) {
443 for (size_t i
= 0; i
< mQueuedEntries
.Length(); ++i
) {
444 RefPtr
<DOMIntersectionObserverEntry
> next
= mQueuedEntries
[i
];
445 *entries
.AppendElement(mozilla::fallible
) = next
;
448 mQueuedEntries
.Clear();
449 RefPtr
<dom::IntersectionCallback
> callback(mCallback
);
450 callback
->Call(this, entries
, *this);
454 } // namespace mozilla