LibWeb: Add initial implementation of IntersectionObserver

The main missing features are rootMargin, proper nested browsing
context support and content clip/clip-path support.

This makes images appear on some sites, such as YouTube and
howstuffworks.com.
This commit is contained in:
Luke Wilde 2023-07-06 23:44:07 +01:00 committed by Andreas Kling
parent 6f8afd8cd9
commit 165abafb80
Notes: sideshowbarker 2024-07-17 03:16:02 +09:00
16 changed files with 664 additions and 22 deletions

View file

@ -39,6 +39,7 @@ static bool is_platform_object(Type const& type)
"FormData"sv,
"ImageData"sv,
"Instance"sv,
"IntersectionObserverEntry"sv,
"Memory"sv,
"Module"sv,
"MutationRecord"sv,

View file

@ -396,6 +396,7 @@ set(SOURCES
Infra/JSON.cpp
Infra/Strings.cpp
IntersectionObserver/IntersectionObserver.cpp
IntersectionObserver/IntersectionObserverEntry.cpp
Layout/AudioBox.cpp
Layout/AvailableSpace.cpp
Layout/BlockContainer.cpp

View file

@ -12,6 +12,7 @@
#include <AK/StringBuilder.h>
#include <AK/Utf8View.h>
#include <LibJS/Interpreter.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/FunctionObject.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/MediaQueryList.h>
@ -75,6 +76,7 @@
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/HighResolutionTime/TimeOrigin.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/IntersectionObserver/IntersectionObserver.h>
#include <LibWeb/Layout/BlockFormattingContext.h>
#include <LibWeb/Layout/TreeBuilder.h>
#include <LibWeb/Layout/Viewport.h>
@ -90,6 +92,7 @@
#include <LibWeb/UIEvents/FocusEvent.h>
#include <LibWeb/UIEvents/KeyboardEvent.h>
#include <LibWeb/UIEvents/MouseEvent.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
@ -2763,4 +2766,221 @@ HTML::ListOfAvailableImages const& Document::list_of_available_images() const
return *m_list_of_available_images;
}
void Document::register_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserver& observer)
{
auto result = m_intersection_observers.set(observer);
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
}
void Document::unregister_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserver& observer)
{
bool was_removed = m_intersection_observers.remove(observer);
VERIFY(was_removed);
}
// https://www.w3.org/TR/intersection-observer/#queue-an-intersection-observer-task
void Document::queue_intersection_observer_task()
{
// 1. If documents IntersectionObserverTaskQueued flag is set to true, return.
if (m_intersection_observer_task_queued)
return;
// 2. Set documents IntersectionObserverTaskQueued flag to true.
m_intersection_observer_task_queued = true;
// 3. Queue a task on the IntersectionObserver task source associated with the document's event loop to notify intersection observers.
HTML::queue_global_task(HTML::Task::Source::IntersectionObserver, window(), [this]() {
auto& realm = this->realm();
// https://www.w3.org/TR/intersection-observer/#notify-intersection-observers
// 1. Set documents IntersectionObserverTaskQueued flag to false.
m_intersection_observer_task_queued = false;
// 2. Let notify list be a list of all IntersectionObservers whose root is in the DOM tree of document.
Vector<JS::Handle<IntersectionObserver::IntersectionObserver>> notify_list;
notify_list.try_ensure_capacity(m_intersection_observers.size()).release_value_but_fixme_should_propagate_errors();
for (auto& observer : m_intersection_observers) {
notify_list.append(JS::make_handle(observer));
}
// 3. For each IntersectionObserver object observer in notify list, run these steps:
for (auto& observer : notify_list) {
// 2. Let queue be a copy of observers internal [[QueuedEntries]] slot.
// 3. Clear observers internal [[QueuedEntries]] slot.
auto queue = observer->take_records();
// 1. If observers internal [[QueuedEntries]] slot is empty, continue.
if (queue.is_empty())
continue;
auto wrapped_queue = MUST(JS::Array::create(realm, 0));
for (size_t i = 0; i < queue.size(); ++i) {
auto& record = queue.at(i);
auto property_index = JS::PropertyKey { i };
MUST(wrapped_queue->create_data_property(property_index, record.ptr()));
}
// 4. Let callback be the value of observers internal [[callback]] slot.
auto& callback = observer->callback();
// 5. Invoke callback with queue as the first argument, observer as the second argument, and observer as the callback this value. If this throws an exception, report the exception.
auto completion = WebIDL::invoke_callback(callback, observer.ptr(), wrapped_queue, observer.ptr());
if (completion.is_abrupt())
HTML::report_exception(completion, realm);
}
});
}
// https://www.w3.org/TR/intersection-observer/#queue-an-intersectionobserverentry
void Document::queue_an_intersection_observer_entry(IntersectionObserver::IntersectionObserver& observer, HighResolutionTime::DOMHighResTimeStamp time, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> root_bounds, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> bounding_client_rect, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr<Element> target)
{
auto& realm = this->realm();
// 1. Construct an IntersectionObserverEntry, passing in time, rootBounds, boundingClientRect, intersectionRect, isIntersecting, and target.
auto entry = realm.heap().allocate<IntersectionObserver::IntersectionObserverEntry>(realm, realm, time, root_bounds, bounding_client_rect, intersection_rect, is_intersecting, intersection_ratio, target).release_allocated_value_but_fixme_should_propagate_errors();
// 2. Append it to observers internal [[QueuedEntries]] slot.
observer.queue_entry({}, entry);
// 3. Queue an intersection observer task for document.
queue_intersection_observer_task();
}
// https://www.w3.org/TR/intersection-observer/#compute-the-intersection
static JS::NonnullGCPtr<Geometry::DOMRectReadOnly> compute_intersection(JS::NonnullGCPtr<Element> target, IntersectionObserver::IntersectionObserver const& observer)
{
// 1. Let intersectionRect be the result of getting the bounding box for target.
auto intersection_rect = target->get_bounding_client_rect();
// FIXME: 2. Let container be the containing block of target.
// FIXME: 3. While container is not root:
// FIXME: 1. If container is the document of a nested browsing context, update intersectionRect by clipping to
// the viewport of the document, and update container to be the browsing context container of container.
// FIXME: 2. Map intersectionRect to the coordinate space of container.
// FIXME: 3. If container has a content clip or a css clip-path property, update intersectionRect by applying
// containers clip.
// FIXME: 4. If container is the root element of a browsing context, update container to be the browsing contexts
// document; otherwise, update container to be the containing block of container.
// FIXME: 4. Map intersectionRect to the coordinate space of root.
// 5. Update intersectionRect by intersecting it with the root intersection rectangle.
// FIXME: Pass in target so we can properly apply rootMargin.
auto root_intersection_rectangle = observer.root_intersection_rectangle();
CSSPixelRect intersection_rect_as_pixel_rect(intersection_rect->x(), intersection_rect->y(), intersection_rect->width(), intersection_rect->height());
intersection_rect_as_pixel_rect.intersect(root_intersection_rectangle);
intersection_rect->set_x(static_cast<double>(intersection_rect_as_pixel_rect.x()));
intersection_rect->set_y(static_cast<double>(intersection_rect_as_pixel_rect.y()));
intersection_rect->set_width(static_cast<double>(intersection_rect_as_pixel_rect.width()));
intersection_rect->set_height(static_cast<double>(intersection_rect_as_pixel_rect.height()));
// FIXME: 6. Map intersectionRect to the coordinate space of the viewport of the document containing target.
// 7. Return intersectionRect.
return intersection_rect;
}
// https://www.w3.org/TR/intersection-observer/#run-the-update-intersection-observations-steps
void Document::run_the_update_intersection_observations_steps(HighResolutionTime::DOMHighResTimeStamp time)
{
auto& realm = this->realm();
// 1. Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document.
// For the top-level browsing context, this includes implicit root observers.
// 2. For each observer in observer list:
for (auto& observer : m_intersection_observers) {
// 1. Let rootBounds be observers root intersection rectangle.
auto root_bounds = observer->root_intersection_rectangle();
// 2. For each target in observers internal [[ObservationTargets]] slot, processed in the same order that
// observe() was called on each target:
for (auto& target : observer->observation_targets()) {
// 1. Let:
// thresholdIndex be 0.
size_t threshold_index = 0;
// isIntersecting be false.
bool is_intersecting = false;
// targetRect be a DOMRectReadOnly with x, y, width, and height set to 0.
auto target_rect = Geometry::DOMRectReadOnly::construct_impl(realm, 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors();
// intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0.
auto intersection_rect = Geometry::DOMRectReadOnly::construct_impl(realm, 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors();
// SPEC ISSUE: It doesn't pass in intersection ratio to "queue an IntersectionObserverEntry" despite needing it.
// This is default 0, as isIntersecting is default false, see step 9.
double intersection_ratio = 0.0;
// 2. If the intersection root is not the implicit root, and target is not in the same document as the intersection root, skip to step 11.
// 3. If the intersection root is an Element, and target is not a descendant of the intersection root in the containing block chain, skip to step 11.
// FIXME: Actually use the containing block chain.
auto intersection_root = observer->intersection_root();
auto intersection_root_document = intersection_root.visit([](auto& node) -> JS::NonnullGCPtr<Document> {
return node->document();
});
if (!(observer->root().has<Empty>() && &target->document() == intersection_root_document.ptr())
|| !(intersection_root.has<JS::Handle<DOM::Element>>() && !target->is_descendant_of(*intersection_root.get<JS::Handle<DOM::Element>>()))) {
// 4. Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target.
target_rect = target->get_bounding_client_rect();
// 5. Let intersectionRect be the result of running the compute the intersection algorithm on target and
// observers intersection root.
intersection_rect = compute_intersection(target, observer);
// 6. Let targetArea be targetRects area.
auto target_area = target_rect->width() * target_rect->height();
// 7. Let intersectionArea be intersectionRects area.
auto intersection_area = intersection_rect->width() * intersection_rect->height();
// 8. Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent, even if the
// intersection has zero area (because rootBounds or targetRect have zero area).
CSSPixelRect target_rect_as_pixel_rect(target_rect->x(), target_rect->y(), target_rect->width(), target_rect->height());
is_intersecting = target_rect_as_pixel_rect.intersects(root_bounds);
// 9. If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea.
// Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false.
if (target_area != 0.0)
intersection_ratio = intersection_area / target_area;
else
intersection_ratio = is_intersecting ? 1.0 : 0.0;
// 10. Set thresholdIndex to the index of the first entry in observer.thresholds whose value is greater
// than intersectionRatio, or the length of observer.thresholds if intersectionRatio is greater than
// or equal to the last entry in observer.thresholds.
threshold_index = observer->thresholds().find_first_index_if([&intersection_ratio](double threshold_value) {
return threshold_value > intersection_ratio;
})
.value_or(observer->thresholds().size());
}
// 11. Let intersectionObserverRegistration be the IntersectionObserverRegistration record in targets
// internal [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer.
auto& intersection_observer_registration = target->get_intersection_observer_registration({}, observer);
// 12. Let previousThresholdIndex be the intersectionObserverRegistrations previousThresholdIndex property.
auto previous_threshold_index = intersection_observer_registration.previous_threshold_index;
// 13. Let previousIsIntersecting be the intersectionObserverRegistrations previousIsIntersecting property.
auto previous_is_intersecting = intersection_observer_registration.previous_is_intersecting;
// 14. If thresholdIndex does not equal previousThresholdIndex or if isIntersecting does not equal
// previousIsIntersecting, queue an IntersectionObserverEntry, passing in observer, time,
// rootBounds, targetRect, intersectionRect, isIntersecting, and target.
if (threshold_index != previous_threshold_index || is_intersecting != previous_is_intersecting) {
auto root_bounds_as_dom_rect = Geometry::DOMRectReadOnly::construct_impl(realm, static_cast<double>(root_bounds.x()), static_cast<double>(root_bounds.y()), static_cast<double>(root_bounds.width()), static_cast<double>(root_bounds.height())).release_value_but_fixme_should_propagate_errors();
// SPEC ISSUE: It doesn't pass in intersectionRatio, but it's required.
queue_an_intersection_observer_entry(observer, time, root_bounds_as_dom_rect, target_rect, intersection_rect, is_intersecting, intersection_ratio, target);
}
// 15. Assign thresholdIndex to intersectionObserverRegistrations previousThresholdIndex property.
intersection_observer_registration.previous_threshold_index = threshold_index;
// 16. Assign isIntersecting to intersectionObserverRegistrations previousIsIntersecting property.
intersection_observer_registration.previous_is_intersecting = is_intersecting;
}
}
}
}

View file

@ -488,6 +488,11 @@ public:
HTML::ListOfAvailableImages& list_of_available_images();
HTML::ListOfAvailableImages const& list_of_available_images() const;
void register_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserver&);
void unregister_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserver&);
void run_the_update_intersection_observations_steps(HighResolutionTime::DOMHighResTimeStamp time);
protected:
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
@ -504,6 +509,9 @@ private:
WebIDL::ExceptionOr<void> run_the_document_write_steps(DeprecatedString);
void queue_intersection_observer_task();
void queue_an_intersection_observer_entry(IntersectionObserver::IntersectionObserver&, HighResolutionTime::DOMHighResTimeStamp time, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> root_bounds, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> bounding_client_rect, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr<Element> target);
OwnPtr<CSS::StyleComputer> m_style_computer;
JS::GCPtr<CSS::StyleSheetList> m_style_sheets;
JS::GCPtr<Node> m_hovered_node;
@ -655,6 +663,13 @@ private:
// https://html.spec.whatwg.org/multipage/images.html#list-of-available-images
OwnPtr<HTML::ListOfAvailableImages> m_list_of_available_images;
// NOTE: Not in the spec per say, but Document must be able to access all IntersectionObservers whose root is in the document.
OrderedHashTable<JS::NonnullGCPtr<IntersectionObserver::IntersectionObserver>> m_intersection_observers;
// https://www.w3.org/TR/intersection-observer/#document-intersectionobservertaskqueued
// Each document has an IntersectionObserverTaskQueued flag which is initialized to false.
bool m_intersection_observer_task_queued { false };
};
template<>

