diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 33a2346052..1aec0f3d39 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -39,6 +39,7 @@ static bool is_platform_object(Type const& type) "FormData"sv, "ImageData"sv, "Instance"sv, + "IntersectionObserverEntry"sv, "Memory"sv, "Module"sv, "MutationRecord"sv, diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 0ac2f3cb10..df7b218b49 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -396,6 +396,7 @@ set(SOURCES Infra/JSON.cpp Infra/Strings.cpp IntersectionObserver/IntersectionObserver.cpp + IntersectionObserver/IntersectionObserverEntry.cpp Layout/AudioBox.cpp Layout/AvailableSpace.cpp Layout/BlockContainer.cpp diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 5048c880c3..13bbd33432 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,7 @@ #include #include #include +#include #include #include #include @@ -90,6 +92,7 @@ #include #include #include +#include #include #include @@ -2763,4 +2766,221 @@ HTML::ListOfAvailableImages const& Document::list_of_available_images() const return *m_list_of_available_images; } +void Document::register_intersection_observer(Badge, IntersectionObserver::IntersectionObserver& observer) +{ + auto result = m_intersection_observers.set(observer); + VERIFY(result == AK::HashSetResult::InsertedNewEntry); +} + +void Document::unregister_intersection_observer(Badge, IntersectionObserver::IntersectionObserver& observer) +{ + bool was_removed = m_intersection_observers.remove(observer); + VERIFY(was_removed); +} + +// https://www.w3.org/TR/intersection-observer/#queue-an-intersection-observer-task +void Document::queue_intersection_observer_task() +{ + // 1. If document’s IntersectionObserverTaskQueued flag is set to true, return. + if (m_intersection_observer_task_queued) + return; + + // 2. Set document’s IntersectionObserverTaskQueued flag to true. + m_intersection_observer_task_queued = true; + + // 3. Queue a task on the IntersectionObserver task source associated with the document's event loop to notify intersection observers. + HTML::queue_global_task(HTML::Task::Source::IntersectionObserver, window(), [this]() { + auto& realm = this->realm(); + + // https://www.w3.org/TR/intersection-observer/#notify-intersection-observers + // 1. Set document’s IntersectionObserverTaskQueued flag to false. + m_intersection_observer_task_queued = false; + + // 2. Let notify list be a list of all IntersectionObservers whose root is in the DOM tree of document. + Vector> notify_list; + notify_list.try_ensure_capacity(m_intersection_observers.size()).release_value_but_fixme_should_propagate_errors(); + for (auto& observer : m_intersection_observers) { + notify_list.append(JS::make_handle(observer)); + } + + // 3. For each IntersectionObserver object observer in notify list, run these steps: + for (auto& observer : notify_list) { + // 2. Let queue be a copy of observer’s internal [[QueuedEntries]] slot. + // 3. Clear observer’s internal [[QueuedEntries]] slot. + auto queue = observer->take_records(); + + // 1. If observer’s internal [[QueuedEntries]] slot is empty, continue. + if (queue.is_empty()) + continue; + + auto wrapped_queue = MUST(JS::Array::create(realm, 0)); + for (size_t i = 0; i < queue.size(); ++i) { + auto& record = queue.at(i); + auto property_index = JS::PropertyKey { i }; + MUST(wrapped_queue->create_data_property(property_index, record.ptr())); + } + + // 4. Let callback be the value of observer’s internal [[callback]] slot. + auto& callback = observer->callback(); + + // 5. Invoke callback with queue as the first argument, observer as the second argument, and observer as the callback this value. If this throws an exception, report the exception. + auto completion = WebIDL::invoke_callback(callback, observer.ptr(), wrapped_queue, observer.ptr()); + if (completion.is_abrupt()) + HTML::report_exception(completion, realm); + } + }); +} + +// https://www.w3.org/TR/intersection-observer/#queue-an-intersectionobserverentry +void Document::queue_an_intersection_observer_entry(IntersectionObserver::IntersectionObserver& observer, HighResolutionTime::DOMHighResTimeStamp time, JS::NonnullGCPtr root_bounds, JS::NonnullGCPtr bounding_client_rect, JS::NonnullGCPtr intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr target) +{ + auto& realm = this->realm(); + + // 1. Construct an IntersectionObserverEntry, passing in time, rootBounds, boundingClientRect, intersectionRect, isIntersecting, and target. + auto entry = realm.heap().allocate(realm, realm, time, root_bounds, bounding_client_rect, intersection_rect, is_intersecting, intersection_ratio, target).release_allocated_value_but_fixme_should_propagate_errors(); + + // 2. Append it to observer’s internal [[QueuedEntries]] slot. + observer.queue_entry({}, entry); + + // 3. Queue an intersection observer task for document. + queue_intersection_observer_task(); +} + +// https://www.w3.org/TR/intersection-observer/#compute-the-intersection +static JS::NonnullGCPtr compute_intersection(JS::NonnullGCPtr target, IntersectionObserver::IntersectionObserver const& observer) +{ + // 1. Let intersectionRect be the result of getting the bounding box for target. + auto intersection_rect = target->get_bounding_client_rect(); + + // FIXME: 2. Let container be the containing block of target. + // FIXME: 3. While container is not root: + // FIXME: 1. If container is the document of a nested browsing context, update intersectionRect by clipping to + // the viewport of the document, and update container to be the browsing context container of container. + // FIXME: 2. Map intersectionRect to the coordinate space of container. + // FIXME: 3. If container has a content clip or a css clip-path property, update intersectionRect by applying + // container’s clip. + // FIXME: 4. If container is the root element of a browsing context, update container to be the browsing context’s + // document; otherwise, update container to be the containing block of container. + // FIXME: 4. Map intersectionRect to the coordinate space of root. + + // 5. Update intersectionRect by intersecting it with the root intersection rectangle. + // FIXME: Pass in target so we can properly apply rootMargin. + auto root_intersection_rectangle = observer.root_intersection_rectangle(); + CSSPixelRect intersection_rect_as_pixel_rect(intersection_rect->x(), intersection_rect->y(), intersection_rect->width(), intersection_rect->height()); + intersection_rect_as_pixel_rect.intersect(root_intersection_rectangle); + intersection_rect->set_x(static_cast(intersection_rect_as_pixel_rect.x())); + intersection_rect->set_y(static_cast(intersection_rect_as_pixel_rect.y())); + intersection_rect->set_width(static_cast(intersection_rect_as_pixel_rect.width())); + intersection_rect->set_height(static_cast(intersection_rect_as_pixel_rect.height())); + + // FIXME: 6. Map intersectionRect to the coordinate space of the viewport of the document containing target. + + // 7. Return intersectionRect. + return intersection_rect; +} + +// https://www.w3.org/TR/intersection-observer/#run-the-update-intersection-observations-steps +void Document::run_the_update_intersection_observations_steps(HighResolutionTime::DOMHighResTimeStamp time) +{ + auto& realm = this->realm(); + + // 1. Let observer list be a list of all IntersectionObservers whose root is in the DOM tree of document. + // For the top-level browsing context, this includes implicit root observers. + // 2. For each observer in observer list: + for (auto& observer : m_intersection_observers) { + // 1. Let rootBounds be observer’s root intersection rectangle. + auto root_bounds = observer->root_intersection_rectangle(); + + // 2. For each target in observer’s internal [[ObservationTargets]] slot, processed in the same order that + // observe() was called on each target: + for (auto& target : observer->observation_targets()) { + // 1. Let: + // thresholdIndex be 0. + size_t threshold_index = 0; + + // isIntersecting be false. + bool is_intersecting = false; + + // targetRect be a DOMRectReadOnly with x, y, width, and height set to 0. + auto target_rect = Geometry::DOMRectReadOnly::construct_impl(realm, 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors(); + + // intersectionRect be a DOMRectReadOnly with x, y, width, and height set to 0. + auto intersection_rect = Geometry::DOMRectReadOnly::construct_impl(realm, 0, 0, 0, 0).release_value_but_fixme_should_propagate_errors(); + + // SPEC ISSUE: It doesn't pass in intersection ratio to "queue an IntersectionObserverEntry" despite needing it. + // This is default 0, as isIntersecting is default false, see step 9. + double intersection_ratio = 0.0; + + // 2. If the intersection root is not the implicit root, and target is not in the same document as the intersection root, skip to step 11. + // 3. If the intersection root is an Element, and target is not a descendant of the intersection root in the containing block chain, skip to step 11. + // FIXME: Actually use the containing block chain. + auto intersection_root = observer->intersection_root(); + auto intersection_root_document = intersection_root.visit([](auto& node) -> JS::NonnullGCPtr { + return node->document(); + }); + if (!(observer->root().has() && &target->document() == intersection_root_document.ptr()) + || !(intersection_root.has>() && !target->is_descendant_of(*intersection_root.get>()))) { + // 4. Set targetRect to the DOMRectReadOnly obtained by getting the bounding box for target. + target_rect = target->get_bounding_client_rect(); + + // 5. Let intersectionRect be the result of running the compute the intersection algorithm on target and + // observer’s intersection root. + intersection_rect = compute_intersection(target, observer); + + // 6. Let targetArea be targetRect’s area. + auto target_area = target_rect->width() * target_rect->height(); + + // 7. Let intersectionArea be intersectionRect’s area. + auto intersection_area = intersection_rect->width() * intersection_rect->height(); + + // 8. Let isIntersecting be true if targetRect and rootBounds intersect or are edge-adjacent, even if the + // intersection has zero area (because rootBounds or targetRect have zero area). + CSSPixelRect target_rect_as_pixel_rect(target_rect->x(), target_rect->y(), target_rect->width(), target_rect->height()); + is_intersecting = target_rect_as_pixel_rect.intersects(root_bounds); + + // 9. If targetArea is non-zero, let intersectionRatio be intersectionArea divided by targetArea. + // Otherwise, let intersectionRatio be 1 if isIntersecting is true, or 0 if isIntersecting is false. + if (target_area != 0.0) + intersection_ratio = intersection_area / target_area; + else + intersection_ratio = is_intersecting ? 1.0 : 0.0; + + // 10. Set thresholdIndex to the index of the first entry in observer.thresholds whose value is greater + // than intersectionRatio, or the length of observer.thresholds if intersectionRatio is greater than + // or equal to the last entry in observer.thresholds. + threshold_index = observer->thresholds().find_first_index_if([&intersection_ratio](double threshold_value) { + return threshold_value > intersection_ratio; + }) + .value_or(observer->thresholds().size()); + } + + // 11. Let intersectionObserverRegistration be the IntersectionObserverRegistration record in target’s + // internal [[RegisteredIntersectionObservers]] slot whose observer property is equal to observer. + auto& intersection_observer_registration = target->get_intersection_observer_registration({}, observer); + + // 12. Let previousThresholdIndex be the intersectionObserverRegistration’s previousThresholdIndex property. + auto previous_threshold_index = intersection_observer_registration.previous_threshold_index; + + // 13. Let previousIsIntersecting be the intersectionObserverRegistration’s previousIsIntersecting property. + auto previous_is_intersecting = intersection_observer_registration.previous_is_intersecting; + + // 14. If thresholdIndex does not equal previousThresholdIndex or if isIntersecting does not equal + // previousIsIntersecting, queue an IntersectionObserverEntry, passing in observer, time, + // rootBounds, targetRect, intersectionRect, isIntersecting, and target. + if (threshold_index != previous_threshold_index || is_intersecting != previous_is_intersecting) { + auto root_bounds_as_dom_rect = Geometry::DOMRectReadOnly::construct_impl(realm, static_cast(root_bounds.x()), static_cast(root_bounds.y()), static_cast(root_bounds.width()), static_cast(root_bounds.height())).release_value_but_fixme_should_propagate_errors(); + + // SPEC ISSUE: It doesn't pass in intersectionRatio, but it's required. + queue_an_intersection_observer_entry(observer, time, root_bounds_as_dom_rect, target_rect, intersection_rect, is_intersecting, intersection_ratio, target); + } + + // 15. Assign thresholdIndex to intersectionObserverRegistration’s previousThresholdIndex property. + intersection_observer_registration.previous_threshold_index = threshold_index; + + // 16. Assign isIntersecting to intersectionObserverRegistration’s previousIsIntersecting property. + intersection_observer_registration.previous_is_intersecting = is_intersecting; + } + } +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index b5e7139716..6204e134e8 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -488,6 +488,11 @@ public: HTML::ListOfAvailableImages& list_of_available_images(); HTML::ListOfAvailableImages const& list_of_available_images() const; + void register_intersection_observer(Badge, IntersectionObserver::IntersectionObserver&); + void unregister_intersection_observer(Badge, IntersectionObserver::IntersectionObserver&); + + void run_the_update_intersection_observations_steps(HighResolutionTime::DOMHighResTimeStamp time); + protected: virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -504,6 +509,9 @@ private: WebIDL::ExceptionOr run_the_document_write_steps(DeprecatedString); + void queue_intersection_observer_task(); + void queue_an_intersection_observer_entry(IntersectionObserver::IntersectionObserver&, HighResolutionTime::DOMHighResTimeStamp time, JS::NonnullGCPtr root_bounds, JS::NonnullGCPtr bounding_client_rect, JS::NonnullGCPtr intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr target); + OwnPtr m_style_computer; JS::GCPtr m_style_sheets; JS::GCPtr m_hovered_node; @@ -655,6 +663,13 @@ private: // https://html.spec.whatwg.org/multipage/images.html#list-of-available-images OwnPtr m_list_of_available_images; + + // NOTE: Not in the spec per say, but Document must be able to access all IntersectionObservers whose root is in the document. + OrderedHashTable> m_intersection_observers; + + // 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 }; }; template<> diff --git a/Userland/Libraries/LibWeb/DOM/Element.cpp b/Userland/Libraries/LibWeb/DOM/Element.cpp index 34bca0ed5a..bf742ac6f3 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.cpp +++ b/Userland/Libraries/LibWeb/DOM/Element.cpp @@ -1784,4 +1784,25 @@ bool Element::id_reference_exists(DeprecatedString const& id_reference) const return document().get_element_by_id(id_reference); } +void Element::register_intersection_observer(Badge, IntersectionObserver::IntersectionObserverRegistration registration) +{ + m_registered_intersection_observers.append(move(registration)); +} + +void Element::unregister_intersection_observer(Badge, JS::NonnullGCPtr observer) +{ + m_registered_intersection_observers.remove_first_matching([&observer](IntersectionObserver::IntersectionObserverRegistration const& entry) { + return entry.observer == observer; + }); +} + +IntersectionObserver::IntersectionObserverRegistration& Element::get_intersection_observer_registration(Badge, IntersectionObserver::IntersectionObserver const& observer) +{ + auto registration_iterator = m_registered_intersection_observers.find_if([&observer](IntersectionObserver::IntersectionObserverRegistration const& entry) { + return entry.observer.ptr() == &observer; + }); + VERIFY(!registration_iterator.is_end()); + return *registration_iterator; +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Element.h b/Userland/Libraries/LibWeb/DOM/Element.h index 70c1e6e20f..918bca0630 100644 --- a/Userland/Libraries/LibWeb/DOM/Element.h +++ b/Userland/Libraries/LibWeb/DOM/Element.h @@ -22,6 +22,7 @@ #include #include #include +#include #include namespace Web::DOM { @@ -309,6 +310,10 @@ public: void scroll(HTML::ScrollToOptions const&); void scroll(double x, double y); + void register_intersection_observer(Badge, IntersectionObserver::IntersectionObserverRegistration); + void unregister_intersection_observer(Badge, JS::NonnullGCPtr); + IntersectionObserver::IntersectionObserverRegistration& get_intersection_observer_registration(Badge, IntersectionObserver::IntersectionObserver const&); + protected: Element(Document&, DOM::QualifiedName); virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; @@ -358,6 +363,10 @@ private: // https://dom.spec.whatwg.org/#concept-element-is-value Optional m_is_value; + + // https://www.w3.org/TR/intersection-observer/#dom-element-registeredintersectionobservers-slot + // Element objects have an internal [[RegisteredIntersectionObservers]] slot, which is initialized to an empty list. + Vector m_registered_intersection_observers; }; template<> diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index fac541e70c..e4069095ad 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -451,6 +451,8 @@ class Performance; namespace Web::IntersectionObserver { class IntersectionObserver; +class IntersectionObserverEntry; +struct IntersectionObserverRegistration; } namespace Web::Layout { diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index 35f41979d1..50b2450b03 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -160,6 +160,10 @@ void EventLoop::process() // FIXME: 4. Unnecessary rendering: Remove from docs all Document objects which meet both of the following conditions: // - The user agent believes that updating the rendering of the Document's browsing context would have no visible effect, and // - The Document's map of animation frame callbacks is empty. + // https://www.w3.org/TR/intersection-observer/#pending-initial-observation + // In the HTML Event Loops Processing Model, under the "Update the rendering" step, the "Unnecessary rendering" step should be + // modified to add an additional requirement for skipping the rendering update: + // - The document does not have pending initial IntersectionObserver targets. // FIXME: 5. Remove from docs all Document objects for which the user agent believes that it's preferable to skip updating the rendering for other reasons. @@ -192,7 +196,10 @@ void EventLoop::process() run_animation_frame_callbacks(document, now); }); - // FIXME: 14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER] + // 14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER] + for_each_fully_active_document_in_docs([&](DOM::Document& document) { + document.run_the_update_intersection_observations_steps(now); + }); // FIXME: 15. Invoke the mark paint timing algorithm for each Document object in docs. diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h index 782974f7f4..ba3e0f13f2 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h @@ -38,6 +38,9 @@ public: // https://w3c.github.io/FileAPI/#fileReadingTaskSource FileReading, + // https://www.w3.org/TR/intersection-observer/#intersectionobserver-task-source + IntersectionObserver, + // Some elements, such as the HTMLMediaElement, must have a unique task source per instance. // Keep this field last, to serve as the base value of all unique task sources. UniqueTaskSourceStart, diff --git a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp index 75bb8710dc..ca85ccf04b 100644 --- a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp +++ b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp @@ -4,28 +4,61 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include #include #include namespace Web::IntersectionObserver { // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver -WebIDL::ExceptionOr> IntersectionObserver::construct_impl(JS::Realm& realm, WebIDL::CallbackType* callback, IntersectionObserverInit const& options) +WebIDL::ExceptionOr> IntersectionObserver::construct_impl(JS::Realm& realm, JS::GCPtr callback, IntersectionObserverInit const& options) { - // FIXME: Implement - (void)callback; - (void)options; + // 4. Let thresholds be a list equal to options.threshold. + Vector thresholds; + if (options.threshold.has()) { + thresholds.append(options.threshold.get()); + } else { + VERIFY(options.threshold.has>()); + thresholds = options.threshold.get>(); + } - return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm)); + // 5. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception. + for (auto value : thresholds) { + if (value < 0.0 || value > 1.0) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Threshold values must be between 0.0 and 1.0 inclusive"sv }; + } + + // 6. Sort thresholds in ascending order. + quick_sort(thresholds, [](double left, double right) { + return left < right; + }); + + // 1. Let this be a new IntersectionObserver object + // 2. Set this’s internal [[callback]] slot to callback. + // 8. The thresholds attribute getter will return this sorted thresholds list. + // 9. Return this. + return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, callback, options.root, move(thresholds))); } -IntersectionObserver::IntersectionObserver(JS::Realm& realm) +IntersectionObserver::IntersectionObserver(JS::Realm& realm, JS::GCPtr callback, Optional, JS::Handle>> const& root, Vector&& thresholds) : PlatformObject(realm) + , m_callback(callback) + , m_root(root) + , m_thresholds(move(thresholds)) { + intersection_root().visit([this](auto& node) { + node->document().register_intersection_observer({}, *this); + }); } -IntersectionObserver::~IntersectionObserver() = default; +IntersectionObserver::~IntersectionObserver() +{ + intersection_root().visit([this](auto& node) { + node->document().unregister_intersection_observer({}, *this); + }); +} JS::ThrowCompletionOr IntersectionObserver::initialize(JS::Realm& realm) { @@ -35,24 +68,139 @@ JS::ThrowCompletionOr IntersectionObserver::initialize(JS::Realm& realm) return {}; } +void IntersectionObserver::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_callback); + for (auto& entry : m_queued_entries) + visitor.visit(entry); + for (auto& target : m_observation_targets) + visitor.visit(target); +} + // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-observe void IntersectionObserver::observe(DOM::Element& target) { - // FIXME: Implement - (void)target; + // Run the observe a target Element algorithm, providing this and target. + // https://www.w3.org/TR/intersection-observer/#observe-a-target-element + // 1. If target is in observer’s internal [[ObservationTargets]] slot, return. + if (m_observation_targets.contains_slow(JS::NonnullGCPtr { target })) + return; + + // 2. Let intersectionObserverRegistration be an IntersectionObserverRegistration record with an observer + // property set to observer, a previousThresholdIndex property set to -1, and a previousIsIntersecting + // property set to false. + auto intersection_observer_registration = IntersectionObserverRegistration { + .observer = JS::make_handle(*this), + .previous_threshold_index = OptionalNone {}, + .previous_is_intersecting = false, + }; + + // 3. Append intersectionObserverRegistration to target’s internal [[RegisteredIntersectionObservers]] slot. + target.register_intersection_observer({}, move(intersection_observer_registration)); + + // 4. Add target to observer’s internal [[ObservationTargets]] slot. + m_observation_targets.append(target); } // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-unobserve void IntersectionObserver::unobserve(DOM::Element& target) { - // FIXME: Implement - (void)target; + // Run the unobserve a target Element algorithm, providing this and target. + // https://www.w3.org/TR/intersection-observer/#unobserve-a-target-element + // 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’s internal [[RegisteredIntersectionObservers]] slot, if present. + target.unregister_intersection_observer({}, *this); + + // 2. Remove target from this’s internal [[ObservationTargets]] slot, if present + m_observation_targets.remove_first_matching([&target](JS::NonnullGCPtr const& entry) { + return entry.ptr() == ⌖ + }); } // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-disconnect void IntersectionObserver::disconnect() { - // FIXME: Implement + // For each target in this’s internal [[ObservationTargets]] slot: + // 1. Remove the IntersectionObserverRegistration record whose observer property is equal to this from target’s internal + // [[RegisteredIntersectionObservers]] slot. + // 2. Remove target from this’s internal [[ObservationTargets]] slot. + for (auto& target : m_observation_targets) { + target->unregister_intersection_observer({}, *this); + } + m_observation_targets.clear(); +} + +// https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-takerecords +Vector> IntersectionObserver::take_records() +{ + // 1. Let queue be a copy of this’s internal [[QueuedEntries]] slot. + Vector> queue; + for (auto& entry : m_queued_entries) + queue.append(*entry); + + // 2. Clear this’s internal [[QueuedEntries]] slot. + m_queued_entries.clear(); + + // 3. Return queue. + return queue; +} + +Variant, JS::Handle, Empty> IntersectionObserver::root() const +{ + if (!m_root.has_value()) + return Empty {}; + return m_root.value(); +} + +Variant, JS::Handle> IntersectionObserver::intersection_root() const +{ + if (!m_root.has_value()) + return JS::make_handle(global_object().browsing_context()->top_level_browsing_context().active_document()); + return m_root.value(); +} + +// https://www.w3.org/TR/intersection-observer/#intersectionobserver-root-intersection-rectangle +CSSPixelRect IntersectionObserver::root_intersection_rectangle() const +{ + // If the IntersectionObserver is an implicit root observer, + // it’s treated as if the root were the top-level browsing context’s document, according to the following rule for document. + auto intersection_root = this->intersection_root(); + + CSSPixelRect rect; + + // If the intersection root is a document, + // it’s the size of the document's viewport (note that this processing step can only be reached if the document is fully active). + if (intersection_root.has>()) { + auto document = intersection_root.get>(); + + // Since the spec says that this is only reach if the document is fully active, that means it must have a browsing context. + VERIFY(document->browsing_context()); + rect = document->browsing_context()->viewport_rect(); + } else { + VERIFY(intersection_root.has>()); + auto element = intersection_root.get>(); + + // FIXME: Otherwise, if the intersection root has a content clip, + // it’s the element’s content area. + + // Otherwise, + // it’s the result of getting the bounding box for the intersection root. + auto bounding_client_rect = element->get_bounding_client_rect(); + rect = CSSPixelRect(bounding_client_rect->x(), bounding_client_rect->y(), bounding_client_rect->width(), bounding_client_rect->height()); + } + + // FIXME: When calculating the root intersection rectangle for a same-origin-domain target, the rectangle is then + // expanded according to the offsets in the IntersectionObserver’s [[rootMargin]] slot in a manner similar + // to CSS’s margin property, with the four values indicating the amount the top, right, bottom, and left + // edges, respectively, are offset by, with positive lengths indicating an outward offset. Percentages + // are resolved relative to the width of the undilated rectangle. + + return rect; +} + +void IntersectionObserver::queue_entry(Badge, JS::NonnullGCPtr entry) +{ + m_queued_entries.append(entry); } } diff --git a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h index 1532548870..c4cac3b5de 100644 --- a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h +++ b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h @@ -8,32 +8,79 @@ #include #include +#include +#include namespace Web::IntersectionObserver { struct IntersectionObserverInit { Optional, JS::Handle>> root; - DeprecatedString root_margin { "0px"sv }; + String root_margin { "0px"_short_string }; Variant> threshold { 0 }; }; +// https://www.w3.org/TR/intersection-observer/#intersectionobserverregistration +struct IntersectionObserverRegistration { + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverregistration-observer + // [A]n observer property holding an IntersectionObserver. + JS::Handle observer; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverregistration-observer + // NOTE: Optional is used in place of the spec using -1 to indicate no previous index. + // [A] previousThresholdIndex property holding a number between -1 and the length of the observer’s thresholds property (inclusive). + Optional previous_threshold_index; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverregistration-previousisintersecting + // [A] previousIsIntersecting property holding a boolean. + bool previous_is_intersecting { false }; +}; + // https://w3c.github.io/IntersectionObserver/#intersection-observer-interface -class IntersectionObserver : public Bindings::PlatformObject { +class IntersectionObserver final : public Bindings::PlatformObject { WEB_PLATFORM_OBJECT(IntersectionObserver, Bindings::PlatformObject); public: - static WebIDL::ExceptionOr> construct_impl(JS::Realm&, WebIDL::CallbackType* callback, IntersectionObserverInit const& options = {}); + static WebIDL::ExceptionOr> construct_impl(JS::Realm&, JS::GCPtr callback, IntersectionObserverInit const& options = {}); virtual ~IntersectionObserver() override; void observe(DOM::Element& target); void unobserve(DOM::Element& target); void disconnect(); + Vector> take_records(); + + Vector> const& observation_targets() const { return m_observation_targets; } + + Variant, JS::Handle, Empty> root() const; + Vector const& thresholds() const { return m_thresholds; } + + Variant, JS::Handle> intersection_root() const; + CSSPixelRect root_intersection_rectangle() const; + + void queue_entry(Badge, JS::NonnullGCPtr); + + WebIDL::CallbackType& callback() { return *m_callback; } private: - explicit IntersectionObserver(JS::Realm&); + explicit IntersectionObserver(JS::Realm&, JS::GCPtr callback, Optional, JS::Handle>> const& root, Vector&& thresholds); virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + virtual void visit_edges(JS::Cell::Visitor&) override; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-callback-slot + JS::GCPtr m_callback; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-root + Optional, JS::Handle>> m_root; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-thresholds + Vector m_thresholds; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-queuedentries-slot + Vector> m_queued_entries; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-observationtargets-slot + Vector> m_observation_targets; }; } diff --git a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl index 122859d825..8e60ee5c5b 100644 --- a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl +++ b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl @@ -1,19 +1,21 @@ #import #import #import +#import callback IntersectionObserverCallback = undefined (sequence entries, IntersectionObserver observer); -[Exposed=(Window)] +[Exposed=(Window), UseNewAKString] interface IntersectionObserver { constructor(IntersectionObserverCallback callback, optional IntersectionObserverInit options = {}); - + readonly attribute (Element or Document)? root; + // FIXME: readonly attribute DOMString rootMargin; + // FIXME: `sequence` should be `FrozenArray` + readonly attribute sequence thresholds; undefined observe(Element target); undefined unobserve(Element target); undefined disconnect(); - - // FIXME: - // sequence takeRecords(); + sequence takeRecords(); }; dictionary IntersectionObserverInit { diff --git a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.cpp b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.cpp new file mode 100644 index 0000000000..98658daf84 --- /dev/null +++ b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Web::IntersectionObserver { + +WebIDL::ExceptionOr> IntersectionObserverEntry::construct_impl(JS::Realm& realm, Web::IntersectionObserver::IntersectionObserverEntryInit const& options) +{ + auto& vm = realm.vm(); + + JS::GCPtr root_bounds; + if (options.root_bounds.has_value()) + root_bounds = TRY(Geometry::DOMRectReadOnly::from_rect(vm, options.root_bounds.value())); + + auto bounding_client_rect = TRY(Geometry::DOMRectReadOnly::from_rect(vm, options.bounding_client_rect)); + auto intersection_rect = TRY(Geometry::DOMRectReadOnly::from_rect(vm, options.intersection_rect)); + return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, options.time, root_bounds, bounding_client_rect, intersection_rect, options.is_intersecting, options.intersection_ratio, *options.target)); +} + +IntersectionObserverEntry::IntersectionObserverEntry(JS::Realm& realm, HighResolutionTime::DOMHighResTimeStamp time, JS::GCPtr root_bounds, JS::NonnullGCPtr bounding_client_rect, JS::NonnullGCPtr intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr target) + : Bindings::PlatformObject(realm) + , m_time(time) + , m_root_bounds(root_bounds) + , m_bounding_client_rect(bounding_client_rect) + , m_intersection_rect(intersection_rect) + , m_is_intersecting(is_intersecting) + , m_intersection_ratio(intersection_ratio) + , m_target(target) +{ +} + +IntersectionObserverEntry::~IntersectionObserverEntry() = default; + +JS::ThrowCompletionOr IntersectionObserverEntry::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype(realm, "IntersectionObserverEntry")); + + return {}; +} + +void IntersectionObserverEntry::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_root_bounds); + visitor.visit(m_bounding_client_rect); + visitor.visit(m_intersection_rect); + visitor.visit(m_target); +} + +} diff --git a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.h b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.h new file mode 100644 index 0000000000..94bc6060f4 --- /dev/null +++ b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023, Luke Wilde + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::IntersectionObserver { + +struct IntersectionObserverEntryInit { + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-time + HighResolutionTime::DOMHighResTimeStamp time { 0.0 }; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-rootbounds + Optional root_bounds; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-boundingclientrect + Geometry::DOMRectInit bounding_client_rect; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionrect + Geometry::DOMRectInit intersection_rect; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-isintersecting + bool is_intersecting { false }; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionratio + double intersection_ratio { 0.0 }; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-target + JS::Handle target; +}; + +class IntersectionObserverEntry final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(IntersectionObserverEntry, Bindings::PlatformObject); + +public: + static WebIDL::ExceptionOr> construct_impl(JS::Realm&, IntersectionObserverEntryInit const& options); + + virtual ~IntersectionObserverEntry() override; + + HighResolutionTime::DOMHighResTimeStamp time() const { return m_time; } + JS::GCPtr root_bounds() const { return m_root_bounds; } + JS::NonnullGCPtr bounding_client_rect() const { return m_bounding_client_rect; } + JS::NonnullGCPtr intersection_rect() const { return m_intersection_rect; } + bool is_intersecting() const { return m_is_intersecting; } + double intersection_ratio() const { return m_intersection_ratio; } + JS::NonnullGCPtr target() const { return m_target; } + +private: + IntersectionObserverEntry(JS::Realm&, HighResolutionTime::DOMHighResTimeStamp time, JS::GCPtr root_bounds, JS::NonnullGCPtr bounding_client_rect, JS::NonnullGCPtr intersection_rect, bool is_intersecting, double intersection_ratio, JS::NonnullGCPtr target); + + virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + virtual void visit_edges(JS::Cell::Visitor&) override; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-time + HighResolutionTime::DOMHighResTimeStamp m_time { 0.0 }; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-rootbounds + JS::GCPtr m_root_bounds; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-boundingclientrect + JS::NonnullGCPtr m_bounding_client_rect; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionrect + JS::NonnullGCPtr m_intersection_rect; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-isintersecting + bool m_is_intersecting { false }; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-intersectionratio + double m_intersection_ratio { 0.0 }; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserverentry-target + JS::NonnullGCPtr m_target; +}; + +} diff --git a/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.idl b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.idl new file mode 100644 index 0000000000..6d80a7ff93 --- /dev/null +++ b/Userland/Libraries/LibWeb/IntersectionObserver/IntersectionObserverEntry.idl @@ -0,0 +1,26 @@ +#import +#import +#import + +// https://www.w3.org/TR/intersection-observer/#intersectionobserverentry +[Exposed=Window] +interface IntersectionObserverEntry { + // FIXME: constructor(IntersectionObserverEntryInit intersectionObserverEntryInit); + readonly attribute DOMHighResTimeStamp time; + readonly attribute DOMRectReadOnly? rootBounds; + readonly attribute DOMRectReadOnly boundingClientRect; + readonly attribute DOMRectReadOnly intersectionRect; + readonly attribute boolean isIntersecting; + readonly attribute double intersectionRatio; + readonly attribute Element target; +}; + +dictionary IntersectionObserverEntryInit { + required DOMHighResTimeStamp time; + required DOMRectInit? rootBounds; + required DOMRectInit boundingClientRect; + required DOMRectInit intersectionRect; + required boolean isIntersecting; + required double intersectionRatio; + required Element target; +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 73f6466de8..64a9ac8317 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -183,6 +183,7 @@ libweb_js_bindings(HTML/WorkerLocation) libweb_js_bindings(HTML/WorkerNavigator) libweb_js_bindings(HighResolutionTime/Performance) libweb_js_bindings(IntersectionObserver/IntersectionObserver) +libweb_js_bindings(IntersectionObserver/IntersectionObserverEntry) libweb_js_bindings(NavigationTiming/PerformanceTiming) libweb_js_bindings(PerformanceTimeline/PerformanceEntry) libweb_js_bindings(RequestIdleCallback/IdleDeadline)