IntersectionObserver.cpp 9.8 KB

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