Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/base/DOMIntersectionObserver.cpp
Line
Count
Source (jump to first uncovered line)
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 "nsCSSPropertyID.h"
9
#include "nsIFrame.h"
10
#include "nsContentUtils.h"
11
#include "nsLayoutUtils.h"
12
#include "mozilla/ServoBindings.h"
13
14
namespace mozilla {
15
namespace dom {
16
17
0
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
18
0
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
19
0
  NS_INTERFACE_MAP_ENTRY(nsISupports)
20
0
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
0
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
30
0
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
31
0
  NS_INTERFACE_MAP_ENTRY(nsISupports)
32
0
  NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
33
0
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
0
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
41
0
  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
42
0
NS_IMPL_CYCLE_COLLECTION_TRACE_END
43
44
0
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
45
0
  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
46
0
  tmp->Disconnect();
47
0
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
48
0
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
49
0
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
50
0
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
51
0
  NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
52
0
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
53
54
0
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
55
0
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
56
0
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
57
0
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
58
0
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
59
0
  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
60
0
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
61
62
already_AddRefed<DOMIntersectionObserver>
63
DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
64
                                     mozilla::dom::IntersectionCallback& aCb,
65
                                     mozilla::ErrorResult& aRv)
66
0
{
67
0
  return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
68
0
}
69
70
already_AddRefed<DOMIntersectionObserver>
71
DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
72
                                     mozilla::dom::IntersectionCallback& aCb,
73
                                     const mozilla::dom::IntersectionObserverInit& aOptions,
74
                                     mozilla::ErrorResult& aRv)
