script/dom/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 base::cross_process_instant::CrossProcessInstant;
11use cssparser::{Parser, ParserInput};
12use dom_struct::dom_struct;
13use euclid::{Rect, SideOffsets2D, Size2D};
14use js::rust::{HandleObject, MutableHandleValue};
15use layout_api::BoxAreaType;
16use style::parser::Parse;
17use style::stylesheets::CssRuleType;
18use style::values::computed::Overflow;
19use style::values::specified::intersection_observer::IntersectionObserverMargin;
20use style_traits::{CSSPixel, ParsingMode, ToCss};
21use url::Url;
22
23use crate::css::parser_context_for_anonymous_content;
24use crate::dom::bindings::callback::ExceptionHandling;
25use crate::dom::bindings::cell::DomRefCell;
26use crate::dom::bindings::codegen::Bindings::IntersectionObserverBinding::{
27 IntersectionObserverCallback, IntersectionObserverInit, IntersectionObserverMethods,
28};
29use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
30use crate::dom::bindings::codegen::UnionTypes::{DoubleOrDoubleSequence, ElementOrDocument};
31use crate::dom::bindings::error::{Error, Fallible};
32use crate::dom::bindings::inheritance::Castable;
33use crate::dom::bindings::num::Finite;
34use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
35use crate::dom::bindings::root::{Dom, DomRoot};
36use crate::dom::bindings::str::DOMString;
37use crate::dom::bindings::utils::to_frozen_array;
38use crate::dom::document::{Document, RenderingUpdateReason};
39use crate::dom::domrectreadonly::DOMRectReadOnly;
40use crate::dom::element::Element;
41use crate::dom::intersectionobserverentry::IntersectionObserverEntry;
42use crate::dom::node::{Node, NodeTraits};
43use crate::dom::window::Window;
44use crate::script_runtime::{CanGc, JSContext};
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 window: &Window,
134 proto: Option<HandleObject>,
135 callback: Rc<IntersectionObserverCallback>,
136 init: &IntersectionObserverInit,
137 can_gc: CanGc,
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 Box::new(Self::new_inherited(
164 window,
165 callback,
166 init.root.clone(),
167 root_margin,
168 scroll_margin,
169 )),
170 window,
171 proto,
172 can_gc,
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 "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/#intersectionobserver-implicit-root>
249 fn root_is_implicit_root(&self) -> bool {
250 self.root.is_none()
251 }
252
253 /// Return unwrapped root if it was an element, None if otherwise.
254 fn maybe_element_root(&self) -> Option<&Element> {
255 match &self.root {
256 Some(ElementOrDocument::Element(element)) => Some(element),
257 _ => None,
258 }
259 }
260
261 /// <https://w3c.github.io/IntersectionObserver/#observe-target-element>
262 fn observe_target_element(&self, target: &Element) {
263 // Step 1
264 // > If target is in observer’s internal [[ObservationTargets]] slot, return.
265 let is_present = self
266 .observation_targets
267 .borrow()
268 .iter()
269 .any(|element| &**element == target);
270 if is_present {
271 return;
272 }
273
274 // Step 2
275 // > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
276 // > an observer property set to observer, a previousThresholdIndex property set to -1,
277 // > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
278 // Step 3
279 // > Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
280 target.add_initial_intersection_observer_registration(self);
281
282 if self.observation_targets.borrow().is_empty() {
283 self.connect_to_owner();
284 }
285
286 // Step 4
287 // > Add target to observer’s internal [[ObservationTargets]] slot.
288 self.observation_targets
289 .borrow_mut()
290 .push(Dom::from_ref(target));
291
292 target
293 .owner_window()
294 .Document()
295 .add_rendering_update_reason(
296 RenderingUpdateReason::IntersectionObserverStartedObservingTarget,
297 );
298 }
299
300 /// <https://w3c.github.io/IntersectionObserver/#unobserve-target-element>
301 fn unobserve_target_element(&self, target: &Element) {
302 // Step 1
303 // > Remove the IntersectionObserverRegistration record whose observer property is equal to
304 // > this from target’s internal [[RegisteredIntersectionObservers]] slot, if present.
305 target
306 .registered_intersection_observers_mut()
307 .retain(|registration| &*registration.observer != self);
308
309 // Step 2
310 // > Remove target from this’s internal [[ObservationTargets]] slot, if present
311 self.observation_targets
312 .borrow_mut()
313 .retain(|element| &**element != target);
314
315 // Should disconnect from owner if it is not observing anything.
316 if self.observation_targets.borrow().is_empty() {
317 self.disconnect_from_owner();
318 }
319 }
320
321 /// <https://w3c.github.io/IntersectionObserver/#queue-an-intersectionobserverentry>
322 #[allow(clippy::too_many_arguments)]
323 fn queue_an_intersectionobserverentry(
324 &self,
325 document: &Document,
326 time: CrossProcessInstant,
327 root_bounds: Rect<Au, CSSPixel>,
328 bounding_client_rect: Rect<Au, CSSPixel>,
329 intersection_rect: Rect<Au, CSSPixel>,
330 is_intersecting: bool,
331 is_visible: bool,
332 intersection_ratio: f64,
333 target: &Element,
334 can_gc: CanGc,
335 ) {
336 let rect_to_domrectreadonly = |rect: Rect<Au, CSSPixel>| {
337 DOMRectReadOnly::new(
338 self.owner_doc.window().as_global_scope(),
339 None,
340 rect.origin.x.to_f64_px(),
341 rect.origin.y.to_f64_px(),
342 rect.size.width.to_f64_px(),
343 rect.size.height.to_f64_px(),
344 can_gc,
345 )
346 };
347
348 let root_bounds = rect_to_domrectreadonly(root_bounds);
349 let bounding_client_rect = rect_to_domrectreadonly(bounding_client_rect);
350 let intersection_rect = rect_to_domrectreadonly(intersection_rect);
351
352 // Step 1-2
353 // > 1. Construct an IntersectionObserverEntry, passing in time, rootBounds,
354 // > boundingClientRect, intersectionRect, isIntersecting, and target.
355 // > 2. Append it to observer’s internal [[QueuedEntries]] slot.
356 self.queued_entries.borrow_mut().push(
357 IntersectionObserverEntry::new(
358 self.owner_doc.window(),
359 None,
360 document
361 .owner_global()
362 .performance()
363 .to_dom_high_res_time_stamp(time),
364 Some(&root_bounds),
365 &bounding_client_rect,
366 &intersection_rect,
367 is_intersecting,
368 is_visible,
369 Finite::wrap(intersection_ratio),
370 target,
371 can_gc,
372 )
373 .as_traced(),
374 );
375 // > Step 3
376 // Queue an intersection observer task for document.
377 document.queue_an_intersection_observer_task();
378 }
379
380 /// Step 3.1-3.5 of <https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo>
381 pub(crate) fn invoke_callback_if_necessary(&self, can_gc: CanGc) {
382 // Step 1
383 // > If observer’s internal [[QueuedEntries]] slot is empty, continue.
384 if self.queued_entries.borrow().is_empty() {
385 return;
386 }
387
388 // Step 2-3
389 // We trivially moved the entries and root them.
390 let queued_entries = self
391 .queued_entries
392 .take()
393 .iter_mut()
394 .map(|entry| entry.as_rooted())
395 .collect();
396
397 // Step 4-5
398 let _ = self.callback.Call_(
399 self,
400 queued_entries,
401 self,
402 ExceptionHandling::Report,
403 can_gc,
404 );
405 }
406
407 /// Connect the observer itself into owner doc if it is unconnected.
408 /// If the [`IntersectionObserver`] is already connected, do nothing.
409 fn connect_to_owner(&self) {
410 if !self.connected_to_document.get() {
411 self.owner_doc.add_intersection_observer(self);
412 self.connected_to_document.set(true);
413 }
414 }
415
416 /// Disconnect the observer itself from owner doc.
417 /// If not connected to a [`Document`], do nothing.
418 fn disconnect_from_owner(&self) {
419 if self.connected_to_document.get() {
420 self.owner_doc.remove_intersection_observer(self);
421 }
422 }
423
424 /// > The root intersection rectangle for an IntersectionObserver is
425 /// > the rectangle we’ll use to check against the targets.
426 ///
427 /// <https://w3c.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle>
428 pub(crate) fn root_intersection_rectangle(
429 &self,
430 document: &Document,
431 ) -> Option<Rect<Au, CSSPixel>> {
432 let window = document.window();
433 let intersection_rectangle = match &self.root {
434 // Handle if root is an element.
435 Some(ElementOrDocument::Element(element)) => {
436 // TODO: recheck scrollbar approach and clip-path clipping from Chromium implementation.
437 if element.style().is_some_and(|style| {
438 style.clone_overflow_x() != Overflow::Visible ||
439 style.clone_overflow_y() != Overflow::Visible
440 }) {
441 // > Otherwise, if the intersection root has a content clip, it’s the element’s padding area.
442 window.box_area_query_without_reflow(
443 &DomRoot::upcast::<Node>(element.clone()),
444 BoxAreaType::Padding,
445 false,
446 )
447 } else {
448 // > Otherwise, it’s the result of getting the bounding box for the intersection root.
449 window.box_area_query_without_reflow(
450 &DomRoot::upcast::<Node>(element.clone()),
451 BoxAreaType::Border,
452 false,
453 )
454 }
455 },
456 // Handle if root is a Document, which includes implicit root and explicit Document root.
457 _ => {
458 let document = if self.root.is_none() {
459 // > If the IntersectionObserver is an implicit root observer,
460 // > it’s treated as if the root were the top-level browsing context’s document,
461 // > according to the following rule for document.
462 //
463 // There are uncertainties whether the browsing context we should consider is the browsing
464 // context of the target or observer. <https://github.com/w3c/IntersectionObserver/issues/456>
465 // TODO: This wouldn't work if top level document is in another ScriptThread.
466 document.window().top_level_document_if_local()
467 } else if let Some(ElementOrDocument::Document(document)) = &self.root {
468 Some(document.clone())
469 } else {
470 None
471 };
472
473 // > If the intersection root is a document, it’s the size of the document's viewport
474 // > (note that this processing step can only be reached if the document is fully active).
475 // TODO: viewport should consider native scrollbar if exist. Recheck Servo's scrollbar approach.
476 document.map(|document| {
477 let viewport = document.window().viewport_details().size;
478 Rect::from_size(Size2D::new(
479 Au::from_f32_px(viewport.width),
480 Au::from_f32_px(viewport.height),
481 ))
482 })
483 },
484 };
485
486 // > When calculating the root intersection rectangle for a same-origin-domain target,
487 // > the rectangle is then expanded according to the offsets in the IntersectionObserver’s
488 // > [[rootMargin]] slot in a manner similar to CSS’s margin property, with the four values
489 // > indicating the amount the top, right, bottom, and left edges, respectively, are offset by,
490 // > with positive lengths indicating an outward offset. Percentages are resolved relative to
491 // > the width of the undilated rectangle.
492 // TODO(stevennovaryo): add check for same-origin-domain
493 intersection_rectangle.map(|intersection_rectangle| {
494 let margin = self.resolve_percentages_with_basis(intersection_rectangle);
495 intersection_rectangle.outer_rect(margin)
496 })
497 }
498
499 /// Step 2.2.4-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
500 ///
501 /// If some conditions require to skips "processing further", we will skips those steps and
502 /// return default values conformant to step 2.2.4. See [`IntersectionObservationOutput::default_skipped`].
503 ///
504 /// Note that current draft specs skipped wrong steps, as it should skip computing fields that
505 /// would result in different intersection entry other than the default entry per published spec.
506 /// <https://www.w3.org/TR/intersection-observer/>
507 fn maybe_compute_intersection_output(
508 &self,
509 document: &Document,
510 target: &Element,
511 maybe_root_bounds: Option<Rect<Au, CSSPixel>>,
512 ) -> IntersectionObservationOutput {
513 // Step 5
514 // > If the intersection root is not the implicit root, and target is not in
515 // > the same document as the intersection root, skip to step 11.
516 if !self.root_is_implicit_root() && *target.owner_document() != *document {
517 return IntersectionObservationOutput::default_skipped();
518 }
519
520 // Step 6
521 // > If the intersection root is an Element, and target is not a descendant of
522 // > the intersection root in the containing block chain, skip to step 11.
523 // TODO(stevennovaryo): implement LayoutThread query that support this.
524 if let Some(_element) = self.maybe_element_root() {
525 debug!("descendant of containing block chain is not implemented");
526 }
527
528 // Step 7
529 // > Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target.
530 let maybe_target_rect = document.window().box_area_query_without_reflow(
531 target.upcast::<Node>(),
532 BoxAreaType::Border,
533 false,
534 );
535
536 // Following the implementation of Gecko, we will skip further processing if these
537 // information not available. This would also handle display none element.
538 let (Some(root_bounds), Some(target_rect)) = (maybe_root_bounds, maybe_target_rect) else {
539 return IntersectionObservationOutput::default_skipped();
540 };
541
542 // TODO(stevennovaryo): we should probably also consider adding visibity check, ideally
543 // it would require new query from LayoutThread.
544
545 // Step 8
546 // > Let intersectionRect be the result of running the compute the intersection algorithm on
547 // > target and observer’s intersection root.
548 let intersection_rect =
549 compute_the_intersection(document, target, &self.root, root_bounds, target_rect);
550
551 // Step 9
552 // > Let targetArea be targetRect’s area.
553 // Step 10
554 // > Let intersectionArea be intersectionRect’s area.
555 // These steps are folded in Step 12, rewriting (w1 * h1) / (w2 * h2) as (w1 / w2) * (h1 / h2)
556 // to avoid multiplication overflows.
557
558 // Step 11
559 // > Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent,
560 // > even if the intersection has zero area (because rootBounds or targetRect have zero area).
561 // Because we are considering edge-adjacent, instead of checking whether the rectangle is empty,
562 // we are checking whether the rectangle is negative or not.
563 // TODO(stevennovaryo): there is a dicussion regarding isIntersecting definition, we should update
564 // it accordingly. https://github.com/w3c/IntersectionObserver/issues/432
565 let is_intersecting = !target_rect
566 .to_box2d()
567 .intersection_unchecked(&root_bounds.to_box2d())
568 .is_negative();
569
570 // Step 12
571 // > If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
572 // > Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false.
573 let intersection_ratio = if target_rect.size.width.0 == 0 || target_rect.size.height.0 == 0
574 {
575 is_intersecting.into()
576 } else {
577 (intersection_rect.size.width.0 as f64 / target_rect.size.width.0 as f64) *
578 (intersection_rect.size.height.0 as f64 / target_rect.size.height.0 as f64)
579 };
580
581 // Step 13
582 // > Set thresholdIndex to the index of the first entry in observer.thresholds whose value is
583 // > greater than intersectionRatio, or the length of observer.thresholds if intersectionRatio is
584 // > greater than or equal to the last entry in observer.thresholds.
585 let threshold_index = self
586 .thresholds
587 .borrow()
588 .iter()
589 .position(|threshold| **threshold > intersection_ratio)
590 .unwrap_or(self.thresholds.borrow().len()) as i32;
591
592 // Step 14
593 // > Let isVisible be the result of running the visibility algorithm on target.
594 // TODO: Implement visibility algorithm
595 let is_visible = false;
596
597 IntersectionObservationOutput::new_computed(
598 threshold_index,
599 is_intersecting,
600 target_rect,
601 intersection_rect,
602 intersection_ratio,
603 is_visible,
604 root_bounds,
605 )
606 }
607
608 /// Step 2.2.1-2.2.21 of <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>
609 pub(crate) fn update_intersection_observations_steps(
610 &self,
611 document: &Document,
612 time: CrossProcessInstant,
613 root_bounds: Option<Rect<Au, CSSPixel>>,
614 can_gc: CanGc,
615 ) {
616 for target in &*self.observation_targets.borrow() {
617 // Step 1
618 // > Let registration be the IntersectionObserverRegistration record in target’s internal
619 // > [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer.
620 let registration = target.get_intersection_observer_registration(self).unwrap();
621
622 // Step 2
623 // > If (time - registration.lastUpdateTime < observer.delay), skip further processing for target.
624 if time - registration.last_update_time.get() <
625 Duration::from_millis(self.delay.get().max(0) as u64)
626 {
627 return;
628 }
629
630 // Step 3
631 // > Set registration.lastUpdateTime to time.
632 registration.last_update_time.set(time);
633
634 // step 4-14
635 let intersection_output =
636 self.maybe_compute_intersection_output(document, target, root_bounds);
637
638 // Step 15-17
639 // > 15. Let previousThresholdIndex be the registration’s previousThresholdIndex property.
640 // > 16. Let previousIsIntersecting be the registration’s previousIsIntersecting property.
641 // > 17. Let previousIsVisible be the registration’s previousIsVisible property.
642 let previous_threshold_index = registration.previous_threshold_index.get();
643 let previous_is_intersecting = registration.previous_is_intersecting.get();
644 let previous_is_visible = registration.previous_is_visible.get();
645
646 // Step 18
647 // > If thresholdIndex does not equal previousThresholdIndex, or
648 // > if isIntersecting does not equal previousIsIntersecting, or
649 // > if isVisible does not equal previousIsVisible,
650 // > queue an IntersectionObserverEntry, passing in observer, time, rootBounds,
651 // > targetRect, intersectionRect, isIntersecting, isVisible, and target.
652 if intersection_output.threshold_index != previous_threshold_index ||
653 intersection_output.is_intersecting != previous_is_intersecting ||
654 intersection_output.is_visible != previous_is_visible
655 {
656 // TODO(stevennovaryo): Per IntersectionObserverEntry interface, the rootBounds
657 // should be null for cross-origin-domain target.
658 self.queue_an_intersectionobserverentry(
659 document,
660 time,
661 intersection_output.root_bounds,
662 intersection_output.target_rect,
663 intersection_output.intersection_rect,
664 intersection_output.is_intersecting,
665 intersection_output.is_visible,
666 intersection_output.intersection_ratio,
667 target,
668 can_gc,
669 );
670 }
671
672 // Step 19-21
673 // > 19. Assign thresholdIndex to registration’s previousThresholdIndex property.
674 // > 20. Assign isIntersecting to registration’s previousIsIntersecting property.
675 // > 21. Assign isVisible to registration’s previousIsVisible property.
676 registration
677 .previous_threshold_index
678 .set(intersection_output.threshold_index);
679 registration
680 .previous_is_intersecting
681 .set(intersection_output.is_intersecting);
682 registration
683 .previous_is_visible
684 .set(intersection_output.is_visible);
685 }
686 }
687
688 fn resolve_percentages_with_basis(
689 &self,
690 containing_block: Rect<Au, CSSPixel>,
691 ) -> SideOffsets2D<Au, CSSPixel> {
692 let inner = &self.root_margin.borrow().0;
693 SideOffsets2D::new(
694 inner.0.to_used_value(containing_block.height()),
695 inner.1.to_used_value(containing_block.width()),
696 inner.2.to_used_value(containing_block.height()),
697 inner.3.to_used_value(containing_block.width()),
698 )
699 }
700}
701
702impl IntersectionObserverMethods<crate::DomTypeHolder> for IntersectionObserver {
703 /// > The root provided to the IntersectionObserver constructor, or null if none was provided.
704 ///
705 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-root>
706 fn GetRoot(&self) -> Option<ElementOrDocument> {
707 self.root.clone()
708 }
709
710 /// > Offsets applied to the root intersection rectangle, effectively growing or
711 /// > shrinking the box that is used to calculate intersections. These offsets are only
712 /// > applied when handling same-origin-domain targets; for cross-origin-domain targets
713 /// > they are ignored.
714 ///
715 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin>
716 fn RootMargin(&self) -> DOMString {
717 DOMString::from_string(self.root_margin.borrow().to_css_string())
718 }
719
720 /// > Offsets are applied to scrollports on the path from intersection root to target,
721 /// > effectively growing or shrinking the clip rects used to calculate intersections.
722 ///
723 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin>
724 fn ScrollMargin(&self) -> DOMString {
725 DOMString::from_string(self.scroll_margin.borrow().to_css_string())
726 }
727
728 /// > A list of thresholds, sorted in increasing numeric order, where each threshold
729 /// > is a ratio of intersection area to bounding box area of an observed target.
730 /// > Notifications for a target are generated when any of the thresholds are crossed
731 /// > for that target. If no options.threshold was provided to the IntersectionObserver
732 /// > constructor, or the sequence is empty, the value of this attribute will be `[0]`.
733 ///
734 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-thresholds>
735 fn Thresholds(&self, context: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
736 to_frozen_array(&self.thresholds.borrow(), context, retval, can_gc);
737 }
738
739 /// > A number indicating the minimum delay in milliseconds between notifications from
740 /// > this observer for a given target.
741 ///
742 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay>
743 fn Delay(&self) -> i32 {
744 self.delay.get()
745 }
746
747 /// > A boolean indicating whether this IntersectionObserver will track changes in a target’s visibility.
748 ///
749 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility>
750 fn TrackVisibility(&self) -> bool {
751 self.track_visibility.get()
752 }
753
754 /// > Run the observe a target Element algorithm, providing this and target.
755 ///
756 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe>
757 fn Observe(&self, target: &Element) {
758 self.observe_target_element(target);
759 }
760
761 /// > Run the unobserve a target Element algorithm, providing this and target.
762 ///
763 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve>
764 fn Unobserve(&self, target: &Element) {
765 self.unobserve_target_element(target);
766 }
767
768 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect>
769 fn Disconnect(&self) {
770 // > For each target in this’s internal [[ObservationTargets]] slot:
771 self.observation_targets.borrow().iter().for_each(|target| {
772 // > 1. Remove the IntersectionObserverRegistration record whose observer property is equal to
773 // > this from target’s internal [[RegisteredIntersectionObservers]] slot.
774 target.remove_intersection_observer(self);
775 });
776 // > 2. Remove target from this’s internal [[ObservationTargets]] slot.
777 self.observation_targets.borrow_mut().clear();
778
779 // We should remove this observer from the event loop.
780 self.disconnect_from_owner();
781 }
782
783 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-takerecords>
784 fn TakeRecords(&self) -> Vec<DomRoot<IntersectionObserverEntry>> {
785 // Step 1-3.
786 self.queued_entries
787 .take()
788 .iter()
789 .map(|entry| entry.as_rooted())
790 .collect()
791 }
792
793 /// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver>
794 fn Constructor(
795 window: &Window,
796 proto: Option<HandleObject>,
797 can_gc: CanGc,
798 callback: Rc<IntersectionObserverCallback>,
799 init: &IntersectionObserverInit,
800 ) -> Fallible<DomRoot<IntersectionObserver>> {
801 Self::new(window, proto, callback, init, can_gc)
802 }
803}
804
805/// <https://w3c.github.io/IntersectionObserver/#intersectionobserverregistration>
806#[derive(Clone, JSTraceable, MallocSizeOf)]
807#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
808pub(crate) struct IntersectionObserverRegistration {
809 pub(crate) observer: Dom<IntersectionObserver>,
810 pub(crate) previous_threshold_index: Cell<i32>,
811 pub(crate) previous_is_intersecting: Cell<bool>,
812 #[no_trace]
813 pub(crate) last_update_time: Cell<CrossProcessInstant>,
814 pub(crate) previous_is_visible: Cell<bool>,
815}
816
817impl IntersectionObserverRegistration {
818 /// Initial value of [`IntersectionObserverRegistration`] according to
819 /// step 2 of <https://w3c.github.io/IntersectionObserver/#observe-target-element>.
820 /// > Let intersectionObserverRegistration be an IntersectionObserverRegistration record with
821 /// > an observer property set to observer, a previousThresholdIndex property set to -1,
822 /// > a previousIsIntersecting property set to false, and a previousIsVisible property set to false.
823 pub(crate) fn new_initial(observer: &IntersectionObserver) -> Self {
824 IntersectionObserverRegistration {
825 observer: Dom::from_ref(observer),
826 previous_threshold_index: Cell::new(-1),
827 previous_is_intersecting: Cell::new(false),
828 last_update_time: Cell::new(CrossProcessInstant::epoch()),
829 previous_is_visible: Cell::new(false),
830 }
831 }
832}
833
834/// <https://w3c.github.io/IntersectionObserver/#parse-a-margin>
835fn parse_a_margin(value: Option<&DOMString>) -> Result<IntersectionObserverMargin, ()> {
836 // <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-rootmargin> &&
837 // <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverinit-scrollmargin>
838 // > ... defaulting to "0px".
839 let value = match value {
840 Some(str) => &str.str(),
841 _ => "0px",
842 };
843
844 // Create necessary style ParserContext and utilize stylo's IntersectionObserverMargin
845 let mut input = ParserInput::new(value);
846 let mut parser = Parser::new(&mut input);
847
848 let url = Url::parse("about:blank").unwrap().into();
849 let context =
850 parser_context_for_anonymous_content(CssRuleType::Style, ParsingMode::DEFAULT, &url);
851
852 parser
853 .parse_entirely(|p| IntersectionObserverMargin::parse(&context, p))
854 .map_err(|_| ())
855}
856
857/// <https://w3c.github.io/IntersectionObserver/#compute-the-intersection>
858fn compute_the_intersection(
859 _document: &Document,
860 _target: &Element,
861 _root: &IntersectionRoot,
862 root_bounds: Rect<Au, CSSPixel>,
863 mut intersection_rect: Rect<Au, CSSPixel>,
864) -> Rect<Au, CSSPixel> {
865 // > 1. Let intersectionRect be the result of getting the bounding box for target.
866 // We had delegated the computation of this to the caller of the function.
867
868 // > 2. Let container be the containing block of target.
869 // > 3. While container is not root:
870 // > 1. If container is the document of a nested browsing context, update intersectionRect
871 // > by clipping to the viewport of the document,
872 // > and update container to be the browsing context container of container.
873 // > 2. Map intersectionRect to the coordinate space of container.
874 // > 3. If container is a scroll container, apply the IntersectionObserver’s [[scrollMargin]]
875 // > to the container’s clip rect as described in apply scroll margin to a scrollport.
876 // > 4. If container has a content clip or a css clip-path property, update intersectionRect
877 // > by applying container’s clip.
878 // > 5. If container is the root element of a browsing context, update container to be the
879 // > browsing context’s document; otherwise, update container to be the containing block
880 // > of container.
881 // TODO: Implement rest of step 2 and 3, which will consider transform matrix, window scroll, etc.
882
883 // Step 4
884 // > Map intersectionRect to the coordinate space of root.
885 // TODO: implement this by considering the transform matrix, window scroll, etc.
886
887 // Step 5
888 // > Update intersectionRect by intersecting it with the root intersection rectangle.
889 // Note that we also consider the edge-adjacent intersection.
890 let intersection_box = intersection_rect
891 .to_box2d()
892 .intersection_unchecked(&root_bounds.to_box2d());
893 // Although not specified, the result for non-intersecting rectangle should be zero rectangle.
894 // So we should give zero rectangle immediately without modifying it.
895 if intersection_box.is_negative() {
896 return Rect::zero();
897 }
898 intersection_rect = intersection_box.to_rect();
899
900 // Step 6
901 // > Map intersectionRect to the coordinate space of the viewport of the document containing target.
902 // TODO: implement this by considering the transform matrix, window scroll, etc.
903
904 // Step 7
905 // > Return intersectionRect.
906 intersection_rect
907}
908
909/// The values from computing step 2.2.4-2.2.14 in
910/// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
911/// See [`IntersectionObserver::maybe_compute_intersection_output`].
912struct IntersectionObservationOutput {
913 pub(crate) threshold_index: i32,
914 pub(crate) is_intersecting: bool,
915 pub(crate) target_rect: Rect<Au, CSSPixel>,
916 pub(crate) intersection_rect: Rect<Au, CSSPixel>,
917 pub(crate) intersection_ratio: f64,
918 pub(crate) is_visible: bool,
919
920 /// The root intersection rectangle [`IntersectionObserver::root_intersection_rectangle`].
921 /// If the processing is skipped, computation should report the default zero value.
922 pub(crate) root_bounds: Rect<Au, CSSPixel>,
923}
924
925impl IntersectionObservationOutput {
926 /// Default values according to
927 /// <https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo>.
928 /// Step 4.
929 /// > Let:
930 /// > - thresholdIndex be 0.
931 /// > - isIntersecting be false.
932 /// > - targetRect be a DOMRectReadOnly with x, y, width, and height set to 0.
933 /// > - intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0.
934 ///
935 /// For fields that the default values is not directly mentioned, the values conformant
936 /// to current browser implementation or WPT test is used instead.
937 fn default_skipped() -> Self {
938 Self {
939 threshold_index: 0,
940 is_intersecting: false,
941 target_rect: Rect::zero(),
942 intersection_rect: Rect::zero(),
943 intersection_ratio: 0.,
944 is_visible: false,
945 root_bounds: Rect::zero(),
946 }
947 }
948
949 fn new_computed(
950 threshold_index: i32,
951 is_intersecting: bool,
952 target_rect: Rect<Au, CSSPixel>,
953 intersection_rect: Rect<Au, CSSPixel>,
954 intersection_ratio: f64,
955 is_visible: bool,
956 root_bounds: Rect<Au, CSSPixel>,
957 ) -> Self {
958 Self {
959 threshold_index,
960 is_intersecting,
961 target_rect,
962 intersection_rect,
963 intersection_ratio,
964 is_visible,
965 root_bounds,
966 }
967 }
968}