1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-02 23:32:06 +00:00

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
16 changed files with 664 additions and 22 deletions

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);
}
}