2021-10-13 14:20:50 -04:00
/*
2022-01-31 13:07:22 -05:00
* Copyright ( c ) 2021 , Tim Flynn < trflynn89 @ serenityos . org >
2021-10-13 14:20:50 -04:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
2023-07-06 23:44:07 +01:00
# include <AK/QuickSort.h>
2024-04-27 12:09:58 +12:00
# include <LibWeb/Bindings/IntersectionObserverPrototype.h>
LibWeb: Remove unecessary dependence on Window from assorted classes
These classes only needed Window to get at its realm. Pass a realm
directly to construct Crypto, Encoding, HRT, IntersectionObserver,
NavigationTiming, Page, RequestIdleCallback, Selection, Streams, URL,
and XML classes.
2022-09-25 18:11:21 -06:00
# include <LibWeb/Bindings/Intrinsics.h>
2024-11-22 12:01:42 +01:00
# include <LibWeb/CSS/Parser/Parser.h>
# include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
2023-07-06 23:44:07 +01:00
# include <LibWeb/DOM/Document.h>
2021-10-13 14:20:50 -04:00
# include <LibWeb/DOM/Element.h>
2023-09-12 19:18:00 +02:00
# include <LibWeb/HTML/TraversableNavigable.h>
2023-10-08 11:59:40 +02:00
# include <LibWeb/HTML/Window.h>
2021-10-13 14:20:50 -04:00
# include <LibWeb/IntersectionObserver/IntersectionObserver.h>
2024-02-23 20:50:19 +01:00
# include <LibWeb/Page/Page.h>
2021-10-13 14:20:50 -04:00
namespace Web : : IntersectionObserver {
2024-11-15 04:01:23 +13:00
GC_DEFINE_ALLOCATOR ( IntersectionObserver ) ;
2023-11-19 19:47:52 +01:00
2021-10-13 14:20:50 -04:00
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < IntersectionObserver > > IntersectionObserver : : construct_impl ( JS : : Realm & realm , GC : : Ptr < WebIDL : : CallbackType > callback , IntersectionObserverInit const & options )
2021-10-13 14:20:50 -04:00
{
2024-11-22 12:01:42 +01:00
// https://w3c.github.io/IntersectionObserver/#initialize-a-new-intersectionobserver
// 1. Let this be a new IntersectionObserver object
// 2. Set this’ s internal [[callback]] slot to callback.
// NOTE: Steps 1 and 2 are handled by creating the IntersectionObserver at the very end of this function.
// 3. Attempt to parse a margin from options.rootMargin. If a list is returned, set this’ s internal [[rootMargin]] slot to that. Otherwise, throw a SyntaxError exception.
auto root_margin = parse_a_margin ( realm , options . root_margin ) ;
if ( ! root_margin . has_value ( ) ) {
return WebIDL : : SyntaxError : : create ( realm , " IntersectionObserver: Cannot parse root margin as a margin. " _string ) ;
}
// 4. Attempt to parse a margin from options.scrollMargin. If a list is returned, set this’ s internal [[scrollMargin]] slot to that. Otherwise, throw a SyntaxError exception.
auto scroll_margin = parse_a_margin ( realm , options . scroll_margin ) ;
if ( ! scroll_margin . has_value ( ) ) {
return WebIDL : : SyntaxError : : create ( realm , " IntersectionObserver: Cannot parse scroll margin as a margin. " _string ) ;
}
// 5. Let thresholds be a list equal to options.threshold.
2023-07-06 23:44:07 +01:00
Vector < double > thresholds ;
if ( options . threshold . has < double > ( ) ) {
thresholds . append ( options . threshold . get < double > ( ) ) ;
} else {
VERIFY ( options . threshold . has < Vector < double > > ( ) ) ;
thresholds = options . threshold . get < Vector < double > > ( ) ;
}
2021-10-13 14:20:50 -04:00
2024-11-22 12:01:42 +01:00
// 6. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception.
2023-07-06 23:44:07 +01:00
for ( auto value : thresholds ) {
if ( value < 0.0 | | value > 1.0 )
return WebIDL : : SimpleException { WebIDL : : SimpleExceptionType : : RangeError , " Threshold values must be between 0.0 and 1.0 inclusive " sv } ;
}
2024-11-22 12:01:42 +01:00
// 7. Sort thresholds in ascending order.
2023-07-06 23:44:07 +01:00
quick_sort ( thresholds , [ ] ( double left , double right ) {
return left < right ;
} ) ;
2024-11-22 12:01:42 +01:00
// 8. If thresholds is empty, append 0 to thresholds.
if ( thresholds . is_empty ( ) ) {
thresholds . append ( 0 ) ;
}
// 9. The thresholds attribute getter will return this sorted thresholds list.
// NOTE: Handled implicitly by passing it into the constructor at the end of this function
// 10. Let delay be the value of options.delay.
auto delay = options . delay ;
// 11. If options.trackVisibility is true and delay is less than 100, set delay to 100.
if ( options . track_visibility & & delay < 100 ) {
delay = 100 ;
}
// 12. Set this’ s internal [[delay]] slot to options.delay to delay.
// 13. Set this’ s internal [[trackVisibility]] slot to options.trackVisibility.
// 14. Return this.
return realm . create < IntersectionObserver > ( realm , callback , options . root , move ( root_margin . value ( ) ) , move ( scroll_margin . value ( ) ) , move ( thresholds ) , move ( delay ) , move ( options . track_visibility ) ) ;
2021-10-13 14:20:50 -04:00
}
2024-11-22 12:01:42 +01:00
IntersectionObserver : : IntersectionObserver ( JS : : Realm & realm , GC : : Ptr < WebIDL : : CallbackType > callback , Optional < Variant < GC : : Root < DOM : : Element > , GC : : Root < DOM : : Document > > > const & root , Vector < CSS : : LengthPercentage > root_margin , Vector < CSS : : LengthPercentage > scroll_margin , Vector < double > & & thresholds , double delay , bool track_visibility )
LibWeb: Remove unecessary dependence on Window from assorted classes
These classes only needed Window to get at its realm. Pass a realm
directly to construct Crypto, Encoding, HRT, IntersectionObserver,
NavigationTiming, Page, RequestIdleCallback, Selection, Streams, URL,
and XML classes.
2022-09-25 18:11:21 -06:00
: PlatformObject ( realm )
2023-07-06 23:44:07 +01:00
, m_callback ( callback )
2024-11-22 12:01:42 +01:00
, m_root_margin ( root_margin )
, m_scroll_margin ( scroll_margin )
2023-07-06 23:44:07 +01:00
, m_thresholds ( move ( thresholds ) )
2024-11-22 12:01:42 +01:00
, m_delay ( delay )
, m_track_visibility ( track_visibility )
2022-09-04 14:14:22 +02:00
{
2024-11-15 04:01:23 +13:00
m_root = root . has_value ( ) ? root - > visit ( [ ] ( auto & value ) - > GC : : Ptr < DOM : : Node > { return * value ; } ) : nullptr ;
2023-07-06 23:44:07 +01:00
intersection_root ( ) . visit ( [ this ] ( auto & node ) {
2023-11-22 23:57:34 +01:00
m_document = node - > document ( ) ;
2023-07-06 23:44:07 +01:00
} ) ;
2023-11-22 23:57:34 +01:00
m_document - > register_intersection_observer ( { } , * this ) ;
2022-09-04 14:14:22 +02:00
}
2023-08-09 21:08:52 +01:00
IntersectionObserver : : ~ IntersectionObserver ( ) = default ;
void IntersectionObserver : : finalize ( )
2023-07-06 23:44:07 +01:00
{
2023-11-22 23:57:34 +01:00
if ( m_document )
m_document - > unregister_intersection_observer ( { } , * this ) ;
2023-07-06 23:44:07 +01:00
}
2022-09-04 14:14:22 +02:00
2023-08-07 08:41:28 +02:00
void IntersectionObserver : : initialize ( JS : : Realm & realm )
2023-01-10 06:28:20 -05:00
{
2024-03-16 13:13:08 +01:00
WEB_SET_PROTOTYPE_FOR_INTERFACE ( IntersectionObserver ) ;
2025-04-20 16:22:57 +02:00
Base : : initialize ( realm ) ;
2023-01-10 06:28:20 -05:00
}
2023-07-06 23:44:07 +01:00
void IntersectionObserver : : visit_edges ( JS : : Cell : : Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
2024-04-03 12:44:32 +02:00
visitor . visit ( m_root ) ;
2023-07-06 23:44:07 +01:00
visitor . visit ( m_callback ) ;
2024-04-15 13:58:21 +02:00
visitor . visit ( m_queued_entries ) ;
visitor . visit ( m_observation_targets ) ;
2023-07-06 23:44:07 +01:00
}
2021-10-13 14:20:50 -04:00
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe
void IntersectionObserver : : observe ( DOM : : Element & target )
{
2023-07-06 23:44:07 +01:00
// Run the observe a target Element algorithm, providing this and target.
// https://www.w3.org/TR/intersection-observer/#observe-a-target-element
// 1. If target is in observer’ s internal [[ObservationTargets]] slot, return.
2024-11-15 04:01:23 +13:00
if ( m_observation_targets . contains_slow ( GC : : Ref { target } ) )
2023-07-06 23:44:07 +01:00
return ;
// 2. Let intersectionObserverRegistration be an IntersectionObserverRegistration record with an observer
// property set to observer, a previousThresholdIndex property set to -1, and a previousIsIntersecting
// property set to false.
auto intersection_observer_registration = IntersectionObserverRegistration {
2023-09-26 16:54:39 +02:00
. observer = * this ,
2023-07-06 23:44:07 +01:00
. previous_threshold_index = OptionalNone { } ,
. previous_is_intersecting = false ,
} ;
// 3. Append intersectionObserverRegistration to target’ s internal [[RegisteredIntersectionObservers]] slot.
target . register_intersection_observer ( { } , move ( intersection_observer_registration ) ) ;
// 4. Add target to observer’ s internal [[ObservationTargets]] slot.
m_observation_targets . append ( target ) ;
2021-10-13 14:20:50 -04:00
}
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve
void IntersectionObserver : : unobserve ( DOM : : Element & target )
{
2023-07-06 23:44:07 +01:00
// Run the unobserve a target Element algorithm, providing this and target.
// https://www.w3.org/TR/intersection-observer/#unobserve-a-target-element
// 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’ s internal [[RegisteredIntersectionObservers]] slot, if present.
target . unregister_intersection_observer ( { } , * this ) ;
// 2. Remove target from this’ s internal [[ObservationTargets]] slot, if present
2024-11-15 04:01:23 +13:00
m_observation_targets . remove_first_matching ( [ & target ] ( GC : : Ref < DOM : : Element > const & entry ) {
2023-07-06 23:44:07 +01:00
return entry . ptr ( ) = = & target ;
} ) ;
2021-10-13 14:20:50 -04:00
}
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect
void IntersectionObserver : : disconnect ( )
{
2023-07-06 23:44:07 +01:00
// For each target in this’ s internal [[ObservationTargets]] slot:
// 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’ s internal
// [[RegisteredIntersectionObservers]] slot.
// 2. Remove target from this’ s internal [[ObservationTargets]] slot.
for ( auto & target : m_observation_targets ) {
target - > unregister_intersection_observer ( { } , * this ) ;
}
m_observation_targets . clear ( ) ;
}
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-takerecords
2024-11-15 04:01:23 +13:00
Vector < GC : : Root < IntersectionObserverEntry > > IntersectionObserver : : take_records ( )
2023-07-06 23:44:07 +01:00
{
// 1. Let queue be a copy of this’ s internal [[QueuedEntries]] slot.
2024-11-15 04:01:23 +13:00
Vector < GC : : Root < IntersectionObserverEntry > > queue ;
2023-07-06 23:44:07 +01:00
for ( auto & entry : m_queued_entries )
queue . append ( * entry ) ;
// 2. Clear this’ s internal [[QueuedEntries]] slot.
m_queued_entries . clear ( ) ;
// 3. Return queue.
return queue ;
}
2024-11-15 04:01:23 +13:00
Variant < GC : : Root < DOM : : Element > , GC : : Root < DOM : : Document > , Empty > IntersectionObserver : : root ( ) const
2023-07-06 23:44:07 +01:00
{
2024-04-03 12:44:32 +02:00
if ( ! m_root )
2023-07-06 23:44:07 +01:00
return Empty { } ;
2024-04-03 12:44:32 +02:00
if ( m_root - > is_element ( ) )
2024-11-15 04:01:23 +13:00
return GC : : make_root ( static_cast < DOM : : Element & > ( * m_root ) ) ;
2024-04-03 12:44:32 +02:00
if ( m_root - > is_document ( ) )
2024-11-15 04:01:23 +13:00
return GC : : make_root ( static_cast < DOM : : Document & > ( * m_root ) ) ;
2024-04-03 12:44:32 +02:00
VERIFY_NOT_REACHED ( ) ;
2023-07-06 23:44:07 +01:00
}
2024-11-22 12:01:42 +01:00
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin
String IntersectionObserver : : root_margin ( ) const
{
// On getting, return the result of serializing the elements of [[rootMargin]] space-separated, where pixel
// lengths serialize as the numeric value followed by "px", and percentages serialize as the numeric value
// followed by "%". Note that this is not guaranteed to be identical to the options.rootMargin passed to the
// IntersectionObserver constructor. If no rootMargin was passed to the IntersectionObserver
// constructor, the value of this attribute is "0px 0px 0px 0px".
StringBuilder builder ;
builder . append ( m_root_margin [ 0 ] . to_string ( ) ) ;
builder . append ( ' ' ) ;
builder . append ( m_root_margin [ 1 ] . to_string ( ) ) ;
builder . append ( ' ' ) ;
builder . append ( m_root_margin [ 2 ] . to_string ( ) ) ;
builder . append ( ' ' ) ;
builder . append ( m_root_margin [ 3 ] . to_string ( ) ) ;
return builder . to_string ( ) . value ( ) ;
}
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin
String IntersectionObserver : : scroll_margin ( ) const
{
// On getting, return the result of serializing the elements of [[scrollMargin]] space-separated, where pixel
// lengths serialize as the numeric value followed by "px", and percentages serialize as the numeric value
// followed by "%". Note that this is not guaranteed to be identical to the options.scrollMargin passed to the
// IntersectionObserver constructor. If no scrollMargin was passed to the IntersectionObserver
// constructor, the value of this attribute is "0px 0px 0px 0px".
StringBuilder builder ;
builder . append ( m_scroll_margin [ 0 ] . to_string ( ) ) ;
builder . append ( ' ' ) ;
builder . append ( m_scroll_margin [ 1 ] . to_string ( ) ) ;
builder . append ( ' ' ) ;
builder . append ( m_scroll_margin [ 2 ] . to_string ( ) ) ;
builder . append ( ' ' ) ;
builder . append ( m_scroll_margin [ 3 ] . to_string ( ) ) ;
return builder . to_string ( ) . value ( ) ;
}
2024-02-23 20:50:19 +01:00
// https://www.w3.org/TR/intersection-observer/#intersectionobserver-intersection-root
2024-11-15 04:01:23 +13:00
Variant < GC : : Root < DOM : : Element > , GC : : Root < DOM : : Document > > IntersectionObserver : : intersection_root ( ) const
2023-07-06 23:44:07 +01:00
{
2024-02-23 20:50:19 +01:00
// The intersection root for an IntersectionObserver is the value of its root attribute
// if the attribute is non-null;
2024-04-03 12:44:32 +02:00
if ( m_root ) {
if ( m_root - > is_element ( ) )
2024-11-15 04:01:23 +13:00
return GC : : make_root ( static_cast < DOM : : Element & > ( * m_root ) ) ;
2024-04-03 12:44:32 +02:00
if ( m_root - > is_document ( ) )
2024-11-15 04:01:23 +13:00
return GC : : make_root ( static_cast < DOM : : Document & > ( * m_root ) ) ;
2024-04-03 12:44:32 +02:00
VERIFY_NOT_REACHED ( ) ;
}
2024-02-23 20:50:19 +01:00
// otherwise, it is the top-level browsing context’ s document node, referred to as the implicit root.
2025-01-21 09:12:05 -05:00
return GC : : make_root ( as < HTML : : Window > ( HTML : : relevant_global_object ( * this ) ) . page ( ) . top_level_browsing_context ( ) . active_document ( ) ) ;
2023-07-06 23:44:07 +01:00
}
// https://www.w3.org/TR/intersection-observer/#intersectionobserver-root-intersection-rectangle
CSSPixelRect IntersectionObserver : : root_intersection_rectangle ( ) const
{
// If the IntersectionObserver is an implicit root observer,
// it’ s treated as if the root were the top-level browsing context’ s document, according to the following rule for document.
auto intersection_root = this - > intersection_root ( ) ;
CSSPixelRect rect ;
// If the intersection root is a document,
// it’ s the size of the document's viewport (note that this processing step can only be reached if the document is fully active).
2024-11-15 04:01:23 +13:00
if ( intersection_root . has < GC : : Root < DOM : : Document > > ( ) ) {
auto document = intersection_root . get < GC : : Root < DOM : : Document > > ( ) ;
2023-07-06 23:44:07 +01:00
2023-08-22 16:00:42 +02:00
// Since the spec says that this is only reach if the document is fully active, that means it must have a navigable.
VERIFY ( document - > navigable ( ) ) ;
2023-07-11 07:48:31 +02:00
// NOTE: This rect is the *size* of the viewport. The viewport *offset* is not relevant,
// as intersections are computed using viewport-relative element rects.
2023-08-22 16:00:42 +02:00
rect = CSSPixelRect { CSSPixelPoint { 0 , 0 } , document - > viewport_rect ( ) . size ( ) } ;
2023-07-06 23:44:07 +01:00
} else {
2024-11-15 04:01:23 +13:00
VERIFY ( intersection_root . has < GC : : Root < DOM : : Element > > ( ) ) ;
auto element = intersection_root . get < GC : : Root < DOM : : Element > > ( ) ;
2023-07-06 23:44:07 +01:00
// FIXME: Otherwise, if the intersection root has a content clip,
// it’ s the element’ s content area.
// Otherwise,
// it’ s the result of getting the bounding box for the intersection root.
2025-03-21 19:38:12 -05:00
rect = element - > get_bounding_client_rect ( ) ;
2023-07-06 23:44:07 +01:00
}
2024-11-22 12:01:42 +01:00
// When calculating the root intersection rectangle for a same-origin-domain target, the rectangle is then
// expanded according to the offsets in the IntersectionObserver’ s [[rootMargin]] slot in a manner similar
// to CSS’ s margin property, with the four values indicating the amount the top, right, bottom, and left
// edges, respectively, are offset by, with positive lengths indicating an outward offset. Percentages
// are resolved relative to the width of the undilated rectangle.
DOM : : Document * document = { nullptr } ;
if ( intersection_root . has < GC : : Root < DOM : : Document > > ( ) ) {
document = intersection_root . get < GC : : Root < DOM : : Document > > ( ) . cell ( ) ;
} else {
document = & intersection_root . get < GC : : Root < DOM : : Element > > ( ) . cell ( ) - > document ( ) ;
}
if ( m_document . has_value ( ) & & document - > origin ( ) . is_same_origin ( m_document - > origin ( ) ) ) {
auto layout_node = intersection_root . visit ( [ & ] ( auto & elem ) { return static_cast < GC : : Root < DOM : : Node > > ( * elem ) - > layout_node ( ) ; } ) ;
rect . inflate (
m_root_margin [ 0 ] . to_px ( * layout_node , rect . height ( ) ) ,
m_root_margin [ 1 ] . to_px ( * layout_node , rect . width ( ) ) ,
m_root_margin [ 2 ] . to_px ( * layout_node , rect . height ( ) ) ,
m_root_margin [ 3 ] . to_px ( * layout_node , rect . width ( ) ) ) ;
}
2023-07-06 23:44:07 +01:00
return rect ;
}
2024-11-15 04:01:23 +13:00
void IntersectionObserver : : queue_entry ( Badge < DOM : : Document > , GC : : Ref < IntersectionObserverEntry > entry )
2023-07-06 23:44:07 +01:00
{
m_queued_entries . append ( entry ) ;
2021-10-13 14:20:50 -04:00
}
2024-11-22 12:01:42 +01:00
// https://w3c.github.io/IntersectionObserver/#parse-a-margin
Optional < Vector < CSS : : LengthPercentage > > IntersectionObserver : : parse_a_margin ( JS : : Realm & realm , String margin_string )
{
// 1. Parse a list of component values marginString, storing the result as tokens.
2025-02-05 12:08:27 +00:00
auto tokens = CSS : : Parser : : Parser : : create ( CSS : : Parser : : ParsingParams { realm } , margin_string ) . parse_as_list_of_component_values ( ) ;
2024-11-22 12:01:42 +01:00
// 2. Remove all whitespace tokens from tokens.
tokens . remove_all_matching ( [ ] ( auto componentValue ) { return componentValue . is ( CSS : : Parser : : Token : : Type : : Whitespace ) ; } ) ;
// 3. If the length of tokens is greater than 4, return failure.
if ( tokens . size ( ) > 4 ) {
return { } ;
}
// 4. If there are zero elements in tokens, set tokens to ["0px"].
if ( tokens . size ( ) = = 0 ) {
tokens . append ( CSS : : Parser : : Token : : create_dimension ( 0 , " px " _fly_string ) ) ;
}
// 5. Replace each token in tokens:
// NOTE: In the spec, tokens miraculously changes type from a list of component values
// to a list of pixel lengths or percentages.
Vector < CSS : : LengthPercentage > tokens_length_percentage ;
for ( auto const & token : tokens ) {
// If token is an absolute length dimension token, replace it with a an equivalent pixel length.
if ( token . is ( CSS : : Parser : : Token : : Type : : Dimension ) ) {
auto length = CSS : : Length ( token . token ( ) . dimension_value ( ) , CSS : : Length : : unit_from_name ( token . token ( ) . dimension_unit ( ) ) . value ( ) ) ;
if ( length . is_absolute ( ) ) {
length . absolute_length_to_px ( ) ;
tokens_length_percentage . append ( length ) ;
continue ;
}
}
// If token is a <percentage> token, replace it with an equivalent percentage.
if ( token . is ( CSS : : Parser : : Token : : Type : : Percentage ) ) {
tokens_length_percentage . append ( CSS : : Percentage ( token . token ( ) . percentage ( ) ) ) ;
continue ;
}
// Otherwise, return failure.
return { } ;
}
// 6.
switch ( tokens_length_percentage . size ( ) ) {
// If there is one element in tokens, append three duplicates of that element to tokens.
case 1 :
tokens_length_percentage . append ( tokens_length_percentage . first ( ) ) ;
tokens_length_percentage . append ( tokens_length_percentage . first ( ) ) ;
tokens_length_percentage . append ( tokens_length_percentage . first ( ) ) ;
break ;
// Otherwise, if there are two elements are tokens, append a duplicate of each element to tokens.
case 2 :
tokens_length_percentage . append ( tokens_length_percentage . at ( 0 ) ) ;
tokens_length_percentage . append ( tokens_length_percentage . at ( 1 ) ) ;
break ;
// Otherwise, if there are three elements in tokens, append a duplicate of the second element to tokens.
case 3 :
tokens_length_percentage . append ( tokens_length_percentage . at ( 1 ) ) ;
break ;
}
// 7. Return tokens.
return tokens_length_percentage ;
}
2021-10-13 14:20:50 -04:00
}