Coverage Report

Created: 2025-11-02 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2021, Tim Flynn <trflynn89@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/QuickSort.h>
8
#include <LibWeb/Bindings/IntersectionObserverPrototype.h>
9
#include <LibWeb/Bindings/Intrinsics.h>
10
#include <LibWeb/DOM/Document.h>
11
#include <LibWeb/DOM/Element.h>
12
#include <LibWeb/HTML/TraversableNavigable.h>
13
#include <LibWeb/HTML/Window.h>
14
#include <LibWeb/IntersectionObserver/IntersectionObserver.h>
15
#include <LibWeb/Page/Page.h>
16
17
namespace Web::IntersectionObserver {
18
19
JS_DEFINE_ALLOCATOR(IntersectionObserver);
20
21
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver
22
WebIDL::ExceptionOr<JS::NonnullGCPtr<IntersectionObserver>> IntersectionObserver::construct_impl(JS::Realm& realm, JS::GCPtr<WebIDL::CallbackType> callback, IntersectionObserverInit const& options)
23
0
{
24
    // 4. Let thresholds be a list equal to options.threshold.
25
0
    Vector<double> thresholds;
26
0
    if (options.threshold.has<double>()) {
27
0
        thresholds.append(options.threshold.get<double>());
28
0
    } else {
29
0
        VERIFY(options.threshold.has<Vector<double>>());
30
0
        thresholds = options.threshold.get<Vector<double>>();
31
0
    }
32
33
    // 5. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception.
34
0
    for (auto value : thresholds) {
35
0
        if (value < 0.0 || value > 1.0)
36
0
            return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Threshold values must be between 0.0 and 1.0 inclusive"sv };
37
0
    }
38
39
    // 6. Sort thresholds in ascending order.
40
0
    quick_sort(thresholds, [](double left, double right) {
41
0
        return left < right;
42
0
    });
43
44
    // 1. Let this be a new IntersectionObserver object
45
    // 2. Set this’s internal [[callback]] slot to callback.
46
    // 8. The thresholds attribute getter will return this sorted thresholds list.
47
    // 9. Return this.
48
0
    return realm.heap().allocate<IntersectionObserver>(realm, realm, callback, options.root, move(thresholds));
49
0
}
50
51
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)
52
0
    : PlatformObject(realm)
53
0
    , m_callback(callback)
54
0
    , m_thresholds(move(thresholds))
