LibWeb: Implement more IntersectionObserver attributes

This commit is contained in:
Psychpsyo 2024-11-22 12:01:42 +01:00 committed by Andreas Kling
parent 7444f76b0d
commit 3e536a4cd7
Notes: github-actions[bot] 2024-11-23 08:53:22 +00:00
9 changed files with 345 additions and 17 deletions

View file

@ -6039,6 +6039,11 @@ Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<T>& compo
template Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<Token>& component_values);
template Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<ComponentValue>& component_values);
Vector<ComponentValue> Parser::parse_as_list_of_component_values()
{
return parse_a_list_of_component_values(m_token_stream);
}
RefPtr<CSSStyleValue> Parser::parse_list_style_value(TokenStream<ComponentValue>& tokens)
{
RefPtr<CSSStyleValue> list_position;

View file

@ -69,6 +69,8 @@ public:
Vector<ParsedFontFace::Source> parse_as_font_face_src();
Vector<ComponentValue> parse_as_list_of_component_values();
static NonnullRefPtr<CSSStyleValue> resolve_unresolved_style_value(ParsingContext const&, DOM::Element&, Optional<CSS::Selector::PseudoElement::Type>, PropertyID, UnresolvedStyleValue const&);
[[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr);

View file

@ -7,6 +7,8 @@
#include <AK/QuickSort.h>
#include <LibWeb/Bindings/IntersectionObserverPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/TraversableNavigable.h>
@ -21,7 +23,24 @@ GC_DEFINE_ALLOCATOR(IntersectionObserver);
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver
WebIDL::ExceptionOr<GC::Ref<IntersectionObserver>> IntersectionObserver::construct_impl(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> 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 thiss 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 thiss 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 thiss 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<double> thresholds;
if (options.threshold.has<double>()) {
thresholds.append(options.threshold.get<double>());
@ -30,28 +49,47 @@ WebIDL::ExceptionOr<GC::Ref<IntersectionObserver>> IntersectionObserver::constru
thresholds = options.threshold.get<Vector<double>>();
}
// 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 thiss internal [[callback]] slot to callback.
// 8. The thresholds attribute getter will return this sorted thresholds list.
// 9. Return this.
return realm.create<IntersectionObserver>(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 thiss internal [[delay]] slot to options.delay to delay.
// 13. Set thiss 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));
}
IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> callback, Optional<Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>>> const& root, Vector<double>&& thresholds)
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)
: 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<DOM::Node> { return *value; }) : nullptr;
intersection_root().visit([this](auto& node) {
@ -161,6 +199,44 @@ Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>, 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<DOM::Element>, GC::Root<DOM::Document>> 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 IntersectionObservers [[rootMargin]] slot in a manner similar
// to CSSs 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 IntersectionObservers [[rootMargin]] slot in a manner similar
// to CSSs 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()));
}
return rect;
}
@ -225,4 +315,69 @@ void IntersectionObserver::queue_entry(Badge<DOM::Document>, GC::Ref<Intersectio
m_queued_entries.append(entry);
}
// 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.
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<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;
}
}

View file

@ -16,7 +16,10 @@ namespace Web::IntersectionObserver {
struct IntersectionObserverInit {
Optional<Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>>> root;
String root_margin { "0px"_string };
String scroll_margin { "0px"_string };
Variant<double, Vector<double>> threshold { 0 };
long delay = 0;
bool track_visibility = false;
};
// https://www.w3.org/TR/intersection-observer/#intersectionobserverregistration
@ -53,7 +56,11 @@ public:
Vector<GC::Ref<DOM::Element>> const& observation_targets() const { return m_observation_targets; }
Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>, Empty> root() const;
String root_margin() const;
String scroll_margin() const;
Vector<double> const& thresholds() const { return m_thresholds; }
long delay() const { return m_delay; }
bool track_visibility() const { return m_track_visibility; }
Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>> 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<WebIDL::CallbackType> callback, Optional<Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>>> const& root, Vector<double>&& thresholds);
explicit IntersectionObserver(JS::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 debug, bool track_visibility);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(JS::Cell::Visitor&) override;
virtual void finalize() override;
static Optional<Vector<CSS::LengthPercentage>> parse_a_margin(JS::Realm&, String);
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-callback-slot
GC::Ptr<WebIDL::CallbackType> m_callback;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-root
GC::Ptr<DOM::Node> m_root;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-rootmargin
Vector<CSS::LengthPercentage> m_root_margin;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-scrollmargin
Vector<CSS::LengthPercentage> m_scroll_margin;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-thresholds
Vector<double> 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<GC::Ref<IntersectionObserverEntry>> m_queued_entries;

