diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/Animations/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/Animations/BUILD.gn index 4a650242556..d7b81cc05f6 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/Animations/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/Animations/BUILD.gn @@ -16,5 +16,7 @@ source_set("Animations") { "DocumentTimeline.h", "KeyframeEffect.cpp", "KeyframeEffect.h", + "PseudoElementParsing.cpp", + "PseudoElementParsing.h", ] } diff --git a/Userland/Libraries/LibWeb/Animations/Animatable.cpp b/Userland/Libraries/LibWeb/Animations/Animatable.cpp index c495a2205e0..038e6dd94c4 100644 --- a/Userland/Libraries/LibWeb/Animations/Animatable.cpp +++ b/Userland/Libraries/LibWeb/Animations/Animatable.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -58,43 +59,55 @@ WebIDL::ExceptionOr> Animatable::animate(Optional> Animatable::get_animations(GetAnimationsOptions options) +// https://drafts.csswg.org/web-animations-1/#dom-animatable-getanimations +WebIDL::ExceptionOr>> Animatable::get_animations(Optional options) { verify_cast(*this).document().update_style(); return get_animations_internal(options); } -Vector> Animatable::get_animations_internal(GetAnimationsOptions options) +WebIDL::ExceptionOr>> Animatable::get_animations_internal(Optional options) { - // Returns the set of relevant animations for this object, or, if an options parameter is passed with subtree set to - // true, returns the set of relevant animations for a subtree for this object. + // 1. Let object be the object on which this method was called. - // The returned list is sorted using the composite order described for the associated animations of effects in - // §5.4.2 The effect stack. - if (!m_is_sorted_by_composite_order) { - quick_sort(m_associated_animations, [](JS::NonnullGCPtr& a, JS::NonnullGCPtr& b) { - auto& a_effect = verify_cast(*a->effect()); - auto& b_effect = verify_cast(*b->effect()); - return KeyframeEffect::composite_order(a_effect, b_effect) < 0; - }); - m_is_sorted_by_composite_order = true; + // 2. Let pseudoElement be the result of pseudo-element parsing applied to pseudoElement of options, or null if options is not passed. + // FIXME: Currently only DOM::Element includes Animatable, but that might not always be true. + Optional pseudo_element; + if (options.has_value() && options->pseudo_element.has_value()) { + auto& realm = static_cast(*this).realm(); + pseudo_element = TRY(pseudo_element_parsing(realm, options->pseudo_element)); } + // 3. If pseudoElement is not null, then let target be the pseudo-element identified by pseudoElement with object as the originating element. + // Otherwise, let target be object. + // FIXME: We can't refer to pseudo-elements directly, and they also can't be animated yet. + (void)pseudo_element; + JS::NonnullGCPtr target { *static_cast(this) }; + + // 4. If options is passed with subtree set to true, then return the set of relevant animations for a subtree of target. + // Otherwise, return the set of relevant animations for target. Vector> relevant_animations; for (auto const& animation : m_associated_animations) { if (animation->is_relevant()) relevant_animations.append(*animation); } - if (options.subtree) { - JS::NonnullGCPtr target { *static_cast(this) }; - target->for_each_child_of_type([&](auto& child) { - relevant_animations.extend(child.get_animations(options)); + if (options.has_value() && options->subtree) { + Optional exception; + TRY(target->for_each_child_of_type_fallible([&](auto& child) -> WebIDL::ExceptionOr { + relevant_animations.extend(TRY(child.get_animations(options))); return IterationDecision::Continue; - }); + })); } + // The returned list is sorted using the composite order described for the associated animations of effects in + // §5.4.2 The effect stack. + quick_sort(relevant_animations, [](JS::NonnullGCPtr& a, JS::NonnullGCPtr& b) { + auto& a_effect = verify_cast(*a->effect()); + auto& b_effect = verify_cast(*b->effect()); + return KeyframeEffect::composite_order(a_effect, b_effect) < 0; + }); + return relevant_animations; } diff --git a/Userland/Libraries/LibWeb/Animations/Animatable.h b/Userland/Libraries/LibWeb/Animations/Animatable.h index a7ff426a57c..80f4504e8f5 100644 --- a/Userland/Libraries/LibWeb/Animations/Animatable.h +++ b/Userland/Libraries/LibWeb/Animations/Animatable.h @@ -17,18 +17,19 @@ class CSSTransition; namespace Web::Animations { -// https://www.w3.org/TR/web-animations-1/#dictdef-keyframeanimationoptions +// https://drafts.csswg.org/web-animations-1/#dictdef-keyframeanimationoptions struct KeyframeAnimationOptions : public KeyframeEffectOptions { FlyString id { ""_fly_string }; Optional> timeline; }; -// https://www.w3.org/TR/web-animations-1/#dictdef-getanimationsoptions +// https://drafts.csswg.org/web-animations-1/#dictdef-getanimationsoptions struct GetAnimationsOptions { bool subtree { false }; + Optional pseudo_element {}; }; -// https://www.w3.org/TR/web-animations-1/#animatable +// https://drafts.csswg.org/web-animations-1/#animatable class Animatable { public: struct TransitionAttributes { @@ -40,8 +41,8 @@ public: virtual ~Animatable() = default; WebIDL::ExceptionOr> animate(Optional> keyframes, Variant options = {}); - Vector> get_animations(GetAnimationsOptions options = {}); - Vector> get_animations_internal(GetAnimationsOptions options = {}); + WebIDL::ExceptionOr>> get_animations(Optional options = {}); + WebIDL::ExceptionOr>> get_animations_internal(Optional options = {}); void associate_with_animation(JS::NonnullGCPtr); void disassociate_with_animation(JS::NonnullGCPtr); diff --git a/Userland/Libraries/LibWeb/Animations/Animatable.idl b/Userland/Libraries/LibWeb/Animations/Animatable.idl index 249e7e8b63b..40f6d234f3f 100644 --- a/Userland/Libraries/LibWeb/Animations/Animatable.idl +++ b/Userland/Libraries/LibWeb/Animations/Animatable.idl @@ -1,19 +1,20 @@ #import #import -// https://www.w3.org/TR/web-animations-1/#the-animatable-interface-mixin +// https://drafts.csswg.org/web-animations-1/#the-animatable-interface-mixin interface mixin Animatable { Animation animate(object? keyframes, optional (unrestricted double or KeyframeAnimationOptions) options = {}); sequence getAnimations(optional GetAnimationsOptions options = {}); }; -// https://www.w3.org/TR/web-animations-1/#dictdef-keyframeanimationoptions +// https://drafts.csswg.org/web-animations-1/#dictdef-keyframeanimationoptions dictionary KeyframeAnimationOptions : KeyframeEffectOptions { DOMString id = ""; AnimationTimeline? timeline; }; -// https://www.w3.org/TR/web-animations-1/#dictdef-getanimationsoptions +// https://drafts.csswg.org/web-animations-1/#dictdef-getanimationsoptions dictionary GetAnimationsOptions { boolean subtree = false; + CSSOMString? pseudoElement = null; }; diff --git a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp index 92c0ce6d034..7dae6b4bf01 100644 --- a/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp +++ b/Userland/Libraries/LibWeb/Animations/KeyframeEffect.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -783,42 +784,13 @@ Optional KeyframeEffect::pseudo_element() const return MUST(String::formatted("::{}", m_target_pseudo_selector->name())); } -// https://www.w3.org/TR/web-animations-1/#dom-keyframeeffect-pseudoelement -WebIDL::ExceptionOr KeyframeEffect::set_pseudo_element(Optional pseudo_element) +// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudoelement +WebIDL::ExceptionOr KeyframeEffect::set_pseudo_element(Optional value) { - auto& realm = this->realm(); - - // On setting, sets the target pseudo-selector of the animation effect to the provided value after applying the - // following exceptions: - - // FIXME: - // - If one of the legacy Selectors Level 2 single-colon selectors (':before', ':after', ':first-letter', or - // ':first-line') is specified, the target pseudo-selector must be set to the equivalent two-colon selector - // (e.g. '::before'). - if (pseudo_element.has_value()) { - auto value = pseudo_element.value(); - - if (value == ":before" || value == ":after" || value == ":first-letter" || value == ":first-line") { - m_target_pseudo_selector = CSS::Selector::PseudoElement::from_string(MUST(value.substring_from_byte_offset(1))); - return {}; - } - } - - // - If the provided value is not null and is an invalid , the user agent must throw a - // DOMException with error name SyntaxError and leave the target pseudo-selector of this animation effect - // unchanged. - if (pseudo_element.has_value()) { - if (pseudo_element->starts_with_bytes("::"sv)) { - if (auto value = CSS::Selector::PseudoElement::from_string(MUST(pseudo_element->substring_from_byte_offset(2))); value.has_value()) { - m_target_pseudo_selector = value; - return {}; - } - } - - return WebIDL::SyntaxError::create(realm, MUST(String::formatted("Invalid pseudo-element selector: \"{}\"", pseudo_element.value()))); - } - - m_target_pseudo_selector = {}; + // On setting, sets the target pseudo-selector of the animation effect to the result of + // pseudo-element parsing on the provided value, defined as the following: + // NOTE: The actual definition is in pseudo_element_parsing(). + m_target_pseudo_selector = TRY(pseudo_element_parsing(realm(), value)); return {}; } diff --git a/Userland/Libraries/LibWeb/Animations/PseudoElementParsing.cpp b/Userland/Libraries/LibWeb/Animations/PseudoElementParsing.cpp new file mode 100644 index 00000000000..5607c85a40d --- /dev/null +++ b/Userland/Libraries/LibWeb/Animations/PseudoElementParsing.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "PseudoElementParsing.h" +#include + +namespace Web::Animations { + +// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudo-element-parsing +WebIDL::ExceptionOr> pseudo_element_parsing(JS::Realm& realm, Optional const& value) +{ + // 1. Given the value value, perform the following steps: + + // 2. If value is not null and is an invalid , + Optional pseudo_element; + if (value.has_value()) { + pseudo_element = parse_pseudo_element_selector(CSS::Parser::ParsingContext { realm }, *value); + if (!pseudo_element.has_value()) { + // 1. Throw a DOMException with error name "SyntaxError". + // 2. Abort. + return WebIDL::SyntaxError::create(realm, MUST(String::formatted("Invalid pseudo-element selector: \"{}\"", value.value()))); + } + } + + // 3. If value is one of the legacy Selectors Level 2 single-colon selectors (':before', ':after', ':first-letter', or ':first-line'), + // then return the equivalent two-colon selector (e.g. '::before'). + if (value.has_value() && value->is_one_of(":before", ":after", ":first-letter", ":first-line")) { + return CSS::Selector::PseudoElement::from_string(MUST(value->substring_from_byte_offset(1))); + } + + // 4. Otherwise, return value. + return pseudo_element; +} + +} diff --git a/Userland/Libraries/LibWeb/Animations/PseudoElementParsing.h b/Userland/Libraries/LibWeb/Animations/PseudoElementParsing.h new file mode 100644 index 00000000000..55d7acb316c --- /dev/null +++ b/Userland/Libraries/LibWeb/Animations/PseudoElementParsing.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web::Animations { + +// https://drafts.csswg.org/web-animations-1/#dom-keyframeeffect-pseudo-element-parsing +WebIDL::ExceptionOr> pseudo_element_parsing(JS::Realm&, Optional const&); + +} diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 026233f799a..1aa0ccba80f 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -10,6 +10,7 @@ set(SOURCES Animations/AnimationTimeline.cpp Animations/DocumentTimeline.cpp Animations/KeyframeEffect.cpp + Animations/PseudoElementParsing.cpp ARIA/AriaData.cpp ARIA/ARIAMixin.cpp ARIA/Roles.cpp diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 0baad2aedaf..96c18a12d66 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -1579,12 +1579,16 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element } } - auto animations = element.get_animations_internal({ .subtree = false }); - for (auto& animation : animations) { - if (auto effect = animation->effect(); effect && effect->is_keyframe_effect()) { - auto& keyframe_effect = *static_cast(effect.ptr()); - if (keyframe_effect.pseudo_element_type() == pseudo_element) - collect_animation_into(element, pseudo_element, keyframe_effect, style); + auto animations = element.get_animations_internal(Animations::GetAnimationsOptions { .subtree = false }); + if (animations.is_exception()) { + dbgln("Error getting animations for element {}", element.debug_description()); + } else { + for (auto& animation : animations.value()) { + if (auto effect = animation->effect(); effect && effect->is_keyframe_effect()) { + auto& keyframe_effect = *static_cast(effect.ptr()); + if (keyframe_effect.pseudo_element_type() == pseudo_element) + collect_animation_into(element, pseudo_element, keyframe_effect, style); + } } } diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 1ce87400dfe..f0715a13db5 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -4695,13 +4695,13 @@ void Document::remove_replaced_animations() } } -Vector> Document::get_animations() +WebIDL::ExceptionOr>> Document::get_animations() { Vector> relevant_animations; - for_each_child_of_type([&](auto& child) { - relevant_animations.extend(child.get_animations({ .subtree = true })); + TRY(for_each_child_of_type_fallible([&](auto& child) -> WebIDL::ExceptionOr { + relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true }))); return IterationDecision::Continue; - }); + })); return relevant_animations; } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 8528c37572e..88a10bb5f59 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -636,7 +636,7 @@ public: void update_animations_and_send_events(Optional const& timestamp); void remove_replaced_animations(); - Vector> get_animations(); + WebIDL::ExceptionOr>> get_animations(); bool ready_to_run_scripts() const { return m_ready_to_run_scripts; } void set_ready_to_run_scripts() { m_ready_to_run_scripts = true; } diff --git a/Userland/Libraries/LibWeb/DOM/Node.h b/Userland/Libraries/LibWeb/DOM/Node.h index 12ff41aab34..8270114306a 100644 --- a/Userland/Libraries/LibWeb/DOM/Node.h +++ b/Userland/Libraries/LibWeb/DOM/Node.h @@ -637,6 +637,18 @@ public: return const_cast(this)->template for_each_child_of_type(move(callback)); } + template + WebIDL::ExceptionOr for_each_child_of_type_fallible(Callback callback) + { + for (auto* node = first_child(); node; node = node->next_sibling()) { + if (is(node)) { + if (TRY(callback(verify_cast(*node))) == IterationDecision::Break) + return {}; + } + } + return {}; + } + template U const* next_sibling_of_type() const { diff --git a/Userland/Libraries/LibWeb/DOM/ShadowRoot.cpp b/Userland/Libraries/LibWeb/DOM/ShadowRoot.cpp index 0c9061fdc8f..22e8a26776d 100644 --- a/Userland/Libraries/LibWeb/DOM/ShadowRoot.cpp +++ b/Userland/Libraries/LibWeb/DOM/ShadowRoot.cpp @@ -174,13 +174,13 @@ void ShadowRoot::for_each_css_style_sheet(Function&& } } -Vector> ShadowRoot::get_animations() +WebIDL::ExceptionOr>> ShadowRoot::get_animations() { Vector> relevant_animations; - for_each_child_of_type([&](auto& child) { - relevant_animations.extend(child.get_animations({ .subtree = true })); + TRY(for_each_child_of_type_fallible([&](auto& child) -> WebIDL::ExceptionOr { + relevant_animations.extend(TRY(child.get_animations(Animations::GetAnimationsOptions { .subtree = true }))); return IterationDecision::Continue; - }); + })); return relevant_animations; } diff --git a/Userland/Libraries/LibWeb/DOM/ShadowRoot.h b/Userland/Libraries/LibWeb/DOM/ShadowRoot.h index c76c3b28878..f1c5fa92fe0 100644 --- a/Userland/Libraries/LibWeb/DOM/ShadowRoot.h +++ b/Userland/Libraries/LibWeb/DOM/ShadowRoot.h @@ -60,7 +60,7 @@ public: void for_each_css_style_sheet(Function&& callback) const; - Vector> get_animations(); + WebIDL::ExceptionOr>> get_animations(); virtual void finalize() override;