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/. */
6 
7 #include "DOMIntersectionObserver.h"
8 #include "nsCSSParser.h"
9 #include "nsCSSPropertyID.h"
10 #include "nsIFrame.h"
11 #include "nsContentUtils.h"
12 #include "nsLayoutUtils.h"
13 
14 namespace mozilla {
15 namespace dom {
16 
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)17 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
18   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
19   NS_INTERFACE_MAP_ENTRY(nsISupports)
20 NS_INTERFACE_MAP_END
21 
22 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
23 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
24 
25 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
26                                       mRootBounds, mBoundingClientRect,
27                                       mIntersectionRect, mTarget)
28 
29 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
30   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
31   NS_INTERFACE_MAP_ENTRY(nsISupports)
32   NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
33 NS_INTERFACE_MAP_END
34 
35 NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
36 NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
37 
38 NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
39 
40 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
41   NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
42 NS_IMPL_CYCLE_COLLECTION_TRACE_END
43 
44 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
45   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
46   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
47   NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
48   NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
49   NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
50 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
51 
52 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
53   NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
54   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
55   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
56   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
57   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
59 
60 already_AddRefed<DOMIntersectionObserver>
61 DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
62                                      mozilla::dom::IntersectionCallback& aCb,
63                                      mozilla::ErrorResult& aRv)
64 {
65   return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
66 }
67 
68 already_AddRefed<DOMIntersectionObserver>
Constructor(const mozilla::dom::GlobalObject & aGlobal,mozilla::dom::IntersectionCallback & aCb,const mozilla::dom::IntersectionObserverInit & aOptions,mozilla::ErrorResult & aRv)69 DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
70                                      mozilla::dom::IntersectionCallback& aCb,
71                                      const mozilla::dom::IntersectionObserverInit& aOptions,
72                                      mozilla::ErrorResult& aRv)
73 {
74   nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
75   if (!window) {
76     aRv.Throw(NS_ERROR_FAILURE);
77     return nullptr;
78   }
79   RefPtr<DOMIntersectionObserver> observer =
80     new DOMIntersectionObserver(window.forget(), aCb);
81 
82   observer->mRoot = aOptions.mRoot;
83 
84   if (!observer->SetRootMargin(aOptions.mRootMargin)) {
85     aRv.ThrowDOMException(NS_ERROR_DOM_SYNTAX_ERR,
86       NS_LITERAL_CSTRING("rootMargin must be specified in pixels or percent."));
87     return nullptr;
88   }
89 
90   if (aOptions.mThreshold.IsDoubleSequence()) {
91     const mozilla::dom::Sequence<double>& thresholds = aOptions.mThreshold.GetAsDoubleSequence();
92     observer->mThresholds.SetCapacity(thresholds.Length());
93     for (const auto& thresh : thresholds) {
94       if (thresh < 0.0 || thresh > 1.0) {
95         aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
96         return nullptr;
97       }
98       observer->mThresholds.AppendElement(thresh);
99     }
100     observer->mThresholds.Sort();
101   } else {
102     double thresh = aOptions.mThreshold.GetAsDouble();
103     if (thresh < 0.0 || thresh > 1.0) {
104       aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
105       return nullptr;
106     }
107     observer->mThresholds.AppendElement(thresh);
108   }
109 
110   return observer.forget();
111 }
112 
113 bool
SetRootMargin(const nsAString & aString)114 DOMIntersectionObserver::SetRootMargin(const nsAString& aString)
115 {
116   // By not passing a CSS Loader object we make sure we don't parse in quirks
117   // mode so that pixel/percent and unit-less values will be differentiated.
118   nsCSSParser parser(nullptr);
119   nsCSSValue value;
120   if (!parser.ParseMarginString(aString, nullptr, 0, value, true)) {
121     return false;
122   }
123 
124   mRootMargin = value.GetRectValue();
125 
126   for (uint32_t i = 0; i < ArrayLength(nsCSSRect::sides); ++i) {
127     nsCSSValue value = mRootMargin.*nsCSSRect::sides[i];
128     if (!(value.IsPixelLengthUnit() || value.IsPercentLengthUnit())) {
129       return false;
130     }
131   }
132 
133   return true;
134 }
135 
136 void
GetRootMargin(mozilla::dom::DOMString & aRetVal)137 DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal)
138 {
139   mRootMargin.AppendToString(eCSSProperty_DOM, aRetVal, nsCSSValue::eNormalized);
140 }
141 
142 void
GetThresholds(nsTArray<double> & aRetVal)143 DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal)
144 {
145   aRetVal = mThresholds;
146 }
147 
148 void
Observe(Element & aTarget)149 DOMIntersectionObserver::Observe(Element& aTarget)
150 {
151   if (mObservationTargets.Contains(&aTarget)) {
152     return;
153   }
154   aTarget.RegisterIntersectionObserver(this);
155   mObservationTargets.PutEntry(&aTarget);
156   Connect();
157 }
158 
159 void
Unobserve(Element & aTarget)160 DOMIntersectionObserver::Unobserve(Element& aTarget)
161 {
162   if (UnlinkTarget(aTarget)) {
163     aTarget.UnregisterIntersectionObserver(this);
164   }
165 }
166 
167 bool
UnlinkTarget(Element & aTarget)168 DOMIntersectionObserver::UnlinkTarget(Element& aTarget)
169 {
170     if (!mObservationTargets.Contains(&aTarget)) {
171         return false;
172     }
173     if (mObservationTargets.Count() == 1) {
174         Disconnect();
175         return false;
176     }
177     mObservationTargets.RemoveEntry(&aTarget);
178     return true;
179 }
180 
181 void
Connect()182 DOMIntersectionObserver::Connect()
183 {
184   if (mConnected) {
185     return;
186   }
187   nsIDocument* document = mOwner->GetExtantDoc();
188   document->AddIntersectionObserver(this);
189   mConnected = true;
190 }
191 
192 void
Disconnect()193 DOMIntersectionObserver::Disconnect()
194 {
195   if (!mConnected) {
196     return;
197   }
198   for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
199     Element* target = iter.Get()->GetKey();
200     target->UnregisterIntersectionObserver(this);
201   }
202   mObservationTargets.Clear();
203   if (mOwner) {
204     nsIDocument* document = mOwner->GetExtantDoc();
205     document->RemoveIntersectionObserver(this);
206   }
207   mConnected = false;
208 }
209 
210 void
TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>> & aRetVal)211 DOMIntersectionObserver::TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal)
212 {
213   aRetVal.SwapElements(mQueuedEntries);
214   mQueuedEntries.Clear();
215 }
216 
217 static bool
CheckSimilarOrigin(nsINode * aNode1,nsINode * aNode2)218 CheckSimilarOrigin(nsINode* aNode1, nsINode* aNode2)
219 {
220   nsIPrincipal* principal1 = aNode1->NodePrincipal();
221   nsIPrincipal* principal2 = aNode2->NodePrincipal();
222   nsAutoCString baseDomain1;
223   nsAutoCString baseDomain2;
224 
225   nsresult rv = principal1->GetBaseDomain(baseDomain1);
226   if (NS_FAILED(rv)) {
227     return principal1 == principal2;
228   }
229 
230   rv = principal2->GetBaseDomain(baseDomain2);
231   if (NS_FAILED(rv)) {
232     return principal1 == principal2;
233   }
234 
235   return baseDomain1 == baseDomain2;
236 }
237 
238 static Maybe<nsRect>
EdgeInclusiveIntersection(const nsRect & aRect,const nsRect & aOtherRect)239 EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect)
240 {
241   nscoord left = std::max(aRect.x, aOtherRect.x);
242   nscoord top = std::max(aRect.y, aOtherRect.y);
243   nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
244   nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
245   if (left > right || top > bottom) {
246     return Nothing();
247   }
248   return Some(nsRect(left, top, right - left, bottom - top));
249 }
250 
251 void
Update(nsIDocument * aDocument,DOMHighResTimeStamp time)252 DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
253 {
254   Element* root = nullptr;
255   nsIFrame* rootFrame = nullptr;
256   nsRect rootRect;
257 
258   if (mRoot) {
259     root = mRoot;
260     rootFrame = root->GetPrimaryFrame();
261     if (rootFrame) {
262       if (rootFrame->GetType() == nsGkAtoms::scrollFrame) {
263         nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
264         rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
265           rootFrame,
266           rootFrame->GetContentRectRelativeToSelf(),
267           scrollFrame->GetScrolledFrame());
268       } else {
269         rootRect = nsLayoutUtils::GetAllInFlowRectsUnion(rootFrame,
270           nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
271           nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
272       }
273     }
274   } else {
275     nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
276     if (presShell) {
277       rootFrame = presShell->GetRootScrollFrame();
278       if (rootFrame) {
279         nsPresContext* presContext = rootFrame->PresContext();
280         while (!presContext->IsRootContentDocument()) {
281           presContext = rootFrame->PresContext()->GetParentPresContext();
282           rootFrame = presContext->PresShell()->GetRootScrollFrame();
283         }
284         root = rootFrame->GetContent()->AsElement();
285         nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
286         rootRect = scrollFrame->GetScrollPortRect();
287       }
288     }
289   }
290 
291   nsMargin rootMargin;
292   NS_FOR_CSS_SIDES(side) {
293     nscoord basis = side == NS_SIDE_TOP || side == NS_SIDE_BOTTOM ?
294       rootRect.height : rootRect.width;
295     nsCSSValue value = mRootMargin.*nsCSSRect::sides[side];
296     nsStyleCoord coord;
297     if (value.IsPixelLengthUnit()) {
298       coord.SetCoordValue(value.GetPixelLength());
299     } else if (value.IsPercentLengthUnit()) {
300       coord.SetPercentValue(value.GetPercentValue());
301     } else {
302       MOZ_ASSERT_UNREACHABLE("invalid length unit");
303     }
304     rootMargin.Side(side) = nsLayoutUtils::ComputeCBDependentValue(basis, coord);
305   }
306 
307   for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
308     Element* target = iter.Get()->GetKey();
309     nsIFrame* targetFrame = target->GetPrimaryFrame();
310     nsRect targetRect;
311     Maybe<nsRect> intersectionRect;
312 
313     if (rootFrame && targetFrame) {
314       // If mRoot is set we are testing intersection with a container element
315       // instead of the implicit root.
316       if (mRoot) {
317         // Skip further processing of this target if it is not in the same
318         // Document as the intersection root, e.g. if root is an element of
319         // the main document and target an element from an embedded iframe.
320         if (target->GetComposedDoc() != root->GetComposedDoc()) {
321           continue;
322         }
323         // Skip further processing of this target if is not a descendant of the
324         // intersection root in the containing block chain. E.g. this would be
325         // the case if the target is in a position:absolute element whose
326         // containing block is an ancestor of root.
327         if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
328           continue;
329         }
330       }
331 
332       targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
333         targetFrame,
334         nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
335         nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
336       );
337       intersectionRect = Some(targetFrame->GetVisualOverflowRect());
338 
339       nsIFrame* containerFrame = nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
340       while (containerFrame && containerFrame != rootFrame) {
341         if (containerFrame->GetType() == nsGkAtoms::scrollFrame) {
342           nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
343           nsRect subFrameRect = scrollFrame->GetScrollPortRect();
344           nsRect intersectionRectRelativeToContainer =
345             nsLayoutUtils::TransformFrameRectToAncestor(targetFrame,
346                                                         intersectionRect.value(),
347                                                         containerFrame);
348           intersectionRect = EdgeInclusiveIntersection(intersectionRectRelativeToContainer,
349                                                        subFrameRect);
350           if (!intersectionRect) {
351             break;
352           }
353           targetFrame = containerFrame;
354         }
355 
356         // TODO: Apply clip-path.
357 
358         containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
359       }
360     }
361 
362     nsRect rootIntersectionRect = rootRect;
363     bool isInSimilarOriginBrowsingContext = rootFrame && targetFrame &&
364                                             CheckSimilarOrigin(root, target);
365 
366     if (isInSimilarOriginBrowsingContext) {
367       rootIntersectionRect.Inflate(rootMargin);
368     }
369 
370     if (intersectionRect.isSome()) {
371       nsRect intersectionRectRelativeToRoot =
372         nsLayoutUtils::TransformFrameRectToAncestor(
373           targetFrame,
374           intersectionRect.value(),
375           nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
376       );
377       intersectionRect = EdgeInclusiveIntersection(
378         intersectionRectRelativeToRoot,
379         rootIntersectionRect
380       );
381       if (intersectionRect.isSome()) {
382         intersectionRect = Some(nsLayoutUtils::TransformFrameRectToAncestor(
383           nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
384           intersectionRect.value(),
385           targetFrame->PresContext()->PresShell()->GetRootScrollFrame()
386         ));
387       }
388     }
389 
390     double targetArea = targetRect.width * targetRect.height;
391     double intersectionArea = !intersectionRect ?
392       0 : intersectionRect->width * intersectionRect->height;
393     double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
394 
395     size_t threshold = -1;
396     if (intersectionRatio > 0.0) {
397       if (intersectionRatio >= 1.0) {
398         intersectionRatio = 1.0;
399         threshold = mThresholds.Length();
400       } else {
401         for (size_t k = 0; k < mThresholds.Length(); ++k) {
402           if (mThresholds[k] <= intersectionRatio) {
403             threshold = k + 1;
404           } else {
405             break;
406           }
407         }
408       }
409     } else if (intersectionRect.isSome()) {
410       threshold = 0;
411     }
412 
413     if (target->UpdateIntersectionObservation(this, threshold)) {
414       QueueIntersectionObserverEntry(
415         target, time,
416         isInSimilarOriginBrowsingContext ? Some(rootIntersectionRect) : Nothing(),
417         targetRect, intersectionRect, intersectionRatio
418       );
419     }
420   }
421 }
422 
423 void
QueueIntersectionObserverEntry(Element * aTarget,DOMHighResTimeStamp time,const Maybe<nsRect> & aRootRect,const nsRect & aTargetRect,const Maybe<nsRect> & aIntersectionRect,double aIntersectionRatio)424 DOMIntersectionObserver::QueueIntersectionObserverEntry(Element* aTarget,
425                                                         DOMHighResTimeStamp time,
426                                                         const Maybe<nsRect>& aRootRect,
427                                                         const nsRect& aTargetRect,
428                                                         const Maybe<nsRect>& aIntersectionRect,
429                                                         double aIntersectionRatio)
430 {
431   RefPtr<DOMRect> rootBounds;
432   if (aRootRect.isSome()) {
433     rootBounds = new DOMRect(this);
434     rootBounds->SetLayoutRect(aRootRect.value());
435   }
436   RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
437   boundingClientRect->SetLayoutRect(aTargetRect);
438   RefPtr<DOMRect> intersectionRect = new DOMRect(this);
439   if (aIntersectionRect.isSome()) {
440     intersectionRect->SetLayoutRect(aIntersectionRect.value());
441   }
442   RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
443     this,
444     time,
445     rootBounds.forget(),
446     boundingClientRect.forget(),
447     intersectionRect.forget(),
448     aTarget, aIntersectionRatio);
449   mQueuedEntries.AppendElement(entry.forget());
450 }
451 
452 void
Notify()453 DOMIntersectionObserver::Notify()
454 {
455   if (!mQueuedEntries.Length()) {
456     return;
457   }
458   mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>> entries;
459   if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
460     for (uint32_t i = 0; i < mQueuedEntries.Length(); ++i) {
461       RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
462       *entries.AppendElement(mozilla::fallible) = next;
463     }
464   }
465   mQueuedEntries.Clear();
466   mCallback->Call(this, entries, *this);
467 }
468 
469 
470 } // namespace dom
471 } // namespace mozilla
472