View file

@ -11,9 +11,12 @@ callback IntersectionObserverCallback = undefined (sequence<IntersectionObserver
interface IntersectionObserver {
constructor(IntersectionObserverCallback callback, optional IntersectionObserverInit options = {});
readonly attribute (Element or Document)? root;
[FIXME] readonly attribute DOMString rootMargin;
readonly attribute DOMString rootMargin;
readonly attribute DOMString scrollMargin;
// FIXME: `sequence<double>` should be `FrozenArray<double>`
readonly attribute sequence<double> 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<double>) threshold = 0;
long delay = 0;
boolean trackVisibility = false;
};

View file

@ -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

View file

@ -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")

View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<div id="root"></div>
<script>
test(function() {
var observer = new IntersectionObserver(function(e) {}, {});
test(function() { assert_equals(observer.root, null) },
"observer.root");
test(function() { assert_array_equals(observer.thresholds, [0]) },
"observer.thresholds");
test(function() { assert_equals(observer.rootMargin, "0px 0px 0px 0px") },
"observer.rootMargin");
observer = new IntersectionObserver(function(e) {}, {
rootMargin: " ",
threshold: []
});
test(function() { assert_array_equals(observer.thresholds, [0]) },
"empty observer.thresholds");
test(function() { assert_equals(observer.rootMargin, "0px 0px 0px 0px") },
"whitespace observer.rootMargin");
var rootDiv = document.getElementById("root");
observer = new IntersectionObserver(function(e) {}, {
root: rootDiv,
threshold: [0, 0.25, 0.5, 1.0],
rootMargin: "10% 20px"
});
test(function() { assert_equals(observer.root, rootDiv) },
"set observer.root");
test(function() { assert_array_equals(observer.thresholds, [0, 0.25, 0.5, 1.0]) },
"set observer.thresholds");
test(function() { assert_equals(observer.rootMargin, "10% 20px 10% 20px") },
"set observer.rootMargin");
}, "Observer attribute getters.");
</script>

View file

@ -0,0 +1,61 @@
<!DOCTYPE html>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script>
test(function () {
assert_throws_js(RangeError, function() {
new IntersectionObserver(e => {}, {threshold: [1.1]})
})
}, "IntersectionObserver constructor with { threshold: [1.1] }");
test(function () {
assert_throws_js(TypeError, function() {
new IntersectionObserver(e => {}, {threshold: ["foo"]})
})
}, 'IntersectionObserver constructor with { threshold: ["foo"] }');
test(function () {
assert_throws_dom("SYNTAX_ERR", function() {
new IntersectionObserver(e => {}, {rootMargin: "1"})
})
}, 'IntersectionObserver constructor with { rootMargin: "1" }');
test(function () {
assert_throws_dom("SYNTAX_ERR", function() {
new IntersectionObserver(e => {}, {rootMargin: "2em"})
})
}, 'IntersectionObserver constructor with { rootMargin: "2em" }');
test(function () {
assert_throws_dom("SYNTAX_ERR", function() {
new IntersectionObserver(e => {}, {rootMargin: "auto"})
})
}, 'IntersectionObserver constructor with { rootMargin: "auto" }');
test(function () {
assert_throws_dom("SYNTAX_ERR", function() {
new IntersectionObserver(e => {}, {rootMargin: "calc(1px + 2px)"})
})
}, 'IntersectionObserver constructor with { rootMargin: "calc(1px + 2px)" }');
test(function () {
assert_throws_dom("SYNTAX_ERR", function() {
new IntersectionObserver(e => {}, {rootMargin: "1px !important"})
})
}, 'IntersectionObserver constructor with { rootMargin: "1px !important" }');
test(function () {
assert_throws_dom("SYNTAX_ERR", function() {
new IntersectionObserver(e => {}, {rootMargin: "1px 1px 1px 1px 1px"})
})
}, 'IntersectionObserver constructor with { rootMargin: "1px 1px 1px 1px 1px" }');
test(function () {
assert_throws_js(TypeError, function() {
let observer = new IntersectionObserver(c => {}, {});
observer.observe("foo");
})
}, 'IntersectionObserver.observe("foo")');
</script>