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::{BindContext, ChildrenMutation, Node, ShadowIncluding, UnbindContext};
33use crate::dom::text::Text;
34use crate::dom::validation::Validatable;
35use crate::dom::validitystate::ValidationFlags;
36use crate::dom::virtualmethods::VirtualMethods;
37use crate::dom::window::Window;
38use crate::script_runtime::CanGc;
39
40#[dom_struct]
41pub(crate) struct HTMLOptionElement {
42    htmlelement: HTMLElement,
43
44    /// <https://html.spec.whatwg.org/multipage/#attr-option-selected>
45    selectedness: Cell<bool>,
46
47    /// <https://html.spec.whatwg.org/multipage/#concept-option-dirtiness>
48    dirtiness: Cell<bool>,
49}
50
51impl HTMLOptionElement {
52    fn new_inherited(
53        local_name: LocalName,
54        prefix: Option<Prefix>,
55        document: &Document,
56    ) -> HTMLOptionElement {
57        HTMLOptionElement {
58            htmlelement: HTMLElement::new_inherited_with_state(
59                ElementState::ENABLED,
60                local_name,
61                prefix,
62                document,
63            ),
64            selectedness: Cell::new(false),
65            dirtiness: Cell::new(false),
66        }
67    }
68
69    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
70    pub(crate) fn new(
71        local_name: LocalName,
72        prefix: Option<Prefix>,
73        document: &Document,
74        proto: Option<HandleObject>,
75        can_gc: CanGc,
76    ) -> DomRoot<HTMLOptionElement> {
77        Node::reflect_node_with_proto(
78            Box::new(HTMLOptionElement::new_inherited(
79                local_name, prefix, document,
80            )),
81            document,
82            proto,
83            can_gc,
84        )
85    }
86
87    pub(crate) fn set_selectedness(&self, selected: bool) {
88        self.selectedness.set(selected);
89    }
90
91    pub(crate) fn set_dirtiness(&self, dirtiness: bool) {
92        self.dirtiness.set(dirtiness);
93    }
94
95    fn pick_if_selected_and_reset(&self) {
96        if let Some(select) = self.owner_select_element() {
97            if self.Selected() {
98                select.pick_option(self);
99            }
100            select.ask_for_reset();
101        }
102    }
103
104    // https://html.spec.whatwg.org/multipage/#concept-option-index
105    fn index(&self) -> i32 {
106        let Some(owner_select) = self.owner_select_element() else {
107            return 0;
108        };
109
110        let Some(position) = owner_select.list_of_options().position(|n| &*n == self) else {
111            // An option should always be in it's owner's list of options, but it's not worth a browser panic
112            warn!("HTMLOptionElement called index_in_select at a select that did not contain it");
113            return 0;
114        };
115
116        position.try_into().unwrap_or(0)
117    }
118
119    fn owner_select_element(&self) -> Option<DomRoot<HTMLSelectElement>> {
120        let parent = self.upcast::<Node>().GetParentNode()?;
121
122        if parent.is::<HTMLOptGroupElement>() {
123            DomRoot::downcast::<HTMLSelectElement>(parent.GetParentNode()?)
124        } else {
125            DomRoot::downcast::<HTMLSelectElement>(parent)
126        }
127    }
128
129    fn update_select_validity(&self, can_gc: CanGc) {
130        if let Some(select) = self.owner_select_element() {
131            select
132                .validity_state()
133                .perform_validation_and_update(ValidationFlags::all(), can_gc);
134        }
135    }
136
137    /// <https://html.spec.whatwg.org/multipage/#concept-option-label>
138    ///
139    /// Note that this is not equivalent to <https://html.spec.whatwg.org/multipage/#dom-option-label>.
140    pub(crate) fn displayed_label(&self) -> DOMString {
141        // > The label of an option element is the value of the label content attribute, if there is one
142        // > and its value is not the empty string, or, otherwise, the value of the element's text IDL attribute.
143        let label = self
144            .upcast::<Element>()
145            .get_string_attribute(&local_name!("label"));
146
147        if label.is_empty() {
148            return self.Text();
149        }
150
151        label
152    }
153}
154
155impl HTMLOptionElementMethods<crate::DomTypeHolder> for HTMLOptionElement {
156    /// <https://html.spec.whatwg.org/multipage/#dom-option>
157    fn Option(
158        window: &Window,
159        proto: Option<HandleObject>,
160        can_gc: CanGc,
161        text: DOMString,
162        value: Option<DOMString>,
163        default_selected: bool,
164        selected: bool,
165    ) -> Fallible<DomRoot<HTMLOptionElement>> {
166        let element = Element::create(
167            QualName::new(None, ns!(html), local_name!("option")),
168            None,
169            &window.Document(),
170            ElementCreator::ScriptCreated,
171            CustomElementCreationMode::Synchronous,
172            proto,
173            can_gc,
174        );
175
176        let option = DomRoot::downcast::<HTMLOptionElement>(element).unwrap();
177
178        if !text.is_empty() {
179            option
180                .upcast::<Node>()
181                .set_text_content_for_element(Some(text), can_gc)
182        }
183
184        if let Some(val) = value {
185            option.SetValue(val)
186        }
187
188        option.SetDefaultSelected(default_selected);
189        option.set_selectedness(selected);
190        option.update_select_validity(can_gc);
191        Ok(option)
192    }
193
194    // https://html.spec.whatwg.org/multipage/#dom-option-disabled
195    make_bool_getter!(Disabled, "disabled");
196
197    // https://html.spec.whatwg.org/multipage/#dom-option-disabled
198    make_bool_setter!(SetDisabled, "disabled");
199
200    /// <https://html.spec.whatwg.org/multipage/#dom-option-text>
201    fn Text(&self) -> DOMString {
202        let mut content = DOMString::new();
203
204        let mut iterator = self.upcast::<Node>().traverse_preorder(ShadowIncluding::No);
205        while let Some(node) = iterator.peek() {
206            if let Some(element) = node.downcast::<Element>() {
207                let html_script = element.is::<HTMLScriptElement>();
208                let svg_script = *element.namespace() == ns!(svg) &&
209                    element.local_name() == &local_name!("script");
210                if html_script || svg_script {
211                    iterator.next_skipping_children();
212                    continue;
213                }
214            }
215
216            if node.is::<Text>() {
217                let characterdata = node.downcast::<CharacterData>().unwrap();
218                content.push_str(&characterdata.Data());
219            }
220
221            iterator.next();
222        }
223
224        DOMString::from(str_join(split_html_space_chars(&content), " "))
225    }
226
227    /// <https://html.spec.whatwg.org/multipage/#dom-option-text>
228    fn SetText(&self, value: DOMString, can_gc: CanGc) {
229        self.upcast::<Node>()
230            .set_text_content_for_element(Some(value), can_gc)
231    }
232
233    /// <https://html.spec.whatwg.org/multipage/#dom-option-form>
234    fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> {
235        let parent = self.upcast::<Node>().GetParentNode().and_then(|p| {
236            if p.is::<HTMLOptGroupElement>() {
237                p.upcast::<Node>().GetParentNode()
238            } else {
239                Some(p)
240            }
241        });
242
243        parent.and_then(|p| p.downcast::<HTMLSelectElement>().and_then(|s| s.GetForm()))
244    }
245
246    /// <https://html.spec.whatwg.org/multipage/#attr-option-value>
247    fn Value(&self) -> DOMString {
248        let element = self.upcast::<Element>();
249        let attr = &local_name!("value");
250        if element.has_attribute(attr) {
251            element.get_string_attribute(attr)
252        } else {
253            self.Text()
254        }
255    }
256
257    // https://html.spec.whatwg.org/multipage/#attr-option-value
258    make_setter!(SetValue, "value");
259
260    /// <https://html.spec.whatwg.org/multipage/#attr-option-label>
261    fn Label(&self) -> DOMString {
262        let element = self.upcast::<Element>();
263        let attr = &local_name!("label");
264        if element.has_attribute(attr) {
265            element.get_string_attribute(attr)
266        } else {
267            self.Text()
268        }
269    }
270
271    // https://html.spec.whatwg.org/multipage/#attr-option-label
272    make_setter!(SetLabel, "label");
273
274    // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected
275    make_bool_getter!(DefaultSelected, "selected");
276
277    // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected
278    make_bool_setter!(SetDefaultSelected, "selected");
279
280    /// <https://html.spec.whatwg.org/multipage/#dom-option-selected>
281    fn Selected(&self) -> bool {
282        self.selectedness.get()
283    }
284
285    /// <https://html.spec.whatwg.org/multipage/#dom-option-selected>
286    fn SetSelected(&self, selected: bool) {
287        self.dirtiness.set(true);
288        self.selectedness.set(selected);
289        self.pick_if_selected_and_reset();
290        self.update_select_validity(CanGc::note());
291    }
292
293    /// <https://html.spec.whatwg.org/multipage/#dom-option-index>
294    fn Index(&self) -> i32 {
295        self.index()
296    }
297}
298
299impl VirtualMethods for HTMLOptionElement {
300    fn super_type(&self) -> Option<&dyn VirtualMethods> {
301        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
302    }
303
304    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
305        self.super_type()
306            .unwrap()
307            .attribute_mutated(attr, mutation, can_gc);
308        match *attr.local_name() {
309            local_name!("disabled") => {
310                let el = self.upcast::<Element>();
311                match mutation {
312                    AttributeMutation::Set(_) => {
313                        el.set_disabled_state(true);
314                        el.set_enabled_state(false);
315                    },
316                    AttributeMutation::Removed => {
317                        el.set_disabled_state(false);
318                        el.set_enabled_state(true);
319                        el.check_parent_disabled_state_for_option();
320                    },
321                }
322                self.update_select_validity(can_gc);
323            },
324            local_name!("selected") => {
325                match mutation {
326                    AttributeMutation::Set(_) => {
327                        // https://html.spec.whatwg.org/multipage/#concept-option-selectedness
328                        if !self.dirtiness.get() {
329                            self.selectedness.set(true);
330                        }
331                    },
332                    AttributeMutation::Removed => {
333                        // https://html.spec.whatwg.org/multipage/#concept-option-selectedness
334                        if !self.dirtiness.get() {
335                            self.selectedness.set(false);
336                        }
337                    },
338                }
339                self.update_select_validity(can_gc);
340            },
341            local_name!("label") => {
342                // The label of the selected option is displayed inside the select element, so we need to repaint
343                // when it changes
344                if let Some(select_element) = self.owner_select_element() {
345                    select_element.update_shadow_tree(CanGc::note());
346                }
347            },
348            _ => {},
349        }
350    }
351
352    fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
353        if let Some(s) = self.super_type() {
354            s.bind_to_tree(context, can_gc);
355        }
356
357        self.upcast::<Element>()
358            .check_parent_disabled_state_for_option();
359
360        self.pick_if_selected_and_reset();
361        self.update_select_validity(can_gc);
362    }
363
364    fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
365        self.super_type().unwrap().unbind_from_tree(context, can_gc);
366
367        if let Some(select) = context
368            .parent
369            .inclusive_ancestors(ShadowIncluding::No)
370            .filter_map(DomRoot::downcast::<HTMLSelectElement>)
371            .next()
372        {
373            select
374                .validity_state()
375                .perform_validation_and_update(ValidationFlags::all(), can_gc);
376            select.ask_for_reset();
377        }
378
379        let node = self.upcast::<Node>();
380        let el = self.upcast::<Element>();
381        if node.GetParentNode().is_some() {
382            el.check_parent_disabled_state_for_option();
383        } else {
384            el.check_disabled_attribute();
385        }
386    }
387
388    fn children_changed(&self, mutation: &ChildrenMutation) {
389        if let Some(super_type) = self.super_type() {
390            super_type.children_changed(mutation);
391        }
392
393        // Changing the descendants of a selected option can change it's displayed label
394        // if it does not have a label attribute
395        if !self
396            .upcast::<Element>()
397            .has_attribute(&local_name!("label"))
398        {
399            if let Some(owner_select) = self.owner_select_element() {
400                if owner_select
401                    .selected_option()
402                    .is_some_and(|selected_option| self == &*selected_option)
403                {
404                    owner_select.update_shadow_tree(CanGc::note());
405                }
406            }
407        }
408    }
409}