script/dom/intersectionobserver/intersectionobserver.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::{Cell, RefCell};
6use std::rc::Rc;
7use std::time::Duration;
8
9use app_units::Au;
10use cssparser::{Parser, ParserInput};
11use dom_struct::dom_struct;
12use euclid::{Rect, SideOffsets2D, Size2D, Vector2D};
13use js::context::JSContext;
14use js::rust::{HandleObject, MutableHandleValue};
15use script_bindings::cell::DomRefCell;
16use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
17use servo_base::cross_process_instant::CrossProcessInstant;
18use servo_geometry::f32_rect_to_au_rect;
19use style::parser::Parse;
20use style::stylesheets::CssRuleType;
21use style::values::computed::Overflow;
22use style::values::specified::intersection_observer::IntersectionObserverMargin;
23use style_traits::{CSSPixel, ParsingMode, ToCss};
24use url::Url;
25
26use crate::css::parser_context_for_anonymous_content;
27use crate::dom::bindings::callback::ExceptionHandling;
28use crate::dom::bindings::codegen::Bindings::IntersectionObserverBinding::{
29 IntersectionObserverCallback, IntersectionObserverInit, IntersectionObserverMethods,
30};
31use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
32use crate::dom::bindings::codegen::UnionTypes::{DoubleOrDoubleSequence, ElementOrDocument};
33use crate::dom::bindings::error::{Error, Fallible};
34use crate::dom::bindings::inheritance::Castable;
35use crate::dom::bindings::num::Finite;
36use crate::dom::bindings::root::{Dom, DomRoot};
37use crate::dom::bindings::str::DOMString;
38use crate::dom::bindings::utils::to_frozen_array;
39use crate::dom::document::{Document, RenderingUpdateReason};
40use crate::dom::domrectreadonly::DOMRectReadOnly;
41use crate::dom::element::Element;
42use crate::dom::intersectionobserverentry::IntersectionObserverEntry;
43use crate::dom::node::{Node, NodeTraits};
44use crate::dom::window::Window;
45
46/// > The intersection root for an IntersectionObserver is the value of its root attribute if the attribute is non-null;
47/// > otherwise, it is the top-level browsing context’s document node, referred to as the implicit root.
48///
49/// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-intersection-root>
50pub type IntersectionRoot = Option<ElementOrDocument>;
51
52/// The Intersection Observer interface
53///
54/// > The IntersectionObserver interface can be used to observe changes in the intersection
55/// > of an intersection root and one or more target Elements.
56///
57/// <https://w3c.github.io/IntersectionObserver/#intersection-observer-interface>
58#[dom_struct]
59pub(crate) struct IntersectionObserver {
60 reflector_: Reflector,
61
62 /// [`Document`] that should process this observer's observation steps.
63 /// Following Chrome and Firefox, it is the current document on construction.
64 /// <https://github.com/w3c/IntersectionObserver/issues/525>
65 owner_doc: Dom<Document>,
66
67 /// > The root provided to the IntersectionObserver constructor, or null if none was provided.
68 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
69 root: IntersectionRoot,
70
71 /// > This callback will be invoked when there are changes to a target’s intersection
72 /// > with the intersection root, as per the processing model.
73 ///
74 /// <https://w3c.github.io/IntersectionObserver/#intersection-observer-callback>
75 #[conditional_malloc_size_of]
76 callback: Rc<IntersectionObserverCallback>,
77
78 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-queuedentries-slot>
79 queued_entries: DomRefCell<Vec<Dom<IntersectionObserverEntry>>>,
80
81 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observationtargets-slot>
82 observation_targets: DomRefCell<Vec<Dom<Element>>>,
83
84 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin-slot>
85 #[no_trace]
86 #[ignore_malloc_size_of = "Defined in style"]
87 root_margin: RefCell<IntersectionObserverMargin>,
88
89 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin-slot>
90 #[no_trace]
91 #[ignore_malloc_size_of = "Defined in style"]
92 scroll_margin: RefCell<IntersectionObserverMargin>,
93
94 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds-slot>
95 thresholds: RefCell<Vec<Finite<f64>>>,
96
97 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay-slot>
98 delay: Cell<i32>,
99
100 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility-slot>
101 track_visibility: Cell<bool>,
102
103 /// Whether or not this [`IntersectionObserver`] is connected to its owning [`Document`].
104 connected_to_document: Cell<bool>,
105}
106
107impl IntersectionObserver {
108 fn new_inherited(
109 window: &Window,
110 callback: Rc<IntersectionObserverCallback>,
111 root: IntersectionRoot,
112 root_margin: IntersectionObserverMargin,
113 scroll_margin: IntersectionObserverMargin,
114 ) -> Self {
115 Self {
116 reflector_: Reflector::new(),
117 owner_doc: window.Document().as_traced(),
118 root,
119 callback,
120 queued_entries: Default::default(),
121 observation_targets: Default::default(),
122 root_margin: RefCell::new(root_margin),
123 scroll_margin: RefCell::new(scroll_margin),
124 thresholds: Default::default(),
125 delay: Default::default(),
126 track_visibility: Default::default(),
127 connected_to_document: Cell::new(false),
128 }
129 }
130
131 /// <https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer>
132 fn new(
133 cx: &mut JSContext,
134 window: &Window,
135 proto: Option<HandleObject>,
136 callback: Rc<IntersectionObserverCallback>,
137 init: &IntersectionObserverInit,
138 ) -> Fallible<DomRoot<Self>> {
139 // Step 3.
140 // > Attempt to parse a margin from options.rootMargin. If a list is returned,
141 // > set this’s internal [[rootMargin]] slot to that. Otherwise, throw a SyntaxError exception.
142 let root_margin = if let Ok(margin) = parse_a_margin(init.rootMargin.as_ref()) {
143 margin
144 } else {
145 return Err(Error::Syntax(None));
146 };
147
148 // Step 4.
149 // > Attempt to parse a margin from options.scrollMargin. If a list is returned,
150 // > set this’s internal [[scrollMargin]] slot to that. Otherwise, throw a SyntaxError exception.
151 let scroll_margin = if let Ok(margin) = parse_a_margin(init.scrollMargin.as_ref()) {
152 margin
153 } else {
154 return Err(Error::Syntax(None));
155 };
156
157 // Step 1 and step 2, 3, 4 setter
158 // > 1. Let this be a new IntersectionObserver object
159 // > 2. Set this’s internal [[callback]] slot to callback.
160 // > 3. ... set this’s internal [[rootMargin]] slot to that.
161 // > 4. ... set this’s internal [[scrollMargin]] slot to that.
162 let observer = reflect_dom_object_with_proto(
163 cx,
164 Box::new(Self::new_inherited(
165 window,
166 callback,
167 init.root.clone(),
168 root_margin,
169 scroll_margin,
170 )),
171 window,
172 proto,
173 );
174
175 // Step 5-13
176 observer.init_observer(init)?;
177
178 Ok(observer)
179 }
180
181 /// Step 5-13 of <https://w3c.github.io/IntersectionObserver/#initialize-new-intersection-observer>
182 fn init_observer(&self, init: &IntersectionObserverInit) -> Fallible<()> {
183 // Step 5
184 // > Let thresholds be a list equal to options.threshold.
185 //
186 // Non-sequence value should be converted into Vec.
187 // Default value of thresholds is [0].
188 let mut thresholds = match &init.threshold {
189 Some(DoubleOrDoubleSequence::Double(num)) => vec![*num],
190 Some(DoubleOrDoubleSequence::DoubleSequence(sequence)) => sequence.clone(),
191 None => vec![Finite::wrap(0.)],
192 };
193
194 // Step 6
195 // > If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception.
196 for num in &thresholds {
197 if **num < 0.0 || **num > 1.0 {
198 return Err(Error::Range(
199 c"Value in thresholds should not be less than 0.0 or greater than 1.0"
200 .to_owned(),
201 ));
202 }
203 }
204
205 // Step 7
206 // > Sort thresholds in ascending order.
207 thresholds.sort_by(|lhs, rhs| lhs.partial_cmp(&**rhs).unwrap());
208
209 // Step 8
210 // > If thresholds is empty, append 0 to thresholds.
211 if thresholds.is_empty() {
212 thresholds.push(Finite::wrap(0.));
213 }
214
215 // Step 9
216 // > The thresholds attribute getter will return this sorted thresholds list.
217 //
218 // Set this internal [[thresholds]] slot to the sorted thresholds list
219 // and getter will return the internal [[thresholds]] slot.
220 self.thresholds.replace(thresholds);
221
222 // Step 10
223 // > Let delay be the value of options.delay.
224 //
225 // Default value of delay is 0.
226 let mut delay = init.delay.unwrap_or(0);
227
228 // Step 11
229 // > If options.trackVisibility is true and delay is less than 100, set delay to 100.
230 //
231 // In Chromium, the minimum delay required is 100 milliseconds for observation that consider trackVisibilty.
232 // Currently, visibility is not implemented.
233 if init.trackVisibility {
234 delay = delay.max(100);
235 }
236
237 // Step 12
238 // > Set this’s internal [[delay]] slot to options.delay to delay.
239 self.delay.set(delay);
240
241 // Step 13
242 // > Set this’s internal [[trackVisibility]] slot to options.trackVisibility.
243 self.track_visibility.set(init.trackVisibility);
244
245 Ok(())
246 }
247
248 /// <https://w3c.github.io/IntersectionObserver/#observe-target-element>
249 fn observe_target_element(&self, target: &Element) {
250 // Step 1
251 // > If target is in observer’s internal [[ObservationTargets]] slot, return.
252 let is_present = self
253 .observation_targets
254 .borrow()
255 .iter()
256 .any(|element| &**element == target);
257 if is_present {
258 return;
259 }
260
261 // Step 2
262 // > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
263 // > an observer property set to observer, a previousThresholdIndex property set to -1,
264 // > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
265 // Step 3
266 // > Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
267 target.add_initial_intersection_observer_registration(self);
268
269 if self.observation_targets.borrow().is_empty() {
270 self.connect_to_owner();
271 }
272
273 // Step 4
274 // > Add target to observer’s internal [[ObservationTargets]] slot.
275 self.observation_targets
276 .borrow_mut()
277 .push(Dom::from_ref(target));
278
279 target
280 .owner_window()
281 .Document()
282 .add_rendering_update_reason(
283 RenderingUpdateReason::IntersectionObserverStartedObservingTarget,
284 );
285 }
286
287 /// <https://w3c.github.io/IntersectionObserver/#unobserve-target-element>
288 fn unobserve_target_element(&self, target: &Element) {
289 // Step 1
290 // > Remove the IntersectionObserverRegistration record whose observer property is equal to
291 // > this from target’s internal [[RegisteredIntersectionObservers]] slot, if present.
292 target
293 .registered_intersection_observers_mut()
294 .retain(|registration| &*registration.observer != self);
295
296 // Step 2
297 // > Remove target from this’s internal [[ObservationTargets]] slot, if present
298 self.observation_targets
299 .borrow_mut()
300 .retain(|element| &**element != target);
301
302 // Should disconnect from owner if it is not observing anything.
303 if self.observation_targets.borrow().is_empty() {
304 self.disconnect_from_owner();
305 }
306 }
307
308 /// <https://w3c.github.io/IntersectionObserver/#queue-an-intersectionobserverentry>
309 #[allow(clippy::too_many_arguments)]
310 fn queue_an_intersectionobserverentry(
311 &self,
312 cx: &mut JSContext,
313 document: &Document,
314 time: CrossProcessInstant,
315 root_bounds: Rect<Au, CSSPixel>,
316 bounding_client_rect: Rect<Au, CSSPixel>,
317 intersection_rect: Rect<Au, CSSPixel>,
318 is_intersecting: bool,
319 is_visible: bool,
320 intersection_ratio: f64,
321 target: &Element,
322 ) {
323 let mut rect_to_domrectreadonly = |rect: Rect<Au, CSSPixel>| {
324 DOMRectReadOnly::new(
325 cx,
326 self.owner_doc.window().as_global_scope(),
327 None,
328 rect.origin.x.to_f64_px(),
329 rect.origin.y.to_f64_px(),
330 rect.size.width.to_f64_px(),
331 rect.size.height.to_f64_px(),
332 )
333 };
334
335 let root_bounds = rect_to_domrectreadonly(root_bounds);
336 let bounding_client_rect = rect_to_domrectreadonly(bounding_client_rect);
337 let intersection_rect = rect_to_domrectreadonly(intersection_rect);
338
339 // Step 1. Construct an IntersectionObserverEntry, passing in time, rootBounds,
340 // > boundingClientRect, intersectionRect, isIntersecting, and target.
341 let time = document
342 .owner_global()
343 .performance(cx)
344 .to_dom_high_res_time_stamp(time);
345 let entry = IntersectionObserverEntry::new(
346 cx,
347 self.owner_doc.window(),
348 None,
349 time,
350 Some(&root_bounds),
351 &bounding_client_rect,
352 &intersection_rect,
353 is_intersecting,
354 is_visible,
355 Finite::wrap(intersection_ratio),
356 target,
357 );
358
359 // Step 2. Append it to observer's internal [[QueuedEntries]] slot.
360 self.queued_entries.borrow_mut().push(entry.as_traced());
361
362 // Step 3. Queue an intersection observer task for document.
363 document.queue_an_intersection_observer_task();
364 }
365
366 /// Step 3.1-3.5 of <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo>
367 pub(crate) fn invoke_callback_if_necessary(&self, cx: &mut js::context::JSContext) {
368 // Step 1
369 // > If observer’s internal [[QueuedEntries]] slot is empty, continue.
370 if self.queued_entries.borrow().is_empty() {
371 return;
372 }
373
374 // Step 2-3
375 // We trivially moved the entries and root them.
376 let queued_entries = self
377 .queued_entries
378 .take()
379 .iter_mut()
380 .map(|entry| entry.as_rooted())
381 .collect();
382
383 // Step 4-5
384 let _ = self
385 .callback
386 .Call_(cx, self, queued_entries, self, ExceptionHandling::Report);
387 }
388
389 /// Connect the observer itself into owner doc if it is unconnected.
390 /// If the [`IntersectionObserver`] is already connected, do nothing.
391 fn connect_to_owner(&self) {
392 if !self.connected_to_document.get() {
393 self.owner_doc.add_intersection_observer(self);
394 self.connected_to_document.set(true);
395 }
396 }
397
398 /// Disconnect the observer itself from owner doc.
399 /// If not connected to a [`Document`], do nothing.
400 fn disconnect_from_owner(&self) {
401 if self.connected_to_document.get() {
402 self.owner_doc.remove_intersection_observer(self);
403 }
404 }
405
406 /// <https://w3c.github.io/IntersectionObserver/#ref-for-intersectionobserver-content-clip>
407 /// An Element is defined as having a content clip if its computed style has overflow properties
408 /// that cause its content to be clipped to the element’s padding edge.
409 // TODO: this is not clear for `overflow: clip` since it is clipped based on overflow clip rect.
410 fn has_content_clip(element: &Element) -> bool {
411 element
412 .upcast::<Node>()
413 .effective_overflow_without_reflow()
414 .is_some_and(|overflow_axes| {
415 overflow_axes.x != Overflow::Visible || overflow_axes.y != Overflow::Visible
416 })
417 }
418
419 /// > The root intersection rectangle for an IntersectionObserver is
420 /// > the rectangle we’ll use to check against the targets.
421 ///
422 /// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle>
423 pub(crate) fn root_intersection_rectangle(&self) -> Option<Rect<Au, CSSPixel>> {
424 let intersection_rectangle = match self.concrete_root() {
425 // Handle if root is an element.
426 Some(ElementOrDocument::Element(element)) => {
427 // TODO: recheck scrollbar approach and clip-path clipping from Chromium implementation.
428 if IntersectionObserver::has_content_clip(&element) {
429 // > Otherwise, if the intersection root has a content clip, it’s the element’s padding area.
430 element.upcast::<Node>().padding_box_without_reflow()
431 } else {
432 // > Otherwise, it’s the result of getting the bounding box for the intersection root.
433 element.upcast::<Node>().border_box_without_reflow()
434 }
435 },
436 // Handle if root is a Document, which includes implicit root and explicit Document root.
437 Some(ElementOrDocument::Document(document)) => {
438 // > If the intersection root is a document, it’s the size of the document's viewport
439 // > (note that this processing step can only be reached if the document is fully active).
440 // TODO: viewport should consider native scrollbar if exist. Recheck Servo's scrollbar approach.
441 let viewport = document.window().viewport_details().size;
442 Some(Rect::from_size(Size2D::new(
443 Au::from_f32_px(viewport.width),
444 Au::from_f32_px(viewport.height),
445 )))
446 },
447 None => None,
448 };
449
450 // > When calculating the root intersection rectangle for a same-origin-domain target,
451 // > the rectangle is then expanded according to the offsets in the IntersectionObserver’s
452 // > [[rootMargin]] slot in a manner similar to CSS’s margin property, with the four values
453 // > indicating the amount the top, right, bottom, and left edges, respectively, are offset by,
454 // > with positive lengths indicating an outward offset. Percentages are resolved relative to
455 // > the width of the undilated rectangle.
456 // TODO(stevennovaryo): add check for same-origin-domain
457 intersection_rectangle.map(|intersection_rectangle| {
458 let margin = Self::resolve_percentages_with_basis(
459 &self.root_margin.borrow(),
460 intersection_rectangle,
461 );
462 intersection_rectangle.outer_rect(margin)
463 })
464 }
465
466 /// Return root or try to get the top-level browsing context document in case if this is a implicit root.
467 /// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-intersection-root>
468 // TODO: Currently we are unable to get the cross `ScriptThread` document.
469 fn concrete_root(&self) -> Option<ElementOrDocument> {
470 match &self.root {
471 Some(root) => Some(root.clone()),
472 None => self
473 .owner_doc
474 .window()
475 .top_level_document_if_local()
476 .map(ElementOrDocument::Document),
477 }
478 }
479
480 /// Step 2.2.4-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
481 ///
482 /// If some conditions require to skips "processing further", we will skips those steps and
483 /// return default values conformant to step 2.2.4. See [`IntersectionObservationOutput::default_skipped`].
484 ///
485 /// Note that current draft specs skipped wrong steps, as it should skip computing fields that
486 /// would result in different intersection entry other than the default entry per published spec.
487 /// <https://www.w3.org/TR/intersection-observer/>
488 fn maybe_compute_intersection_output(
489 &self,
490 target: &Element,
491 maybe_root_bounds: Option<Rect<Au, CSSPixel>>,
492 ) -> IntersectionObservationOutput {
493 // Step 5
494 // > If the intersection root is not the implicit root, and target is not in
495 // > the same document as the intersection root, skip to step 11.
496 // Step 6
497 // > If the intersection root is an Element, and target is not a descendant of
498 // > the intersection root in the containing block chain, skip to step 11.
499 match &self.root {
500 Some(ElementOrDocument::Document(document)) if document != &target.owner_document() => {
501 return IntersectionObservationOutput::default_skipped();
502 },
503 Some(ElementOrDocument::Element(element)) => {
504 // To ensure consistency, we also check for elements right now, but we can depend on the
505 // layout query later.
506 if element.owner_document() != target.owner_document() {
507 return IntersectionObservationOutput::default_skipped();
508 }
509 if !element
510 .owner_window()
511 .is_containing_block_descendant_query_without_reflow(
512 element.upcast(),
513 target.upcast(),
514 )
515 {
516 return IntersectionObservationOutput::default_skipped();
517 }
518 },
519 _ => {},
520 }
521
522 // Step 7
523 // > Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target.
524 let maybe_target_rect = target.upcast::<Node>().border_box_without_reflow();
525
526 // Following the implementation of Gecko, we will skip further processing if these
527 // information not available. This would also handle display none element.
528 let (Some(root_bounds), Some(target_rect), Some(root_intersection)) =
529 (maybe_root_bounds, maybe_target_rect, self.concrete_root())
530 else {
531 return IntersectionObservationOutput::default_skipped();
532 };
533
534 // TODO(stevennovaryo): we should probably also consider adding visibity check, ideally
535 // it would require new query from LayoutThread.
536
537 // Step 8
538 // > Let intersectionRect be the result of running the compute the intersection algorithm on
539 // > target and observer’s intersection root.
540 let maybe_intersection_rect = compute_the_intersection(
541 target,
542 &root_intersection,
543 root_bounds,
544 target_rect,
545 &self.scroll_margin.borrow(),
546 );
547 let intersection_rect = maybe_intersection_rect.unwrap_or_default();
548
549 // Step 9
550 // > Let targetArea be targetRect’s area.
551 // Step 10
552 // > Let intersectionArea be intersectionRect’s area.
553 // These steps are folded in Step 12, rewriting (w1 * h1) / (w2 * h2) as (w1 / w2) * (h1 / h2)
554 // to avoid multiplication overflows.
555
556 // Step 11
557 // > Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent,
558 // > even if the intersection has zero area (because rootBounds or targetRect have zero area).
559 // Because we are considering edge-adjacent, instead of checking whether the rectangle is empty,
560 // we are checking whether the rectangle is negative or not.
561 let is_intersecting = maybe_intersection_rect.is_some();
562
563 // Step 12
564 // > If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
565 // > Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false.
566 let intersection_ratio = if target_rect.size.width.0 == 0 || target_rect.size.height.0 == 0
567 {
568 is_intersecting.into()
569 } else {
570 (intersection_rect.size.width.0 as f64 / target_rect.size.width.0 as f64) *
571 (intersection_rect.size.height.0 as f64 / target_rect.size.height.0 as f64)
572 };
573
574 // Step 13
575 // > Set thresholdIndex to the index of the first entry in observer.thresholds whose value is
576 // > greater than intersectionRatio, or the length of observer.thresholds if intersectionRatio is
577 // > greater than or equal to the last entry in observer.thresholds.
578 let threshold_index = self
579 .thresholds
580 .borrow()
581 .iter()
582 .position(|threshold| **threshold > intersection_ratio)
583 .unwrap_or(self.thresholds.borrow().len());
584
585 // If the index is 0, the first threshold value is greater
586 // than the observed ratio, so we're not actually matching yet.
587 // The spec differentiates between this case and the case where
588 // there is no intersection, but other browser engines do not.
589 let threshold_index = if is_intersecting && threshold_index > 0 {
590 ThresholdIndex::Matching(threshold_index)
591 } else {
592 ThresholdIndex::NotMatching
593 };
594
595 // Step 14
596 // > Let isVisible be the result of running the visibility algorithm on target.
597 // TODO: Implement visibility algorithm
598 let is_visible = false;
599
600 // We never report isIntersecting as true unless we have exceeded a threshold,
601 // which matches other browser eengines.
602 // See https://github.com/w3c/IntersectionObserver/issues/432 for background.
603 let is_intersecting = matches!(threshold_index, ThresholdIndex::Matching(..));
604
605 IntersectionObservationOutput::new_computed(
606 threshold_index,
607 is_intersecting,
608 target_rect,
609 intersection_rect,
610 intersection_ratio,
611 is_visible,
612 root_bounds,
613 )
614 }
615
616 /// Step 2.2.1-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
617 pub(crate) fn update_intersection_observations_steps(
618 &self,
619 cx: &mut JSContext,
620 document: &Document,
621 time: CrossProcessInstant,
622 root_bounds: Option<Rect<Au, CSSPixel>>,
623 ) {
624 for target in &*self.observation_targets.borrow() {
625 // Step 1
626 // > Let registration be the IntersectionObserverRegistration record in target’s internal
627 // > [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer.
628 let registration = target.get_intersection_observer_registration(self).unwrap();
629
630 // Step 2
631 // > If (time - registration.lastUpdateTime < observer.delay), skip further processing for target.
632 if time - registration.last_update_time.get() <
633 Duration::from_millis(self.delay.get().max(0) as u64)
634 {
635 return;
636 }
637
638 // Step 3
639 // > Set registration.lastUpdateTime to time.
640 registration.last_update_time.set(time);
641
642 // step 4-14
643 let intersection_output = self.maybe_compute_intersection_output(target, root_bounds);
644
645 // Step 15-17
646 // > 15. Let previousThresholdIndex be the registration’s previousThresholdIndex property.
647 // > 16. Let previousIsIntersecting be the registration’s previousIsIntersecting property.
648 // > 17. Let previousIsVisible be the registration’s previousIsVisible property.
649 let previous_threshold_index = registration.previous_threshold_index.get();
650 let previous_is_intersecting = registration.previous_is_intersecting.get();
651 let previous_is_visible = registration.previous_is_visible.get();
652
653 // Step 18
654 // > If thresholdIndex does not equal previousThresholdIndex, or
655 // > if isIntersecting does not equal previousIsIntersecting, or
656 // > if isVisible does not equal previousIsVisible,
657 // > queue an IntersectionObserverEntry, passing in observer, time, rootBounds,
658 // > targetRect, intersectionRect, isIntersecting, isVisible, and target.
659 if Some(intersection_output.threshold_index) != previous_threshold_index ||
660 intersection_output.is_intersecting != previous_is_intersecting ||
661 intersection_output.is_visible != previous_is_visible
662 {
663 // TODO(stevennovaryo): Per IntersectionObserverEntry interface, the rootBounds
664 // should be null for cross-origin-domain target.
665 self.queue_an_intersectionobserverentry(
666 cx,
667 document,
668 time,
669 intersection_output.root_bounds,
670 intersection_output.target_rect,
671 intersection_output.intersection_rect,
672 intersection_output.is_intersecting,
673 intersection_output.is_visible,
674 intersection_output.intersection_ratio,
675 target,
676 );
677 }
678
679 // Step 19-21
680 // > 19. Assign thresholdIndex to registration’s previousThresholdIndex property.
681 // > 20. Assign isIntersecting to registration’s previousIsIntersecting property.
682 // > 21. Assign isVisible to registration’s previousIsVisible property.
683 registration
684 .previous_threshold_index
685 .set(Some(intersection_output.threshold_index));
686 registration
687 .previous_is_intersecting
688 .set(intersection_output.is_intersecting);
689 registration
690 .previous_is_visible
691 .set(intersection_output.is_visible);
692 }
693 }
694
695 fn resolve_percentages_with_basis(
696 margin: &IntersectionObserverMargin,
697 containing_block: Rect<Au, CSSPixel>,
698 ) -> SideOffsets2D<Au, CSSPixel> {
699 let inner = &margin.0;
700 SideOffsets2D::new(
701 inner.0.to_used_value(containing_block.height()),
702 inner.1.to_used_value(containing_block.width()),
703 inner.2.to_used_value(containing_block.height()),
704 inner.3.to_used_value(containing_block.width()),
705 )
706 }
707}
708
709impl IntersectionObserverMethods<crate::DomTypeHolder> for IntersectionObserver {
710 /// > The root provided to the IntersectionObserver constructor, or null if none was provided.
711 ///
712 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
713 fn GetRoot(&self) -> Option<ElementOrDocument> {
714 self.root.clone()
715 }
716
717 /// > Offsets applied to the root intersection rectangle, effectively growing or
718 /// > shrinking the box that is used to calculate intersections. These offsets are only
719 /// > applied when handling same-origin-domain targets; for cross-origin-domain targets
720 /// > they are ignored.
721 ///
722 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin>
723 fn RootMargin(&self) -> DOMString {
724 self.root_margin.borrow().to_css_string().into()
725 }
726
727 /// > Offsets are applied to scrollports on the path from intersection root to target,
728 /// > effectively growing or shrinking the clip rects used to calculate intersections.
729 ///
730 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin>
731 fn ScrollMargin(&self) -> DOMString {
732 self.scroll_margin.borrow().to_css_string().into()
733 }
734
735 /// > A list of thresholds, sorted in increasing numeric order, where each threshold
736 /// > is a ratio of intersection area to bounding box area of an observed target.
737 /// > Notifications for a target are generated when any of the thresholds are crossed
738 /// > for that target. If no options.threshold was provided to the IntersectionObserver
739 /// > constructor, or the sequence is empty, the value of this attribute will be `[0]`.
740 ///
741 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds>
742 fn Thresholds(&self, cx: &mut JSContext, retval: MutableHandleValue) {
743 to_frozen_array(cx, &self.thresholds.borrow(), retval);
744 }
745
746 /// > A number indicating the minimum delay in milliseconds between notifications from
747 /// > this observer for a given target.
748 ///
749 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay>
750 fn Delay(&self) -> i32 {
751 self.delay.get()
752 }
753
754 /// > A boolean indicating whether this IntersectionObserver will track changes in a target’s visibility.
755 ///
756 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility>
757 fn TrackVisibility(&self) -> bool {
758 self.track_visibility.get()
759 }
760
761 /// > Run the observe a target Element algorithm, providing this and target.
762 ///
763 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe>
764 fn Observe(&self, target: &Element) {
765 self.observe_target_element(target);
766 }
767
768 /// > Run the unobserve a target Element algorithm, providing this and target.
769 ///
770 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve>
771 fn Unobserve(&self, target: &Element) {
772 self.unobserve_target_element(target);
773 }
774
775 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect>
776 fn Disconnect(&self) {
777 // > For each target in this’s internal [[ObservationTargets]] slot:
778 self.observation_targets.borrow().iter().for_each(|target| {
779 // > 1. Remove the IntersectionObserverRegistration record whose observer property is equal to
780 // > this from target’s internal [[RegisteredIntersectionObservers]] slot.
781 target.remove_intersection_observer(self);
782 });
783 // > 2. Remove target from this’s internal [[ObservationTargets]] slot.
784 self.observation_targets.borrow_mut().clear();
785
786 // We should remove this observer from the event loop.
787 self.disconnect_from_owner();
788 }
789
790 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-takerecords>
791 fn TakeRecords(&self) -> Vec<DomRoot<IntersectionObserverEntry>> {
792 // Step 1-3.
793 self.queued_entries
794 .take()
795 .iter()
796 .map(|entry| entry.as_rooted())
797 .collect()
798 }
799
800 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver>
801 fn Constructor(
802 cx: &mut JSContext,
803 window: &Window,
804 proto: Option<HandleObject>,
805 callback: Rc<IntersectionObserverCallback>,
806 init: &IntersectionObserverInit,
807 ) -> Fallible<DomRoot<IntersectionObserver>> {
808 Self::new(cx, window, proto, callback, init)
809 }
810}
811
812#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
813enum ThresholdIndex {
814 NotMatching,
815 Matching(usize),
816}
817
818/// <https://w3c.github.io/IntersectionObserver/#intersectionobserverregistration>
819#[derive(Clone, JSTraceable, MallocSizeOf)]
820#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
821pub(crate) struct IntersectionObserverRegistration {
822 pub(crate) observer: Dom<IntersectionObserver>,
823 previous_threshold_index: Cell<Option<ThresholdIndex>>,
824 previous_is_intersecting: Cell<bool>,
825 #[no_trace]
826 last_update_time: Cell<CrossProcessInstant>,
827 previous_is_visible: Cell<bool>,
828}
829
830impl IntersectionObserverRegistration {
831 /// Initial value of [`IntersectionObserverRegistration`] according to
832 /// step 2 of <https://w3c.github.io/IntersectionObserver/#observe-target-element>.
833 /// > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
834 /// > an observer property set to observer, a previousThresholdIndex property set to -1,
835 /// > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
836 pub(crate) fn new_initial(observer: &IntersectionObserver) -> Self {
837 IntersectionObserverRegistration {
838 observer: Dom::from_ref(observer),
839 previous_threshold_index: Cell::new(None),
840 previous_is_intersecting: Cell::new(false),
841 last_update_time: Cell::new(CrossProcessInstant::epoch()),
842 previous_is_visible: Cell::new(false),
843 }
844 }
845}
846
847/// <https://w3c.github.io/IntersectionObserver/#parse-a-margin>
848fn parse_a_margin(value: Option<&DOMString>) -> Result<IntersectionObserverMargin, ()> {
849 // <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin> &&
850 // <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-scrollmargin>
851 // > ... defaulting to "0px".
852 let value = match value {
853 Some(str) => &str.str(),
854 _ => "0px",
855 };
856
857 // Create necessary style ParserContext and utilize stylo's IntersectionObserverMargin
858 let mut input = ParserInput::new(value);
859 let mut parser = Parser::new(&mut input);
860
861 let url = Url::parse("about:blank").unwrap().into();
862 let context =
863 parser_context_for_anonymous_content(CssRuleType::Style, ParsingMode::DEFAULT, &url);
864
865 parser
866 .parse_entirely(|p| IntersectionObserverMargin::parse(&context, p))
867 .map_err(|_| ())
868}
869
870/// In terms of intersection observer, we consider zero-area rectangles as long as the area is not negative.
871fn intersect_rectangle(
872 lhs: &Rect<Au, CSSPixel>,
873 rhs: &Rect<Au, CSSPixel>,
874) -> Option<Rect<Au, CSSPixel>> {
875 let box_result = lhs.to_box2d().intersection_unchecked(&rhs.to_box2d());
876 if box_result.is_negative() {
877 None
878 } else {
879 Some(box_result.to_rect())
880 }
881}
882
883/// Compute the intersection rectangle of the target [`Element`] returning the results of intersection in the coordinate
884/// space of the target's owning [`Document`]. Additionally, we assume that both the target and the root is connected.
885/// <https://w3c.github.io/IntersectionObserver/#compute-the-intersection>
886fn compute_the_intersection(
887 target: &Element,
888 root: &ElementOrDocument,
889 root_bounds: Rect<Au, CSSPixel>,
890 mut intersection_rect: Rect<Au, CSSPixel>,
891 scroll_margin: &IntersectionObserverMargin,
892) -> Option<Rect<Au, CSSPixel>> {
893 // > 1. Let intersectionRect be the result of getting the bounding box for target.
894 // We had delegated the computation of this to the caller of the function.
895
896 // > 2. Let container be the containing block of target.
897 let mut container = match target
898 .upcast::<Node>()
899 .containing_block_node_without_reflow()
900 {
901 Some(node) => ElementOrDocument::Element(DomRoot::downcast(node).unwrap()),
902 None => ElementOrDocument::Document(target.owner_document()),
903 };
904
905 // Total offsets gained from traversing through multiple navigables. We use this to map the coordinate space.
906 // TODO: We should store the product sum of transformation matrices instead. But this should be enough to handle
907 // scrolling, simple translation, and offset from containing block.
908 let mut total_inter_document_offset = Vector2D::zero();
909
910 // > 3. While container is not root:
911 while container != *root {
912 let containing_element = match container {
913 ElementOrDocument::Document(ref containing_document) => {
914 // > 3.1. If container is the document of a nested browsing context, update intersectionRect by clipping
915 // > to the viewport of the document, and update container to be the browsing context container of container.
916 if let Some(frame_container) = containing_document
917 .browsing_context()
918 .and_then(|window| window.frame_element().map(DomRoot::from_ref))
919 {
920 let viewport_rect = f32_rect_to_au_rect(Rect::from_size(
921 containing_document.window().viewport_details().size,
922 ));
923
924 if let Some(rect) = intersect_rectangle(&intersection_rect, &viewport_rect) {
925 intersection_rect = rect;
926 } else {
927 return None;
928 }
929
930 let current_offset = frame_container
931 .upcast::<Node>()
932 .padding_box()
933 .unwrap()
934 .origin
935 .to_vector();
936 intersection_rect.origin += current_offset;
937 total_inter_document_offset += current_offset;
938
939 frame_container
940 } else {
941 // TODO: Theoritically, this shouldn't be reachable as we have ensured that the root is reachable
942 // in the previous steps. But we are still unable to iterate through cross-origin ancestor iframes,
943 // and we will need to stop the iteration for that case.
944 break;
945 }
946 },
947 ElementOrDocument::Element(ref root) => root.clone(),
948 };
949
950 // > 3.2. Map intersectionRect to the coordinate space of container.
951 // TODO(#35767): We don't map the coordinate space per each iteration yet, instead all the rectangles are
952 // in the viewport coordinate space. But this would cause the scroll margin calculation to be inaccurate
953 // with respect to transforms.
954
955 // > 3.3. If container is a scroll container, apply the IntersectionObserver’s [[scrollMargin]]
956 // > to the container’s clip rect as described in apply scroll margin to a scrollport.
957 // > 3.4. If container has a content clip or a css clip-path property, update intersectionRect
958 // > by applying container’s clip.
959 // TODO(#35767): handle `overflow: clip` and resolve clipping for x-axis and y-axis independently.
960 // Additionally, handle css `clip-path` as well.
961 if IntersectionObserver::has_content_clip(&containing_element) &&
962 let Some(container_padding_box) = containing_element
963 .upcast::<Node>()
964 .padding_box_without_reflow()
965 {
966 let container_padding_box =
967 if containing_element.establishes_scroll_container_without_reflow() {
968 let margin = IntersectionObserver::resolve_percentages_with_basis(
969 scroll_margin,
970 container_padding_box,
971 );
972 container_padding_box.outer_rect(margin)
973 } else {
974 container_padding_box
975 };
976
977 if let Some(rect) = intersect_rectangle(&intersection_rect, &container_padding_box) {
978 intersection_rect = rect;
979 } else {
980 return None;
981 }
982 }
983
984 // > 3.5. If container is the root element of a browsing context, update container to be the
985 // > browsing context’s document; otherwise, update container to be the containing block
986 // > of container.
987 // Additionally, for a node that doesn't have an element that establishes its containing block, we should
988 // refer to the browsing context's document.
989 container = match containing_element
990 .upcast::<Node>()
991 .containing_block_node_without_reflow()
992 .and_then(DomRoot::downcast::<Element>)
993 {
994 Some(element) => ElementOrDocument::Element(element),
995 None => ElementOrDocument::Document(containing_element.owner_document()),
996 };
997 }
998
999 // Step 4
1000 // > Map intersectionRect to the coordinate space of root.
1001 // TODO(#35767): we don't map the coordinate space per each iteration yet, instead all the rectangles are
1002 // in the viewport coordinate space.
1003
1004 // Step 5
1005 // > Update intersectionRect by intersecting it with the root intersection rectangle.
1006 intersection_rect = intersect_rectangle(&intersection_rect, &root_bounds)?;
1007
1008 // Step 6
1009 // > Map intersectionRect to the coordinate space of the viewport of the document containing target.
1010 // Offset the intersectionRect back to the coordinate space of target's document.
1011 intersection_rect.origin -= total_inter_document_offset;
1012
1013 // Step 7
1014 // > Return intersectionRect.
1015 Some(intersection_rect)
1016}
1017
1018/// The values from computing step 2.2.4-2.2.14 in
1019/// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
1020/// See [`IntersectionObserver::maybe_compute_intersection_output`].
1021struct IntersectionObservationOutput {
1022 pub(crate) threshold_index: ThresholdIndex,
1023 pub(crate) is_intersecting: bool,
1024 pub(crate) target_rect: Rect<Au, CSSPixel>,
1025 pub(crate) intersection_rect: Rect<Au, CSSPixel>,
1026 pub(crate) intersection_ratio: f64,
1027 pub(crate) is_visible: bool,
1028
1029 /// The root intersection rectangle [`IntersectionObserver::root_intersection_rectangle`].
1030 /// If the processing is skipped, computation should report the default zero value.
1031 pub(crate) root_bounds: Rect<Au, CSSPixel>,
1032}
1033
1034impl IntersectionObservationOutput {
1035 /// Default values according to
1036 /// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
1037 /// Step 4.
1038 /// > Let:
1039 /// > - thresholdIndex be 0.
1040 /// > - isIntersecting be false.
1041 /// > - targetRect be a DOMRectReadOnly with x, y, width, and height set to 0.
1042 /// > - intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0.
1043 ///
1044 /// For fields that the default values is not directly mentioned, the values conformant
1045 /// to current browser implementation or WPT test is used instead.
1046 fn default_skipped() -> Self {
1047 Self {
1048 threshold_index: ThresholdIndex::NotMatching,
1049 is_intersecting: false,
1050 target_rect: Rect::zero(),
1051 intersection_rect: Rect::zero(),
1052 intersection_ratio: 0.,
1053 is_visible: false,
1054 root_bounds: Rect::zero(),
1055 }
1056 }
1057
1058 fn new_computed(
1059 threshold_index: ThresholdIndex,
1060 is_intersecting: bool,
1061 target_rect: Rect<Au, CSSPixel>,
1062 intersection_rect: Rect<Au, CSSPixel>,
1063 intersection_ratio: f64,
1064 is_visible: bool,
1065 root_bounds: Rect<Au, CSSPixel>,
1066 ) -> Self {
1067 Self {
1068 threshold_index,
1069 is_intersecting,
1070 target_rect,
1071 intersection_rect,
1072 intersection_ratio,
1073 is_visible,
1074 root_bounds,
1075 }
1076 }
1077}
1078
1079impl script_bindings::callback::OwnerWindow<crate::DomTypeHolder> for IntersectionObserver {}