script: Implement HTMLSelectElement.selectedOptions (#43017)

This introduces a `CollectionSource` trait as an alternative to
`CollectionFilter` in `HTMLCollection`, allowing collections to provide
elements directly via a custom iterator rather than filtering a full
subtree traversal, which would otherwise be rather inefficient for
smaller iterable sequences of options that can be determined without
traversing the whole subtree again.

The newly implemented `selectedOptions` attribute on the `<select>`
element uses this to iterate only the element's list of options that are
marked as selected.

Testing: 14 assertions in the existing WPT went from failing to passing.

Fixes #15522.

---------

Signed-off-by: Jacob Adam <software@jacobadam.net>
This commit is contained in:
Jacob Adam W.
2026-03-07 06:16:33 +00:00
committed by GitHub
parent 42d228dcc7
commit d6fdd30c19
10 changed files with 250 additions and 141 deletions

View File

@@ -39,7 +39,7 @@ use crate::dom::document::Document;
use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator};
use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget;
use crate::dom::html::htmlcollection::CollectionFilter;
use crate::dom::html::htmlcollection::{CollectionFilter, CollectionSource, HTMLCollection};
use crate::dom::html::htmlelement::HTMLElement;
use crate::dom::html::htmlfieldsetelement::HTMLFieldSetElement;
use crate::dom::html::htmlformelement::{FormControl, FormDatum, FormDatumValue, HTMLFormElement};
@@ -90,10 +90,29 @@ impl CollectionFilter for OptionsFilter {
}
}
/// Provides selected options directly via [`HTMLSelectElement::list_of_options`],
/// avoiding a full subtree traversal.
#[derive(JSTraceable, MallocSizeOf)]
struct SelectedOptionsSource;
impl CollectionSource for SelectedOptionsSource {
fn iter<'a>(&'a self, root: &'a Node) -> Box<dyn Iterator<Item = DomRoot<Element>> + 'a> {
let select = root
.downcast::<HTMLSelectElement>()
.expect("SelectedOptionsSource must be rooted on an HTMLSelectElement");
Box::new(
select
.list_of_options()
.filter(|option| option.Selected())
.map(DomRoot::upcast::<Element>),
)
}
}
#[dom_struct]
pub(crate) struct HTMLSelectElement {
htmlelement: HTMLElement,
options: MutNullableDom<HTMLOptionsCollection>,
selected_options: MutNullableDom<HTMLCollection>,
form_owner: MutNullableDom<HTMLFormElement>,
labels_node_list: MutNullableDom<NodeList>,
validity_state: MutNullableDom<ValidityState>,
@@ -121,6 +140,7 @@ impl HTMLSelectElement {
document,
),
options: Default::default(),
selected_options: Default::default(),
form_owner: Default::default(),
labels_node_list: Default::default(),
validity_state: Default::default(),
@@ -534,6 +554,19 @@ impl HTMLSelectElementMethods<crate::DomTypeHolder> for HTMLSelectElement {
})
}
/// <https://html.spec.whatwg.org/multipage/#dom-select-selectedoptions>
fn SelectedOptions(&self) -> DomRoot<HTMLCollection> {
self.selected_options.or_init(|| {
let window = self.owner_window();
HTMLCollection::new_with_source(
&window,
self.upcast(),
Box::new(SelectedOptionsSource),
CanGc::note(),
)
})
}
/// <https://html.spec.whatwg.org/multipage/#dom-select-length>
fn Length(&self) -> u32 {
self.Options().Length()