Skip to main content

script/dom/html/form_controls/
htmloptionelement.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::cell::Cell;
6use std::convert::TryInto;
7
8use dom_struct::dom_struct;
9use html5ever::{LocalName, Prefix, QualName, local_name, ns};
10use js::context::{JSContext, NoGC};
11use js::rust::HandleObject;
12use style::str::{split_html_space_chars, str_join};
13use stylo_dom::ElementState;
14
15use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods;
16use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
17use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElement_Binding::HTMLSelectElementMethods;
18use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
19use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
20use crate::dom::bindings::error::Fallible;
21use crate::dom::bindings::inheritance::Castable;
22use crate::dom::bindings::root::DomRoot;
23use crate::dom::bindings::str::DOMString;
24use crate::dom::characterdata::CharacterData;
25use crate::dom::document::Document;
26use crate::dom::element::attributes::storage::AttrRef;
27use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator};
28use crate::dom::html::htmlelement::HTMLElement;
29use crate::dom::html::htmlformelement::HTMLFormElement;
30use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement;
31use crate::dom::html::htmlscriptelement::HTMLScriptElement;
32use crate::dom::html::htmlselectelement::HTMLSelectElement;
33use crate::dom::iterators::ShadowIncluding;
34use crate::dom::node::virtualmethods::VirtualMethods;
35use crate::dom::node::{
36    BindContext, ChildrenMutation, CloneChildrenFlag, MoveContext, Node, NodeTraits, UnbindContext,
37};
38use crate::dom::text::Text;
39use crate::dom::types::DocumentFragment;
40use crate::dom::validation::Validatable;
41use crate::dom::validitystate::ValidationFlags;
42use crate::dom::window::Window;
43
44#[dom_struct]
45pub(crate) struct HTMLOptionElement {
46    htmlelement: HTMLElement,
47
48    /// <https://html.spec.whatwg.org/multipage/#attr-option-selected>
49    selectedness: Cell<bool>,
50
51    /// <https://html.spec.whatwg.org/multipage/#concept-option-dirtiness>
52    dirtiness: Cell<bool>,
53}
54
55impl HTMLOptionElement {
56    fn new_inherited(
57        local_name: LocalName,
58        prefix: Option<Prefix>,
59        document: &Document,
60    ) -> HTMLOptionElement {
61        HTMLOptionElement {
62            htmlelement: HTMLElement::new_inherited_with_state(
63                ElementState::ENABLED,
64                local_name,
65                prefix,
66                document,
67            ),
68            selectedness: Cell::new(false),
69            dirtiness: Cell::new(false),
70        }
71    }
72
73    pub(crate) fn new(
74        cx: &mut js::context::JSContext,
75        local_name: LocalName,
76        prefix: Option<Prefix>,
77        document: &Document,
78        proto: Option<HandleObject>,
79    ) -> DomRoot<HTMLOptionElement> {
80        Node::reflect_node_with_proto(
81            cx,
82            Box::new(HTMLOptionElement::new_inherited(
83                local_name, prefix, document,
84            )),
85            document,
86            proto,
87        )
88    }
89
90    pub(crate) fn set_selectedness(&self, no_gc: &NoGC, selected: bool) {
91        self.selectedness.set(selected);
92        self.upcast::<Element>()
93            .set_state(ElementState::CHECKED, selected);
94        // Bump the tree version so that any live HTMLCollection (e.g. selectedOptions)
95        // rooted at an ancestor invalidates its cached length and cursor.
96        self.upcast::<Node>().rev_version(no_gc);
97    }
98
99    pub(crate) fn set_dirtiness(&self, dirtiness: bool) {
100        self.dirtiness.set(dirtiness);
101    }
102
103    fn pick_if_selected_and_reset(&self, cx: &mut JSContext) {
104        if let Some(select) = self.owner_select_element() {
105            if self.Selected() {
106                select.pick_option(cx.no_gc(), self);
107                select.update_shadow_tree(cx);
108            }
109            select.ask_for_reset(cx.no_gc());
110        }
111    }
112
113    /// <https://html.spec.whatwg.org/multipage/#concept-option-index>
114    fn index(&self, no_gc: &NoGC) -> i32 {
115        let Some(owner_select) = self.owner_select_element() else {
116            return 0;
117        };
118
119        let Some(position) = owner_select.list_of_options(no_gc).position(|n| *n == self) else {
120            // An option should always be in it's owner's list of options, but it's not worth a browser panic
121            warn!("HTMLOptionElement called index_in_select at a select that did not contain it");
122            return 0;
123        };
124
125        position.try_into().unwrap_or(0)
126    }
127
128    fn owner_select_element(&self) -> Option<DomRoot<HTMLSelectElement>> {
129        let parent = self.upcast::<Node>().GetParentNode()?;
130
131        if parent.is::<HTMLOptGroupElement>() {
132            DomRoot::downcast::<HTMLSelectElement>(parent.GetParentNode()?)
133        } else {
134            DomRoot::downcast::<HTMLSelectElement>(parent)
135        }
136    }
137
138    fn update_select_validity(&self, cx: &mut JSContext) {
139        if let Some(select) = self.owner_select_element() {
140            select
141                .validity_state(cx)
142                .perform_validation_and_update(cx, ValidationFlags::all());
143        }
144    }
145
146    /// <https://html.spec.whatwg.org/multipage/#concept-option-label>
147    ///
148    /// Note that this is not equivalent to <https://html.spec.whatwg.org/multipage/#dom-option-label>.
149    pub(crate) fn displayed_label(&self) -> DOMString {
150        // > The label of an option element is the value of the label content attribute, if there is one
151        // > and its value is not the empty string, or, otherwise, the value of the element's text IDL attribute.
152        let label = self
153            .upcast::<Element>()
154            .get_string_attribute(&local_name!("label"));
155
156        if label.is_empty() {
157            return self.Text();
158        }
159
160        label
161    }
162
163    /// <https://html.spec.whatwg.org/multipage/#option-element-nearest-ancestor-select>
164    pub(crate) fn nearest_ancestor_select(&self) -> Option<DomRoot<HTMLSelectElement>> {
165        // Step 1. Let ancestorOptgroup be null.
166        // NOTE: We only care whether the value is non-null, so a boolean is enough
167        let mut did_see_ancestor_optgroup = false;
168
169        // Step 2. For each ancestor of option's ancestors, in reverse tree order:
170        for ancestor in self
171            .upcast::<Node>()
172            .ancestors()
173            .filter_map(DomRoot::downcast::<Element>)
174        {
175            // Step 2.1 If ancestor is a datalist, hr, or option element, then return null.
176            if matches!(
177                ancestor.local_name(),
178                &local_name!("datalist") | &local_name!("hr") | &local_name!("option")
179            ) {
180                return None;
181            }
182
183            // Step 2.2 If ancestor is an optgroup element
184            if ancestor.local_name() == &local_name!("optgroup") {
185                // Step 2.1 If ancestorOptgroup is not null, then return null.
186                if did_see_ancestor_optgroup {
187                    return None;
188                }
189
190                // Step 2.2 Set ancestorOptgroup to ancestor.
191                did_see_ancestor_optgroup = true;
192            }
193
194            // Step 2.3 If ancestor is a select, then return ancestor.
195            if let Some(select) = DomRoot::downcast::<HTMLSelectElement>(ancestor) {
196                return Some(select);
197            }
198        }
199
200        // Step 3. Return null.
201        None
202    }
203
204    /// <https://html.spec.whatwg.org/multipage/#maybe-clone-an-option-into-selectedcontent>
205    pub(crate) fn maybe_clone_an_option_into_selectedcontent(&self, cx: &mut JSContext) {
206        // Step 1. Let select be option's option element nearest ancestor select.
207        let select = self.nearest_ancestor_select();
208
209        // Step 2. If all of the following conditions are true:
210        // * select is not null;
211        // * option's selectedness is true; and
212        // * select's enabled selectedcontent is not null,
213        // * then run clone an option into a selectedcontent given option and select's enabled selectedcontent.
214        if self.selectedness.get() &&
215            let Some(selectedcontent) =
216                select.and_then(|select| select.get_enabled_selectedcontent())
217        {
218            self.clone_an_option_into_selectedcontent(cx, &selectedcontent);
219        }
220    }
221
222    /// <https://html.spec.whatwg.org/multipage/#clone-an-option-into-a-selectedcontent>
223    fn clone_an_option_into_selectedcontent(&self, cx: &mut JSContext, selectedcontent: &Element) {
224        // Step 1. Let documentFragment be a new DocumentFragment whose node document is option's node document.
225        let document_fragment = DocumentFragment::new(cx, &self.owner_document());
226
227        // Step 2. For each child of option's children:
228        for child in self.upcast::<Node>().children() {
229            // Step 2.1 Let childClone be the result of running clone given child with subtree set to true.
230            let child_clone = Node::clone(cx, &child, None, CloneChildrenFlag::CloneChildren, None);
231
232            // Step 2.2 Append childClone to documentFragment.
233            let _ = document_fragment
234                .upcast::<Node>()
235                .AppendChild(cx, &child_clone);
236        }
237
238        // Step 3. Replace all with documentFragment within selectedcontent.
239        Node::replace_all(
240            cx,
241            Some(document_fragment.upcast()),
242            selectedcontent.upcast(),
243        );
244    }
245}
246
247impl HTMLOptionElementMethods<crate::DomTypeHolder> for HTMLOptionElement {
248    /// <https://html.spec.whatwg.org/multipage/#dom-option>
249    fn Option(
250        cx: &mut JSContext,
251        window: &Window,
252        proto: Option<HandleObject>,
253        text: DOMString,
254        value: Option<DOMString>,
255        default_selected: bool,
256        selected: bool,
257    ) -> Fallible<DomRoot<HTMLOptionElement>> {
258        let element = Element::create(
259            cx,
260            QualName::new(None, ns!(html), local_name!("option")),
261            None,
262            &window.Document(),
263            ElementCreator::ScriptCreated,
264            CustomElementCreationMode::Synchronous,
265            proto,
266        );
267
268        let option = DomRoot::downcast::<HTMLOptionElement>(element).unwrap();
269
270        if !text.is_empty() {
271            option
272                .upcast::<Node>()
273                .set_text_content_for_element(cx, Some(text))
274        }
275
276        if let Some(val) = value {
277            option.SetValue(cx, val)
278        }
279
280        option.SetDefaultSelected(cx, default_selected);
281        option.set_selectedness(cx.no_gc(), selected);
282        option.update_select_validity(cx);
283        Ok(option)
284    }
285
286    // https://html.spec.whatwg.org/multipage/#dom-option-disabled
287    make_bool_getter!(Disabled, "disabled");
288
289    // https://html.spec.whatwg.org/multipage/#dom-option-disabled
290    make_bool_setter!(SetDisabled, "disabled");
291
292    /// <https://html.spec.whatwg.org/multipage/#dom-option-text>
293    fn Text(&self) -> DOMString {
294        let mut content = DOMString::new();
295
296        let mut iterator = self.upcast::<Node>().traverse_preorder(ShadowIncluding::No);
297        while let Some(node) = iterator.peek() {
298            if let Some(element) = node.downcast::<Element>() {
299                let html_script = element.is::<HTMLScriptElement>();
300                let svg_script = *element.namespace() == ns!(svg) &&
301                    element.local_name() == &local_name!("script");
302                if html_script || svg_script {
303                    iterator.next_skipping_children();
304                    continue;
305                }
306            }
307
308            if node.is::<Text>() {
309                let characterdata = node.downcast::<CharacterData>().unwrap();
310                content.push_str(&characterdata.Data().str());
311            }
312
313            iterator.next();
314        }
315
316        DOMString::from(str_join(split_html_space_chars(&content.str()), " "))
317    }
318
319    /// <https://html.spec.whatwg.org/multipage/#dom-option-text>
320    fn SetText(&self, cx: &mut JSContext, value: DOMString) {
321        self.upcast::<Node>()
322            .set_text_content_for_element(cx, Some(value))
323    }
324
325    /// <https://html.spec.whatwg.org/multipage/#dom-option-form>
326    fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> {
327        let parent = self.upcast::<Node>().GetParentNode().and_then(|p| {
328            if p.is::<HTMLOptGroupElement>() {
329                p.upcast::<Node>().GetParentNode()
330            } else {
331                Some(p)
332            }
333        });
334
335        parent.and_then(|p| p.downcast::<HTMLSelectElement>().and_then(|s| s.GetForm()))
336    }
337
338    /// <https://html.spec.whatwg.org/multipage/#attr-option-value>
339    fn Value(&self) -> DOMString {
340        let element = self.upcast::<Element>();
341        let attr = &local_name!("value");
342        if element.has_attribute(attr) {
343            element.get_string_attribute(attr)
344        } else {
345            self.Text()
346        }
347    }
348
349    // https://html.spec.whatwg.org/multipage/#attr-option-value
350    make_setter!(SetValue, "value");
351
352    /// <https://html.spec.whatwg.org/multipage/#attr-option-label>
353    fn Label(&self) -> DOMString {
354        let element = self.upcast::<Element>();
355        let attr = &local_name!("label");
356        if element.has_attribute(attr) {
357            element.get_string_attribute(attr)
358        } else {
359            self.Text()
360        }
361    }
362
363    // https://html.spec.whatwg.org/multipage/#attr-option-label
364    make_setter!(SetLabel, "label");
365
366    // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected
367    make_bool_getter!(DefaultSelected, "selected");
368
369    // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected
370    make_bool_setter!(SetDefaultSelected, "selected");
371
372    /// <https://html.spec.whatwg.org/multipage/#dom-option-selected>
373    fn Selected(&self) -> bool {
374        self.selectedness.get()
375    }
376
377    /// <https://html.spec.whatwg.org/multipage/#dom-option-selected>
378    fn SetSelected(&self, cx: &mut JSContext, selected: bool) {
379        self.dirtiness.set(true);
380        self.set_selectedness(cx.no_gc(), selected);
381        self.pick_if_selected_and_reset(cx);
382        self.update_select_validity(cx);
383    }
384
385    /// <https://html.spec.whatwg.org/multipage/#dom-option-index>
386    fn Index(&self, cx: &JSContext) -> i32 {
387        self.index(cx.no_gc())
388    }
389}
390
391impl VirtualMethods for HTMLOptionElement {
392    fn super_type(&self) -> Option<&dyn VirtualMethods> {
393        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
394    }
395
396    fn attribute_mutated(
397        &self,
398        cx: &mut js::context::JSContext,
399        attr: AttrRef<'_>,
400        mutation: AttributeMutation,
401    ) {
402        self.super_type()
403            .unwrap()
404            .attribute_mutated(cx, attr, mutation);
405        match *attr.local_name() {
406            local_name!("disabled") => {
407                let el = self.upcast::<Element>();
408                match mutation {
409                    AttributeMutation::Set(..) => {
410                        el.set_disabled_state(true);
411                        el.set_enabled_state(false);
412                    },
413                    AttributeMutation::Removed => {
414                        el.set_disabled_state(false);
415                        el.set_enabled_state(true);
416                        el.check_parent_disabled_state_for_option();
417                    },
418                }
419                self.update_select_validity(cx);
420            },
421            local_name!("selected") => {
422                let mut selectedness_changed = false;
423                match mutation {
424                    AttributeMutation::Set(..) => {
425                        // https://html.spec.whatwg.org/multipage/#concept-option-selectedness
426                        if !self.dirtiness.get() && !self.selectedness.get() {
427                            self.set_selectedness(cx.no_gc(), true);
428                            selectedness_changed = true;
429                        }
430                    },
431                    AttributeMutation::Removed => {
432                        // https://html.spec.whatwg.org/multipage/#concept-option-selectedness
433                        if !self.dirtiness.get() && self.selectedness.get() {
434                            self.set_selectedness(cx.no_gc(), false);
435                            selectedness_changed = true;
436                        }
437                    },
438                }
439
440                if selectedness_changed {
441                    self.pick_if_selected_and_reset(cx);
442
443                    if let Some(select_element) = self.owner_select_element() {
444                        select_element.update_shadow_tree(cx);
445                    }
446                }
447
448                self.update_select_validity(cx);
449            },
450            local_name!("label") => {
451                // The label of the selected option is displayed inside the select element, so we need to repaint
452                // when it changes
453                if let Some(select_element) = self.owner_select_element() {
454                    select_element.update_shadow_tree(cx);
455                }
456            },
457            _ => {},
458        }
459    }
460
461    fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
462        if let Some(s) = self.super_type() {
463            s.bind_to_tree(cx, context);
464        }
465
466        self.upcast::<Element>()
467            .check_parent_disabled_state_for_option();
468
469        self.pick_if_selected_and_reset(cx);
470        self.update_select_validity(cx);
471    }
472
473    fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
474        self.super_type().unwrap().unbind_from_tree(cx, context);
475
476        if let Some(select) = context
477            .parent
478            .inclusive_ancestors(ShadowIncluding::No)
479            .find_map(DomRoot::downcast::<HTMLSelectElement>)
480        {
481            select
482                .validity_state(cx)
483                .perform_validation_and_update(cx, ValidationFlags::all());
484            select.ask_for_reset(cx.no_gc());
485        }
486
487        let node = self.upcast::<Node>();
488        let el = self.upcast::<Element>();
489        if node.GetParentNode().is_some() {
490            el.check_parent_disabled_state_for_option();
491        } else {
492            el.check_disabled_attribute();
493        }
494    }
495
496    fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) {
497        if let Some(super_type) = self.super_type() {
498            super_type.children_changed(cx, mutation);
499        }
500
501        // Changing the descendants of a selected option can change it's displayed label
502        // if it does not have a label attribute
503        if !self
504            .upcast::<Element>()
505            .has_attribute(&local_name!("label")) &&
506            let Some(owner_select) = self.owner_select_element() &&
507            owner_select
508                .selected_option(cx.no_gc())
509                .is_some_and(|selected_option| *self == **selected_option)
510        {
511            owner_select.update_shadow_tree(cx);
512        }
513    }
514
515    /// <https://html.spec.whatwg.org/multipage/#the-option-element:html-element-moving-steps>
516    fn moving_steps(&self, cx: &mut JSContext, context: &MoveContext) {
517        if let Some(super_type) = self.super_type() {
518            super_type.moving_steps(cx, context);
519        }
520
521        // The option HTML element moving steps, given movedNode and oldParent, are to run update an
522        // option's nearest ancestor select given movedNode.
523        let element = self.upcast::<Element>();
524        if let Some(old_parent) = context.old_parent {
525            if let Some(select) = old_parent
526                .inclusive_ancestors(ShadowIncluding::No)
527                .find_map(DomRoot::downcast::<HTMLSelectElement>)
528            {
529                select
530                    .validity_state(cx)
531                    .perform_validation_and_update(cx, ValidationFlags::all());
532                select.ask_for_reset(cx.no_gc());
533            }
534
535            if self.upcast::<Node>().GetParentNode().is_some() {
536                element.check_parent_disabled_state_for_option();
537            } else {
538                element.check_disabled_attribute();
539            }
540        }
541
542        element.check_parent_disabled_state_for_option();
543
544        self.pick_if_selected_and_reset(cx);
545        self.update_select_validity(cx);
546    }
547}