75
0
{
76
0
  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
77
0
  if (!window) {
78
0
    aRv.Throw(NS_ERROR_FAILURE);
79
0
    return nullptr;
80
0
  }
81
0
  RefPtr<DOMIntersectionObserver> observer =
82
0
    new DOMIntersectionObserver(window.forget(), aCb);
83
0
84
0
  observer->mRoot = aOptions.mRoot;
85
0
86
0
  if (!observer->SetRootMargin(aOptions.mRootMargin)) {
87
0
    aRv.ThrowDOMException(NS_ERROR_DOM_SYNTAX_ERR,
88
0
      NS_LITERAL_CSTRING("rootMargin must be specified in pixels or percent."));
89
0
    return nullptr;
90
0
  }
91
0
92
0
  if (aOptions.mThreshold.IsDoubleSequence()) {
93
0
    const mozilla::dom::Sequence<double>& thresholds = aOptions.mThreshold.GetAsDoubleSequence();
94
0
    observer->mThresholds.SetCapacity(thresholds.Length());
95
0
    for (const auto& thresh : thresholds) {
96
0
      if (thresh < 0.0 || thresh > 1.0) {
97
0
        aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
98
0
        return nullptr;
99
0
      }
100
0
      observer->mThresholds.AppendElement(thresh);
101
0
    }
102
0
    observer->mThresholds.Sort();
103
0
  } else {
104
0
    double thresh = aOptions.mThreshold.GetAsDouble();
105
0
    if (thresh < 0.0 || thresh > 1.0) {
106
0
      aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
107
0
      return nullptr;
108
0
    }
109
0
    observer->mThresholds.AppendElement(thresh);
110
0
  }
111
0
112
0
  return observer.forget();
113
0
}
114
115
bool
116
DOMIntersectionObserver::SetRootMargin(const nsAString& aString)
117
0
{
118
0
  return Servo_IntersectionObserverRootMargin_Parse(&aString, &mRootMargin);
119
0
}
120
121
void
122
DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal)
123
0
{
124
0
  nsString& retVal = aRetVal;
125
0
  Servo_IntersectionObserverRootMargin_ToString(&mRootMargin, &retVal);
126
0
}
127
128
void
129
DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal)
130
0
{
131
0
  aRetVal = mThresholds;
132
0
}
133
134
void
135
DOMIntersectionObserver::Observe(Element& aTarget)
136
0
{
137
0
  if (mObservationTargets.Contains(&aTarget)) {
138
0
    return;
139
0
  }
140
0
  aTarget.RegisterIntersectionObserver(this);
141
0
  mObservationTargets.AppendElement(&aTarget);
142
0
  Connect();
143
0
}
144
145
void
146
DOMIntersectionObserver::Unobserve(Element& aTarget)
147
0
{
148
0
  if (!mObservationTargets.Contains(&aTarget)) {
149
0
    return;
150
0
  }
151
0
152
0
  if (mObservationTargets.Length() == 1) {
153
0
    Disconnect();
154
0
    return;
155
0
  }
156
0
157
0
  mObservationTargets.RemoveElement(&aTarget);
158
0
  aTarget.UnregisterIntersectionObserver(this);
159
0
}
160
161
void
162
DOMIntersectionObserver::UnlinkTarget(Element& aTarget)
163
0
{
164
0
  mObservationTargets.RemoveElement(&aTarget);
165
0
  if (mObservationTargets.Length() == 0) {
166
0
    Disconnect();
167
0
  }
168
0
}
169
170
void
171
DOMIntersectionObserver::Connect()
172
0
{
173
0
  if (mConnected) {
174
0
    return;
175
0
  }
176
0
177
0
  mConnected = true;
178
0
  if (mDocument) {
179
0
    mDocument->AddIntersectionObserver(this);
180
0
  }
181
0
}
182
183
void
184
DOMIntersectionObserver::Disconnect()
185
0
{
186
0
  if (!mConnected) {
187
0
    return;
188
0
  }
189
0
190
0
  mConnected = false;
191
0
  for (size_t i = 0; i < mObservationTargets.Length(); ++i) {
192
0
    Element* target = mObservationTargets.ElementAt(i);
193
0
    target->UnregisterIntersectionObserver(this);
194
0
  }
195
0
  mObservationTargets.Clear();
196
0
  if (mDocument) {
197
0
    mDocument->RemoveIntersectionObserver(this);
198
0
  }
199
0
}
200
201
void
202
DOMIntersectionObserver::TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal)
203
0
{
204
0
  aRetVal.SwapElements(mQueuedEntries);
205
0
  mQueuedEntries.Clear();
206
0
}
207
208
static bool
209
CheckSimilarOrigin(nsINode* aNode1, nsINode* aNode2)
210
0
{
211
0
  nsIPrincipal* principal1 = aNode1->NodePrincipal();
212
0
  nsIPrincipal* principal2 = aNode2->NodePrincipal();
213
0
  nsAutoCString baseDomain1;
214
0
  nsAutoCString baseDomain2;
215
0
216
0
  nsresult rv = principal1->GetBaseDomain(baseDomain1);
217
0
  if (NS_FAILED(rv)) {
218
0
    return principal1 == principal2;
219
0
  }
220
0
221
0
  rv = principal2->GetBaseDomain(baseDomain2);
222
0
  if (NS_FAILED(rv)) {
223
0
    return principal1 == principal2;
224
0
  }
225
0
226
0
  return baseDomain1 == baseDomain2;
227
0
}
228
229
static Maybe<nsRect>
230
EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect)
231
0
{
232
0
  nscoord left = std::max(aRect.x, aOtherRect.x);
233
0
  nscoord top = std::max(aRect.y, aOtherRect.y);
234
0
  nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
235
0
  nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
236
0
  if (left > right || top > bottom) {
237
0
    return Nothing();
238
0
  }
239
0
  return Some(nsRect(left, top, right - left, bottom - top));
240
0
}
241
242
enum class BrowsingContextInfo {
243
  SimilarOriginBrowsingContext,
244
  DifferentOriginBrowsingContext,
245
  UnknownBrowsingContext
246
};
247
248
void
249
DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
250
0
{
251
0
  Element* root = nullptr;
252
0
  nsIFrame* rootFrame = nullptr;
253
0
  nsRect rootRect;
254
0
255
0
  if (mRoot) {
256
0
    root = mRoot;
257
0
    rootFrame = root->GetPrimaryFrame();
258
0
    if (rootFrame) {
259
0
      nsRect rootRectRelativeToRootFrame;
260
0
      if (rootFrame->IsScrollFrame()) {
261
0
        // rootRectRelativeToRootFrame should be the content rect of rootFrame, not including the scrollbars.
262
0
        nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
263
0
        rootRectRelativeToRootFrame = scrollFrame->GetScrollPortRect();
264
0
      } else {
265
0
        // rootRectRelativeToRootFrame should be the border rect of rootFrame.
266
0
        rootRectRelativeToRootFrame = rootFrame->GetRectRelativeToSelf();
267
0
      }
268
0
      nsIFrame* containingBlock =
269
0
        nsLayoutUtils::GetContainingBlockForClientRect(rootFrame);
270
0
      rootRect =
271
0
        nsLayoutUtils::TransformFrameRectToAncestor(rootFrame,
272
0
                                                    rootRectRelativeToRootFrame,
273
0
                                                    containingBlock);
274
0
    }
275
0
  } else {
276
0
    nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
277
0
    if (presShell) {
278
0
      rootFrame = presShell->GetRootScrollFrame();
279
0
      if (rootFrame) {
280
0
        nsPresContext* presContext = rootFrame->PresContext();
281
0
        while (!presContext->IsRootContentDocument()) {
282
0
          presContext = presContext->GetParentPresContext();
283
0
          if (!presContext) {
284
0
            break;
285
0
          }
286
0
          nsIFrame* rootScrollFrame = presContext->PresShell()->GetRootScrollFrame();
287
0
          if (rootScrollFrame) {
288
0
            rootFrame = rootScrollFrame;
289
0
          } else {
290
0
            break;
291
0
          }
292
0
        }
293
0
        root = rootFrame->GetContent()->AsElement();
294
0
        nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
295
0
        // If we end up with a null root frame for some reason, we'll proceed
296
0
        // with an empty root intersection rect.
297
0
        if (scrollFrame) {
298
0
          rootRect = scrollFrame->GetScrollPortRect();
299
0
        }
300
0
      }
301
0
    }
302
0
  }
303
0
304
0
  nsMargin rootMargin;
305
0
  NS_FOR_CSS_SIDES(side) {
306
0
    nscoord basis = side == eSideTop || side == eSideBottom ?
307
0
      rootRect.Height() : rootRect.Width();
308
0
    nsStyleCoord coord = mRootMargin.Get(side);
309
0
    rootMargin.Side(side) = nsLayoutUtils::ComputeCBDependentValue(basis, coord);
310
0
  }
311
0
312
0
  for (size_t i = 0; i < mObservationTargets.Length(); ++i) {
313
0
    Element* target = mObservationTargets.ElementAt(i);
314
0
    nsIFrame* targetFrame = target->GetPrimaryFrame();
315
0
    nsRect targetRect;
316
0
    Maybe<nsRect> intersectionRect;
317
0
    bool isSameDoc = root && root->GetComposedDoc() == target->GetComposedDoc();
318
0
319
0
    if (rootFrame && targetFrame) {
320
0
      // If mRoot is set we are testing intersection with a container element
321
0
      // instead of the implicit root.
322
0
      if (mRoot) {
323
0
        // Skip further processing of this target if it is not in the same
324
0
        // Document as the intersection root, e.g. if root is an element of
325
0
        // the main document and target an element from an embedded iframe.
326
0
        if (!isSameDoc) {
327
0
          continue;
328
0
        }
329
0
        // Skip further processing of this target if is not a descendant of the
330
0
        // intersection root in the containing block chain. E.g. this would be
331
0
        // the case if the target is in a position:absolute element whose
332
0
        // containing block is an ancestor of root.
333
0
        if (!nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame)) {
334
0
          continue;
335
0
        }
336
0
      }
337
0
338
0
      targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
339
0
        targetFrame,
340
0
        nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
341
0
        nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
342
0
      );
343
0
      intersectionRect = Some(targetFrame->GetRectRelativeToSelf());
344
0
345
0
      nsIFrame* containerFrame = nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
346
0
      while (containerFrame && containerFrame != rootFrame) {
347
0
        if (containerFrame->IsScrollFrame()) {
348
0
          nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
349
0
          nsRect subFrameRect = scrollFrame->GetScrollPortRect();
350
0
          nsRect intersectionRectRelativeToContainer =
351
0
            nsLayoutUtils::TransformFrameRectToAncestor(targetFrame,
352
0
                                                        intersectionRect.value(),
353
0
                                                        containerFrame);
354
0
          intersectionRect = EdgeInclusiveIntersection(intersectionRectRelativeToContainer,
355
0
                                                       subFrameRect);
356
0
          if (!intersectionRect) {
357
0
            break;
358
0
          }
359
0
          targetFrame = containerFrame;
360
0
        }
361
0
362
0
        // TODO: Apply clip-path.
363
0
364
0
        containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
365
0
      }
366
0
    }
