
When the intersection root is a Document, we use the viewport itself as the root intersection rectangle. However, we should only use the size of the viewport and strip away the current scroll offset. This is important, as intersections are computed using viewport-relative element rects, so we're already in a coordinate system where (0, 0) is the top left of the scrolled viewport. This fixes an issue where IntersectionObservers would fire at entirely wrong scroll offsets. :^)
209 lines
9 KiB
C++
209 lines
9 KiB
C++
/*
|
||
* Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
|
||
*
|
||
* 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, JS::GCPtr<WebIDL::CallbackType> callback, IntersectionObserverInit const& 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>>();
|
||
}
|
||
|
||
// 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 this’s 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, 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()
|
||
{
|
||
intersection_root().visit([this](auto& node) {
|
||
node->document().unregister_intersection_observer({}, *this);
|
||
});
|
||
}
|
||
|
||
JS::ThrowCompletionOr<void> IntersectionObserver::initialize(JS::Realm& realm)
|
||
{
|
||
MUST_OR_THROW_OOM(Base::initialize(realm));
|
||
set_prototype(&Bindings::ensure_web_prototype<Bindings::IntersectionObserverPrototype>(realm, "IntersectionObserver"));
|
||
|
||
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)
|
||
{
|
||
// 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.
|
||
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 target’s internal [[RegisteredIntersectionObservers]] slot.
|
||
target.register_intersection_observer({}, move(intersection_observer_registration));
|
||
|
||
// 4. Add target to observer’s internal [[ObservationTargets]] slot.
|
||
m_observation_targets.append(target);
|
||
}
|
||
|
||
// 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](JS::NonnullGCPtr<DOM::Element> const& entry) {
|
||
return entry.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& 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 this’s internal [[QueuedEntries]] slot.
|
||
Vector<JS::Handle<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;
|
||
}
|
||
|
||
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,
|
||
// 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<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());
|
||
|
||
// 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->browsing_context()->viewport_rect().size() };
|
||
} 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,
|
||
// it’s the element’s content area.
|
||
|
||
// Otherwise,
|
||
// it’s 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 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.
|
||
|
||
return rect;
|
||
}
|
||
|
||
void IntersectionObserver::queue_entry(Badge<DOM::Document>, JS::NonnullGCPtr<IntersectionObserverEntry> entry)
|
||
{
|
||
m_queued_entries.append(entry);
|
||
}
|
||
|
||
}
|