Skip to main content

script/dom/html/form_controls/
htmlselectelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::default::Default;
6use std::iter;
7
8use dom_struct::dom_struct;
9use embedder_traits::{EmbedderControlRequest, SelectElementRequest};
10use embedder_traits::{SelectElementOption, SelectElementOptionOrOptgroup};
11use html5ever::{local_name, LocalName, Prefix};
12use js::context::{JSContext, NoGC};
13use js::rust::HandleObject;
14use script_bindings::cell::{DomRefCell, Ref};
15use script_bindings::dom::UnrootedDom;
16use style::attr::AttrValue;
17use stylo_dom::ElementState;
18
19use crate::dom::activation::Activatable;
20use crate::dom::element::attributes::storage::AttrRef;
21use crate::dom::iterators::ShadowIncluding;
22use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
23use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
24use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods;
25use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
26use crate::dom::bindings::codegen::Bindings::HTMLOptionsCollectionBinding::HTMLOptionsCollectionMethods;
27use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
28use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
29use crate::dom::bindings::codegen::GenericBindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods;
30use crate::dom::bindings::codegen::GenericBindings::HTMLOptGroupElementBinding::HTMLOptGroupElement_Binding::HTMLOptGroupElementMethods;
31use crate::dom::bindings::codegen::UnionTypes::{
32    HTMLElementOrLong, HTMLOptionElementOrHTMLOptGroupElement,
33};
34use crate::dom::bindings::error::ErrorResult;
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::refcounted::Trusted;
37use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
38use crate::dom::bindings::str::DOMString;
39use crate::dom::characterdata::CharacterData;
40use crate::dom::document::Document;
41use crate::dom::document_embedder_controls::ControlElement;
42use crate::dom::element::{AttributeMutation, Element};
43use crate::dom::event::Event;
44use crate::dom::event::{EventBubbles, EventCancelable, EventComposed};
45use crate::dom::eventtarget::EventTarget;
46use crate::dom::html::htmlcollection::{CollectionFilter, CollectionSource, HTMLCollection};
47use crate::dom::html::htmlelement::HTMLElement;
48use crate::dom::html::htmlformelement::{FormControl, FormDatum, FormDatumValue, HTMLFormElement};
49use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
50use crate::dom::html::htmloptionelement::HTMLOptionElement;
51use crate::dom::html::htmlhrelement::HTMLHRElement;
52use crate::dom::html::htmloptionscollection::HTMLOptionsCollection;
53use crate::dom::node::{ChildrenMutation, Node, NodeTraits,  UnbindContext};
54use crate::dom::nodelist::NodeList;
55use crate::dom::shadowroot::ShadowRoot;
56use crate::dom::shadowroot::ua_shadowroot::{CreateUAShadowRootForSelf, UAShadowRoot};
57use crate::dom::text::Text;
58use crate::dom::types::{FocusEvent, HTMLDataListElement};
59use crate::dom::validation::{is_barred_by_datalist_ancestor, Validatable};
60use crate::dom::validitystate::{ValidationFlags, ValidityState};
61use crate::dom::node::virtualmethods::VirtualMethods;
62
63const DEFAULT_SELECT_SIZE: u32 = 0;
64
65const SELECT_BOX_STYLE: &str = "
66    display: flex;
67    align-items: center;
68    height: 100%;
69    gap: 4px;
70";
71
72const TEXT_CONTAINER_STYLE: &str = "flex: 1;";
73
74const CHEVRON_CONTAINER_STYLE: &str = "
75    background-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"180\" height=\"180\" viewBox=\"0 0 180 180\"> <path d=\"M10 50h160L90 130z\" style=\"fill:currentcolor\"/> </svg>');
76    background-size: 100%;
77    background-repeat: no-repeat;
78    background-position: center;
79
80    vertical-align: middle;
81    line-height: 1;
82    display: inline-block;
83    width: 0.75em;
84    height: 0.75em;
85";
86
87#[derive(JSTraceable, MallocSizeOf)]
88struct OptionsFilter;
89impl CollectionFilter for OptionsFilter {
90    fn filter<'a>(&self, element: &'a Element, root: &'a Node) -> bool {
91        let Some(option) = element.downcast::<HTMLOptionElement>() else {
92            return false;
93        };
94        let Some(select) = option.cached_nearest_ancestor_select() else {
95            return false;
96        };
97        root == select.upcast()
98    }
99}
100
101/// Provides selected options directly via [`HTMLSelectElement::list_of_options`].
102#[derive(JSTraceable, MallocSizeOf)]
103struct SelectedOptionsSource;
104
105impl CollectionSource for SelectedOptionsSource {
106    fn iter<'b>(
107        &'b self,
108        no_gc: &'b NoGC,
109        root: &'b Node,
110    ) -> Box<dyn Iterator<Item = UnrootedDom<'b, Element>> + 'b> {
111        let select = root
112            .downcast::<HTMLSelectElement>()
113            .expect("SelectedOptionsSource must be rooted on an HTMLSelectElement");
114        Box::new(
115            select
116                .list_of_options(no_gc)
117                .filter(|option| option.Selected())
118                .map(UnrootedDom::upcast::<Element>),
119        )
120    }
121}
122
123#[dom_struct]
124pub(crate) struct HTMLSelectElement {
125    htmlelement: HTMLElement,
126    options: MutNullableDom<HTMLOptionsCollection>,
127    selected_options: MutNullableDom<HTMLCollection>,
128    form_owner: MutNullableDom<HTMLFormElement>,
129    labels_node_list: MutNullableDom<NodeList>,
130    validity_state: MutNullableDom<ValidityState>,
131    shadow_tree: DomRefCell<Option<ShadowTree>>,
132}
133
134/// Holds handles to all elements in the UA shadow tree
135#[derive(Clone, JSTraceable, MallocSizeOf)]
136#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
137struct ShadowTree {
138    selected_option: Dom<Text>,
139}
140
141impl UAShadowRoot<ShadowTree> for HTMLSelectElement {
142    fn store_for_shadow_tree(&self, cx: &mut JSContext, shadow_root: DomRoot<ShadowRoot>) {
143        let document = self.owner_document();
144
145        let select_box = self.create_element_in_ua_shadowroot(cx, local_name!("div"));
146        select_box.set_string_attribute(cx, &local_name!("style"), SELECT_BOX_STYLE.into());
147        shadow_root
148            .upcast::<Node>()
149            .AppendChild(cx, select_box.upcast::<Node>())
150            .unwrap();
151
152        let text_container = self.create_element_in_ua_shadowroot(cx, local_name!("div"));
153        text_container.set_string_attribute(cx, &local_name!("style"), TEXT_CONTAINER_STYLE.into());
154        select_box
155            .upcast::<Node>()
156            .AppendChild(cx, text_container.upcast::<Node>())
157            .unwrap();
158
159        let text = Text::new(cx, DOMString::new(), &document);
160        text_container
161            .upcast::<Node>()
162            .AppendChild(cx, text.upcast::<Node>())
163            .unwrap();
164
165        let chevron_container = self.create_element_in_ua_shadowroot(cx, local_name!("div"));
166        chevron_container.set_string_attribute(
167            cx,
168            &local_name!("style"),
169            CHEVRON_CONTAINER_STYLE.into(),
170        );
171        select_box
172            .upcast::<Node>()
173            .AppendChild(cx, chevron_container.upcast::<Node>())
174            .unwrap();
175
176        let _ = self
177            .shadow_tree
178            .safe_borrow_mut(cx.no_gc())
179            .insert(ShadowTree {
180                selected_option: text.as_traced(),
181            });
182    }
183
184    fn borrow_for_shadow_tree(&self) -> Ref<'_, Option<ShadowTree>> {
185        self.shadow_tree.borrow()
186    }
187}
188
189impl HTMLSelectElement {
190    fn new_inherited(
191        local_name: LocalName,
192        prefix: Option<Prefix>,
193        document: &Document,
194    ) -> HTMLSelectElement {
195        HTMLSelectElement {
196            htmlelement: HTMLElement::new_inherited_with_state(
197                ElementState::ENABLED | ElementState::VALID,
198                local_name,
199                prefix,
200                document,
201            ),
202            options: Default::default(),
203            selected_options: Default::default(),
204            form_owner: Default::default(),
205            labels_node_list: Default::default(),
206            validity_state: Default::default(),
207            shadow_tree: Default::default(),
208        }
209    }
210
211    pub(crate) fn new(
212        cx: &mut js::context::JSContext,
213        local_name: LocalName,
214        prefix: Option<Prefix>,
215        document: &Document,
216        proto: Option<HandleObject>,
217    ) -> DomRoot<HTMLSelectElement> {
218        let n = Node::reflect_node_with_proto(
219            cx,
220            Box::new(HTMLSelectElement::new_inherited(
221                local_name, prefix, document,
222            )),
223            document,
224            proto,
225        );
226
227        n.upcast::<Node>().set_weird_parser_insertion_mode();
228        n
229    }
230
231    /// <https://html.spec.whatwg.org/multipage/#concept-select-option-list>
232    pub(crate) fn list_of_options<'b>(
233        &self,
234        no_gc: &'b NoGC,
235    ) -> impl Iterator<Item = UnrootedDom<'b, HTMLOptionElement>> + use<'b> {
236        let self_node = self.upcast::<Node>();
237
238        // Step 1. Let options be « ».
239        let mut iterator = self_node.traverse_preorder_unrooted(no_gc, ShadowIncluding::No);
240
241        // Step 2. Let node be the first child of select in tree order.
242        iterator.next();
243
244        // Step 3. While node is not null:
245        iter::from_fn(move || {
246            loop {
247                let node = iterator.peek()?.clone();
248
249                // Step 3.2. If any of the following conditions are true:
250                //   - node is a select element;
251                //   - node is an hr element;
252                //   - node is an option element;
253                //   - node is a datalist element;
254                //   - node is an optgroup element and node has an ancestor optgroup in between itself
255                //     and select,
256                // then set node to the next descendant of select in tree order, excluding node's
257                // descendants, if any such node exists; otherwise null.
258                if skip_descendants_for_list_of_options(no_gc, &node) {
259                    iterator.next_skipping_children();
260                } else {
261                    // Step 3.3. Otherwise, set node to the next descendant of select in tree order, if any
262                    // such node exists; otherwise null.
263                    iterator.next();
264                }
265
266                // Step 3.1. If node is an option element, then append node to options.
267                if let Some(option) = UnrootedDom::downcast::<HTMLOptionElement>(node) {
268                    return Some(option);
269                }
270            }
271        })
272    }
273
274    /// <https://html.spec.whatwg.org/multipage/#placeholder-label-option>
275    /// > If a select element has a required attribute specified, and has a display size of 1; and
276    /// > if the value of the first option element in the select element's list of options (if any) is
277    /// > the empty string, and that option element does not have an ancestor optgroup in between
278    /// > itself and the select element, then that option is the select element's placeholder label
279    /// > option.
280    /// >
281    /// > If a select element has a required attribute specified, does not have a multiple attribute
282    /// > specified, and has a display size of 1, then the select element must have a placeholder label
283    /// > option.
284    fn get_placeholder_label_option(&self, no_gc: &NoGC) -> Option<DomRoot<HTMLOptionElement>> {
285        if self.Required() && !self.Multiple() && self.display_size() == 1 {
286            self.list_of_options(no_gc)
287                .next()
288                .filter(|node| {
289                    node.Value().is_empty() &&
290                        !has_ancestor_optgroup_between_self_and_select(no_gc, node.upcast())
291                })
292                .map(|node| node.as_rooted())
293        } else {
294            None
295        }
296    }
297
298    /// <https://html.spec.whatwg.org/multipage/#the-select-element:concept-form-reset-control>
299    pub(crate) fn reset(&self, no_gc: &NoGC) {
300        for opt in self.list_of_options(no_gc) {
301            opt.set_selectedness(no_gc, opt.DefaultSelected());
302            opt.set_dirtiness(false);
303        }
304        self.selectedness_setting_algorithm(no_gc);
305    }
306
307    /// <https://html.spec.whatwg.org/multipage/#selectedness-setting-algorithm>
308    pub(crate) fn selectedness_setting_algorithm(&self, no_gc: &NoGC) -> bool {
309        // Step 1. If select has the multiple attribute, then return false.
310        if self.Multiple() {
311            return false;
312        }
313
314        // Step 2. Let hasChanged be false.
315        let mut has_changed = false;
316        // Step 3. Let firstEnabledOption be null.
317        let mut first_enabled: Option<DomRoot<HTMLOptionElement>> = None;
318        // Step 4. Let lastSelectedOption be null.
319        let mut last_selected: Option<DomRoot<HTMLOptionElement>> = None;
320
321        // Step 5. For each option of select's list of options:
322        for option in self.list_of_options(no_gc) {
323            // Step 5.1. If option's selectedness is true:
324            if option.Selected() {
325                // Step 5.1.1. If lastSelectedOption is not null:
326                if let Some(last_selected) = &last_selected {
327                    // Step 5.1.1.1. Set lastSelectedOption's selectedness to false.
328                    last_selected.set_selectedness(no_gc, false);
329                    // Step 5.1.1.2. Set hasChanged to true.
330                    has_changed = true;
331                }
332                // Step 5.1.2. Set lastSelectedOption to option.
333                last_selected = Some(option.as_rooted());
334            }
335            // Step 5.2 If firstEnabledOption is null and option is not disabled, then set
336            // firstEnabledOption to option.
337            let element = option.upcast::<Element>();
338            if first_enabled.is_none() && !element.disabled_state() {
339                first_enabled = Some(option.as_rooted());
340            }
341        }
342
343        // Step 6. If lastSelectedOption is null, firstEnabledOption is not null, and select's
344        // display size is 1:
345        if last_selected.is_none() &&
346            let Some(first_enabled) = first_enabled &&
347            self.display_size() == 1
348        {
349            // Step 6.1. Set firstEnabledOption's selectedness to true.
350            first_enabled.set_selectedness(no_gc, true);
351            // Step 6.2 Set hasChanged to true.
352            has_changed = true;
353        }
354
355        // Step 7. Return hasChanged.
356        has_changed
357    }
358
359    pub(crate) fn push_form_data(&self, no_gc: &NoGC, data_set: &mut Vec<FormDatum>) {
360        if self.Name().is_empty() {
361            return;
362        }
363        for opt in self.list_of_options(no_gc) {
364            let element = opt.upcast::<Element>();
365            if opt.Selected() && element.enabled_state() {
366                data_set.push(FormDatum {
367                    ty: self.Type(),
368                    name: self.Name(),
369                    value: FormDatumValue::String(opt.Value()),
370                });
371            }
372        }
373    }
374
375    /// <https://html.spec.whatwg.org/multipage/#concept-select-pick>
376    pub(crate) fn pick_option(&self, no_gc: &NoGC, picked: &HTMLOptionElement) {
377        if !self.Multiple() {
378            let picked = picked.upcast();
379            for opt in self.list_of_options(no_gc) {
380                if opt.upcast::<HTMLElement>() != picked {
381                    opt.set_selectedness(no_gc, false);
382                }
383            }
384        }
385    }
386
387    /// <https://html.spec.whatwg.org/multipage/#concept-select-size>
388    fn display_size(&self) -> u32 {
389        if self.Size() == 0 {
390            if self.Multiple() { 4 } else { 1 }
391        } else {
392            self.Size()
393        }
394    }
395
396    pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext) {
397        let shadow_tree = self.shadow_tree(cx);
398
399        let selected_options = self.selected_options(cx.no_gc());
400        let selected_options_count = selected_options.len();
401
402        let displayed_text = if selected_options_count == 1 {
403            let first_selected_option = self
404                .selected_option(cx.no_gc())
405                .or_else(|| self.list_of_options(cx.no_gc()).next());
406
407            let first_selected_option_text = first_selected_option
408                .map(|option| option.displayed_label())
409                .unwrap_or_default();
410
411            // Replace newlines with whitespace, then collapse and trim whitespace
412            itertools::join(first_selected_option_text.str().split_whitespace(), " ")
413        } else {
414            format!("{selected_options_count} selected")
415        };
416
417        shadow_tree
418            .selected_option
419            .upcast::<CharacterData>()
420            .SetData(cx, displayed_text.trim().into());
421    }
422
423    pub(crate) fn selected_option<'b>(
424        &self,
425        no_gc: &'b NoGC,
426    ) -> Option<UnrootedDom<'b, HTMLOptionElement>> {
427        self.list_of_options(no_gc)
428            .find(|opt_elem| opt_elem.Selected())
429            .or_else(|| self.list_of_options(no_gc).next())
430    }
431
432    pub(crate) fn selected_options<'b>(
433        &self,
434        no_gc: &'b NoGC,
435    ) -> Vec<UnrootedDom<'b, HTMLOptionElement>> {
436        self.list_of_options(no_gc)
437            .filter(|opt_elem| opt_elem.Selected())
438            .collect()
439    }
440
441    pub(crate) fn show_menu(&self, cx: &mut JSContext) {
442        // Collect list of optgroups and options
443        let mut index = 0;
444        let mut embedder_option_from_option = |option: &HTMLOptionElement| {
445            let embedder_option = SelectElementOption {
446                id: index,
447                label: option.displayed_label().into(),
448                is_disabled: option.Disabled(),
449            };
450            index += 1;
451            embedder_option
452        };
453        let options = self
454            .upcast::<Node>()
455            .children()
456            .flat_map(|child| {
457                if let Some(option) = child.downcast::<HTMLOptionElement>() {
458                    return Some(embedder_option_from_option(option).into());
459                }
460
461                if let Some(optgroup) = child.downcast::<HTMLOptGroupElement>() {
462                    let options = optgroup
463                        .upcast::<Node>()
464                        .children()
465                        .flat_map(DomRoot::downcast::<HTMLOptionElement>)
466                        .map(|option| embedder_option_from_option(&option))
467                        .collect();
468                    let label = optgroup.Label().into();
469
470                    return Some(SelectElementOptionOrOptgroup::Optgroup { label, options });
471                }
472
473                None
474            })
475            .collect();
476
477        let selected_options = self
478            .list_of_options(cx.no_gc())
479            .enumerate()
480            .filter(|(_, option)| option.Selected())
481            .map(|(index, _)| index)
482            .collect();
483
484        self.owner_document()
485            .embedder_controls()
486            .show_embedder_control(
487                cx,
488                ControlElement::Select(Dom::from_ref(self)),
489                EmbedderControlRequest::SelectElement(SelectElementRequest {
490                    options,
491                    selected_options,
492                    allow_select_multiple: self.Multiple(),
493                }),
494                None,
495            );
496        self.upcast::<Element>().set_open_state(cx.no_gc(), true);
497    }
498
499    pub(crate) fn handle_embedder_response(&self, cx: &mut JSContext, selected_values: Vec<usize>) {
500        self.upcast::<Element>().set_open_state(cx.no_gc(), false);
501
502        let selected_values = if self.Multiple() {
503            selected_values
504        } else {
505            selected_values.into_iter().take(1).collect()
506        };
507
508        let mut selection_did_change = false;
509        for (index, option) in self.list_of_options(cx.no_gc()).enumerate() {
510            let should_be_selected = selected_values.contains(&index);
511            let option_selected_did_change = option.Selected() != should_be_selected;
512
513            if option_selected_did_change {
514                selection_did_change = true;
515            }
516
517            option.set_selectedness(cx.no_gc(), should_be_selected);
518
519            if option_selected_did_change {
520                option.set_dirtiness(true);
521            }
522        }
523
524        if selection_did_change {
525            self.update_shadow_tree(cx);
526            self.send_update_notifications();
527        }
528    }
529
530    fn multiple_attribute_mutated(&self, cx: &mut JSContext, mutation: AttributeMutation) {
531        if mutation.is_removal() {
532            let mut first_enabled: Option<DomRoot<HTMLOptionElement>> = None;
533            let mut first_selected: Option<DomRoot<HTMLOptionElement>> = None;
534
535            for option in self.list_of_options(cx.no_gc()) {
536                if first_selected.is_none() && option.Selected() {
537                    first_selected = Some(DomRoot::from_ref(&option));
538                }
539                option.set_selectedness(cx.no_gc(), false);
540                let element = option.upcast::<Element>();
541                if first_enabled.is_none() && !element.disabled_state() {
542                    first_enabled = Some(DomRoot::from_ref(&option));
543                }
544            }
545
546            if let Some(first_selected) = first_selected {
547                first_selected.set_selectedness(cx.no_gc(), true);
548            } else if self.display_size() == 1 &&
549                let Some(first_enabled) = first_enabled
550            {
551                first_enabled.set_selectedness(cx.no_gc(), true);
552            }
553
554            self.update_shadow_tree(cx);
555        }
556    }
557
558    /// <https://html.spec.whatwg.org/multipage/#send-select-update-notifications>
559    fn send_update_notifications(&self) {
560        // > When the user agent is to send select update notifications, queue an element task on the
561        // > user interaction task source given the select element to run these steps:
562        let this = Trusted::new(self);
563        self.owner_global()
564            .task_manager()
565            .user_interaction_task_source()
566            .queue(task!(send_select_update_notification: move |cx| {
567                let this = this.root();
568
569                // TODO: Step 1. Set the select element's user validity to true.
570
571                // Step 2. Fire an event named input at the select element, with the bubbles and composed
572                // attributes initialized to true.
573                this.upcast::<EventTarget>()
574                    .fire_event_with_params(
575                        cx,
576                        atom!("input"),
577                        EventBubbles::Bubbles,
578                        EventCancelable::NotCancelable,
579                        EventComposed::Composed,
580                    );
581
582                // Step 3. Fire an event named change at the select element, with the bubbles attribute initialized
583                // to true.
584                this.upcast::<EventTarget>()
585                    .fire_bubbling_event(cx, atom!("change"));
586            }));
587    }
588
589    fn may_have_embedder_control(&self) -> bool {
590        let el = self.upcast::<Element>();
591        !el.disabled_state()
592    }
593
594    /// <https://html.spec.whatwg.org/multipage/#select-enabled-selectedcontent>
595    pub(crate) fn get_enabled_selectedcontent(&self) -> Option<DomRoot<Element>> {
596        // Step 1. If select has the multiple attribute, then return null.
597        if self.Multiple() {
598            return None;
599        }
600
601        // Step 2. Let selectedcontent be the first selectedcontent element descendant
602        // of select in tree order if any such element exists; otherwise return null.
603        // TODO: Step 3. If selectedcontent's disabled is true, then return null.
604        // NOTE: We don't actually implement selectedcontent yet
605        // Step 4. Return selectedcontent.
606        self.upcast::<Node>()
607            .traverse_preorder(ShadowIncluding::No)
608            .skip(1)
609            .filter_map(DomRoot::downcast::<Element>)
610            .find(|element| element.local_name() == &local_name!("selectedcontent"))
611    }
612
613    /// <https://html.spec.whatwg.org/multipage/#update-a-select's-descendant-selectedcontent-elements>
614    pub(crate) fn update_a_selects_descendant_selectedcontent_elements(&self, cx: &mut JSContext) {
615        // Step 1. If select has the multiple attribute, then return.
616        if self.Multiple() {
617            return;
618        }
619
620        // Step 2. Let descendantSelectedcontents be a list of select's descendants, in tree order,
621        // that are selectedcontent elements whose disabled is false.
622        let descendant_selected_contents = self.get_enabled_selectedcontent();
623
624        // Step 3. For each selectedcontent of descendantSelectedcontents: update a selectedcontent
625        // given select and selectedcontent.
626        //
627        // Note: This is a bit different than the specification, because we do not yet support
628        // `selectedcontent`.
629        if let Some(selected_content) = descendant_selected_contents {
630            self.update_a_selected_content(cx, &selected_content);
631        }
632    }
633
634    /// <https://html.spec.whatwg.org/multipage/#update-a-selectedcontent>
635    fn update_a_selected_content(&self, cx: &mut JSContext, selected_content: &Element) {
636        // Step 1. If selectedcontent's disabled is true, then return.
637        // TODO: We do not yet calculate the selectedcontent's disabledness and it is
638        // different than the form control disabled state.
639
640        // Step 2. Let option be the first option in select's list of options whose selectedness is
641        // true, if any such option exists; otherwise null.
642        let option = self
643            .list_of_options(cx.no_gc())
644            .find(|option| option.Selected())
645            .map(|option| option.as_rooted());
646
647        match option {
648            None => {
649                // Step 3. If option is null, then clear a selectedcontent given selectedcontent.
650                // This is <https://html.spec.whatwg.org/multipage/#clear-a-selectedcontent>
651                // Step 1. Replace all with null within selectedcontent.
652                Node::replace_all(cx, None, selected_content.upcast());
653            },
654            Some(option) => {
655                // Step 4. Otherwise, clone an option into a selectedcontent given option and
656                // selectedcontent.
657                option.clone_an_option_into_selectedcontent(cx, selected_content);
658            },
659        }
660    }
661}
662
663impl HTMLSelectElementMethods<crate::DomTypeHolder> for HTMLSelectElement {
664    /// <https://html.spec.whatwg.org/multipage/#dom-select-add>
665    fn Add(
666        &self,
667        cx: &mut JSContext,
668        element: HTMLOptionElementOrHTMLOptGroupElement,
669        before: Option<HTMLElementOrLong>,
670    ) -> ErrorResult {
671        self.Options(cx).Add(cx, element, before)
672    }
673
674    // https://html.spec.whatwg.org/multipage/#dom-fe-disabled
675    make_bool_getter!(Disabled, "disabled");
676
677    // https://html.spec.whatwg.org/multipage/#dom-fe-disabled
678    make_bool_setter!(SetDisabled, "disabled");
679
680    /// <https://html.spec.whatwg.org/multipage/#dom-fae-form>
681    fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> {
682        self.form_owner()
683    }
684
685    // https://html.spec.whatwg.org/multipage/#dom-select-multiple
686    make_bool_getter!(Multiple, "multiple");
687
688    // https://html.spec.whatwg.org/multipage/#dom-select-multiple
689    make_bool_setter!(SetMultiple, "multiple");
690
691    // https://html.spec.whatwg.org/multipage/#dom-fe-name
692    make_getter!(Name, "name");
693
694    // https://html.spec.whatwg.org/multipage/#dom-fe-name
695    make_atomic_setter!(SetName, "name");
696
697    // https://html.spec.whatwg.org/multipage/#dom-select-required
698    make_bool_getter!(Required, "required");
699
700    // https://html.spec.whatwg.org/multipage/#dom-select-required
701    make_bool_setter!(SetRequired, "required");
702
703    // https://html.spec.whatwg.org/multipage/#dom-select-size
704    make_uint_getter!(Size, "size", DEFAULT_SELECT_SIZE);
705
706    // https://html.spec.whatwg.org/multipage/#dom-select-size
707    make_uint_setter!(SetSize, "size", DEFAULT_SELECT_SIZE);
708
709    /// <https://html.spec.whatwg.org/multipage/#dom-select-type>
710    fn Type(&self) -> DOMString {
711        DOMString::from(if self.Multiple() {
712            "select-multiple"
713        } else {
714            "select-one"
715        })
716    }
717
718    // https://html.spec.whatwg.org/multipage/#dom-lfe-labels
719    make_labels_getter!(Labels, labels_node_list);
720
721    /// <https://html.spec.whatwg.org/multipage/#dom-select-options>
722    fn Options(&self, cx: &mut JSContext) -> DomRoot<HTMLOptionsCollection> {
723        self.options.or_init(|| {
724            let window = self.owner_window();
725            HTMLOptionsCollection::new(cx, &window, self, Box::new(OptionsFilter))
726        })
727    }
728
729    /// <https://html.spec.whatwg.org/multipage/#dom-select-selectedoptions>
730    fn SelectedOptions(&self, cx: &mut JSContext) -> DomRoot<HTMLCollection> {
731        self.selected_options.or_init(|| {
732            let window = self.owner_window();
733            HTMLCollection::new_with_source(
734                cx,
735                &window,
736                self.upcast(),
737                Box::new(SelectedOptionsSource),
738            )
739        })
740    }
741
742    /// <https://html.spec.whatwg.org/multipage/#dom-select-length>
743    fn Length(&self, cx: &mut JSContext) -> u32 {
744        self.Options(cx).Length(cx)
745    }
746
747    /// <https://html.spec.whatwg.org/multipage/#dom-select-length>
748    fn SetLength(&self, cx: &mut JSContext, length: u32) {
749        self.Options(cx).SetLength(cx, length)
750    }
751
752    /// <https://html.spec.whatwg.org/multipage/#dom-select-item>
753    fn Item(&self, cx: &mut JSContext, index: u32) -> Option<DomRoot<Element>> {
754        self.Options(cx).upcast().Item(cx, index)
755    }
756
757    /// <https://html.spec.whatwg.org/multipage/#dom-select-item>
758    fn IndexedGetter(&self, cx: &mut JSContext, index: u32) -> Option<DomRoot<Element>> {
759        self.Options(cx).IndexedGetter(cx, index)
760    }
761
762    /// <https://html.spec.whatwg.org/multipage/#dom-select-setter>
763    fn IndexedSetter(
764        &self,
765        cx: &mut JSContext,
766        index: u32,
767        value: Option<&HTMLOptionElement>,
768    ) -> ErrorResult {
769        self.Options(cx).IndexedSetter(cx, index, value)
770    }
771
772    /// <https://html.spec.whatwg.org/multipage/#dom-select-nameditem>
773    fn NamedItem(&self, cx: &mut JSContext, name: DOMString) -> Option<DomRoot<HTMLOptionElement>> {
774        self.Options(cx)
775            .NamedGetter(cx, name)
776            .and_then(DomRoot::downcast::<HTMLOptionElement>)
777    }
778
779    /// <https://html.spec.whatwg.org/multipage/#dom-select-remove>
780    fn Remove_(&self, cx: &mut JSContext, index: i32) {
781        self.Options(cx).Remove(cx, index)
782    }
783
784    /// <https://html.spec.whatwg.org/multipage/#dom-select-remove>
785    fn Remove(&self, cx: &mut JSContext) {
786        self.upcast::<Element>().Remove(cx)
787    }
788
789    /// <https://html.spec.whatwg.org/multipage/#dom-select-value>
790    fn Value(&self, cx: &JSContext) -> DOMString {
791        self.list_of_options(cx.no_gc())
792            .find(|opt_elem| opt_elem.Selected())
793            .map(|opt_elem| opt_elem.Value())
794            .unwrap_or_default()
795    }
796
797    /// <https://html.spec.whatwg.org/multipage/#dom-select-value>
798    fn SetValue(&self, cx: &mut JSContext, value: DOMString) {
799        let mut opt_iter = self.list_of_options(cx.no_gc());
800        // Reset until we find an <option> with a matching value
801        for opt in opt_iter.by_ref() {
802            if opt.Value() == value {
803                opt.set_selectedness(cx.no_gc(), true);
804                opt.set_dirtiness(true);
805                break;
806            }
807            opt.set_selectedness(cx.no_gc(), false);
808        }
809        // Reset remaining <option> elements
810        for opt in opt_iter {
811            opt.set_selectedness(cx.no_gc(), false);
812        }
813
814        self.validity_state(cx)
815            .perform_validation_and_update(cx, ValidationFlags::VALUE_MISSING);
816
817        self.update_shadow_tree(cx);
818    }
819
820    /// <https://html.spec.whatwg.org/multipage/#dom-select-selectedindex>
821    fn SelectedIndex(&self, cx: &JSContext) -> i32 {
822        self.list_of_options(cx.no_gc())
823            .enumerate()
824            .filter(|(_, opt_elem)| opt_elem.Selected())
825            .map(|(i, _)| i as i32)
826            .next()
827            .unwrap_or(-1)
828    }
829
830    /// <https://html.spec.whatwg.org/multipage/#dom-select-selectedindex>
831    fn SetSelectedIndex(&self, cx: &mut JSContext, index: i32) {
832        let mut selection_did_change = false;
833
834        {
835            let mut opt_iter = self.list_of_options(cx.no_gc());
836            for opt in opt_iter.by_ref().take(index as usize) {
837                selection_did_change |= opt.Selected();
838                opt.set_selectedness(cx.no_gc(), false);
839            }
840            if let Some(selected_option) = opt_iter.next() {
841                selection_did_change |= !selected_option.Selected();
842                selected_option.set_selectedness(cx.no_gc(), true);
843                selected_option.set_dirtiness(true);
844
845                // Reset remaining <option> elements
846                for opt in opt_iter {
847                    selection_did_change |= opt.Selected();
848                    opt.set_selectedness(cx.no_gc(), false);
849                }
850            }
851        }
852
853        if selection_did_change {
854            self.update_shadow_tree(cx);
855        }
856    }
857
858    /// <https://html.spec.whatwg.org/multipage/#dom-cva-willvalidate>
859    fn WillValidate(&self, no_gc: &NoGC) -> bool {
860        self.is_instance_validatable(no_gc)
861    }
862
863    /// <https://html.spec.whatwg.org/multipage/#dom-cva-validity>
864    fn Validity(&self, cx: &mut JSContext) -> DomRoot<ValidityState> {
865        self.validity_state(cx)
866    }
867
868    /// <https://html.spec.whatwg.org/multipage/#dom-cva-checkvalidity>
869    fn CheckValidity(&self, cx: &mut JSContext) -> bool {
870        self.check_validity(cx)
871    }
872
873    /// <https://html.spec.whatwg.org/multipage/#dom-cva-reportvalidity>
874    fn ReportValidity(&self, cx: &mut JSContext) -> bool {
875        self.report_validity(cx)
876    }
877
878    /// <https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage>
879    fn ValidationMessage(&self, cx: &mut JSContext) -> DOMString {
880        self.validation_message(cx)
881    }
882
883    /// <https://html.spec.whatwg.org/multipage/#dom-cva-setcustomvalidity>
884    fn SetCustomValidity(&self, cx: &mut JSContext, error: DOMString) {
885        self.validity_state(cx).set_custom_error_message(cx, error);
886    }
887}
888
889impl VirtualMethods for HTMLSelectElement {
890    fn super_type(&self) -> Option<&dyn VirtualMethods> {
891        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
892    }
893
894    fn attribute_mutated(
895        &self,
896        cx: &mut js::context::JSContext,
897        attr: AttrRef<'_>,
898        mutation: AttributeMutation,
899    ) {
900        let could_have_had_embedder_control = self.may_have_embedder_control();
901        self.super_type()
902            .unwrap()
903            .attribute_mutated(cx, attr, mutation);
904        match *attr.local_name() {
905            local_name!("multiple") => {
906                self.multiple_attribute_mutated(cx, mutation);
907            },
908            local_name!("required") => {
909                self.validity_state(cx)
910                    .perform_validation_and_update(cx, ValidationFlags::VALUE_MISSING);
911            },
912            local_name!("disabled") => {
913                self.validity_state(cx)
914                    .perform_validation_and_update(cx, ValidationFlags::VALUE_MISSING);
915            },
916            local_name!("form") => {
917                self.form_attribute_mutated(cx, mutation);
918            },
919            _ => {},
920        }
921        if could_have_had_embedder_control && !self.may_have_embedder_control() {
922            self.owner_document()
923                .embedder_controls()
924                .hide_embedder_control(self.upcast());
925        }
926    }
927
928    fn unbind_from_tree(&self, cx: &mut JSContext, context: &UnbindContext) {
929        self.super_type().unwrap().unbind_from_tree(cx, context);
930        self.owner_document_unrooted(cx.no_gc())
931            .embedder_controls()
932            .hide_embedder_control(self.upcast());
933    }
934
935    fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
936        if let Some(s) = self.super_type() {
937            s.children_changed(cx, mutation);
938        }
939
940        self.update_shadow_tree(cx);
941    }
942
943    fn parse_plain_attribute(&self, local_name: &LocalName, value: DOMString) -> AttrValue {
944        match *local_name {
945            local_name!("size") => AttrValue::from_u32(value.into(), DEFAULT_SELECT_SIZE),
946            _ => self
947                .super_type()
948                .unwrap()
949                .parse_plain_attribute(local_name, value),
950        }
951    }
952
953    fn handle_event(&self, cx: &mut js::context::JSContext, event: &Event) {
954        self.super_type().unwrap().handle_event(cx, event);
955        if let Some(event) = event.downcast::<FocusEvent>() &&
956            *event.upcast::<Event>().type_() != *"blur"
957        {
958            self.owner_document()
959                .embedder_controls()
960                .hide_embedder_control(self.upcast());
961        }
962    }
963}
964
965impl FormControl for HTMLSelectElement {
966    fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> {
967        self.form_owner.get()
968    }
969
970    fn set_form_owner(&self, _cx: &mut JSContext, form: Option<&HTMLFormElement>) {
971        self.form_owner.set(form);
972    }
973
974    fn to_html_element(&self) -> &HTMLElement {
975        self.upcast::<HTMLElement>()
976    }
977}
978
979impl Validatable for HTMLSelectElement {
980    fn as_element(&self) -> &Element {
981        self.upcast()
982    }
983
984    fn validity_state(&self, cx: &mut JSContext) -> DomRoot<ValidityState> {
985        self.validity_state
986            .or_init(|| ValidityState::new(cx, &self.owner_window(), self.upcast()))
987    }
988
989    fn is_instance_validatable(&self, no_gc: &NoGC) -> bool {
990        // https://html.spec.whatwg.org/multipage/#enabling-and-disabling-form-controls%3A-the-disabled-attribute%3Abarred-from-constraint-validation
991        // https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation
992        !self.upcast::<Element>().disabled_state() &&
993            !is_barred_by_datalist_ancestor(no_gc, self.upcast())
994    }
995
996    fn perform_validation(
997        &self,
998        cx: &mut JSContext,
999        validate_flags: ValidationFlags,
1000    ) -> ValidationFlags {
1001        let mut failed_flags = ValidationFlags::empty();
1002
1003        // https://html.spec.whatwg.org/multipage/#suffering-from-being-missing
1004        // https://html.spec.whatwg.org/multipage/#the-select-element%3Asuffering-from-being-missing
1005        if validate_flags.contains(ValidationFlags::VALUE_MISSING) && self.Required() {
1006            let placeholder = self.get_placeholder_label_option(cx.no_gc());
1007            let is_value_missing = !self.list_of_options(cx.no_gc()).any(|e| {
1008                e.Selected() &&
1009                    placeholder
1010                        .as_ref()
1011                        .map(|placeholder| **placeholder != **e)
1012                        .unwrap_or(true)
1013            });
1014            failed_flags.set(ValidationFlags::VALUE_MISSING, is_value_missing);
1015        }
1016
1017        failed_flags
1018    }
1019}
1020
1021impl Activatable for HTMLSelectElement {
1022    fn as_element(&self) -> &Element {
1023        self.upcast()
1024    }
1025
1026    fn is_instance_activatable(&self) -> bool {
1027        !self.upcast::<Element>().disabled_state()
1028    }
1029
1030    fn activation_behavior(
1031        &self,
1032        cx: &mut js::context::JSContext,
1033        event: &Event,
1034        _target: &EventTarget,
1035    ) {
1036        if !event.IsTrusted() {
1037            return;
1038        }
1039
1040        self.show_menu(cx);
1041    }
1042}
1043
1044/// Implements Step 3.2 of <https://html.spec.whatwg.org/multipage/#concept-select-option-list>.
1045fn skip_descendants_for_list_of_options(no_gc: &NoGC, node: &Node) -> bool {
1046    // Step 3.2. If any of the following conditions are true:
1047    //   - node is a select element;
1048    //   - node is an hr element;
1049    //   - node is an option element;
1050    //   - node is a datalist element;
1051    if node.is::<HTMLSelectElement>() ||
1052        node.is::<HTMLHRElement>() ||
1053        node.is::<HTMLOptionElement>() ||
1054        node.is::<HTMLDataListElement>()
1055    {
1056        return true;
1057    }
1058
1059    //   - node is an optgroup element and node has an ancestor optgroup in between itself
1060    //     and select,
1061    //
1062    // We shouldn't recurse into them because of the check above and `select` here is the
1063    // originating select element of this call.
1064    if node.is::<HTMLOptGroupElement>() &&
1065        has_ancestor_optgroup_between_self_and_select(no_gc, node)
1066    {
1067        return true;
1068    }
1069
1070    false
1071}
1072
1073fn has_ancestor_optgroup_between_self_and_select(no_gc: &NoGC, node: &Node) -> bool {
1074    for ancestor in node.ancestors_unrooted(no_gc) {
1075        if ancestor.is::<HTMLSelectElement>() {
1076            return false;
1077        }
1078        if ancestor.is::<HTMLOptGroupElement>() {
1079            return true;
1080        }
1081    }
1082    false
1083}