diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 13bbd33432..0cb778d95b 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -375,6 +376,7 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_selection); visitor.visit(m_first_base_element_with_href_in_tree_order); visitor.visit(m_parser); + visitor.visit(m_lazy_load_intersection_observer); for (auto& script : m_scripts_to_execute_when_parsing_has_finished) visitor.visit(script); @@ -2983,4 +2985,68 @@ void Document::run_the_update_intersection_observations_steps(HighResolutionTime } } +// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#start-intersection-observing-a-lazy-loading-element +void Document::start_intersection_observing_a_lazy_loading_element(Element& element) +{ + auto& realm = this->realm(); + + // 1. Let doc be element's node document. + VERIFY(&element.document() == this); + + // 2. If doc's lazy load intersection observer is null, set it to a new IntersectionObserver instance, initialized as follows: + if (!m_lazy_load_intersection_observer) { + // - The callback is these steps, with arguments entries and observer: + auto callback = JS::NativeFunction::create(realm, "", [this](JS::VM& vm) -> JS::ThrowCompletionOr { + // For each entry in entries using a method of iteration which does not trigger developer-modifiable array accessors or iteration hooks: + auto& entries = verify_cast(vm.argument(0).as_object()); + auto entries_length = MUST(MUST(entries.get(vm.names.length)).to_length(vm)); + + for (size_t i = 0; i < entries_length; ++i) { + auto property_key = JS::PropertyKey { i }; + auto& entry = verify_cast(entries.get_without_side_effects(property_key).as_object()); + + // 1. Let resumptionSteps be null. + JS::SafeFunction resumption_steps; + + // 2. If entry.isIntersecting is true, then set resumptionSteps to entry.target's lazy load resumption steps. + if (entry.is_intersecting()) { + // 5. Set entry.target's lazy load resumption steps to null. + resumption_steps = verify_cast(*entry.target()).take_lazy_load_resumption_steps({}); + } + + // 3. If resumptionSteps is null, then return. + if (!resumption_steps) + return JS::js_undefined(); + + // 4. Stop intersection-observing a lazy loading element for entry.target. + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#stop-intersection-observing-a-lazy-loading-element + // 1. Let doc be element's node document. + // NOTE: It's `this`. + + // 2. Assert: doc's lazy load intersection observer is not null. + VERIFY(m_lazy_load_intersection_observer); + + // 3. Call doc's lazy load intersection observer unobserve method with element as the argument. + m_lazy_load_intersection_observer->unobserve(entry.target()); + + // 6. Invoke resumptionSteps. + resumption_steps(); + } + + return JS::js_undefined(); + }); + + // FIXME: The options is an IntersectionObserverInit dictionary with the following dictionary members: «[ "rootMargin" → lazy load root margin ]» + // Spec Note: This allows for fetching the image during scrolling, when it does not yet — but is about to — intersect the viewport. + auto options = IntersectionObserver::IntersectionObserverInit {}; + + auto wrapped_callback = realm.heap().allocate_without_realm(callback, Bindings::host_defined_environment_settings_object(realm)); + m_lazy_load_intersection_observer = IntersectionObserver::IntersectionObserver::construct_impl(realm, wrapped_callback, options).release_value_but_fixme_should_propagate_errors(); + } + + // 3. Call doc's lazy load intersection observer's observe method with element as the argument. + VERIFY(m_lazy_load_intersection_observer); + m_lazy_load_intersection_observer->observe(element); +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 6204e134e8..6005a0dede 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -493,6 +493,8 @@ public: void run_the_update_intersection_observations_steps(HighResolutionTime::DOMHighResTimeStamp time); + void start_intersection_observing_a_lazy_loading_element(Element& element); + protected: virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -670,6 +672,10 @@ private: // https://www.w3.org/TR/intersection-observer/#document-intersectionobservertaskqueued // Each document has an IntersectionObserverTaskQueued flag which is initialized to false. bool m_intersection_observer_task_queued { false }; + + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-load-intersection-observer + // Each Document has a lazy load intersection observer, initially set to null but can be set to an IntersectionObserver instance. + JS::GCPtr m_lazy_load_intersection_observer; }; template<> diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp index 8d6e257834..92502b3b9f 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.cpp @@ -553,10 +553,19 @@ after_step_7: // FIXME: 22. Set request's priority to the current state of the element's fetchpriority attribute. - // FIXME: 24. If the will lazy load element steps given the img return true, then: - // FIXME: 1. Set the img's lazy load resumption steps to the rest of this algorithm starting with the step labeled fetch the image. - // FIXME: 2. Start intersection-observing a lazy loading element for the img element. - // FIXME: 3. Return. + // 24. If the will lazy load element steps given the img return true, then: + if (will_lazy_load()) { + // 1. Set the img's lazy load resumption steps to the rest of this algorithm starting with the step labeled fetch the image. + m_lazy_load_resumption_steps = [this, request, image_request]() { + image_request->fetch_image(realm(), request); + }; + + // 2. Start intersection-observing a lazy loading element for the img element. + document().start_intersection_observing_a_lazy_loading_element(*this); + + // 3. Return. + return; + } image_request->fetch_image(realm(), request); }); @@ -769,4 +778,25 @@ HTMLImageElement::LazyLoading HTMLImageElement::lazy_loading() const return LazyLoading::Eager; } +// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#will-lazy-load-element-steps +bool HTMLImageElement::will_lazy_load() const +{ + // 1. If scripting is disabled for element, then return false. + // Spec Note: This is an anti-tracking measure, because if a user agent supported lazy loading when scripting is + // disabled, it would still be possible for a site to track a user's approximate scroll position throughout + // a session, by strategically placing images in a page's markup such that a server can track how many + // images are requested and when. + if (is_scripting_disabled()) + return false; + + // 2. If element's lazy loading attribute is in the Lazy state, then return true. + // 3. Return false. + return lazy_loading() == LazyLoading::Lazy; +} + +JS::SafeFunction HTMLImageElement::take_lazy_load_resumption_steps(Badge) +{ + return move(m_lazy_load_resumption_steps); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h index 8b8e33c2b4..ea854589b5 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLImageElement.h @@ -72,6 +72,7 @@ public: Eager, }; [[nodiscard]] LazyLoading lazy_loading() const; + [[nodiscard]] bool will_lazy_load() const; // https://html.spec.whatwg.org/multipage/images.html#upgrade-the-pending-request-to-the-current-request void upgrade_pending_request_to_current_request(); @@ -83,6 +84,8 @@ public: virtual RefPtr current_image_bitmap(Gfx::IntSize = {}) const override; virtual void set_visible_in_viewport(bool) override; + JS::SafeFunction take_lazy_load_resumption_steps(Badge); + private: HTMLImageElement(DOM::Document&, DOM::QualifiedName); @@ -116,6 +119,10 @@ private: RefPtr m_pending_request; SourceSet m_source_set; + + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-load-resumption-steps + // Each img and iframe element has associated lazy load resumption steps, initially null. + JS::SafeFunction m_lazy_load_resumption_steps; }; }