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