/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() == ⌖ |
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 | | } |