mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-21 19:30:27 +00:00
LibWeb: Add HTMLSelectedContentElement for customizable select
Introduce the HTMLSelectedContentElement and integrate it into <select>, <option> and HTMLParser. See whatwg/html#10548. There are two bugs with WPT tests which causes the third subtest in selectedcontent.html and selectedcontent-mutations.html fail. See whatwg/html#11882, web-platform-tests/wpt#55849.
This commit is contained in:
parent
89d50befb0
commit
b58fcaeecf
Notes:
github-actions[bot]
2025-12-12 12:07:51 +00:00
Author: https://github.com/F3n67u
Commit: b58fcaeecf
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7024
Reviewed-by: https://github.com/AtkinsSJ ✅
25 changed files with 945 additions and 205 deletions
|
|
@ -22,6 +22,7 @@
|
|||
#include <LibWeb/HTML/HTMLOptGroupElement.h>
|
||||
#include <LibWeb/HTML/HTMLOptionElement.h>
|
||||
#include <LibWeb/HTML/HTMLSelectElement.h>
|
||||
#include <LibWeb/HTML/HTMLSelectedContentElement.h>
|
||||
#include <LibWeb/HTML/Navigable.h>
|
||||
#include <LibWeb/HTML/Numbers.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
|
|
@ -290,8 +291,8 @@ void HTMLSelectElement::reset_algorithm()
|
|||
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-selectedindex
|
||||
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.
|
||||
// The selectedIndex getter steps are to return the index of the first option element in this's list of options
|
||||
// in tree order that has its selectedness set to true, if any. If there isn't one, then return −1.
|
||||
update_cached_list_of_options();
|
||||
|
||||
WebIDL::Long index = 0;
|
||||
|
|
@ -303,23 +304,41 @@ WebIDL::Long HTMLSelectElement::selected_index() const
|
|||
return -1;
|
||||
}
|
||||
|
||||
void HTMLSelectElement::set_selected_index(WebIDL::Long index)
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-selectedindex
|
||||
WebIDL::ExceptionOr<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.
|
||||
for (auto& option : m_cached_list_of_options)
|
||||
option->set_selected_internal(false);
|
||||
|
||||
// The selectedIndex setter steps are:
|
||||
ScopeGuard guard { [&]() { update_inner_text_element(); } };
|
||||
|
||||
if (index < 0 || static_cast<size_t>(index) >= m_cached_list_of_options.size())
|
||||
return;
|
||||
// 1. Let firstMatchingOption be null.
|
||||
GC::Ptr<HTMLOptionElement> first_matching_option;
|
||||
|
||||
auto& selected_option = m_cached_list_of_options[index];
|
||||
selected_option->set_selected_internal(true);
|
||||
selected_option->m_dirty = true;
|
||||
// 2. For each option of this's list of options:
|
||||
update_cached_list_of_options();
|
||||
WebIDL::Long current_index = 0;
|
||||
for (auto const& option : m_cached_list_of_options) {
|
||||
// 1. Set option's selectedness to false.
|
||||
option->set_selected_internal(false);
|
||||
|
||||
// 2. If firstMatchingOption is null and option's index is equal to the given value, then
|
||||
// set firstMatchingOption to option.
|
||||
if (!first_matching_option && current_index == index)
|
||||
first_matching_option = option;
|
||||
|
||||
current_index++;
|
||||
}
|
||||
|
||||
// 3. If firstMatchingOption is not null, then set firstMatchingOption's selectedness to true
|
||||
// and set firstMatchingOption's dirtiness to true.
|
||||
if (first_matching_option) {
|
||||
first_matching_option->set_selected_internal(true);
|
||||
first_matching_option->m_dirty = true;
|
||||
}
|
||||
|
||||
// 4. Run update a select's selectedcontent given this.
|
||||
TRY(update_selectedcontent());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/interaction.html#dom-tabindex
|
||||
|
|
@ -393,8 +412,12 @@ Optional<ARIA::Role> HTMLSelectElement::default_role() const
|
|||
return ARIA::Role::combobox;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-value
|
||||
Utf16String HTMLSelectElement::value() const
|
||||
{
|
||||
// The value getter steps are to return the value of the first option element in this's
|
||||
// list of options in tree order that has its selectedness set to true, if any. If there
|
||||
// isn't one, then return the empty string.
|
||||
update_cached_list_of_options();
|
||||
for (auto const& option_element : m_cached_list_of_options)
|
||||
if (option_element->selected())
|
||||
|
|
@ -402,29 +425,61 @@ Utf16String HTMLSelectElement::value() const
|
|||
return {};
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-value
|
||||
WebIDL::ExceptionOr<void> HTMLSelectElement::set_value(Utf16String const& value)
|
||||
{
|
||||
// The value setter steps are:
|
||||
ScopeGuard guard { [&]() { update_inner_text_element(); } };
|
||||
update_cached_list_of_options();
|
||||
for (auto const& option_element : list_of_options())
|
||||
option_element->set_selected(option_element->value() == value);
|
||||
update_inner_text_element();
|
||||
|
||||
// 1. Let firstMatchingOption be null.
|
||||
GC::Ptr<HTMLOptionElement> first_matching_option;
|
||||
|
||||
// 2. For each option of this's list of options:
|
||||
for (auto const& option_element : m_cached_list_of_options) {
|
||||
// 1. Set option's selectedness to false.
|
||||
option_element->set_selected_internal(false);
|
||||
|
||||
// 2. If firstMatchingOption is null and option's value is equal to the given value, then set
|
||||
// firstMatchingOption to option.
|
||||
if (!first_matching_option && option_element->value() == value)
|
||||
first_matching_option = option_element;
|
||||
}
|
||||
|
||||
// 3. If firstMatchingOption is not null, then set firstMatchingOption's selectedness to true and set
|
||||
// firstMatchingOption's dirtiness to true.
|
||||
if (first_matching_option) {
|
||||
first_matching_option->set_selected_internal(true);
|
||||
first_matching_option->m_dirty = true;
|
||||
}
|
||||
|
||||
// 4. Run update a select's selectedcontent given this.
|
||||
TRY(update_selectedcontent());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void HTMLSelectElement::queue_input_and_change_events()
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#send-select-update-notifications
|
||||
void HTMLSelectElement::send_select_update_notifications()
|
||||
{
|
||||
// When the user agent is to send select update notifications, queue an element task on the user interaction task source given the select element to run these steps:
|
||||
// To send select update notifications for a select element element, queue an element task on
|
||||
// the user interaction task source given element to run these steps:
|
||||
queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
|
||||
// 1. Set the select element's user validity to true.
|
||||
m_user_validity = true;
|
||||
|
||||
// 2. Fire an event named input at the select element, with the bubbles and composed attributes initialized to true.
|
||||
// 2. Run update a select's selectedcontent given element.
|
||||
MUST(update_selectedcontent());
|
||||
|
||||
// FIXME: 3. Run clone selected option into select button given element.
|
||||
|
||||
// 4. Fire an event named input at element, with the bubbles and composed attributes initialized to true.
|
||||
auto input_event = DOM::Event::create(realm(), HTML::EventNames::input);
|
||||
input_event->set_bubbles(true);
|
||||
input_event->set_composed(true);
|
||||
dispatch_event(input_event);
|
||||
|
||||
// 3. Fire an event named change at the select element, with the bubbles attribute initialized to true.
|
||||
// 5. Fire an event named change at element, with the bubbles attribute initialized to true.
|
||||
auto change_event = DOM::Event::create(realm(), HTML::EventNames::change);
|
||||
change_event->set_bubbles(true);
|
||||
dispatch_event(*change_event);
|
||||
|
|
@ -577,7 +632,7 @@ void HTMLSelectElement::did_select_item(Optional<u32> const& id)
|
|||
}
|
||||
|
||||
update_inner_text_element();
|
||||
queue_input_and_change_events();
|
||||
send_select_update_notifications();
|
||||
}
|
||||
|
||||
void HTMLSelectElement::form_associated_element_was_inserted()
|
||||
|
|
@ -674,37 +729,35 @@ void HTMLSelectElement::update_inner_text_element()
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm
|
||||
// https://whatpr.org/html/11890/form-elements.html#selectedness-setting-algorithm
|
||||
void HTMLSelectElement::update_selectedness()
|
||||
{
|
||||
if (has_attribute(AttributeNames::multiple))
|
||||
return;
|
||||
|
||||
// The selectedness setting algorithm, given a select element element, is to run the following steps:
|
||||
update_cached_list_of_options();
|
||||
|
||||
// If element's multiple attribute is absent, and element's display size is 1,
|
||||
if (display_size() == 1) {
|
||||
// and no option elements in the element's list of options have their selectedness set to true,
|
||||
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 : m_cached_list_of_options) {
|
||||
if (!option_element->disabled()) {
|
||||
option_element->set_selected_internal(true);
|
||||
update_inner_text_element();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 1. Let updateSelectedcontent be false.
|
||||
auto should_update_selectedcontent = false;
|
||||
|
||||
// If element's multiple attribute is absent,
|
||||
// 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.
|
||||
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.
|
||||
// 2. If element 's multiple attribute is absent, and element's display size is 1,
|
||||
// and no option elements in the element's list of options have their selectedness set to true, then
|
||||
if (!has_attribute(AttributeNames::multiple) && display_size() == 1 && m_cached_number_of_selected_options == 0) {
|
||||
// 1. Set the selectedness of the first option element in the list of options in tree order
|
||||
// that is not disabled, if any, to true.
|
||||
for (auto const& option_element : m_cached_list_of_options) {
|
||||
if (!option_element->disabled()) {
|
||||
option_element->set_selected_internal(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Set updateSelectedcontent to true.
|
||||
should_update_selectedcontent = true;
|
||||
}
|
||||
// Otherwise, if element's multiple attribute is absent,
|
||||
// and two or more option elements in element's list of options have their selectedness set to true, then:
|
||||
else if (!has_attribute(AttributeNames::multiple) && m_cached_number_of_selected_options >= 2) {
|
||||
// 1. 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<HTML::HTMLOptionElement> last_selected_option;
|
||||
u64 last_selected_option_update_index = 0;
|
||||
|
||||
|
|
@ -722,8 +775,16 @@ void HTMLSelectElement::update_selectedness()
|
|||
if (option_element != last_selected_option)
|
||||
option_element->set_selected_internal(false);
|
||||
}
|
||||
|
||||
// 2. Set updateSelectedcontent to true.
|
||||
should_update_selectedcontent = true;
|
||||
}
|
||||
|
||||
// 4. If updateSelectedcontent is true, then run update a select's selectedcontent given element.
|
||||
if (should_update_selectedcontent) {
|
||||
MUST(update_selectedcontent());
|
||||
update_inner_text_element();
|
||||
}
|
||||
update_inner_text_element();
|
||||
}
|
||||
|
||||
bool HTMLSelectElement::is_focusable() const
|
||||
|
|
@ -746,6 +807,88 @@ HTMLOptionElement* HTMLSelectElement::placeholder_label_option() const
|
|||
return {};
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#select-enabled-selectedcontent
|
||||
GC::Ptr<HTMLSelectedContentElement> HTMLSelectElement::enabled_selectedcontent() const
|
||||
{
|
||||
// To get a select's enabled selectedcontent given a select element select:
|
||||
|
||||
// 1. If select has the multiple attribute, then return null.
|
||||
if (has_attribute(AttributeNames::multiple))
|
||||
return nullptr;
|
||||
|
||||
// 2. Let selectedcontent be the first selectedcontent element descendant of select in tree order if any such
|
||||
// element exists; otherwise return null.
|
||||
GC::Ptr<HTMLSelectedContentElement> selectedcontent;
|
||||
for_each_in_subtree_of_type<HTMLSelectedContentElement>([&](auto& element) {
|
||||
selectedcontent = const_cast<HTMLSelectedContentElement*>(&element);
|
||||
return TraversalDecision::Break;
|
||||
});
|
||||
if (!selectedcontent)
|
||||
return nullptr;
|
||||
|
||||
// 3. If selectedcontent is disabled, then return null.
|
||||
if (selectedcontent->disabled())
|
||||
return nullptr;
|
||||
|
||||
// 4. Return selectedcontent.
|
||||
return selectedcontent;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#clear-a-select%27s-non-primary-selectedcontent-elements
|
||||
void HTMLSelectElement::clear_non_primary_selectedcontent()
|
||||
{
|
||||
// To clear a select's non-primary selectedcontent elements, given a select element select:
|
||||
|
||||
// 1. Let passedFirstSelectedcontent be false.
|
||||
bool passed_first_selectedcontent = false;
|
||||
|
||||
// 2. For each descendant of select's descendants in tree order that is a selectedcontent element:
|
||||
for_each_in_subtree_of_type<HTMLSelectedContentElement>([&](auto& element) {
|
||||
// 1. If passedFirstSelectedcontent is false, then set passedFirstSelectedcontent to true.
|
||||
if (!passed_first_selectedcontent)
|
||||
passed_first_selectedcontent = true;
|
||||
// 2. Otherwise, run clear a selectedcontent given descendant.
|
||||
else
|
||||
element.clear_selectedcontent();
|
||||
|
||||
return TraversalDecision::Continue;
|
||||
});
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#update-a-select%27s-selectedcontent
|
||||
WebIDL::ExceptionOr<void> HTMLSelectElement::update_selectedcontent()
|
||||
{
|
||||
// To update a select's selectedcontent given a select element select:
|
||||
|
||||
// 1. Let selectedcontent be the result of get a select's enabled selectedcontent given select.
|
||||
auto selectedcontent = enabled_selectedcontent();
|
||||
|
||||
// 2. If selectedcontent is null, then return.
|
||||
if (!selectedcontent)
|
||||
return {};
|
||||
|
||||
// 3. Let option be the first option in select's list of options whose selectedness is true,
|
||||
// if any such option exists, otherwise null.
|
||||
update_cached_list_of_options();
|
||||
GC::Ptr<HTML::HTMLOptionElement> option;
|
||||
for (auto const& candidate : m_cached_list_of_options) {
|
||||
if (candidate->selected()) {
|
||||
option = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. If option is null, then run clear a selectedcontent given selectedcontent.
|
||||
if (!option) {
|
||||
selectedcontent->clear_selectedcontent();
|
||||
return {};
|
||||
}
|
||||
|
||||
// 5. Otherwise, run clone an option into a selectedcontent given option and selectedcontent.
|
||||
TRY(option->clone_into_selectedcontent(*selectedcontent));
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element%3Asuffering-from-being-missing
|
||||
bool HTMLSelectElement::suffering_from_being_missing() const
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue