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