diff --git a/bridge/bindings/qjs/binding_initializer.cc b/bridge/bindings/qjs/binding_initializer.cc index 0a58fab2de..57e18bc57d 100644 --- a/bridge/bindings/qjs/binding_initializer.cc +++ b/bridge/bindings/qjs/binding_initializer.cc @@ -77,6 +77,7 @@ #include "qjs_html_iframe_element.h" #include "qjs_html_image_element.h" #include "qjs_html_input_element.h" +#include "qjs_html_optgroup_element.h" #include "qjs_html_option_element.h" #include "qjs_html_select_element.h" #include "qjs_html_italic_element.h" @@ -221,6 +222,7 @@ void InstallBindings(ExecutingContext* context) { QJSHTMLImageElement::Install(context); QJSHTMLInputElement::Install(context); QJSHTMLSelectElement::Install(context); + QJSHTMLOptgroupElement::Install(context); QJSHTMLOptionElement::Install(context); QJSHTMLParagraphElement::Install(context); QJSHTMLStyleElement::Install(context); diff --git a/bridge/bindings/qjs/wrapper_type_info.h b/bridge/bindings/qjs/wrapper_type_info.h index f0d980a848..7a69e3fb92 100644 --- a/bridge/bindings/qjs/wrapper_type_info.h +++ b/bridge/bindings/qjs/wrapper_type_info.h @@ -145,6 +145,7 @@ enum { JS_CLASS_HTML_DEL_ELEMENT, JS_CLASS_HTML_INS_ELEMENT, JS_CLASS_HTML_SELECT_ELEMENT, + JS_CLASS_HTML_OPTGROUP_ELEMENT, JS_CLASS_HTML_OPTION_ELEMENT, JS_CLASS_HTML_TITLE_ELEMENT, JS_CLASS_HTML_META_ELEMENT, diff --git a/bridge/bridge_sources.json5 b/bridge/bridge_sources.json5 index 13bdda443b..29c81fb7f7 100644 --- a/bridge/bridge_sources.json5 +++ b/bridge/bridge_sources.json5 @@ -544,6 +544,7 @@ "core/html/forms/html_button_element.cc", "core/html/forms/html_input_element.cc", "core/html/forms/html_select_element.cc", + "core/html/forms/html_optgroup_element.cc", "core/html/forms/html_option_element.cc", "core/html/forms/html_form_element.cc", "core/html/forms/html_textarea_element.cc", @@ -775,6 +776,7 @@ "code_gen/qjs_html_button_element.cc", "code_gen/qjs_html_input_element.cc", "code_gen/qjs_html_select_element.cc", + "code_gen/qjs_html_optgroup_element.cc", "code_gen/qjs_html_option_element.cc", "code_gen/qjs_html_form_element.cc", "code_gen/qjs_html_textarea_element.cc", diff --git a/bridge/core/binding_object.cc b/bridge/core/binding_object.cc index 86b702ab58..c5d109ddaa 100644 --- a/bridge/core/binding_object.cc +++ b/bridge/core/binding_object.cc @@ -8,11 +8,16 @@ */ #include "binding_object.h" +#include "core/css/style_change_reason.h" +#include "core/css/style_engine.h" +#include "core/css/style_recalc_change.h" +#include "core/dom/qualified_name.h" #include "binding_call_methods.h" #include "bindings/qjs/exception_state.h" #include "bindings/qjs/script_promise_resolver.h" #include "core/dom/container_node.h" #include "core/dom/document.h" +#include "core/dom/element.h" #include "core/dom/events/event_target.h" #include "core/dom/mutation_observer_interest_group.h" #include "core/executing_context.h" @@ -46,6 +51,31 @@ static void UpdateStyleForThisDocumentIfBlinkEnabled(ExecutingContext* context) doc->UpdateStyleForThisDocument(); } +static bool NativeValueToBoolean(NativeValue value, JSContext* ctx) { + switch (value.tag) { + case NativeTag::TAG_BOOL: + return value.u.int64 != 0; + case NativeTag::TAG_INT: + return value.u.int64 != 0; + case NativeTag::TAG_NULL: + case NativeTag::TAG_UNDEFINED: + return false; + case NativeTag::TAG_STRING: { + AtomicString str = NativeValueConverter::FromNativeValueShared(ctx, value); + if (str.IsNull() || str.empty()) { + return false; + } + auto lowered = str.LowerASCII(); + if (lowered == "false"_s || lowered == "0"_s) { + return false; + } + return true; + } + default: + return true; + } +} + static void ReturnEventResultToDart(Dart_Handle persistent_handle, NativeValue* result, DartInvokeResultCallback result_callback) { @@ -302,6 +332,47 @@ void BindingObject::SetBindingPropertyAsync(const webf::AtomicString& prop, canvas_context->requestPaint(); } + if (auto element = const_cast(DynamicTo(this))) { + AtomicString old_value = element->attributes()->getAttribute(prop, exception_state); + AtomicString new_value = AtomicString::Null(); + + static const AtomicString kChecked = AtomicString::CreateFromUTF8("checked"); + static const AtomicString kSelected = AtomicString::CreateFromUTF8("selected"); + static const AtomicString kDisabled = AtomicString::CreateFromUTF8("disabled"); + static const AtomicString kRequired = AtomicString::CreateFromUTF8("required"); + + const bool is_boolean_attribute = + prop == kChecked || prop == kSelected || prop == kDisabled || prop == kRequired; + const bool bool_value = is_boolean_attribute ? NativeValueToBoolean(value, ctx()) : false; + + if (is_boolean_attribute && !bool_value) { + element->attributes()->removeAttribute(prop, exception_state, true); + new_value = AtomicString::Null(); + } else { + if (value.tag == NativeTag::TAG_STRING) { + new_value = NativeValueConverter::FromNativeValueShared(ctx(), value); + } else if (is_boolean_attribute) { + new_value = prop; + } else { + ScriptValue script_value = ScriptValue(ctx(), value); + new_value = script_value.ToAtomicString(ctx()); + } + element->attributes()->setAttribute(prop, new_value, exception_state, true); + } + + element->AttributeChanged(Element::AttributeModificationParams( + prop, old_value, new_value, Element::AttributeModificationReason::kDirectly)); + + if (is_boolean_attribute && GetExecutingContext()->isBlinkEnabled()) { + element->GetDocument().EnsureStyleEngine().SetNeedsHasPseudoStateRecalc(); + if (Element* root = element->GetDocument().documentElement()) { + root->SetNeedsStyleRecalc(kSubtreeStyleChange, + StyleChangeReasonForTracing::FromAttribute(QualifiedName(prop))); + } + element->GetDocument().UpdateStyleForThisDocument(); + } + } + std::unique_ptr args_01 = prop.ToNativeString(); auto* args_02 = (NativeValue*)dart_malloc(sizeof(NativeValue)); @@ -402,13 +473,44 @@ NativeValue BindingObject::SetBindingProperty(const AtomicString& prop, MutationRecord::CreateAttributes(element, prop, AtomicString::Null(), old_value)); } - // Sync property to attributes - if (value.tag == NativeTag::TAG_STRING) { - element->attributes()->setAttribute( - prop, NativeValueConverter::FromNativeValueShared(ctx(), value), exception_state, true); + AtomicString old_value = element->attributes()->getAttribute(prop, exception_state); + AtomicString new_value = AtomicString::Null(); + + static const AtomicString kChecked = AtomicString::CreateFromUTF8("checked"); + static const AtomicString kSelected = AtomicString::CreateFromUTF8("selected"); + static const AtomicString kDisabled = AtomicString::CreateFromUTF8("disabled"); + static const AtomicString kRequired = AtomicString::CreateFromUTF8("required"); + + const bool is_boolean_attribute = + prop == kChecked || prop == kSelected || prop == kDisabled || prop == kRequired; + const bool bool_value = is_boolean_attribute ? NativeValueToBoolean(value, ctx()) : false; + + // Sync property to attributes for selector matching without emitting UI commands. + if (is_boolean_attribute && !bool_value) { + element->attributes()->removeAttribute(prop, exception_state, true); + new_value = AtomicString::Null(); } else { - ScriptValue script_value = ScriptValue(ctx(), value); - element->attributes()->setAttribute(prop, script_value.ToAtomicString(ctx()), exception_state, true); + if (value.tag == NativeTag::TAG_STRING) { + new_value = NativeValueConverter::FromNativeValueShared(ctx(), value); + } else if (is_boolean_attribute) { + new_value = prop; + } else { + ScriptValue script_value = ScriptValue(ctx(), value); + new_value = script_value.ToAtomicString(ctx()); + } + element->attributes()->setAttribute(prop, new_value, exception_state, true); + } + + element->AttributeChanged(Element::AttributeModificationParams( + prop, old_value, new_value, Element::AttributeModificationReason::kDirectly)); + + if (is_boolean_attribute && GetExecutingContext()->isBlinkEnabled()) { + element->GetDocument().EnsureStyleEngine().SetNeedsHasPseudoStateRecalc(); + if (Element* root = element->GetDocument().documentElement()) { + root->SetNeedsStyleRecalc(kSubtreeStyleChange, + StyleChangeReasonForTracing::FromAttribute(QualifiedName(prop))); + } + element->GetDocument().UpdateStyleForThisDocument(); } } diff --git a/bridge/core/css/check_pseudo_has_argument_context.h b/bridge/core/css/check_pseudo_has_argument_context.h index 242e0f2ac7..f519a7b7d2 100644 --- a/bridge/core/css/check_pseudo_has_argument_context.h +++ b/bridge/core/css/check_pseudo_has_argument_context.h @@ -6,6 +6,8 @@ #define WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_ #include "core/css/css_selector.h" +#include "core/css/css_selector_list.h" +#include "core/dom/has_invalidation_flags.h" #include "foundation/macros.h" #include @@ -19,28 +21,233 @@ using Vector = std::vector; class CheckPseudoHasArgumentContext { WEBF_STACK_ALLOCATED(); public: - explicit CheckPseudoHasArgumentContext(const CSSSelector* selector) - : selector_(selector) {} - - CSSSelector::RelationType LeftmostRelation() const { - // Return a default relation type - return CSSSelector::kRelativeDescendant; + explicit CheckPseudoHasArgumentContext(const CSSSelector* selector) + : selector_(selector) { + BuildContext(); } - int AdjacentDistanceLimit() const { return 0; } - bool AdjacentDistanceFixed() const { return false; } - int DepthLimit() const { return 0; } - bool DepthFixed() const { return false; } - bool AllowSiblingsAffectedByHas() const { return false; } - unsigned GetSiblingsAffectedByHasFlags() const { return 0; } - bool SiblingCombinatorAtRightmost() const { return false; } - bool SiblingCombinatorBetweenChildOrDescendantCombinator() const { return false; } - Vector GetPseudoHasArgumentHashes() const { return Vector(); } + CSSSelector::RelationType LeftmostRelation() const { return leftmost_relation_; } + + int AdjacentDistanceLimit() const { return adjacent_distance_limit_; } + bool AdjacentDistanceFixed() const { return adjacent_distance_fixed_; } + int DepthLimit() const { return depth_limit_; } + bool DepthFixed() const { return depth_fixed_; } + bool AllowSiblingsAffectedByHas() const { return allow_siblings_affected_by_has_; } + unsigned GetSiblingsAffectedByHasFlags() const { return siblings_affected_by_has_flags_; } + bool SiblingCombinatorAtRightmost() const { return sibling_combinator_at_rightmost_; } + bool SiblingCombinatorBetweenChildOrDescendantCombinator() const { + return sibling_combinator_between_child_or_descendant_; + } + Vector GetPseudoHasArgumentHashes() const { return pseudo_has_argument_hashes_; } private: + static inline bool IsRelativeRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kRelativeDescendant || relation == CSSSelector::kRelativeChild || + relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsAdjacentRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kDirectAdjacent || relation == CSSSelector::kIndirectAdjacent || + relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsIndirectAdjacent(CSSSelector::RelationType relation) { + return relation == CSSSelector::kIndirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsRelativeAdjacent(CSSSelector::RelationType relation) { + return relation == CSSSelector::kRelativeDirectAdjacent || relation == CSSSelector::kRelativeIndirectAdjacent; + } + + static inline bool IsDescendantRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kDescendant || relation == CSSSelector::kRelativeDescendant; + } + + static inline bool IsChildRelation(CSSSelector::RelationType relation) { + return relation == CSSSelector::kChild || relation == CSSSelector::kRelativeChild; + } + + static inline bool IsChildOrDescendantRelation(CSSSelector::RelationType relation) { + return IsDescendantRelation(relation) || IsChildRelation(relation); + } + + void BuildContext() { + leftmost_relation_ = CSSSelector::kRelativeDescendant; + adjacent_distance_limit_ = 0; + adjacent_distance_fixed_ = true; + depth_limit_ = 0; + depth_fixed_ = true; + allow_siblings_affected_by_has_ = false; + siblings_affected_by_has_flags_ = kNoSiblingsAffectedByHasFlags; + sibling_combinator_at_rightmost_ = false; + sibling_combinator_between_child_or_descendant_ = false; + pseudo_has_argument_hashes_.clear(); + + if (!selector_) { + return; + } + + // Collect relations from rightmost to leftmost (towards the relative anchor). + Vector relations; + bool found_relative_relation = false; + for (const CSSSelector* current = selector_; current; current = current->NextSimpleSelector()) { + AddHashesForSimpleSelector(*current); + + CSSSelector::RelationType relation = current->Relation(); + if (relation == CSSSelector::kSubSelector || relation == CSSSelector::kScopeActivation) { + continue; + } + relations.push_back(relation); + if (IsRelativeRelation(relation)) { + found_relative_relation = true; + break; + } + } + + if (relations.empty()) { + // No explicit combinator in :has() argument (e.g. :has(.child)). + // Treat it as descendant matching so we traverse the full subtree. + depth_limit_ = 1; + depth_fixed_ = false; + return; + } + + leftmost_relation_ = relations.back(); + if (!found_relative_relation) { + // Be defensive: if the parser didn't convert the leftmost combinator + // to a relative relation, map it here so :has() matching can proceed. + leftmost_relation_ = ConvertRelationToRelative(leftmost_relation_); + } + sibling_combinator_at_rightmost_ = IsAdjacentRelation(relations.front()); + + // Determine if an adjacent combinator appears between child/descendant combinators. + bool seen_child_or_descendant_to_right = false; + for (const auto& relation : relations) { + if (IsChildOrDescendantRelation(relation)) { + seen_child_or_descendant_to_right = true; + continue; + } + if (IsAdjacentRelation(relation) && seen_child_or_descendant_to_right) { + sibling_combinator_between_child_or_descendant_ = true; + } + } + + // Compute adjacency and depth constraints from leftmost (anchor side) to rightmost. + bool in_sibling_phase = IsRelativeAdjacent(leftmost_relation_); + for (auto it = relations.rbegin(); it != relations.rend(); ++it) { + CSSSelector::RelationType relation = *it; + if (IsChildOrDescendantRelation(relation)) { + depth_limit_++; + if (IsDescendantRelation(relation)) { + depth_fixed_ = false; + } + in_sibling_phase = false; + continue; + } + + if (IsAdjacentRelation(relation) && in_sibling_phase) { + if (IsIndirectAdjacent(relation)) { + // Indirect adjacency is unbounded. + adjacent_distance_fixed_ = false; + adjacent_distance_limit_ = 0; + } else if (adjacent_distance_fixed_) { + adjacent_distance_limit_++; + } + } + } + + allow_siblings_affected_by_has_ = IsRelativeAdjacent(leftmost_relation_); + if (allow_siblings_affected_by_has_) { + siblings_affected_by_has_flags_ = + depth_limit_ > 0 ? kFlagForSiblingDescendantRelationship : kFlagForSiblingRelationship; + } + } + + void AddHash(unsigned hash) { + if (hash) { + pseudo_has_argument_hashes_.push_back(hash); + } + } + + void AddHashesForSelectorList(const CSSSelector* selector_list_first) { + if (!selector_list_first) { + return; + } + for (const CSSSelector* complex = selector_list_first; complex; complex = CSSSelectorList::Next(*complex)) { + for (const CSSSelector* simple = complex; simple; simple = simple->NextSimpleSelector()) { + AddHashesForSimpleSelector(*simple); + } + } + } + + void AddHashesForSimpleSelector(const CSSSelector& selector) { + switch (selector.Match()) { + case CSSSelector::kTag: + if (selector.TagQName().LocalName() != CSSSelector::UniversalSelectorAtom()) { + AddHash(selector.TagQName().LocalName().Hash() * kTagSalt); + } + break; + case CSSSelector::kId: + AddHash(selector.Value().Hash() * kIdSalt); + break; + case CSSSelector::kClass: + AddHash(selector.Value().Hash() * kClassSalt); + break; + case CSSSelector::kAttributeExact: + case CSSSelector::kAttributeSet: + case CSSSelector::kAttributeList: + case CSSSelector::kAttributeHyphen: + case CSSSelector::kAttributeContain: + case CSSSelector::kAttributeBegin: + case CSSSelector::kAttributeEnd: + AddHash(selector.Attribute().LocalName().Hash() * kAttributeSalt); + break; + case CSSSelector::kPseudoClass: { + CSSSelector::PseudoType pseudo_type = selector.GetPseudoType(); + switch (pseudo_type) { + case CSSSelector::kPseudoNot: + case CSSSelector::kPseudoIs: + case CSSSelector::kPseudoWhere: + case CSSSelector::kPseudoParent: + AddHashesForSelectorList(selector.SelectorListOrParent()); + break; + case CSSSelector::kPseudoVisited: + case CSSSelector::kPseudoRelativeAnchor: + break; + default: + AddHash(static_cast(pseudo_type) * kPseudoSalt); + if (const CSSSelectorList* list = selector.SelectorList()) { + AddHashesForSelectorList(list->First()); + } + break; + } + break; + } + default: + break; + } + } + + static constexpr unsigned kClassSalt = 13; + static constexpr unsigned kIdSalt = 29; + static constexpr unsigned kTagSalt = 7; + static constexpr unsigned kAttributeSalt = 19; + static constexpr unsigned kPseudoSalt = 23; + const CSSSelector* selector_; + + CSSSelector::RelationType leftmost_relation_; + int adjacent_distance_limit_; + bool adjacent_distance_fixed_; + int depth_limit_; + bool depth_fixed_; + bool allow_siblings_affected_by_has_; + unsigned siblings_affected_by_has_flags_; + bool sibling_combinator_at_rightmost_; + bool sibling_combinator_between_child_or_descendant_; + Vector pseudo_has_argument_hashes_; }; } // namespace webf -#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_ \ No newline at end of file +#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_ARGUMENT_CONTEXT_H_ diff --git a/bridge/core/css/check_pseudo_has_traversal_iterator.h b/bridge/core/css/check_pseudo_has_traversal_iterator.h index efe0e40fc1..9fe5d544f8 100644 --- a/bridge/core/css/check_pseudo_has_traversal_iterator.h +++ b/bridge/core/css/check_pseudo_has_traversal_iterator.h @@ -6,8 +6,10 @@ #define WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_ #include "core/dom/element.h" +#include "core/dom/element_traversal.h" #include "core/css/check_pseudo_has_argument_context.h" #include "foundation/macros.h" +#include #include namespace webf { @@ -22,16 +24,90 @@ class CheckPseudoHasArgumentTraversalIterator { public: CheckPseudoHasArgumentTraversalIterator(Element& element, const CheckPseudoHasArgumentContext& context) - : current_(&element), depth_(0) {} + : context_(context) { + Initialize(element); + } bool AtEnd() const { return current_ == nullptr; } - void operator++() { current_ = nullptr; } // Stub - stop after first element + void operator++() { Advance(); } Element* CurrentElement() const { return current_; } int CurrentDepth() const { return depth_; } private: - Element* current_; - int depth_; + struct TraversalEntry { + Element* element; + int depth; + }; + + void Initialize(Element& anchor) { + stack_.clear(); + + const CSSSelector::RelationType leftmost_relation = context_.LeftmostRelation(); + if (leftmost_relation == CSSSelector::kRelativeDirectAdjacent || + leftmost_relation == CSSSelector::kRelativeIndirectAdjacent) { + int distance = 0; + for (Element* sibling = ElementTraversal::NextSibling(anchor); sibling; + sibling = ElementTraversal::NextSibling(*sibling)) { + distance++; + if (context_.AdjacentDistanceFixed() && context_.AdjacentDistanceLimit() > 0 && + distance > context_.AdjacentDistanceLimit()) { + break; + } + stack_.push_back({sibling, 0}); + if (context_.AdjacentDistanceFixed() && context_.AdjacentDistanceLimit() > 0 && + distance == context_.AdjacentDistanceLimit()) { + break; + } + } + } else { + for (Element* child = ElementTraversal::FirstChild(anchor); child; + child = ElementTraversal::NextSibling(*child)) { + stack_.push_back({child, 1}); + } + } + + std::reverse(stack_.begin(), stack_.end()); + Advance(); + } + + bool ShouldDescendFrom(int depth) const { + if (context_.DepthLimit() == 0) { + return false; + } + if (context_.DepthFixed() && depth >= context_.DepthLimit()) { + return false; + } + return true; + } + + void PushChildren(Element& element, int depth) { + if (!ShouldDescendFrom(depth)) { + return; + } + for (Element* child = ElementTraversal::LastChild(element); child; + child = ElementTraversal::PreviousSibling(*child)) { + stack_.push_back({child, depth + 1}); + } + } + + void Advance() { + if (stack_.empty()) { + current_ = nullptr; + return; + } + TraversalEntry entry = stack_.back(); + stack_.pop_back(); + current_ = entry.element; + depth_ = entry.depth; + if (current_) { + PushChildren(*current_, depth_); + } + } + + const CheckPseudoHasArgumentContext& context_; + Vector stack_; + Element* current_{nullptr}; + int depth_{0}; }; // Stub implementation of CheckPseudoHasFastRejectFilter @@ -50,4 +126,4 @@ class CheckPseudoHasFastRejectFilter { } // namespace webf -#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_ \ No newline at end of file +#endif // WEBF_CORE_CSS_CHECK_PSEUDO_HAS_TRAVERSAL_ITERATOR_H_ diff --git a/bridge/core/css/resolver/style_cascade.cc b/bridge/core/css/resolver/style_cascade.cc index 085d1d2a27..5c70ac05b1 100644 --- a/bridge/core/css/resolver/style_cascade.cc +++ b/bridge/core/css/resolver/style_cascade.cc @@ -401,7 +401,7 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe CSSPropertyName name; std::shared_ptr value; bool important; - uint32_t position; + CascadePriority priority; }; std::vector exported_properties; @@ -461,17 +461,16 @@ std::shared_ptr StyleCascade::BuildWinningPropertySe // so the UICommand receives the original logical names unchanged. CSSPropertyName declared_name = prop_ref.Name(); exported_properties.push_back( - ExportedProperty{id, declared_name, to_set, important, pos}); + ExportedProperty{id, declared_name, to_set, important, *prio}); } - // Sort by encoded position so that later declarations (including shorthands) - // are emitted after earlier ones, matching cascade/source order. This is - // important for consumers like the Dart style engine that replay SetStyle - // commands sequentially. + // Sort by cascade priority so higher-priority declarations are emitted last. + // This keeps shorthand/longhand interactions correct when Dart replays + // SetStyle commands sequentially without full cascade context. std::sort(exported_properties.begin(), exported_properties.end(), [](const ExportedProperty& a, const ExportedProperty& b) { - if (a.position != b.position) { - return a.position < b.position; + if (a.priority != b.priority) { + return a.priority < b.priority; } // Tie-breaker: keep deterministic order by property id. return static_cast(a.id) < static_cast(b.id); diff --git a/bridge/core/css/selector_checker.cc b/bridge/core/css/selector_checker.cc index 7a204fee96..29cfce0201 100644 --- a/bridge/core/css/selector_checker.cc +++ b/bridge/core/css/selector_checker.cc @@ -58,7 +58,9 @@ #include "foundation/string/atomic_string.h" #include "foundation/casting.h" #include "foundation/logging.h" +#include "foundation/native_value_converter.h" #include "bindings/qjs/exception_state.h" +#include "core/dart_methods.h" namespace webf { @@ -104,6 +106,82 @@ static bool IsFrameFocused(const Element& element) { return true; } +static const AtomicString& SelectTagName() { + static const AtomicString kSelect = AtomicString::CreateFromUTF8("select"); + return kSelect; +} + +static const AtomicString& OptionTagName() { + static const AtomicString kOption = AtomicString::CreateFromUTF8("option"); + return kOption; +} + +static const AtomicString& SelectedAttrName() { + static const AtomicString kSelected = AtomicString::CreateFromUTF8("selected"); + return kSelected; +} + +static bool ReadWidgetBooleanProperty(Element& element, const AtomicString& property) { + if (!element.IsWidgetElement()) { + return false; + } + ExceptionState exception_state; + NativeValue result = + element.GetBindingProperty(property, FlushUICommandReason::kDependentsOnElement, exception_state); + if (exception_state.HasException()) { + return false; + } + switch (result.tag) { + case NativeTag::TAG_BOOL: + return NativeValueConverter::FromNativeValue(result); + case NativeTag::TAG_INT: + return result.u.int64 != 0; + case NativeTag::TAG_STRING: { + AtomicString value = NativeValueConverter::FromNativeValueShared(element.ctx(), result); + if (value.IsNull() || value.empty()) { + return false; + } + auto lowered = value.LowerASCII(); + if (lowered == "false"_s || lowered == "0"_s) { + return false; + } + return true; + } + default: + return false; + } +} + +static Element* FindSelectAncestor(Element& element) { + for (Element* current = element.parentElement(); current; current = current->parentElement()) { + if (current->HasTagName(SelectTagName())) { + return current; + } + } + return nullptr; +} + +static bool HasAnySelectedOption(Element& select) { + for (Element& descendant : ElementTraversal::DescendantsOf(select)) { + if (!descendant.HasTagName(OptionTagName())) { + continue; + } + if (descendant.HasAttributeIgnoringNamespace(SelectedAttrName())) { + return true; + } + } + return false; +} + +static Element* FirstOptionElement(Element& select) { + for (Element& descendant : ElementTraversal::DescendantsOf(select)) { + if (descendant.HasTagName(OptionTagName())) { + return &descendant; + } + } + return nullptr; +} + // Type alias for vector compatibility template using Vector = std::vector; @@ -1805,16 +1883,25 @@ bool SelectorChecker::CheckPseudoClass(const SelectorCheckingContext& context, M case CSSSelector::kPseudoInvalid: return element.MatchesValidityPseudoClasses() && !element.IsValidElement(); case CSSSelector::kPseudoChecked: { - // TODO: Implement form control checked state - // if (auto* input_element = DynamicTo(element)) { - // if (input_element->ShouldAppearChecked() && !input_element->ShouldAppearIndeterminate()) { - // return true; - // } - // } else if (auto* option_element = DynamicTo(element)) { - // if (option_element->Selected()) { - // return true; - // } - // } + // Minimal :checked support for WebF's DOM model. + // Match checkable inputs with [checked] and