55
0
{
56
0
    m_root = root.has_value() ? root->visit([](auto& value) -> JS::GCPtr<DOM::Node> { return *value; }) : nullptr;
Unexecuted instantiation: IntersectionObserver.cpp:JS::GCPtr<Web::DOM::Node> Web::IntersectionObserver::IntersectionObserver::IntersectionObserver(JS::Realm&, JS::GCPtr<Web::WebIDL::CallbackType>, AK::Optional<AK::Variant<JS::Handle<Web::DOM::Element>, JS::Handle<Web::DOM::Document> > > const&, AK::Vector<double, 0ul>&&)::$_0::operator()<JS::Handle<Web::DOM::Element> const>(JS::Handle<Web::DOM::Element> const&) const
Unexecuted instantiation: IntersectionObserver.cpp:JS::GCPtr<Web::DOM::Node> Web::IntersectionObserver::IntersectionObserver::IntersectionObserver(JS::Realm&, JS::GCPtr<Web::WebIDL::CallbackType>, AK::Optional<AK::Variant<JS::Handle<Web::DOM::Element>, JS::Handle<Web::DOM::Document> > > const&, AK::Vector<double, 0ul>&&)::$_0::operator()<JS::Handle<Web::DOM::Document> const>(JS::Handle<Web::DOM::Document> const&) const
57
0
    intersection_root().visit([this](auto& node) {
58
0
        m_document = node->document();
59
0
    });
Unexecuted instantiation: IntersectionObserver.cpp:auto Web::IntersectionObserver::IntersectionObserver::IntersectionObserver(JS::Realm&, JS::GCPtr<Web::WebIDL::CallbackType>, AK::Optional<AK::Variant<JS::Handle<Web::DOM::Element>, JS::Handle<Web::DOM::Document> > > const&, AK::Vector<double, 0ul>&&)::$_1::operator()<JS::Handle<Web::DOM::Element> >(JS::Handle<Web::DOM::Element>&) const
Unexecuted instantiation: IntersectionObserver.cpp:auto Web::IntersectionObserver::IntersectionObserver::IntersectionObserver(JS::Realm&, JS::GCPtr<Web::WebIDL::CallbackType>, AK::Optional<AK::Variant<JS::Handle<Web::DOM::Element>, JS::Handle<Web::DOM::Document> > > const&, AK::Vector<double, 0ul>&&)::$_1::operator()<JS::Handle<Web::DOM::Document> >(JS::Handle<Web::DOM::Document>&) const
60
0
    m_document->register_intersection_observer({}, *this);
61
0
}
62
63
0
IntersectionObserver::~IntersectionObserver() = default;
64
65
void IntersectionObserver::finalize()
66
0
{
67
0
    if (m_document)
68
0
        m_document->unregister_intersection_observer({}, *this);
69
0
}
70
71
void IntersectionObserver::initialize(JS::Realm& realm)
72
0
{
73
0
    Base::initialize(realm);
74
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE(IntersectionObserver);
75
0
}
76
77
void IntersectionObserver::visit_edges(JS::Cell::Visitor& visitor)
78
0
{
79
0
    Base::visit_edges(visitor);
80
0
    visitor.visit(m_root);
81
0
    visitor.visit(m_callback);
82
0
    visitor.visit(m_queued_entries);
83
0
    visitor.visit(m_observation_targets);
84
0
}
85
86
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe
87
void IntersectionObserver::observe(DOM::Element& target)
88
0
{
89
    // Run the observe a target Element algorithm, providing this and target.
90
    // https://www.w3.org/TR/intersection-observer/#observe-a-target-element
91
    // 1. If target is in observer’s internal [[ObservationTargets]] slot, return.
92
0
    if (m_observation_targets.contains_slow(JS::NonnullGCPtr { target }))
93
0
        return;
94
95
    // 2. Let intersectionObserverRegistration be an IntersectionObserverRegistration record with an observer
96
    //    property set to observer, a previousThresholdIndex property set to -1, and a previousIsIntersecting
97
    //    property set to false.
98
0
    auto intersection_observer_registration = IntersectionObserverRegistration {
99
0
        .observer = *this,
100
0
        .previous_threshold_index = OptionalNone {},
101
0
        .previous_is_intersecting = false,
102
0
    };
103
104
    // 3. Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
105
0
    target.register_intersection_observer({}, move(intersection_observer_registration));
106
107
    // 4. Add target to observer’s internal [[ObservationTargets]] slot.
108
0
    m_observation_targets.append(target);
109
0
}
110
111
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve
112
void IntersectionObserver::unobserve(DOM::Element& target)
113
0
{
114
    // Run the unobserve a target Element algorithm, providing this and target.
115
    // https://www.w3.org/TR/intersection-observer/#unobserve-a-target-element
116
    // 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’s internal [[RegisteredIntersectionObservers]] slot, if present.
117
0
    target.unregister_intersection_observer({}, *this);
118
119
    // 2. Remove target from this’s internal [[ObservationTargets]] slot, if present
120
0
    m_observation_targets.remove_first_matching([&target](JS::NonnullGCPtr<DOM::Element> const& entry) {
121
0
        return entry.ptr() == &target;
122
0
    });
123
0
}
124
125
// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect
126
void IntersectionObserver::disconnect()
127
0
{
128
    // For each target in this’s internal [[ObservationTargets]] slot:
129
    // 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’s internal
130
    //    [[RegisteredIntersectionObservers]] slot.
131
    // 2. Remove target from this’s internal [[ObservationTargets]] slot.
132
0
    for (auto& target : m_observation_targets) {
133
0
        target->unregister_intersection_observer({}, *this);
134
0
    }
135
0
    m_observation_targets.clear();
136
0
}
137
138
// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-takerecords
139
Vector<JS::Handle<IntersectionObserverEntry>> IntersectionObserver::take_records()
140
0
{
141
    // 1. Let queue be a copy of this’s internal [[QueuedEntries]] slot.
142
0
    Vector<JS::Handle<IntersectionObserverEntry>> queue;
143
0
    for (auto& entry : m_queued_entries)
144
0
        queue.append(*entry);
145
146
    // 2. Clear this’s internal [[QueuedEntries]] slot.
147
0
    m_queued_entries.clear();
148
149
    // 3. Return queue.
150
0
    return queue;
151
0
}
152
153
Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>, Empty> IntersectionObserver::root() const
154
0
{
155
0
    if (!m_root)
156
0
        return Empty {};
157
0
    if (m_root->is_element())
158
0
        return JS::make_handle(static_cast<DOM::Element&>(*m_root));
159
0
    if (m_root->is_document())
160
0
        return JS::make_handle(static_cast<DOM::Document&>(*m_root));
161
0
    VERIFY_NOT_REACHED();
162
0
}
163
164
// https://www.w3.org/TR/intersection-observer/#intersectionobserver-intersection-root
165
Variant<JS::Handle<DOM::Element>, JS::Handle<DOM::Document>> IntersectionObserver::intersection_root() const
166
0
{
167
    // The intersection root for an IntersectionObserver is the value of its root attribute
168
    // if the attribute is non-null;
169
0
    if (m_root) {
170
0
        if (m_root->is_element())
171
0
            return JS::make_handle(static_cast<DOM::Element&>(*m_root));
172
0
        if (m_root->is_document())
173
0
            return JS::make_handle(static_cast<DOM::Document&>(*m_root));
174
0
        VERIFY_NOT_REACHED();
175
0
    }
176
177
    // otherwise, it is the top-level browsing context’s document node, referred to as the implicit root.
178
0
    return JS::make_handle(verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).page().top_level_browsing_context().active_document());