View file

@ -1784,4 +1784,25 @@ bool Element::id_reference_exists(DeprecatedString const& id_reference) const
return document().get_element_by_id(id_reference);
}
void Element::register_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserverRegistration registration)
{
m_registered_intersection_observers.append(move(registration));
}
void Element::unregister_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, JS::NonnullGCPtr<IntersectionObserver::IntersectionObserver> observer)
{
m_registered_intersection_observers.remove_first_matching([&observer](IntersectionObserver::IntersectionObserverRegistration const& entry) {
return entry.observer == observer;
});
}
IntersectionObserver::IntersectionObserverRegistration& Element::get_intersection_observer_registration(Badge<DOM::Document>, IntersectionObserver::IntersectionObserver const& observer)
{
auto registration_iterator = m_registered_intersection_observers.find_if([&observer](IntersectionObserver::IntersectionObserverRegistration const& entry) {
return entry.observer.ptr() == &observer;
});
VERIFY(!registration_iterator.is_end());
return *registration_iterator;
}
}

View file

@ -22,6 +22,7 @@
#include <LibWeb/HTML/ScrollOptions.h>
#include <LibWeb/HTML/TagNames.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/IntersectionObserver/IntersectionObserver.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::DOM {
@ -309,6 +310,10 @@ public:
void scroll(HTML::ScrollToOptions const&);
void scroll(double x, double y);
void register_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, IntersectionObserver::IntersectionObserverRegistration);
void unregister_intersection_observer(Badge<IntersectionObserver::IntersectionObserver>, JS::NonnullGCPtr<IntersectionObserver::IntersectionObserver>);
IntersectionObserver::IntersectionObserverRegistration& get_intersection_observer_registration(Badge<DOM::Document>, IntersectionObserver::IntersectionObserver const&);
protected:
Element(Document&, DOM::QualifiedName);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
@ -358,6 +363,10 @@ private:
// https://dom.spec.whatwg.org/#concept-element-is-value
Optional<String> m_is_value;
// https://www.w3.org/TR/intersection-observer/#dom-element-registeredintersectionobservers-slot
// Element objects have an internal [[RegisteredIntersectionObservers]] slot, which is initialized to an empty list.
Vector<IntersectionObserver::IntersectionObserverRegistration> m_registered_intersection_observers;
};
template<>

