diff --git a/Libraries/LibWeb/HTML/HTMLSelectElement.cpp b/Libraries/LibWeb/HTML/HTMLSelectElement.cpp index a96862e45b8..d7a6891738a 100644 --- a/Libraries/LibWeb/HTML/HTMLSelectElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLSelectElement.cpp @@ -51,6 +51,7 @@ void HTMLSelectElement::visit_edges(Cell::Visitor& visitor) visitor.visit(m_selected_options); visitor.visit(m_inner_text_element); visitor.visit(m_chevron_icon_element); + visitor.visit(m_cached_list_of_options); for (auto const& item : m_select_items) { if (item.has()) @@ -197,25 +198,40 @@ GC::Ref HTMLSelectElement::selected_options() } // https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-list -Vector> HTMLSelectElement::list_of_options() const +void HTMLSelectElement::update_cached_list_of_options() const { // The list of options for a select element consists of all the option element children of the select element, // and all the option element children of all the optgroup element children of the select element, in tree order. - Vector> list; + m_cached_list_of_options.clear(); + m_cached_number_of_selected_options = 0; for (auto* node = first_child(); node; node = node->next_sibling()) { if (auto* maybe_option = as_if(*node)) { - list.append(GC::make_root(const_cast(*maybe_option))); + if (maybe_option->selected()) + ++m_cached_number_of_selected_options; + m_cached_list_of_options.append(const_cast(*maybe_option)); continue; } if (auto* maybe_opt_group = as_if(node)) { maybe_opt_group->for_each_child_of_type([&](HTMLOptionElement& option_element) { - list.append(GC::make_root(option_element)); + if (option_element.selected()) + ++m_cached_number_of_selected_options; + m_cached_list_of_options.append(option_element); return IterationDecision::Continue; }); } } +} + +// https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-list +Vector> HTMLSelectElement::list_of_options() const +{ + update_cached_list_of_options(); + Vector> list; + list.ensure_capacity(m_cached_list_of_options.size()); + for (auto& item : m_cached_list_of_options) + list.unchecked_append(GC::make_root(item)); return list; } @@ -223,8 +239,10 @@ Vector> HTMLSelectElement::list_of_options() const // https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element:concept-form-reset-control void HTMLSelectElement::reset_algorithm() { + update_cached_list_of_options(); + // The reset algorithm for select elements is to go through all the option elements in the element's list of options, - for (auto const& option_element : list_of_options()) { + for (auto const& option_element : m_cached_list_of_options) { // set their selectedness to true if the option element has a selected attribute, and false otherwise, option_element->set_selected_internal(option_element->has_attribute(AttributeNames::selected)); // set their dirtiness to false, @@ -239,9 +257,10 @@ WebIDL::Long HTMLSelectElement::selected_index() const { // The selectedIndex IDL attribute, on getting, must return the index of the first option element in the list of options // in tree order that has its selectedness set to true, if any. If there isn't one, then it must return −1. + update_cached_list_of_options(); WebIDL::Long index = 0; - for (auto const& option_element : list_of_options()) { + for (auto const& option_element : m_cached_list_of_options) { if (option_element->selected()) return index; ++index; @@ -251,17 +270,17 @@ WebIDL::Long HTMLSelectElement::selected_index() const void HTMLSelectElement::set_selected_index(WebIDL::Long index) { + update_cached_list_of_options(); // On setting, the selectedIndex attribute must set the selectedness of all the option elements in the list of options to false, // and then the option element in the list of options whose index is the given new value, // if any, must have its selectedness set to true and its dirtiness set to true. - auto options = list_of_options(); - for (auto& option : options) + for (auto& option : m_cached_list_of_options) option->set_selected_internal(false); - if (index < 0 || index >= static_cast(options.size())) + if (index < 0 || static_cast(index) >= m_cached_list_of_options.size()) return; - auto& selected_option = options[index]; + auto& selected_option = m_cached_list_of_options[index]; selected_option->set_selected_internal(true); selected_option->m_dirty = true; } @@ -276,6 +295,7 @@ i32 HTMLSelectElement::default_tab_index_value() const void HTMLSelectElement::children_changed() { Base::children_changed(); + update_cached_list_of_options(); update_selectedness(); } @@ -307,7 +327,8 @@ Optional HTMLSelectElement::default_role() const String HTMLSelectElement::value() const { - for (auto const& option_element : list_of_options()) + update_cached_list_of_options(); + for (auto const& option_element : m_cached_list_of_options) if (option_element->selected()) return option_element->value(); return ""_string; @@ -315,7 +336,8 @@ String HTMLSelectElement::value() const WebIDL::ExceptionOr HTMLSelectElement::set_value(String const& value) { - for (auto const& option_element : list_of_options()) + update_cached_list_of_options(); + for (auto const& option_element : m_cached_list_of_options) option_element->set_selected(option_element->value() == value); update_inner_text_element(); return {}; @@ -474,7 +496,8 @@ void HTMLSelectElement::did_select_item(Optional const& id) if (!id.has_value()) return; - for (auto const& option_element : list_of_options()) + update_cached_list_of_options(); + for (auto const& option_element : m_cached_list_of_options) option_element->set_selected(false); for (auto const& item : m_select_items) { @@ -559,6 +582,7 @@ void HTMLSelectElement::create_shadow_tree_if_needed() void HTMLSelectElement::update_inner_text_element(Badge) { + update_cached_list_of_options(); update_inner_text_element(); } @@ -569,7 +593,7 @@ void HTMLSelectElement::update_inner_text_element() return; // Update inner text element to the label of the selected option - for (auto const& option_element : list_of_options()) { + for (auto const& option_element : m_cached_list_of_options) { if (option_element->selected()) { m_inner_text_element->set_text_content(strip_newlines(option_element->label())); return; @@ -583,23 +607,15 @@ void HTMLSelectElement::update_selectedness() if (has_attribute(AttributeNames::multiple)) return; - auto list_of_options = this->list_of_options(); + update_cached_list_of_options(); // If element's multiple attribute is absent, and element's display size is 1, if (display_size() == 1) { - bool has_selected_elements = false; - for (auto const& option_element : list_of_options) { - if (option_element->selected()) { - has_selected_elements = true; - break; - } - } - // and no option elements in the element's list of options have their selectedness set to true, - if (!has_selected_elements) { + if (m_cached_number_of_selected_options == 0) { // then set the selectedness of the first option element in the list of options in tree order // that is not disabled, if any, to true, and return. - for (auto const& option_element : list_of_options) { + for (auto const& option_element : m_cached_list_of_options) { if (!option_element->disabled()) { option_element->set_selected_internal(true); update_inner_text_element(); @@ -614,19 +630,13 @@ void HTMLSelectElement::update_selectedness() // and two or more option elements in element's list of options have their selectedness set to true, // then set the selectedness of all but the last option element with its selectedness set to true // in the list of options in tree order to false. - int number_of_selected = 0; - for (auto const& option_element : list_of_options) { - if (option_element->selected()) - ++number_of_selected; - } - // and two or more option elements in element's list of options have their selectedness set to true, - if (number_of_selected >= 2) { + if (m_cached_number_of_selected_options >= 2) { // then set the selectedness of all but the last option element with its selectedness set to true // in the list of options in tree order to false. GC::Ptr last_selected_option; u64 last_selected_option_update_index = 0; - for (auto const& option_element : list_of_options) { + for (auto const& option_element : m_cached_list_of_options) { if (!option_element->selected()) continue; if (!last_selected_option @@ -636,7 +646,7 @@ void HTMLSelectElement::update_selectedness() } } - for (auto const& option_element : list_of_options) { + for (auto const& option_element : m_cached_list_of_options) { if (option_element != last_selected_option) option_element->set_selected_internal(false); } diff --git a/Libraries/LibWeb/HTML/HTMLSelectElement.h b/Libraries/LibWeb/HTML/HTMLSelectElement.h index 640c081c196..7801e703f66 100644 --- a/Libraries/LibWeb/HTML/HTMLSelectElement.h +++ b/Libraries/LibWeb/HTML/HTMLSelectElement.h @@ -111,6 +111,7 @@ private: virtual void children_changed() override; + void update_cached_list_of_options() const; void show_the_picker_if_applicable(); void create_shadow_tree_if_needed(); @@ -119,6 +120,9 @@ private: u32 display_size() const; + mutable Vector> m_cached_list_of_options; + mutable size_t m_cached_number_of_selected_options { 0 }; + GC::Ptr m_options; GC::Ptr m_selected_options; bool m_is_open { false };