179
0
}
180
181
// https://www.w3.org/TR/intersection-observer/#intersectionobserver-root-intersection-rectangle
182
CSSPixelRect IntersectionObserver::root_intersection_rectangle() const
183
0
{
184
    // If the IntersectionObserver is an implicit root observer,
185
    //    it’s treated as if the root were the top-level browsing context’s document, according to the following rule for document.
186
0
    auto intersection_root = this->intersection_root();
187
188
0
    CSSPixelRect rect;
189
190
    // If the intersection root is a document,
191
    //    it’s the size of the document's viewport (note that this processing step can only be reached if the document is fully active).
192
0
    if (intersection_root.has<JS::Handle<DOM::Document>>()) {
193
0
        auto document = intersection_root.get<JS::Handle<DOM::Document>>();
194
195
        // Since the spec says that this is only reach if the document is fully active, that means it must have a navigable.
196
0
        VERIFY(document->navigable());
197
198
        // NOTE: This rect is the *size* of the viewport. The viewport *offset* is not relevant,
199
        //       as intersections are computed using viewport-relative element rects.
200
0
        rect = CSSPixelRect { CSSPixelPoint { 0, 0 }, document->viewport_rect().size() };
201
0
    } else {
202
0
        VERIFY(intersection_root.has<JS::Handle<DOM::Element>>());
203
0
        auto element = intersection_root.get<JS::Handle<DOM::Element>>();
204
205
        // FIXME: Otherwise, if the intersection root has a content clip,
206
        //          it’s the element’s content area.
207
208
        // Otherwise,
209
        //    it’s the result of getting the bounding box for the intersection root.
210
0
        auto bounding_client_rect = element->get_bounding_client_rect();
211
0
        rect = CSSPixelRect(bounding_client_rect->x(), bounding_client_rect->y(), bounding_client_rect->width(), bounding_client_rect->height());
212
0
    }
213
214
    // FIXME: When calculating the root intersection rectangle for a same-origin-domain target, the rectangle is then
215
    //        expanded according to the offsets in the IntersectionObserver’s [[rootMargin]] slot in a manner similar
216
    //        to CSS’s margin property, with the four values indicating the amount the top, right, bottom, and left
217
    //        edges, respectively, are offset by, with positive lengths indicating an outward offset. Percentages
218
    //        are resolved relative to the width of the undilated rectangle.
219
220
0
    return rect;
221
0
}
222
223
void IntersectionObserver::queue_entry(Badge<DOM::Document>, JS::NonnullGCPtr<IntersectionObserverEntry> entry)
224
0
{
225
0
    m_queued_entries.append(entry);
226
0
}
227
228
}