View file

@ -451,6 +451,8 @@ class Performance;
namespace Web::IntersectionObserver {
class IntersectionObserver;
class IntersectionObserverEntry;
struct IntersectionObserverRegistration;
}
namespace Web::Layout {

View file

@ -160,6 +160,10 @@ void EventLoop::process()
// FIXME: 4. Unnecessary rendering: Remove from docs all Document objects which meet both of the following conditions:
// - The user agent believes that updating the rendering of the Document's browsing context would have no visible effect, and
// - The Document's map of animation frame callbacks is empty.
// https://www.w3.org/TR/intersection-observer/#pending-initial-observation
// In the HTML Event Loops Processing Model, under the "Update the rendering" step, the "Unnecessary rendering" step should be
// modified to add an additional requirement for skipping the rendering update:
// - The document does not have pending initial IntersectionObserver targets.
// FIXME: 5. Remove from docs all Document objects for which the user agent believes that it's preferable to skip updating the rendering for other reasons.
@ -192,7 +196,10 @@ void EventLoop::process()
run_animation_frame_callbacks(document, now);
});
// FIXME: 14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]
// 14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]
for_each_fully_active_document_in_docs([&](DOM::Document& document) {
document.run_the_update_intersection_observations_steps(now);
});
// FIXME: 15. Invoke the mark paint timing algorithm for each Document object in docs.

