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