mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-18 18:00:31 +00:00
IntersectionObserver updates already iterate over each observer and its observation targets. We then looked the same target and observer pair up again through Element's registered observer list just to read and write previousThresholdIndex and previousIsIntersecting. Store that mutable state with the observer-side observation target instead. The element-side list now only keeps strong observer references for lifetime management and unobserve/disconnect. This deviates from the spec's storage model, so document the difference next to the preserved spec comments.
395 lines
18 KiB
C++
395 lines
18 KiB
C++
/*
|
||
* Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#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/BrowsingContext.h>
|
||
#include <LibWeb/HTML/TraversableNavigable.h>
|
||
#include <LibWeb/HTML/Window.h>
|
||
#include <LibWeb/IntersectionObserver/IntersectionObserver.h>
|
||
#include <LibWeb/Layout/Viewport.h>
|
||
#include <LibWeb/Page/Page.h>
|
||
|
||
namespace Web::IntersectionObserver {
|
||
|
||
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)
|
||
{
|
||
// 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."_utf16);
|
||
}
|
||
|
||
// 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."_utf16);
|
||
}
|
||
|
||
// 5. Let thresholds be a list equal to options.threshold.
|
||
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>>();
|
||
}
|
||
|
||
// 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 };
|
||
}
|
||
|
||
// 7. Sort thresholds in ascending order.
|
||
quick_sort(thresholds, [](double left, double right) {
|
||
return left < right;
|
||
});
|
||
|
||
// 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));
|
||
}
|
||
|
||
IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> callback, NullableIntersectionObserverRoot 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<Empty>() ? nullptr : root.visit([](GC::Root<DOM::Element> const& value) -> GC::Ptr<DOM::Node> { return *value; }, [](GC::Root<DOM::Document> const& value) -> GC::Ptr<DOM::Node> { return *value; }, [](Empty) -> GC::Ptr<DOM::Node> { return nullptr; });
|
||
intersection_root().visit([this](auto& node) {
|
||
m_document = node->document();
|
||
});
|
||
m_document->register_intersection_observer({}, *this);
|
||
}
|
||
|
||
IntersectionObserver::~IntersectionObserver() = default;
|
||
|
||
void IntersectionObserver::finalize()
|
||
{
|
||
Base::finalize();
|
||
if (m_document)
|
||
m_document->unregister_intersection_observer({}, *this);
|
||
}
|
||
|
||
void IntersectionObserver::initialize(JS::Realm& realm)
|
||
{
|
||
WEB_SET_PROTOTYPE_FOR_INTERFACE(IntersectionObserver);
|
||
Base::initialize(realm);
|
||
}
|
||
|
||
void IntersectionObserver::visit_edges(JS::Cell::Visitor& visitor)
|
||
{
|
||
Base::visit_edges(visitor);
|
||
visitor.visit(m_root);
|
||
visitor.visit(m_callback);
|
||
visitor.visit(m_queued_entries);
|
||
for (auto& observed_target : m_observation_targets)
|
||
visitor.visit(observed_target.target);
|
||
}
|
||
|
||
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe
|
||
void IntersectionObserver::observe(DOM::Element& target)
|
||
{
|
||
// 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.
|
||
auto observed_target = m_observation_targets.find_if([&target](auto const& entry) {
|
||
return entry.target.ptr() == ⌖
|
||
});
|
||
if (!observed_target.is_end())
|
||
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.
|
||
// NB: This implementation deviates from the spec's storage model. target's
|
||
// [[RegisteredIntersectionObservers]] only keeps a strong reference to the observer for lifetime
|
||
// management, while the mutable registration fields live in the observer-side observation target below.
|
||
target.register_intersection_observer({}, *this);
|
||
|
||
// 3. Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
|
||
// NB: Keeping previousThresholdIndex and previousIsIntersecting on the observer-side observation target lets
|
||
// the update loop read and write them without a second lookup through target’s registered observers.
|
||
//
|
||
// 4. Add target to observer’s internal [[ObservationTargets]] slot.
|
||
m_observation_targets.append(ObservationTarget {
|
||
.target = target,
|
||
.previous_threshold_index = OptionalNone {},
|
||
.previous_is_intersecting = false,
|
||
});
|
||
}
|
||
|
||
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve
|
||
void IntersectionObserver::unobserve(DOM::Element& target)
|
||
{
|
||
// 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
|
||
m_observation_targets.remove_first_matching([&target](ObservationTarget const& entry) {
|
||
return entry.target.ptr() == ⌖
|
||
});
|
||
}
|
||
|
||
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect
|
||
void IntersectionObserver::disconnect()
|
||
{
|
||
// 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& observed_target : m_observation_targets) {
|
||
observed_target.target->unregister_intersection_observer({}, *this);
|
||
}
|
||
m_observation_targets.clear();
|
||
}
|
||
|
||
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-takerecords
|
||
Vector<GC::Root<IntersectionObserverEntry>> IntersectionObserver::take_records()
|
||
{
|
||
// 1. Let queue be a copy of this’s internal [[QueuedEntries]] slot.
|
||
Vector<GC::Root<IntersectionObserverEntry>> queue;
|
||
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;
|
||
}
|
||
|
||
NullableIntersectionObserverRoot IntersectionObserver::root() const
|
||
{
|
||
if (!m_root)
|
||
return Empty {};
|
||
if (m_root->is_element())
|
||
return GC::make_root(static_cast<DOM::Element&>(*m_root));
|
||
if (m_root->is_document())
|
||
return GC::make_root(static_cast<DOM::Document&>(*m_root));
|
||
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(CSS::SerializationMode::ResolvedValue));
|
||
builder.append(' ');
|
||
builder.append(m_root_margin[1].to_string(CSS::SerializationMode::ResolvedValue));
|
||
builder.append(' ');
|
||
builder.append(m_root_margin[2].to_string(CSS::SerializationMode::ResolvedValue));
|
||
builder.append(' ');
|
||
builder.append(m_root_margin[3].to_string(CSS::SerializationMode::ResolvedValue));
|
||
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(CSS::SerializationMode::ResolvedValue));
|
||
builder.append(' ');
|
||
builder.append(m_scroll_margin[1].to_string(CSS::SerializationMode::ResolvedValue));
|
||
builder.append(' ');
|
||
builder.append(m_scroll_margin[2].to_string(CSS::SerializationMode::ResolvedValue));
|
||
builder.append(' ');
|
||
builder.append(m_scroll_margin[3].to_string(CSS::SerializationMode::ResolvedValue));
|
||
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
|
||
{
|
||
auto root_node = intersection_root_node();
|
||
if (root_node->is_element())
|
||
return GC::make_root(static_cast<DOM::Element&>(*root_node));
|
||
return GC::make_root(static_cast<DOM::Document&>(*root_node));
|
||
}
|
||
|
||
GC::Ref<DOM::Node> IntersectionObserver::intersection_root_node() const
|
||
{
|
||
if (m_root)
|
||
return *m_root;
|
||
|
||
// The implicit root is the top-level browsing context’s document node.
|
||
return *as<HTML::Window>(HTML::relevant_global_object(*this)).page().top_level_browsing_context().active_document();
|
||
}
|
||
|
||
// 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).
|
||
if (intersection_root.has<GC::Root<DOM::Document>>()) {
|
||
auto document = intersection_root.get<GC::Root<DOM::Document>>();
|
||
|
||
// 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());
|
||
|
||
// NOTE: This rect is the *size* of the viewport. The viewport *offset* is not relevant,
|
||
// as intersections are computed using viewport-relative element rects.
|
||
rect = CSSPixelRect { CSSPixelPoint { 0, 0 }, document->viewport_rect().size() };
|
||
} else {
|
||
VERIFY(intersection_root.has<GC::Root<DOM::Element>>());
|
||
auto element = intersection_root.get<GC::Root<DOM::Element>>();
|
||
|
||
// 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.
|
||
rect = element->get_bounding_client_rect();
|
||
}
|
||
|
||
// 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 && document->origin().is_same_origin(m_document->origin())) {
|
||
if (auto layout_node = intersection_root.visit([&](auto& node) -> GC::Ptr<Layout::Node> { return node->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;
|
||
}
|
||
|
||
void IntersectionObserver::queue_entry(Badge<DOM::Document>, GC::Ref<IntersectionObserverEntry> entry)
|
||
{
|
||
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::ParsingParams { 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)) {
|
||
if (auto length_unit = CSS::string_to_length_unit(token.token().dimension_unit()); length_unit.has_value()) {
|
||
if (auto length = CSS::Length(token.token().dimension_value(), length_unit.release_value()); length.is_absolute()) {
|
||
tokens_length_percentage.append(length);
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
// If token is a <percentage> token, replace it with an equivalent percentage.
|
||
else 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;
|
||
}
|
||
|
||
}
|