diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 7b9a7e28b41..cc16c88ff32 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -6039,6 +6039,11 @@ Vector Parser::parse_font_face_src(TokenStream& compo template Vector Parser::parse_font_face_src(TokenStream& component_values); template Vector Parser::parse_font_face_src(TokenStream& component_values); +Vector Parser::parse_as_list_of_component_values() +{ + return parse_a_list_of_component_values(m_token_stream); +} + RefPtr Parser::parse_list_style_value(TokenStream& tokens) { RefPtr list_position; diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 4698eeb0687..3c9862d6715 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -69,6 +69,8 @@ public: Vector parse_as_font_face_src(); + Vector parse_as_list_of_component_values(); + static NonnullRefPtr resolve_unresolved_style_value(ParsingContext const&, DOM::Element&, Optional, PropertyID, UnresolvedStyleValue const&); [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr); diff --git a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp index f8fa224e502..5fa8cdc7484 100644 --- a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp +++ b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -21,7 +23,24 @@ GC_DEFINE_ALLOCATOR(IntersectionObserver); // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver WebIDL::ExceptionOr> IntersectionObserver::construct_impl(JS::Realm& realm, GC::Ptr callback, IntersectionObserverInit const& options) { - // 4. Let thresholds be a list equal to options.threshold. + // 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. Vector thresholds; if (options.threshold.has()) { thresholds.append(options.threshold.get()); @@ -30,28 +49,47 @@ WebIDL::ExceptionOr> IntersectionObserver::constru thresholds = options.threshold.get>(); } - // 5. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception. + // 6. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception. 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 }; } - // 6. Sort thresholds in ascending order. + // 7. Sort thresholds in ascending order. quick_sort(thresholds, [](double left, double right) { return left < right; }); - // 1. Let this be a new IntersectionObserver object - // 2. Set this’s internal [[callback]] slot to callback. - // 8. The thresholds attribute getter will return this sorted thresholds list. - // 9. Return this. - return realm.create(realm, callback, options.root, move(thresholds)); + // 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(realm, callback, options.root, move(root_margin.value()), move(scroll_margin.value()), move(thresholds), move(delay), move(options.track_visibility)); } -IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr callback, Optional, GC::Root>> const& root, Vector&& thresholds) +IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr callback, Optional, GC::Root>> const& root, Vector root_margin, Vector scroll_margin, Vector&& thresholds, double delay, bool track_visibility) : PlatformObject(realm) , m_callback(callback) + , m_root_margin(root_margin) + , m_scroll_margin(scroll_margin) , m_thresholds(move(thresholds)) + , m_delay(delay) + , m_track_visibility(track_visibility) { m_root = root.has_value() ? root->visit([](auto& value) -> GC::Ptr { return *value; }) : nullptr; intersection_root().visit([this](auto& node) { @@ -161,6 +199,44 @@ Variant, GC::Root, Empty> IntersectionObse VERIFY_NOT_REACHED(); } +// 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(); +} + // https://www.w3.org/TR/intersection-observer/#intersectionobserver-intersection-root Variant, GC::Root> IntersectionObserver::intersection_root() const { @@ -211,11 +287,25 @@ CSSPixelRect IntersectionObserver::root_intersection_rectangle() const rect = CSSPixelRect(bounding_client_rect->x(), bounding_client_rect->y(), bounding_client_rect->width(), bounding_client_rect->height()); } - // FIXME: 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. + // 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>()) { + document = intersection_root.get>().cell(); + } else { + document = &intersection_root.get>().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>(*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())); + } return rect; } @@ -225,4 +315,69 @@ void IntersectionObserver::queue_entry(Badge, GC::Ref> IntersectionObserver::parse_a_margin(JS::Realm& realm, String margin_string) +{ + // 1. Parse a list of component values marginString, storing the result as tokens. + auto tokens = CSS::Parser::Parser::create(CSS::Parser::ParsingContext { realm }, margin_string).parse_as_list_of_component_values(); + + // 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 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 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; +} + } diff --git a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h index 2ef1ae2899b..1ea66401e6b 100644 --- a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h +++ b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h @@ -16,7 +16,10 @@ namespace Web::IntersectionObserver { struct IntersectionObserverInit { Optional, GC::Root>> root; String root_margin { "0px"_string }; + String scroll_margin { "0px"_string }; Variant> threshold { 0 }; + long delay = 0; + bool track_visibility = false; }; // https://www.w3.org/TR/intersection-observer/#intersectionobserverregistration @@ -53,7 +56,11 @@ public: Vector> const& observation_targets() const { return m_observation_targets; } Variant, GC::Root, Empty> root() const; + String root_margin() const; + String scroll_margin() const; Vector const& thresholds() const { return m_thresholds; } + long delay() const { return m_delay; } + bool track_visibility() const { return m_track_visibility; } Variant, GC::Root> intersection_root() const; CSSPixelRect root_intersection_rectangle() const; @@ -63,21 +70,35 @@ public: WebIDL::CallbackType& callback() { return *m_callback; } private: - explicit IntersectionObserver(JS::Realm&, GC::Ptr callback, Optional, GC::Root>> const& root, Vector&& thresholds); + explicit IntersectionObserver(JS::Realm&, GC::Ptr callback, Optional, GC::Root>> const& root, Vector root_margin, Vector scroll_margin, Vector&& thresholds, double debug, bool track_visibility); virtual void initialize(JS::Realm&) override; virtual void visit_edges(JS::Cell::Visitor&) override; virtual void finalize() override; + static Optional> parse_a_margin(JS::Realm&, String); + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-callback-slot GC::Ptr m_callback; // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-root GC::Ptr m_root; + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-rootmargin + Vector m_root_margin; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-scrollmargin + Vector m_scroll_margin; + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-thresholds Vector m_thresholds; + // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay + long m_delay; + + // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility + bool m_track_visibility; + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-queuedentries-slot Vector> m_queued_entries; diff --git a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl index 86f5e288624..f17877a1b71 100644 --- a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl +++ b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl @@ -11,9 +11,12 @@ callback IntersectionObserverCallback = undefined (sequence` should be `FrozenArray` readonly attribute sequence thresholds; + readonly attribute long delay; + readonly attribute boolean trackVisibility; undefined observe(Element target); undefined unobserve(Element target); undefined disconnect(); @@ -24,6 +27,8 @@ interface IntersectionObserver { dictionary IntersectionObserverInit { (Element or Document)? root = null; DOMString rootMargin = "0px"; - // FIXME: DOMString scrollMargin = "0px"; + DOMString scrollMargin = "0px"; (double or sequence) threshold = 0; + long delay = 0; + boolean trackVisibility = false; }; diff --git a/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt new file mode 100644 index 00000000000..dfd7b88d652 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt @@ -0,0 +1,19 @@ +Summary + +Harness status: OK + +Rerun + +Found 9 tests + +9 Pass +Details +Result Test Name MessagePass Observer attribute getters. +Pass observer.root +Pass observer.thresholds +Pass observer.rootMargin +Pass empty observer.thresholds +Pass whitespace observer.rootMargin +Pass set observer.root +Pass set observer.thresholds +Pass set observer.rootMargin \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt new file mode 100644 index 00000000000..50c169118df --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt @@ -0,0 +1,19 @@ +Summary + +Harness status: OK + +Rerun + +Found 9 tests + +9 Pass +Details +Result Test Name MessagePass IntersectionObserver constructor with { threshold: [1.1] } +Pass IntersectionObserver constructor with { threshold: ["foo"] } +Pass IntersectionObserver constructor with { rootMargin: "1" } +Pass IntersectionObserver constructor with { rootMargin: "2em" } +Pass IntersectionObserver constructor with { rootMargin: "auto" } +Pass IntersectionObserver constructor with { rootMargin: "calc(1px + 2px)" } +Pass IntersectionObserver constructor with { rootMargin: "1px !important" } +Pass IntersectionObserver constructor with { rootMargin: "1px 1px 1px 1px 1px" } +Pass IntersectionObserver.observe("foo") \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html new file mode 100644 index 00000000000..2fe66e4d2df --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html @@ -0,0 +1,41 @@ + + + + + +
+ + diff --git a/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html new file mode 100644 index 00000000000..30e7887ec0b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html @@ -0,0 +1,61 @@ + + + + + +