IntersectionObserver.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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/IntersectionObserverPrototype.h>
  8. #include <LibWeb/Bindings/Intrinsics.h>
  9. #include <LibWeb/CSS/Parser/Parser.h>
  10. #include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
  11. #include <LibWeb/DOM/Document.h>
  12. #include <LibWeb/DOM/Element.h>
  13. #include <LibWeb/HTML/TraversableNavigable.h>
  14. #include <LibWeb/HTML/Window.h>
  15. #include <LibWeb/IntersectionObserver/IntersectionObserver.h>
  16. #include <LibWeb/Page/Page.h>
  17. namespace Web::IntersectionObserver {
  18. GC_DEFINE_ALLOCATOR(IntersectionObserver);
  19. // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver
  20. WebIDL::ExceptionOr<GC::Ref<IntersectionObserver>> IntersectionObserver::construct_impl(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> callback, IntersectionObserverInit const& options)
  21. {
  22. // https://w3c.github.io/IntersectionObserver/#initialize-a-new-intersectionobserver
  23. // 1. Let this be a new IntersectionObserver object
  24. // 2. Set this’s internal [[callback]] slot to callback.
  25. // NOTE: Steps 1 and 2 are handled by creating the IntersectionObserver at the very end of this function.
  26. // 3. Attempt to parse a margin from options.rootMargin. If a list is returned, set this’s internal [[rootMargin]] slot to that. Otherwise, throw a SyntaxError exception.
  27. auto root_margin = parse_a_margin(realm, options.root_margin);
  28. if (!root_margin.has_value()) {
  29. return WebIDL::SyntaxError::create(realm, "IntersectionObserver: Cannot parse root margin as a margin."_string);
  30. }
  31. // 4. Attempt to parse a margin from options.scrollMargin. If a list is returned, set this’s internal [[scrollMargin]] slot to that. Otherwise, throw a SyntaxError exception.
  32. auto scroll_margin = parse_a_margin(realm, options.scroll_margin);
  33. if (!scroll_margin.has_value()) {
  34. return WebIDL::SyntaxError::create(realm, "IntersectionObserver: Cannot parse scroll margin as a margin."_string);
  35. }
  36. // 5. Let thresholds be a list equal to options.threshold.
  37. Vector<double> thresholds;
  38. if (options.threshold.has<double>()) {
  39. thresholds.append(options.threshold.get<double>());
  40. } else {
  41. VERIFY(options.threshold.has<Vector<double>>());
  42. thresholds = options.threshold.get<Vector<double>>();
  43. }
  44. // 6. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception.
  45. for (auto value : thresholds) {
  46. if (value < 0.0 || value > 1.0)
  47. return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Threshold values must be between 0.0 and 1.0 inclusive"sv };
  48. }
  49. // 7. Sort thresholds in ascending order.
  50. quick_sort(thresholds, [](double left, double right) {
  51. return left < right;
  52. });
  53. // 8. If thresholds is empty, append 0 to thresholds.
  54. if (thresholds.is_empty()) {
  55. thresholds.append(0);
  56. }
  57. // 9. The thresholds attribute getter will return this sorted thresholds list.
  58. // NOTE: Handled implicitly by passing it into the constructor at the end of this function
  59. // 10. Let delay be the value of options.delay.
  60. auto delay = options.delay;
  61. // 11. If options.trackVisibility is true and delay is less than 100, set delay to 100.
  62. if (options.track_visibility && delay < 100) {
  63. delay = 100;
  64. }
  65. // 12. Set this’s internal [[delay]] slot to options.delay to delay.
  66. // 13. Set this’s internal [[trackVisibility]] slot to options.trackVisibility.
  67. // 14. Return this.
  68. return realm.create<IntersectionObserver>(realm, callback, options.root, move(root_margin.value()), move(scroll_margin.value()), move(thresholds), move(delay), move(options.track_visibility));
  69. }
  70. IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr<WebIDL::CallbackType> callback, Optional<Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>>> const& root, Vector<CSS::LengthPercentage> root_margin, Vector<CSS::LengthPercentage> scroll_margin, Vector<double>&& thresholds, double delay, bool track_visibility)
  71. : PlatformObject(realm)
  72. , m_callback(callback)
  73. , m_root_margin(root_margin)
  74. , m_scroll_margin(scroll_margin)
  75. , m_thresholds(move(thresholds))
  76. , m_delay(delay)
  77. , m_track_visibility(track_visibility)
  78. {
  79. m_root = root.has_value() ? root->visit([](auto& value) -> GC::Ptr<DOM::Node> { return *value; }) : nullptr;
  80. intersection_root().visit([this](auto& node) {
  81. m_document = node->document();
  82. });
  83. m_document->register_intersection_observer({}, *this);
  84. }
  85. IntersectionObserver::~IntersectionObserver() = default;
  86. void IntersectionObserver::finalize()
  87. {
  88. if (m_document)
  89. m_document->unregister_intersection_observer({}, *this);
  90. }
  91. void IntersectionObserver::initialize(JS::Realm& realm)
  92. {
  93. Base::initialize(realm);
  94. WEB_SET_PROTOTYPE_FOR_INTERFACE(IntersectionObserver);
  95. }
  96. void IntersectionObserver::visit_edges(JS::Cell::Visitor& visitor)
  97. {
  98. Base::visit_edges(visitor);
  99. visitor.visit(m_root);
  100. visitor.visit(m_callback);
  101. visitor.visit(m_queued_entries);
  102. visitor.visit(m_observation_targets);
  103. }
  104. // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe
  105. void IntersectionObserver::observe(DOM::Element& target)
  106. {
  107. // Run the observe a target Element algorithm, providing this and target.
  108. // https://www.w3.org/TR/intersection-observer/#observe-a-target-element
  109. // 1. If target is in observer’s internal [[ObservationTargets]] slot, return.
  110. if (m_observation_targets.contains_slow(GC::Ref { target }))
  111. return;
  112. // 2. Let intersectionObserverRegistration be an IntersectionObserverRegistration record with an observer
  113. // property set to observer, a previousThresholdIndex property set to -1, and a previousIsIntersecting
  114. // property set to false.
  115. auto intersection_observer_registration = IntersectionObserverRegistration {
  116. .observer = *this,
  117. .previous_threshold_index = OptionalNone {},
  118. .previous_is_intersecting = false,
  119. };
  120. // 3. Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot.
  121. target.register_intersection_observer({}, move(intersection_observer_registration));
  122. // 4. Add target to observer’s internal [[ObservationTargets]] slot.
  123. m_observation_targets.append(target);
  124. }
  125. // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve
  126. void IntersectionObserver::unobserve(DOM::Element& target)
  127. {
  128. // Run the unobserve a target Element algorithm, providing this and target.
  129. // https://www.w3.org/TR/intersection-observer/#unobserve-a-target-element
  130. // 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’s internal [[RegisteredIntersectionObservers]] slot, if present.
  131. target.unregister_intersection_observer({}, *this);
  132. // 2. Remove target from this’s internal [[ObservationTargets]] slot, if present
  133. m_observation_targets.remove_first_matching([&target](GC::Ref<DOM::Element> const& entry) {
  134. return entry.ptr() == &target;
  135. });
  136. }
  137. // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect
  138. void IntersectionObserver::disconnect()
  139. {
  140. // For each target in this’s internal [[ObservationTargets]] slot:
  141. // 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’s internal
  142. // [[RegisteredIntersectionObservers]] slot.
  143. // 2. Remove target from this’s internal [[ObservationTargets]] slot.
  144. for (auto& target : m_observation_targets) {
  145. target->unregister_intersection_observer({}, *this);
  146. }
  147. m_observation_targets.clear();
  148. }
  149. // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-takerecords
  150. Vector<GC::Root<IntersectionObserverEntry>> IntersectionObserver::take_records()
  151. {
  152. // 1. Let queue be a copy of this’s internal [[QueuedEntries]] slot.
  153. Vector<GC::Root<IntersectionObserverEntry>> queue;
  154. for (auto& entry : m_queued_entries)
  155. queue.append(*entry);
  156. // 2. Clear this’s internal [[QueuedEntries]] slot.
  157. m_queued_entries.clear();
  158. // 3. Return queue.
  159. return queue;
  160. }
  161. Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>, Empty> IntersectionObserver::root() const
  162. {
  163. if (!m_root)
  164. return Empty {};
  165. if (m_root->is_element())
  166. return GC::make_root(static_cast<DOM::Element&>(*m_root));
  167. if (m_root->is_document())
  168. return GC::make_root(static_cast<DOM::Document&>(*m_root));
  169. VERIFY_NOT_REACHED();
  170. }
  171. // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin
  172. String IntersectionObserver::root_margin() const
  173. {
  174. // On getting, return the result of serializing the elements of [[rootMargin]] space-separated, where pixel
  175. // lengths serialize as the numeric value followed by "px", and percentages serialize as the numeric value
  176. // followed by "%". Note that this is not guaranteed to be identical to the options.rootMargin passed to the
  177. // IntersectionObserver constructor. If no rootMargin was passed to the IntersectionObserver
  178. // constructor, the value of this attribute is "0px 0px 0px 0px".
  179. StringBuilder builder;
  180. builder.append(m_root_margin[0].to_string());
  181. builder.append(' ');
  182. builder.append(m_root_margin[1].to_string());
  183. builder.append(' ');
  184. builder.append(m_root_margin[2].to_string());
  185. builder.append(' ');
  186. builder.append(m_root_margin[3].to_string());
  187. return builder.to_string().value();
  188. }
  189. // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin
  190. String IntersectionObserver::scroll_margin() const
  191. {
  192. // On getting, return the result of serializing the elements of [[scrollMargin]] space-separated, where pixel
  193. // lengths serialize as the numeric value followed by "px", and percentages serialize as the numeric value
  194. // followed by "%". Note that this is not guaranteed to be identical to the options.scrollMargin passed to the
  195. // IntersectionObserver constructor. If no scrollMargin was passed to the IntersectionObserver
  196. // constructor, the value of this attribute is "0px 0px 0px 0px".
  197. StringBuilder builder;
  198. builder.append(m_scroll_margin[0].to_string());
  199. builder.append(' ');
  200. builder.append(m_scroll_margin[1].to_string());
  201. builder.append(' ');
  202. builder.append(m_scroll_margin[2].to_string());
  203. builder.append(' ');
  204. builder.append(m_scroll_margin[3].to_string());
  205. return builder.to_string().value();
  206. }
  207. // https://www.w3.org/TR/intersection-observer/#intersectionobserver-intersection-root
  208. Variant<GC::Root<DOM::Element>, GC::Root<DOM::Document>> IntersectionObserver::intersection_root() const
  209. {
  210. // The intersection root for an IntersectionObserver is the value of its root attribute
  211. // if the attribute is non-null;
  212. if (m_root) {
  213. if (m_root->is_element())
  214. return GC::make_root(static_cast<DOM::Element&>(*m_root));
  215. if (m_root->is_document())
  216. return GC::make_root(static_cast<DOM::Document&>(*m_root));
  217. VERIFY_NOT_REACHED();
  218. }
  219. // otherwise, it is the top-level browsing context’s document node, referred to as the implicit root.
  220. return GC::make_root(verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).page().top_level_browsing_context().active_document());
  221. }
  222. // https://www.w3.org/TR/intersection-observer/#intersectionobserver-root-intersection-rectangle
  223. CSSPixelRect IntersectionObserver::root_intersection_rectangle() const
  224. {
  225. // If the IntersectionObserver is an implicit root observer,
  226. // it’s treated as if the root were the top-level browsing context’s document, according to the following rule for document.
  227. auto intersection_root = this->intersection_root();
  228. CSSPixelRect rect;
  229. // If the intersection root is a document,
  230. // it’s the size of the document's viewport (note that this processing step can only be reached if the document is fully active).
  231. if (intersection_root.has<GC::Root<DOM::Document>>()) {
  232. auto document = intersection_root.get<GC::Root<DOM::Document>>();
  233. // Since the spec says that this is only reach if the document is fully active, that means it must have a navigable.
  234. VERIFY(document->navigable());
  235. // NOTE: This rect is the *size* of the viewport. The viewport *offset* is not relevant,
  236. // as intersections are computed using viewport-relative element rects.
  237. rect = CSSPixelRect { CSSPixelPoint { 0, 0 }, document->viewport_rect().size() };
  238. } else {
  239. VERIFY(intersection_root.has<GC::Root<DOM::Element>>());
  240. auto element = intersection_root.get<GC::Root<DOM::Element>>();
  241. // FIXME: Otherwise, if the intersection root has a content clip,
  242. // it’s the element’s content area.
  243. // Otherwise,
  244. // it’s the result of getting the bounding box for the intersection root.
  245. auto bounding_client_rect = element->get_bounding_client_rect();
  246. rect = CSSPixelRect(bounding_client_rect->x(), bounding_client_rect->y(), bounding_client_rect->width(), bounding_client_rect->height());
  247. }
  248. // When calculating the root intersection rectangle for a same-origin-domain target, the rectangle is then
  249. // expanded according to the offsets in the IntersectionObserver’s [[rootMargin]] slot in a manner similar
  250. // to CSS’s margin property, with the four values indicating the amount the top, right, bottom, and left
  251. // edges, respectively, are offset by, with positive lengths indicating an outward offset. Percentages
  252. // are resolved relative to the width of the undilated rectangle.
  253. DOM::Document* document = { nullptr };
  254. if (intersection_root.has<GC::Root<DOM::Document>>()) {
  255. document = intersection_root.get<GC::Root<DOM::Document>>().cell();
  256. } else {
  257. document = &intersection_root.get<GC::Root<DOM::Element>>().cell()->document();
  258. }
  259. if (m_document.has_value() && document->origin().is_same_origin(m_document->origin())) {
  260. auto layout_node = intersection_root.visit([&](auto& elem) { return static_cast<GC::Root<DOM::Node>>(*elem)->layout_node(); });
  261. rect.inflate(
  262. m_root_margin[0].to_px(*layout_node, rect.height()),
  263. m_root_margin[1].to_px(*layout_node, rect.width()),
  264. m_root_margin[2].to_px(*layout_node, rect.height()),
  265. m_root_margin[3].to_px(*layout_node, rect.width()));
  266. }
  267. return rect;
  268. }
  269. void IntersectionObserver::queue_entry(Badge<DOM::Document>, GC::Ref<IntersectionObserverEntry> entry)
  270. {
  271. m_queued_entries.append(entry);
  272. }
  273. // https://w3c.github.io/IntersectionObserver/#parse-a-margin
  274. Optional<Vector<CSS::LengthPercentage>> IntersectionObserver::parse_a_margin(JS::Realm& realm, String margin_string)
  275. {
  276. // 1. Parse a list of component values marginString, storing the result as tokens.
  277. auto tokens = CSS::Parser::Parser::create(CSS::Parser::ParsingContext { realm }, margin_string).parse_as_list_of_component_values();
  278. // 2. Remove all whitespace tokens from tokens.
  279. tokens.remove_all_matching([](auto componentValue) { return componentValue.is(CSS::Parser::Token::Type::Whitespace); });
  280. // 3. If the length of tokens is greater than 4, return failure.
  281. if (tokens.size() > 4) {
  282. return {};
  283. }
  284. // 4. If there are zero elements in tokens, set tokens to ["0px"].
  285. if (tokens.size() == 0) {
  286. tokens.append(CSS::Parser::Token::create_dimension(0, "px"_fly_string));
  287. }
  288. // 5. Replace each token in tokens:
  289. // NOTE: In the spec, tokens miraculously changes type from a list of component values
  290. // to a list of pixel lengths or percentages.
  291. Vector<CSS::LengthPercentage> tokens_length_percentage;
  292. for (auto const& token : tokens) {
  293. // If token is an absolute length dimension token, replace it with a an equivalent pixel length.
  294. if (token.is(CSS::Parser::Token::Type::Dimension)) {
  295. auto length = CSS::Length(token.token().dimension_value(), CSS::Length::unit_from_name(token.token().dimension_unit()).value());
  296. if (length.is_absolute()) {
  297. length.absolute_length_to_px();
  298. tokens_length_percentage.append(length);
  299. continue;
  300. }
  301. }
  302. // If token is a <percentage> token, replace it with an equivalent percentage.
  303. if (token.is(CSS::Parser::Token::Type::Percentage)) {
  304. tokens_length_percentage.append(CSS::Percentage(token.token().percentage()));
  305. continue;
  306. }
  307. // Otherwise, return failure.
  308. return {};
  309. }
  310. // 6.
  311. switch (tokens_length_percentage.size()) {
  312. // If there is one element in tokens, append three duplicates of that element to tokens.
  313. case 1:
  314. tokens_length_percentage.append(tokens_length_percentage.first());
  315. tokens_length_percentage.append(tokens_length_percentage.first());
  316. tokens_length_percentage.append(tokens_length_percentage.first());
  317. break;
  318. // Otherwise, if there are two elements are tokens, append a duplicate of each element to tokens.
  319. case 2:
  320. tokens_length_percentage.append(tokens_length_percentage.at(0));
  321. tokens_length_percentage.append(tokens_length_percentage.at(1));
  322. break;
  323. // Otherwise, if there are three elements in tokens, append a duplicate of the second element to tokens.
  324. case 3:
  325. tokens_length_percentage.append(tokens_length_percentage.at(1));
  326. break;
  327. }
  328. // 7. Return tokens.
  329. return tokens_length_percentage;
  330. }
  331. }