View file

@ -38,6 +38,9 @@ public:
// https://w3c.github.io/FileAPI/#fileReadingTaskSource
FileReading,
// https://www.w3.org/TR/intersection-observer/#intersectionobserver-task-source
IntersectionObserver,
// Some elements, such as the HTMLMediaElement, must have a unique task source per instance.
// Keep this field last, to serve as the base value of all unique task sources.
UniqueTaskSourceStart,

View file

@ -4,28 +4,61 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/QuickSort.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/IntersectionObserver/IntersectionObserver.h>
namespace Web::IntersectionObserver {
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver
WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserver>> IntersectionObserver::construct_impl(JS::Realm& realm, WebIDL::CallbackType* callback, IntersectionObserverInit const& options)
WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserver>> IntersectionObserver::construct_impl(JS::Realm& realm, JS::GCPtr<WebIDL::CallbackType> callback, IntersectionObserverInit const& options)
{
// FIXME: Implement
(void)callback;
(void)options;
// 4. 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>>();
}
return MUST_OR_THROW_OOM(realm.heap().allocate<IntersectionObserver>(realm, realm));
// 5. 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.
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 MUST_OR_THROW_OOM(realm.heap().allocate<IntersectionObserver>(realm, realm, callback, options.root, move(thresholds)));
}
IntersectionObserver::IntersectionObserver(JS::Realm& realm)
IntersectionObserver::IntersectionObserver(JS::Realm& realm, JS::GCPtr<WebIDL::CallbackType> callback, Optional<Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>>> const& root, Vector<double>&& thresholds)
: PlatformObject(realm)
, m_callback(callback)
, m_root(root)
, m_thresholds(move(thresholds))
{
intersection_root().visit([this](auto& node) {
node->document().register_intersection_observer({}, *this);
});
}
IntersectionObserver::~IntersectionObserver() = default;
IntersectionObserver::~IntersectionObserver()
{
intersection_root().visit([this](auto& node) {
node->document().unregister_intersection_observer({}, *this);
});
}
JS::ThrowCompletionOr<void> IntersectionObserver::initialize(JS::Realm& realm)
{
@ -35,24 +68,139 @@ JS::ThrowCompletionOr<void> IntersectionObserver::initialize(JS::Realm& realm)
return {};
}
void IntersectionObserver::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_callback);
for (auto& entry : m_queued_entries)
visitor.visit(entry);
for (auto& target : m_observation_targets)
visitor.visit(target);
}
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe
void IntersectionObserver::observe(DOM::Element& target)
{
// FIXME: Implement
(void)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 observers internal [[ObservationTargets]] slot, return.
if (m_observation_targets.contains_slow(JS::NonnullGCPtr { target }))
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 {
.observer = JS::make_handle(*this),
.previous_threshold_index = OptionalNone {},
.previous_is_intersecting = false,
};
// 3. Append intersectionObserverRegistration to targets internal [[RegisteredIntersectionObservers]] slot.
target.register_intersection_observer({}, move(intersection_observer_registration));
// 4. Add target to observers internal [[ObservationTargets]] slot.
m_observation_targets.append(target);
}
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve
void IntersectionObserver::unobserve(DOM::Element& target)
{
// FIXME: Implement
(void)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 targets internal [[RegisteredIntersectionObservers]] slot, if present.
target.unregister_intersection_observer({}, *this);
// 2. Remove target from thiss internal [[ObservationTargets]] slot, if present
m_observation_targets.remove_first_matching([&target](JS::NonnullGCPtr<DOM::Element> const& entry) {
return entry.ptr() == &target;
});
}
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect
void IntersectionObserver::disconnect()
{
// FIXME: Implement
// For each target in thiss internal [[ObservationTargets]] slot:
// 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from targets internal
// [[RegisteredIntersectionObservers]] slot.
// 2. Remove target from thiss 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
Vector<JS::Handle<IntersectionObserverEntry>> IntersectionObserver::take_records()
{
// 1. Let queue be a copy of thiss internal [[QueuedEntries]] slot.
Vector<JS::Handle<IntersectionObserverEntry>> queue;
for (auto& entry : m_queued_entries)
queue.append(*entry);
// 2. Clear thiss internal [[QueuedEntries]] slot.
m_queued_entries.clear();
// 3. Return queue.
return queue;
}
Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>, Empty> IntersectionObserver::root() const
{
if (!m_root.has_value())
return Empty {};
return m_root.value();
}
Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>> IntersectionObserver::intersection_root() const
{
if (!m_root.has_value())
return JS::make_handle(global_object().browsing_context()->top_level_browsing_context().active_document());
return m_root.value();
}
// 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,
// its treated as if the root were the top-level browsing contexts document, according to the following rule for document.
auto intersection_root = this->intersection_root();
CSSPixelRect rect;
// If the intersection root is a document,
// its 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<JS::Handle<DOM::Document>>()) {
auto document = intersection_root.get<JS::Handle<DOM::Document>>();
// Since the spec says that this is only reach if the document is fully active, that means it must have a browsing context.
VERIFY(document->browsing_context());
rect = document->browsing_context()->viewport_rect();
} else {
VERIFY(intersection_root.has<JS::Handle<DOM::Element>>());
auto element = intersection_root.get<JS::Handle<DOM::Element>>();
// FIXME: Otherwise, if the intersection root has a content clip,
// its the elements content area.
// Otherwise,
// its the result of getting the bounding box for the intersection root.
auto bounding_client_rect = element->get_bounding_client_rect();
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.
return rect;
}
void IntersectionObserver::queue_entry(Badge<DOM::Document>, JS::NonnullGCPtr<IntersectionObserverEntry> entry)
{
m_queued_entries.append(entry);
}
}