367
0
368
0
    nsRect rootIntersectionRect;
369
0
    BrowsingContextInfo isInSimilarOriginBrowsingContext =
370
0
      BrowsingContextInfo::UnknownBrowsingContext;
371
0
372
0
    if (rootFrame && targetFrame) {
373
0
      rootIntersectionRect = rootRect;
374
0
    }
375
0
376
0
    if (root && target) {
377
0
      isInSimilarOriginBrowsingContext = CheckSimilarOrigin(root, target) ?
378
0
        BrowsingContextInfo::SimilarOriginBrowsingContext :
379
0
        BrowsingContextInfo::DifferentOriginBrowsingContext;
380
0
    }
381
0
382
0
    if (isInSimilarOriginBrowsingContext ==
383
0
        BrowsingContextInfo::SimilarOriginBrowsingContext) {
384
0
      rootIntersectionRect.Inflate(rootMargin);
385
0
    }
386
0
387
0
    if (intersectionRect.isSome()) {
388
0
      nsRect intersectionRectRelativeToRoot =
389
0
        nsLayoutUtils::TransformFrameRectToAncestor(
390
0
          targetFrame,
391
0
          intersectionRect.value(),
392
0
          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
393
0
      );
394
0
      intersectionRect = EdgeInclusiveIntersection(
395
0
        intersectionRectRelativeToRoot,
396
0
        rootIntersectionRect
397
0
      );
398
0
      if (intersectionRect.isSome() && !isSameDoc) {
399
0
        nsRect rect = intersectionRect.value();
400
0
        nsPresContext* presContext = targetFrame->PresContext();
401
0
        nsIFrame* rootScrollFrame = presContext->PresShell()->GetRootScrollFrame();
402
0
        if (rootScrollFrame) {
403
0
          nsLayoutUtils::TransformRect(rootFrame, rootScrollFrame, rect);
404
0
        }
405
0
        intersectionRect = Some(rect);
406
0
      }
407
0
    }
