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