/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::Cell; use std::convert::TryInto; use dom_struct::dom_struct; use html5ever::{LocalName, Prefix, QualName, local_name, ns}; use js::context::JSContext; use js::rust::HandleObject; use style::str::{split_html_space_chars, str_join}; use stylo_dom::ElementState; use crate::dom::attr::Attr; use crate::dom::bindings::codegen::Bindings::CharacterDataBinding::CharacterDataMethods; use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElement_Binding::HTMLSelectElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::error::Fallible; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::str::DOMString; use crate::dom::characterdata::CharacterData; use crate::dom::document::Document; use crate::dom::element::{AttributeMutation, CustomElementCreationMode, Element, ElementCreator}; use crate::dom::html::htmlelement::HTMLElement; use crate::dom::html::htmlformelement::HTMLFormElement; use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement; use crate::dom::html::htmlscriptelement::HTMLScriptElement; use crate::dom::html::htmlselectelement::HTMLSelectElement; use crate::dom::node::{ BindContext, ChildrenMutation, CloneChildrenFlag, MoveContext, Node, NodeTraits, ShadowIncluding, UnbindContext, }; use crate::dom::text::Text; use crate::dom::types::DocumentFragment; use crate::dom::validation::Validatable; use crate::dom::validitystate::ValidationFlags; use crate::dom::virtualmethods::VirtualMethods; use crate::dom::window::Window; use crate::script_runtime::CanGc; #[dom_struct] pub(crate) struct HTMLOptionElement { htmlelement: HTMLElement, /// selectedness: Cell, /// dirtiness: Cell, } impl HTMLOptionElement { fn new_inherited( local_name: LocalName, prefix: Option, document: &Document, ) -> HTMLOptionElement { HTMLOptionElement { htmlelement: HTMLElement::new_inherited_with_state( ElementState::ENABLED, local_name, prefix, document, ), selectedness: Cell::new(false), dirtiness: Cell::new(false), } } pub(crate) fn new( cx: &mut js::context::JSContext, local_name: LocalName, prefix: Option, document: &Document, proto: Option, ) -> DomRoot { Node::reflect_node_with_proto( cx, Box::new(HTMLOptionElement::new_inherited( local_name, prefix, document, )), document, proto, ) } pub(crate) fn set_selectedness(&self, selected: bool) { self.selectedness.set(selected); // Bump the tree version so that any live HTMLCollection (e.g. selectedOptions) // rooted at an ancestor invalidates its cached length and cursor. self.upcast::().rev_version(); } pub(crate) fn set_dirtiness(&self, dirtiness: bool) { self.dirtiness.set(dirtiness); } fn pick_if_selected_and_reset(&self) { if let Some(select) = self.owner_select_element() { if self.Selected() { select.pick_option(self); } select.ask_for_reset(); } } /// fn index(&self) -> i32 { let Some(owner_select) = self.owner_select_element() else { return 0; }; let Some(position) = owner_select.list_of_options().position(|n| &*n == self) else { // An option should always be in it's owner's list of options, but it's not worth a browser panic warn!("HTMLOptionElement called index_in_select at a select that did not contain it"); return 0; }; position.try_into().unwrap_or(0) } fn owner_select_element(&self) -> Option> { let parent = self.upcast::().GetParentNode()?; if parent.is::() { DomRoot::downcast::(parent.GetParentNode()?) } else { DomRoot::downcast::(parent) } } fn update_select_validity(&self, can_gc: CanGc) { if let Some(select) = self.owner_select_element() { select .validity_state(can_gc) .perform_validation_and_update(ValidationFlags::all(), can_gc); } } /// /// /// Note that this is not equivalent to . pub(crate) fn displayed_label(&self) -> DOMString { // > The label of an option element is the value of the label content attribute, if there is one // > and its value is not the empty string, or, otherwise, the value of the element's text IDL attribute. let label = self .upcast::() .get_string_attribute(&local_name!("label")); if label.is_empty() { return self.Text(); } label } /// pub(crate) fn nearest_ancestor_select(&self) -> Option> { // Step 1. Let ancestorOptgroup be null. // NOTE: We only care whether the value is non-null, so a boolean is enough let mut did_see_ancestor_optgroup = false; // Step 2. For each ancestor of option's ancestors, in reverse tree order: for ancestor in self .upcast::() .ancestors() .filter_map(DomRoot::downcast::) { // Step 2.1 If ancestor is a datalist, hr, or option element, then return null. if matches!( ancestor.local_name(), &local_name!("datalist") | &local_name!("hr") | &local_name!("option") ) { return None; } // Step 2.2 If ancestor is an optgroup element if ancestor.local_name() == &local_name!("optgroup") { // Step 2.1 If ancestorOptgroup is not null, then return null. if did_see_ancestor_optgroup { return None; } // Step 2.2 Set ancestorOptgroup to ancestor. did_see_ancestor_optgroup = true; } // Step 2.3 If ancestor is a select, then return ancestor. if let Some(select) = DomRoot::downcast::(ancestor) { return Some(select); } } // Step 3. Return null. None } /// pub(crate) fn maybe_clone_an_option_into_selectedcontent(&self, cx: &mut JSContext) { // Step 1. Let select be option's option element nearest ancestor select. let select = self.nearest_ancestor_select(); // Step 2. If all of the following conditions are true: // * select is not null; // * option's selectedness is true; and // * select's enabled selectedcontent is not null, // * then run clone an option into a selectedcontent given option and select's enabled selectedcontent. if self.selectedness.get() { if let Some(selectedcontent) = select.and_then(|select| select.get_enabled_selectedcontent()) { self.clone_an_option_into_selectedcontent(cx, &selectedcontent); } } } /// fn clone_an_option_into_selectedcontent(&self, cx: &mut JSContext, selectedcontent: &Element) { // Step 1. Let documentFragment be a new DocumentFragment whose node document is option's node document. let document_fragment = DocumentFragment::new(cx, &self.owner_document()); // Step 2. For each child of option's children: for child in self.upcast::().children() { // Step 2.1 Let childClone be the result of running clone given child with subtree set to true. let child_clone = Node::clone(cx, &child, None, CloneChildrenFlag::CloneChildren, None); // Step 2.2 Append childClone to documentFragment. let _ = document_fragment .upcast::() .AppendChild(cx, &child_clone); } // Step 3. Replace all with documentFragment within selectedcontent. Node::replace_all( cx, Some(document_fragment.upcast()), selectedcontent.upcast(), ); } } impl HTMLOptionElementMethods for HTMLOptionElement { /// fn Option( cx: &mut JSContext, window: &Window, proto: Option, text: DOMString, value: Option, default_selected: bool, selected: bool, ) -> Fallible> { let element = Element::create( cx, QualName::new(None, ns!(html), local_name!("option")), None, &window.Document(), ElementCreator::ScriptCreated, CustomElementCreationMode::Synchronous, proto, ); let option = DomRoot::downcast::(element).unwrap(); if !text.is_empty() { option .upcast::() .set_text_content_for_element(cx, Some(text)) } if let Some(val) = value { option.SetValue(val) } option.SetDefaultSelected(default_selected); option.set_selectedness(selected); option.update_select_validity(CanGc::from_cx(cx)); Ok(option) } // https://html.spec.whatwg.org/multipage/#dom-option-disabled make_bool_getter!(Disabled, "disabled"); // https://html.spec.whatwg.org/multipage/#dom-option-disabled make_bool_setter!(SetDisabled, "disabled"); /// fn Text(&self) -> DOMString { let mut content = DOMString::new(); let mut iterator = self.upcast::().traverse_preorder(ShadowIncluding::No); while let Some(node) = iterator.peek() { if let Some(element) = node.downcast::() { let html_script = element.is::(); let svg_script = *element.namespace() == ns!(svg) && element.local_name() == &local_name!("script"); if html_script || svg_script { iterator.next_skipping_children(); continue; } } if node.is::() { let characterdata = node.downcast::().unwrap(); content.push_str(&characterdata.Data().str()); } iterator.next(); } DOMString::from(str_join(split_html_space_chars(&content.str()), " ")) } /// fn SetText(&self, cx: &mut JSContext, value: DOMString) { self.upcast::() .set_text_content_for_element(cx, Some(value)) } /// fn GetForm(&self) -> Option> { let parent = self.upcast::().GetParentNode().and_then(|p| { if p.is::() { p.upcast::().GetParentNode() } else { Some(p) } }); parent.and_then(|p| p.downcast::().and_then(|s| s.GetForm())) } /// fn Value(&self) -> DOMString { let element = self.upcast::(); let attr = &local_name!("value"); if element.has_attribute(attr) { element.get_string_attribute(attr) } else { self.Text() } } // https://html.spec.whatwg.org/multipage/#attr-option-value make_setter!(SetValue, "value"); /// fn Label(&self) -> DOMString { let element = self.upcast::(); let attr = &local_name!("label"); if element.has_attribute(attr) { element.get_string_attribute(attr) } else { self.Text() } } // https://html.spec.whatwg.org/multipage/#attr-option-label make_setter!(SetLabel, "label"); // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected make_bool_getter!(DefaultSelected, "selected"); // https://html.spec.whatwg.org/multipage/#dom-option-defaultselected make_bool_setter!(SetDefaultSelected, "selected"); /// fn Selected(&self) -> bool { self.selectedness.get() } /// fn SetSelected(&self, selected: bool, can_gc: CanGc) { self.dirtiness.set(true); self.set_selectedness(selected); self.pick_if_selected_and_reset(); self.update_select_validity(can_gc); } /// fn Index(&self) -> i32 { self.index() } } impl VirtualMethods for HTMLOptionElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { Some(self.upcast::() as &dyn VirtualMethods) } fn attribute_mutated( &self, cx: &mut js::context::JSContext, attr: &Attr, mutation: AttributeMutation, ) { self.super_type() .unwrap() .attribute_mutated(cx, attr, mutation); match *attr.local_name() { local_name!("disabled") => { let el = self.upcast::(); match mutation { AttributeMutation::Set(..) => { el.set_disabled_state(true); el.set_enabled_state(false); }, AttributeMutation::Removed => { el.set_disabled_state(false); el.set_enabled_state(true); el.check_parent_disabled_state_for_option(); }, } self.update_select_validity(CanGc::from_cx(cx)); }, local_name!("selected") => { let mut selectedness_changed = false; match mutation { AttributeMutation::Set(..) => { // https://html.spec.whatwg.org/multipage/#concept-option-selectedness if !self.dirtiness.get() && !self.selectedness.get() { self.set_selectedness(true); selectedness_changed = true; } }, AttributeMutation::Removed => { // https://html.spec.whatwg.org/multipage/#concept-option-selectedness if !self.dirtiness.get() && self.selectedness.get() { self.set_selectedness(false); selectedness_changed = true; } }, } if selectedness_changed { self.pick_if_selected_and_reset(); if let Some(select_element) = self.owner_select_element() { select_element.update_shadow_tree(cx); } } self.update_select_validity(CanGc::from_cx(cx)); }, local_name!("label") => { // The label of the selected option is displayed inside the select element, so we need to repaint // when it changes if let Some(select_element) = self.owner_select_element() { select_element.update_shadow_tree(cx); } }, _ => {}, } } fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) { if let Some(s) = self.super_type() { s.bind_to_tree(cx, context); } self.upcast::() .check_parent_disabled_state_for_option(); self.pick_if_selected_and_reset(); self.update_select_validity(CanGc::from_cx(cx)); } fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) { self.super_type().unwrap().unbind_from_tree(cx, context); if let Some(select) = context .parent .inclusive_ancestors(ShadowIncluding::No) .find_map(DomRoot::downcast::) { select .validity_state(CanGc::from_cx(cx)) .perform_validation_and_update(ValidationFlags::all(), CanGc::from_cx(cx)); select.ask_for_reset(); } let node = self.upcast::(); let el = self.upcast::(); if node.GetParentNode().is_some() { el.check_parent_disabled_state_for_option(); } else { el.check_disabled_attribute(); } } fn children_changed(&self, cx: &mut JSContext, mutation: &ChildrenMutation) { if let Some(super_type) = self.super_type() { super_type.children_changed(cx, mutation); } // Changing the descendants of a selected option can change it's displayed label // if it does not have a label attribute if !self .upcast::() .has_attribute(&local_name!("label")) { if let Some(owner_select) = self.owner_select_element() { if owner_select .selected_option() .is_some_and(|selected_option| self == &*selected_option) { owner_select.update_shadow_tree(cx); } } } } /// fn moving_steps(&self, context: &MoveContext, can_gc: CanGc) { if let Some(super_type) = self.super_type() { super_type.moving_steps(context, can_gc); } // The option HTML element moving steps, given movedNode and oldParent, are to run update an // option's nearest ancestor select given movedNode. let element = self.upcast::(); if let Some(old_parent) = context.old_parent { if let Some(select) = old_parent .inclusive_ancestors(ShadowIncluding::No) .find_map(DomRoot::downcast::) { select .validity_state(can_gc) .perform_validation_and_update(ValidationFlags::all(), can_gc); select.ask_for_reset(); } if self.upcast::().GetParentNode().is_some() { element.check_parent_disabled_state_for_option(); } else { element.check_disabled_attribute(); } } element.check_parent_disabled_state_for_option(); self.pick_if_selected_and_reset(); self.update_select_validity(can_gc); } }