View file

@ -8,32 +8,79 @@
#include <LibJS/Heap/Handle.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/IntersectionObserver/IntersectionObserverEntry.h>
#include <LibWeb/PixelUnits.h>
namespace Web::IntersectionObserver {
struct IntersectionObserverInit {
Optional<Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>>> root;
DeprecatedString root_margin { "0px"sv };
String root_margin { "0px"_short_string };
Variant<double, Vector<double>> threshold { 0 };
};
// https://www.w3.org/TR/intersection-observer/#intersectionobserverregistration
struct IntersectionObserverRegistration {
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverregistration-observer
// [A]n observer property holding an IntersectionObserver.
JS::Handle<IntersectionObserver> observer;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverregistration-observer
// NOTE: Optional is used in place of the spec using -1 to indicate no previous index.
// [A] previousThresholdIndex property holding a number between -1 and the length of the observers thresholds property (inclusive).
Optional<size_t> previous_threshold_index;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverregistration-previousisintersecting
// [A] previousIsIntersecting property holding a boolean.
bool previous_is_intersecting { false };
};
// https://w3c.github.io/IntersectionObserver/#intersection-observer-interface
class IntersectionObserver : public Bindings::PlatformObject {
class IntersectionObserver final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(IntersectionObserver, Bindings::PlatformObject);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserver>> construct_impl(JS::Realm&, WebIDL::CallbackType* callback, IntersectionObserverInit const& options = {});
static WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserver>> construct_impl(JS::Realm&, JS::GCPtr<WebIDL::CallbackType> callback, IntersectionObserverInit const& options = {});
virtual ~IntersectionObserver() override;
void observe(DOM::Element& target);
void unobserve(DOM::Element& target);
void disconnect();
Vector<JS::Handle<IntersectionObserverEntry>> take_records();
Vector<JS::NonnullGCPtr<DOM::Element>> const& observation_targets() const { return m_observation_targets; }
Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>, Empty> root() const;
Vector<double> const& thresholds() const { return m_thresholds; }
Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>> intersection_root() const;
CSSPixelRect root_intersection_rectangle() const;
void queue_entry(Badge<DOM::Document>, JS::NonnullGCPtr<IntersectionObserverEntry>);
WebIDL::CallbackType& callback() { return *m_callback; }
private:
explicit IntersectionObserver(JS::Realm&);
explicit IntersectionObserver(JS::Realm&, JS::GCPtr<WebIDL::CallbackType> callback, Optional<Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>>> const& root, Vector<double>&& thresholds);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
virtual void visit_edges(JS::Cell::Visitor&) override;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-callback-slot
JS::GCPtr<WebIDL::CallbackType> m_callback;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-root
Optional<Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>>> m_root;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-thresholds
Vector<double> m_thresholds;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-queuedentries-slot
Vector<JS::NonnullGCPtr<IntersectionObserverEntry>> m_queued_entries;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-observationtargets-slot
Vector<JS::NonnullGCPtr<DOM::Element>> m_observation_targets;
};
}

