Skip to main content

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