408
0
409
0
    int64_t targetArea =
410
0
      (int64_t) targetRect.Width() * (int64_t) targetRect.Height();
411
0
    int64_t intersectionArea = !intersectionRect ? 0 :
412
0
      (int64_t) intersectionRect->Width() *
413
0
      (int64_t) intersectionRect->Height();
414
0
415
0
    double intersectionRatio;
416
0
    if (targetArea > 0.0) {
417
0
      intersectionRatio =
418
0
        std::min((double) intersectionArea / (double) targetArea, 1.0);
419
0
    } else {
420
0
      intersectionRatio = intersectionRect.isSome() ? 1.0 : 0.0;
421
0
    }
422
0
423
0
    int32_t threshold = -1;
424
0
    if (intersectionRect.isSome()) {
425
0
      // Spec: "Let thresholdIndex be the index of the first entry in
426
0
      // observer.thresholds whose value is greater than intersectionRatio."
427
0
      threshold = mThresholds.IndexOfFirstElementGt(intersectionRatio);
428
0
      if (threshold == 0) {
429
0
        // Per the spec, we should leave threshold at 0 and distinguish between
430
0
        // "less than all thresholds and intersecting" and "not intersecting"
431
0
        // (queuing observer entries as both cases come to pass). However,
432
0
        // neither Chrome nor the WPT tests expect this behavior, so treat these
433
0
        // two cases as one.
434
0
        threshold = -1;
435
0
      }
436
0
    }
437
0
438
0
    if (target->UpdateIntersectionObservation(this, threshold)) {
439
0
      QueueIntersectionObserverEntry(
440
0
        target, time,
441
0
        isInSimilarOriginBrowsingContext ==
442
0
          BrowsingContextInfo::DifferentOriginBrowsingContext ?
443
0
          Nothing() : Some(rootIntersectionRect),
444
0
        targetRect, intersectionRect, intersectionRatio
445
0
      );
446
0
    }
447
0
  }
448
0
}
449
450
void
451
DOMIntersectionObserver::QueueIntersectionObserverEntry(Element* aTarget,
452
                                                        DOMHighResTimeStamp time,
453
                                                        const Maybe<nsRect>& aRootRect,
454
                                                        const nsRect& aTargetRect,
455
                                                        const Maybe<nsRect>& aIntersectionRect,
456
                                                        double aIntersectionRatio)
457
0
{
458
0
  RefPtr<DOMRect> rootBounds;
459
0
  if (aRootRect.isSome()) {
460
0
    rootBounds = new DOMRect(this);
461
0
    rootBounds->SetLayoutRect(aRootRect.value());
462
0
  }
463
0
  RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
464
0
  boundingClientRect->SetLayoutRect(aTargetRect);
465
0
  RefPtr<DOMRect> intersectionRect = new DOMRect(this);
466
0
  if (aIntersectionRect.isSome()) {
467
0
    intersectionRect->SetLayoutRect(aIntersectionRect.value());
468
0
  }
469
0
  RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
470
0
    this,
471
0
    time,
472
0
    rootBounds.forget(),
473
0
    boundingClientRect.forget(),
474
0
    intersectionRect.forget(),
475
0
    aIntersectionRect.isSome(),
476
0
    aTarget, aIntersectionRatio);
477
0
  mQueuedEntries.AppendElement(entry.forget());
478
0
}
479
480
void
481
DOMIntersectionObserver::Notify()
482
0
{
483
0
  if (!mQueuedEntries.Length()) {
484
0
    return;
485
0
  }
486
0
  mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>> entries;
487
0
  if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
488
0
    for (size_t i = 0; i < mQueuedEntries.Length(); ++i) {
489
0
      RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
490
0
      *entries.AppendElement(mozilla::fallible) = next;
491
0
    }
492
0
  }
493
0
  mQueuedEntries.Clear();
494
0
  mCallback->Call(this, entries, *this);
495
0
}
496
497
498
} // namespace dom
499
} // namespace mozilla