View file

@ -1,19 +1,21 @@
#import <DOM/Document.idl>
#import <DOM/Element.idl>
#import <DOM/Node.idl>
#import <IntersectionObserver/IntersectionObserverEntry.idl>
callback IntersectionObserverCallback = undefined (sequence<IntersectionObserverEntry> entries, IntersectionObserver observer);
[Exposed=(Window)]
[Exposed=(Window), UseNewAKString]
interface IntersectionObserver {
constructor(IntersectionObserverCallback callback, optional IntersectionObserverInit options = {});
readonly attribute (Element or Document)? root;
// FIXME: readonly attribute DOMString rootMargin;
// FIXME: `sequence<double>` should be `FrozenArray<double>`
readonly attribute sequence<double> thresholds;
undefined observe(Element target);
undefined unobserve(Element target);
undefined disconnect();
// FIXME:
// sequence<IntersectionObserverEntry> takeRecords();
sequence<IntersectionObserverEntry> takeRecords();
};
dictionary IntersectionObserverInit {

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/IntersectionObserver/IntersectionObserverEntry.h>
namespace Web::IntersectionObserver {
WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserverEntry>> IntersectionObserverEntry::construct_impl(JS::Realm& realm, Web::IntersectionObserver::IntersectionObserverEntryInit const& options)
{
auto& vm = realm.vm();
JS::GCPtr<Geometry::DOMRectReadOnly> root_bounds;
if (options.root_bounds.has_value())
root_bounds = TRY(Geometry::DOMRectReadOnly::from_rect(vm, options.root_bounds.value()));
auto bounding_client_rect = TRY(Geometry::DOMRectReadOnly::from_rect(vm, options.bounding_client_rect));
auto intersection_rect = TRY(Geometry::DOMRectReadOnly::from_rect(vm, options.intersection_rect));
return MUST_OR_THROW_OOM(realm.heap().allocate<IntersectionObserverEntry>(realm, realm, options.time, root_bounds, bounding_client_rect, intersection_rect, options.is_intersecting, options.intersection_ratio, *options.target));
}
IntersectionObserverEntry::IntersectionObserverEntry(JS::Realm& realm, HighResolutionTime::DOMHighResTimeStamp time, JS::GCPtr<Geometry::DOMRectReadOnly> root_bounds, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> bounding_client_rect, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr<DOM::Element> target)
: Bindings::PlatformObject(realm)
, m_time(time)
, m_root_bounds(root_bounds)
, m_bounding_client_rect(bounding_client_rect)
, m_intersection_rect(intersection_rect)
, m_is_intersecting(is_intersecting)
, m_intersection_ratio(intersection_ratio)
, m_target(target)
{
}
IntersectionObserverEntry::~IntersectionObserverEntry() = default;
JS::ThrowCompletionOr<void> IntersectionObserverEntry::initialize(JS::Realm& realm)
{
MUST_OR_THROW_OOM(Base::initialize(realm));
set_prototype(&Bindings::ensure_web_prototype<Bindings::IntersectionObserverEntryPrototype>(realm, "IntersectionObserverEntry"));
return {};
}
void IntersectionObserverEntry::visit_edges(JS::Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_root_bounds);
visitor.visit(m_bounding_client_rect);
visitor.visit(m_intersection_rect);
visitor.visit(m_target);
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/Geometry/DOMRect.h>
#include <LibWeb/HighResolutionTime/DOMHighResTimeStamp.h>
namespace Web::IntersectionObserver {
struct IntersectionObserverEntryInit {
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-time
HighResolutionTime::DOMHighResTimeStamp time { 0.0 };
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-rootbounds
Optional<Geometry::DOMRectInit> root_bounds;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-boundingclientrect
Geometry::DOMRectInit bounding_client_rect;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionrect
Geometry::DOMRectInit intersection_rect;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-isintersecting
bool is_intersecting { false };
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionratio
double intersection_ratio { 0.0 };
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-target
JS::Handle<DOM::Element> target;
};
class IntersectionObserverEntry final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(IntersectionObserverEntry, Bindings::PlatformObject);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserverEntry>> construct_impl(JS::Realm&, IntersectionObserverEntryInit const& options);
virtual ~IntersectionObserverEntry() override;
HighResolutionTime::DOMHighResTimeStamp time() const { return m_time; }
JS::GCPtr<Geometry::DOMRectReadOnly> root_bounds() const { return m_root_bounds; }
JS::NonnullGCPtr<Geometry::DOMRectReadOnly> bounding_client_rect() const { return m_bounding_client_rect; }
JS::NonnullGCPtr<Geometry::DOMRectReadOnly> intersection_rect() const { return m_intersection_rect; }
bool is_intersecting() const { return m_is_intersecting; }
double intersection_ratio() const { return m_intersection_ratio; }
JS::NonnullGCPtr<DOM::Element> target() const { return m_target; }
private:
IntersectionObserverEntry(JS::Realm&, HighResolutionTime::DOMHighResTimeStamp time, JS::GCPtr<Geometry::DOMRectReadOnly> root_bounds, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> bounding_client_rect, JS::NonnullGCPtr<Geometry::DOMRectReadOnly> intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr<DOM::Element> target);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
virtual void visit_edges(JS::Cell::Visitor&) override;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-time
HighResolutionTime::DOMHighResTimeStamp m_time { 0.0 };
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-rootbounds
JS::GCPtr<Geometry::DOMRectReadOnly> m_root_bounds;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-boundingclientrect
JS::NonnullGCPtr<Geometry::DOMRectReadOnly> m_bounding_client_rect;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionrect
JS::NonnullGCPtr<Geometry::DOMRectReadOnly> m_intersection_rect;
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-isintersecting
bool m_is_intersecting { false };
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionratio
double m_intersection_ratio { 0.0 };
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-target
JS::NonnullGCPtr<DOM::Element> m_target;
};
}

View file

@ -0,0 +1,26 @@
#import <DOM/Element.idl>
#import <Geometry/DOMRect.idl>
#import <HighResolutionTime/DOMHighResTimeStamp.idl>
// https://www.w3.org/TR/intersection-observer/#intersectionobserverentry
[Exposed=Window]
interface IntersectionObserverEntry {
// FIXME: constructor(IntersectionObserverEntryInit intersectionObserverEntryInit);
readonly attribute DOMHighResTimeStamp time;
readonly attribute DOMRectReadOnly? rootBounds;
readonly attribute DOMRectReadOnly boundingClientRect;
readonly attribute DOMRectReadOnly intersectionRect;
readonly attribute boolean isIntersecting;
readonly attribute double intersectionRatio;
readonly attribute Element target;
};
dictionary IntersectionObserverEntryInit {
required DOMHighResTimeStamp time;
required DOMRectInit? rootBounds;
required DOMRectInit boundingClientRect;
required DOMRectInit intersectionRect;
required boolean isIntersecting;
required double intersectionRatio;
required Element target;
};

View file

@ -183,6 +183,7 @@ libweb_js_bindings(HTML/WorkerLocation)
libweb_js_bindings(HTML/WorkerNavigator)
libweb_js_bindings(HighResolutionTime/Performance)
libweb_js_bindings(IntersectionObserver/IntersectionObserver)
libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry)
libweb_js_bindings(NavigationTiming/PerformanceTiming)
libweb_js_bindings(PerformanceTimeline/PerformanceEntry)
libweb_js_bindings(RequestIdleCallback/IdleDeadline)