| /* 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::default::Default; |
| use std::iter; |
| |
| use dom_struct::dom_struct; |
| use embedder_traits::{EmbedderMsg, FormControl as EmbedderFormControl}; |
| use embedder_traits::{SelectElementOption, SelectElementOptionOrOptgroup}; |
| use euclid::{Point2D, Rect, Size2D}; |
| use html5ever::{LocalName, Prefix, local_name}; |
| use js::rust::HandleObject; |
| use style::attr::AttrValue; |
| use stylo_dom::ElementState; |
| use webrender_api::units::DeviceIntRect; |
| use base::generic_channel; |
| use crate::dom::bindings::refcounted::Trusted; |
| use crate::dom::event::{EventBubbles, EventCancelable, EventComposed}; |
| use crate::dom::bindings::codegen::GenericBindings::HTMLOptGroupElementBinding::HTMLOptGroupElement_Binding::HTMLOptGroupElementMethods; |
| use crate::dom::activation::Activatable; |
| use crate::dom::attr::Attr; |
| use crate::dom::bindings::cell::{DomRefCell, Ref}; |
| use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; |
| use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods; |
| use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods; |
| use crate::dom::bindings::codegen::Bindings::HTMLOptionsCollectionBinding::HTMLOptionsCollectionMethods; |
| use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods; |
| use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; |
| use crate::dom::bindings::codegen::GenericBindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods; |
| use crate::dom::bindings::codegen::UnionTypes::{ |
| HTMLElementOrLong, HTMLOptionElementOrHTMLOptGroupElement, |
| }; |
| use crate::dom::bindings::error::ErrorResult; |
| use crate::dom::bindings::inheritance::Castable; |
| use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; |
| use crate::dom::bindings::str::DOMString; |
| use crate::dom::characterdata::CharacterData; |
| use crate::dom::document::Document; |
| use crate::dom::element::{AttributeMutation, Element}; |
| use crate::dom::event::Event; |
| use crate::dom::eventtarget::EventTarget; |
| use crate::dom::html::htmlcollection::CollectionFilter; |
| use crate::dom::html::htmldivelement::HTMLDivElement; |
| use crate::dom::html::htmlelement::HTMLElement; |
| use crate::dom::html::htmlfieldsetelement::HTMLFieldSetElement; |
| use crate::dom::html::htmlformelement::{FormControl, FormDatum, FormDatumValue, HTMLFormElement}; |
| use crate::dom::html::htmloptgroupelement::HTMLOptGroupElement; |
| use crate::dom::html::htmloptionelement::HTMLOptionElement; |
| use crate::dom::html::htmloptionscollection::HTMLOptionsCollection; |
| use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeTraits, UnbindContext}; |
| use crate::dom::nodelist::NodeList; |
| use crate::dom::text::Text; |
| use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor}; |
| use crate::dom::validitystate::{ValidationFlags, ValidityState}; |
| use crate::dom::virtualmethods::VirtualMethods; |
| use crate::script_runtime::CanGc; |
| |
| const DEFAULT_SELECT_SIZE: u32 = 0; |
| |
| const SELECT_BOX_STYLE: &str = " |
| display: flex; |
| align-items: center; |
| height: 100%; |
| "; |
| |
| const TEXT_CONTAINER_STYLE: &str = "flex: 1;"; |
| |
| const CHEVRON_CONTAINER_STYLE: &str = " |
| font-size: 16px; |
| margin: 4px; |
| "; |
| |
| #[derive(JSTraceable, MallocSizeOf)] |
| struct OptionsFilter; |
| impl CollectionFilter for OptionsFilter { |
| fn filter<'a>(&self, elem: &'a Element, root: &'a Node) -> bool { |
| if !elem.is::<HTMLOptionElement>() { |
| return false; |
| } |
| |
| let node = elem.upcast::<Node>(); |
| if root.is_parent_of(node) { |
| return true; |
| } |
| |
| match node.GetParentNode() { |
| Some(optgroup) => optgroup.is::<HTMLOptGroupElement>() && root.is_parent_of(&optgroup), |
| None => false, |
| } |
| } |
| } |
| |
| #[dom_struct] |
| pub(crate) struct HTMLSelectElement { |
| htmlelement: HTMLElement, |
| options: MutNullableDom<HTMLOptionsCollection>, |
| form_owner: MutNullableDom<HTMLFormElement>, |
| labels_node_list: MutNullableDom<NodeList>, |
| validity_state: MutNullableDom<ValidityState>, |
| shadow_tree: DomRefCell<Option<ShadowTree>>, |
| } |
| |
| /// Holds handles to all elements in the UA shadow tree |
| #[derive(Clone, JSTraceable, MallocSizeOf)] |
| #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] |
| struct ShadowTree { |
| selected_option: Dom<Text>, |
| } |
| |
| impl HTMLSelectElement { |
| fn new_inherited( |
| local_name: LocalName, |
| prefix: Option<Prefix>, |
| document: &Document, |
| ) -> HTMLSelectElement { |
| HTMLSelectElement { |
| htmlelement: HTMLElement::new_inherited_with_state( |
| ElementState::ENABLED | ElementState::VALID, |
| local_name, |
| prefix, |
| document, |
| ), |
| options: Default::default(), |
| form_owner: Default::default(), |
| labels_node_list: Default::default(), |
| validity_state: Default::default(), |
| shadow_tree: Default::default(), |
| } |
| } |
| |
| #[cfg_attr(crown, allow(crown::unrooted_must_root))] |
| pub(crate) fn new( |
| local_name: LocalName, |
| prefix: Option<Prefix>, |
| document: &Document, |
| proto: Option<HandleObject>, |
| can_gc: CanGc, |
| ) -> DomRoot<HTMLSelectElement> { |
| let n = Node::reflect_node_with_proto( |
| Box::new(HTMLSelectElement::new_inherited( |
| local_name, prefix, document, |
| )), |
| document, |
| proto, |
| can_gc, |
| ); |
| |
| n.upcast::<Node>().set_weird_parser_insertion_mode(); |
| n |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#concept-select-option-list> |
| pub(crate) fn list_of_options( |
| &self, |
| ) -> impl Iterator<Item = DomRoot<HTMLOptionElement>> + use<'_> { |
| self.upcast::<Node>().children().flat_map(|node| { |
| if node.is::<HTMLOptionElement>() { |
| let node = DomRoot::downcast::<HTMLOptionElement>(node).unwrap(); |
| Choice3::First(iter::once(node)) |
| } else if node.is::<HTMLOptGroupElement>() { |
| Choice3::Second(node.children().filter_map(DomRoot::downcast)) |
| } else { |
| Choice3::Third(iter::empty()) |
| } |
| }) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#placeholder-label-option |
| fn get_placeholder_label_option(&self) -> Option<DomRoot<HTMLOptionElement>> { |
| if self.Required() && !self.Multiple() && self.display_size() == 1 { |
| self.list_of_options().next().filter(|node| { |
| let parent = node.upcast::<Node>().GetParentNode(); |
| node.Value().is_empty() && parent.as_deref() == Some(self.upcast()) |
| }) |
| } else { |
| None |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#the-select-element:concept-form-reset-control |
| pub(crate) fn reset(&self) { |
| for opt in self.list_of_options() { |
| opt.set_selectedness(opt.DefaultSelected()); |
| opt.set_dirtiness(false); |
| } |
| self.ask_for_reset(); |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#ask-for-a-reset |
| pub(crate) fn ask_for_reset(&self) { |
| if self.Multiple() { |
| return; |
| } |
| |
| let mut first_enabled: Option<DomRoot<HTMLOptionElement>> = None; |
| let mut last_selected: Option<DomRoot<HTMLOptionElement>> = None; |
| |
| for opt in self.list_of_options() { |
| if opt.Selected() { |
| opt.set_selectedness(false); |
| last_selected = Some(DomRoot::from_ref(&opt)); |
| } |
| let element = opt.upcast::<Element>(); |
| if first_enabled.is_none() && !element.disabled_state() { |
| first_enabled = Some(DomRoot::from_ref(&opt)); |
| } |
| } |
| |
| if let Some(last_selected) = last_selected { |
| last_selected.set_selectedness(true); |
| } else if self.display_size() == 1 { |
| if let Some(first_enabled) = first_enabled { |
| first_enabled.set_selectedness(true); |
| } |
| } |
| } |
| |
| pub(crate) fn push_form_data(&self, data_set: &mut Vec<FormDatum>) { |
| if self.Name().is_empty() { |
| return; |
| } |
| for opt in self.list_of_options() { |
| let element = opt.upcast::<Element>(); |
| if opt.Selected() && element.enabled_state() { |
| data_set.push(FormDatum { |
| ty: self.Type(), |
| name: self.Name(), |
| value: FormDatumValue::String(opt.Value()), |
| }); |
| } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#concept-select-pick |
| pub(crate) fn pick_option(&self, picked: &HTMLOptionElement) { |
| if !self.Multiple() { |
| let picked = picked.upcast(); |
| for opt in self.list_of_options() { |
| if opt.upcast::<HTMLElement>() != picked { |
| opt.set_selectedness(false); |
| } |
| } |
| } |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#concept-select-size |
| fn display_size(&self) -> u32 { |
| if self.Size() == 0 { |
| if self.Multiple() { 4 } else { 1 } |
| } else { |
| self.Size() |
| } |
| } |
| |
| fn create_shadow_tree(&self, can_gc: CanGc) { |
| let document = self.owner_document(); |
| let root = self.upcast::<Element>().attach_ua_shadow_root(true, can_gc); |
| |
| let select_box = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); |
| select_box.upcast::<Element>().set_string_attribute( |
| &local_name!("style"), |
| SELECT_BOX_STYLE.into(), |
| can_gc, |
| ); |
| |
| let text_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); |
| text_container.upcast::<Element>().set_string_attribute( |
| &local_name!("style"), |
| TEXT_CONTAINER_STYLE.into(), |
| can_gc, |
| ); |
| select_box |
| .upcast::<Node>() |
| .AppendChild(text_container.upcast::<Node>(), can_gc) |
| .unwrap(); |
| |
| let text = Text::new(DOMString::new(), &document, can_gc); |
| let _ = self.shadow_tree.borrow_mut().insert(ShadowTree { |
| selected_option: text.as_traced(), |
| }); |
| text_container |
| .upcast::<Node>() |
| .AppendChild(text.upcast::<Node>(), can_gc) |
| .unwrap(); |
| |
| let chevron_container = |
| HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); |
| chevron_container.upcast::<Element>().set_string_attribute( |
| &local_name!("style"), |
| CHEVRON_CONTAINER_STYLE.into(), |
| can_gc, |
| ); |
| chevron_container |
| .upcast::<Node>() |
| .set_text_content_for_element(Some("â–¾".into()), can_gc); |
| select_box |
| .upcast::<Node>() |
| .AppendChild(chevron_container.upcast::<Node>(), can_gc) |
| .unwrap(); |
| |
| root.upcast::<Node>() |
| .AppendChild(select_box.upcast::<Node>(), can_gc) |
| .unwrap(); |
| } |
| |
| fn shadow_tree(&self, can_gc: CanGc) -> Ref<'_, ShadowTree> { |
| if !self.upcast::<Element>().is_shadow_host() { |
| self.create_shadow_tree(can_gc); |
| } |
| |
| Ref::filter_map(self.shadow_tree.borrow(), Option::as_ref) |
| .ok() |
| .expect("UA shadow tree was not created") |
| } |
| |
| pub(crate) fn update_shadow_tree(&self, can_gc: CanGc) { |
| let shadow_tree = self.shadow_tree(can_gc); |
| |
| let selected_option_text = self |
| .selected_option() |
| .or_else(|| self.list_of_options().next()) |
| .map(|option| option.displayed_label()) |
| .unwrap_or_default(); |
| |
| // Replace newlines with whitespace, then collapse and trim whitespace |
| let displayed_text = itertools::join(selected_option_text.split_whitespace(), " "); |
| |
| shadow_tree |
| .selected_option |
| .upcast::<CharacterData>() |
| .SetData(displayed_text.trim().into()); |
| } |
| |
| pub(crate) fn selected_option(&self) -> Option<DomRoot<HTMLOptionElement>> { |
| self.list_of_options() |
| .find(|opt_elem| opt_elem.Selected()) |
| .or_else(|| self.list_of_options().next()) |
| } |
| |
| pub(crate) fn show_menu(&self) -> Option<usize> { |
| let (ipc_sender, ipc_receiver) = |
| generic_channel::channel().expect("Failed to create IPC channel!"); |
| |
| // Collect list of optgroups and options |
| let mut index = 0; |
| let mut embedder_option_from_option = |option: &HTMLOptionElement| { |
| let embedder_option = SelectElementOption { |
| id: index, |
| label: option.displayed_label().into(), |
| is_disabled: option.Disabled(), |
| }; |
| index += 1; |
| embedder_option |
| }; |
| let options = self |
| .upcast::<Node>() |
| .children() |
| .flat_map(|child| { |
| if let Some(option) = child.downcast::<HTMLOptionElement>() { |
| return Some(embedder_option_from_option(option).into()); |
| } |
| |
| if let Some(optgroup) = child.downcast::<HTMLOptGroupElement>() { |
| let options = optgroup |
| .upcast::<Node>() |
| .children() |
| .flat_map(DomRoot::downcast::<HTMLOptionElement>) |
| .map(|option| embedder_option_from_option(&option)) |
| .collect(); |
| let label = optgroup.Label().into(); |
| |
| return Some(SelectElementOptionOrOptgroup::Optgroup { label, options }); |
| } |
| |
| None |
| }) |
| .collect(); |
| |
| let rect = self.upcast::<Node>().border_box().unwrap_or_default(); |
| let rect = Rect::new( |
| Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), |
| Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), |
| ); |
| |
| let selected_index = self.list_of_options().position(|option| option.Selected()); |
| |
| let document = self.owner_document(); |
| document.send_to_embedder(EmbedderMsg::ShowFormControl( |
| document.webview_id(), |
| DeviceIntRect::from_untyped(&rect.to_box2d()), |
| EmbedderFormControl::SelectElement(options, selected_index, ipc_sender), |
| )); |
| |
| let Ok(response) = ipc_receiver.recv() else { |
| log::error!("Failed to receive response"); |
| return None; |
| }; |
| |
| response |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#send-select-update-notifications> |
| fn send_update_notifications(&self) { |
| // > When the user agent is to send select update notifications, queue an element task on the |
| // > user interaction task source given the select element to run these steps: |
| let this = Trusted::new(self); |
| self.owner_global() |
| .task_manager() |
| .user_interaction_task_source() |
| .queue(task!(send_select_update_notification: move || { |
| let this = this.root(); |
| |
| // TODO: Step 1. Set the select element's user validity to true. |
| |
| // Step 2. Fire an event named input at the select element, with the bubbles and composed |
| // attributes initialized to true. |
| this.upcast::<EventTarget>() |
| .fire_event_with_params( |
| atom!("input"), |
| EventBubbles::Bubbles, |
| EventCancelable::NotCancelable, |
| EventComposed::Composed, |
| CanGc::note(), |
| ); |
| |
| // Step 3. Fire an event named change at the select element, with the bubbles attribute initialized |
| // to true. |
| this.upcast::<EventTarget>() |
| .fire_bubbling_event(atom!("change"), CanGc::note()); |
| })); |
| } |
| } |
| |
| impl HTMLSelectElementMethods<crate::DomTypeHolder> for HTMLSelectElement { |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-add> |
| fn Add( |
| &self, |
| element: HTMLOptionElementOrHTMLOptGroupElement, |
| before: Option<HTMLElementOrLong>, |
| ) -> ErrorResult { |
| self.Options().Add(element, before) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-fe-disabled |
| make_bool_getter!(Disabled, "disabled"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-fe-disabled |
| make_bool_setter!(SetDisabled, "disabled"); |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-fae-form> |
| fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> { |
| self.form_owner() |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-select-multiple |
| make_bool_getter!(Multiple, "multiple"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-select-multiple |
| make_bool_setter!(SetMultiple, "multiple"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-fe-name |
| make_getter!(Name, "name"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-fe-name |
| make_atomic_setter!(SetName, "name"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-select-required |
| make_bool_getter!(Required, "required"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-select-required |
| make_bool_setter!(SetRequired, "required"); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-select-size |
| make_uint_getter!(Size, "size", DEFAULT_SELECT_SIZE); |
| |
| // https://html.spec.whatwg.org/multipage/#dom-select-size |
| make_uint_setter!(SetSize, "size", DEFAULT_SELECT_SIZE); |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-type> |
| fn Type(&self) -> DOMString { |
| DOMString::from(if self.Multiple() { |
| "select-multiple" |
| } else { |
| "select-one" |
| }) |
| } |
| |
| // https://html.spec.whatwg.org/multipage/#dom-lfe-labels |
| make_labels_getter!(Labels, labels_node_list); |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-options> |
| fn Options(&self) -> DomRoot<HTMLOptionsCollection> { |
| self.options.or_init(|| { |
| let window = self.owner_window(); |
| HTMLOptionsCollection::new(&window, self, Box::new(OptionsFilter), CanGc::note()) |
| }) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-length> |
| fn Length(&self) -> u32 { |
| self.Options().Length() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-length> |
| fn SetLength(&self, length: u32, can_gc: CanGc) { |
| self.Options().SetLength(length, can_gc) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-item> |
| fn Item(&self, index: u32) -> Option<DomRoot<Element>> { |
| self.Options().upcast().Item(index) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-item> |
| fn IndexedGetter(&self, index: u32) -> Option<DomRoot<Element>> { |
| self.Options().IndexedGetter(index) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-setter> |
| fn IndexedSetter( |
| &self, |
| index: u32, |
| value: Option<&HTMLOptionElement>, |
| can_gc: CanGc, |
| ) -> ErrorResult { |
| self.Options().IndexedSetter(index, value, can_gc) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-nameditem> |
| fn NamedItem(&self, name: DOMString) -> Option<DomRoot<HTMLOptionElement>> { |
| self.Options() |
| .NamedGetter(name) |
| .and_then(DomRoot::downcast::<HTMLOptionElement>) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-remove> |
| fn Remove_(&self, index: i32) { |
| self.Options().Remove(index) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-remove> |
| fn Remove(&self) { |
| self.upcast::<Element>().Remove(CanGc::note()) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-value> |
| fn Value(&self) -> DOMString { |
| self.list_of_options() |
| .find(|opt_elem| opt_elem.Selected()) |
| .map(|opt_elem| opt_elem.Value()) |
| .unwrap_or_default() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-value> |
| fn SetValue(&self, value: DOMString) { |
| let mut opt_iter = self.list_of_options(); |
| // Reset until we find an <option> with a matching value |
| for opt in opt_iter.by_ref() { |
| if opt.Value() == value { |
| opt.set_selectedness(true); |
| opt.set_dirtiness(true); |
| break; |
| } |
| opt.set_selectedness(false); |
| } |
| // Reset remaining <option> elements |
| for opt in opt_iter { |
| opt.set_selectedness(false); |
| } |
| |
| self.validity_state() |
| .perform_validation_and_update(ValidationFlags::VALUE_MISSING, CanGc::note()); |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-selectedindex> |
| fn SelectedIndex(&self) -> i32 { |
| self.list_of_options() |
| .enumerate() |
| .filter(|(_, opt_elem)| opt_elem.Selected()) |
| .map(|(i, _)| i as i32) |
| .next() |
| .unwrap_or(-1) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-select-selectedindex> |
| fn SetSelectedIndex(&self, index: i32, can_gc: CanGc) { |
| let mut selection_did_change = false; |
| |
| let mut opt_iter = self.list_of_options(); |
| for opt in opt_iter.by_ref().take(index as usize) { |
| selection_did_change |= opt.Selected(); |
| opt.set_selectedness(false); |
| } |
| if let Some(selected_option) = opt_iter.next() { |
| selection_did_change |= !selected_option.Selected(); |
| selected_option.set_selectedness(true); |
| selected_option.set_dirtiness(true); |
| |
| // Reset remaining <option> elements |
| for opt in opt_iter { |
| selection_did_change |= opt.Selected(); |
| opt.set_selectedness(false); |
| } |
| } |
| |
| if selection_did_change { |
| self.update_shadow_tree(can_gc); |
| } |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-cva-willvalidate> |
| fn WillValidate(&self) -> bool { |
| self.is_instance_validatable() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-cva-validity> |
| fn Validity(&self) -> DomRoot<ValidityState> { |
| self.validity_state() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-cva-checkvalidity> |
| fn CheckValidity(&self, can_gc: CanGc) -> bool { |
| self.check_validity(can_gc) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-cva-reportvalidity> |
| fn ReportValidity(&self, can_gc: CanGc) -> bool { |
| self.report_validity(can_gc) |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage> |
| fn ValidationMessage(&self) -> DOMString { |
| self.validation_message() |
| } |
| |
| /// <https://html.spec.whatwg.org/multipage/#dom-cva-setcustomvalidity> |
| fn SetCustomValidity(&self, error: DOMString) { |
| self.validity_state().set_custom_error_message(error); |
| } |
| } |
| |
| impl VirtualMethods for HTMLSelectElement { |
| fn super_type(&self) -> Option<&dyn VirtualMethods> { |
| Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods) |
| } |
| |
| fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) { |
| self.super_type() |
| .unwrap() |
| .attribute_mutated(attr, mutation, can_gc); |
| match *attr.local_name() { |
| local_name!("required") => { |
| self.validity_state() |
| .perform_validation_and_update(ValidationFlags::VALUE_MISSING, can_gc); |
| }, |
| local_name!("disabled") => { |
| let el = self.upcast::<Element>(); |
| 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_ancestors_disabled_state_for_form_control(); |
| }, |
| } |
| |
| self.validity_state() |
| .perform_validation_and_update(ValidationFlags::VALUE_MISSING, can_gc); |
| }, |
| local_name!("form") => { |
| self.form_attribute_mutated(mutation, can_gc); |
| }, |
| _ => {}, |
| } |
| } |
| |
| fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) { |
| if let Some(s) = self.super_type() { |
| s.bind_to_tree(context, can_gc); |
| } |
| |
| self.upcast::<Element>() |
| .check_ancestors_disabled_state_for_form_control(); |
| } |
| |
| fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { |
| self.super_type().unwrap().unbind_from_tree(context, can_gc); |
| |
| let node = self.upcast::<Node>(); |
| let el = self.upcast::<Element>(); |
| if node |
| .ancestors() |
| .any(|ancestor| ancestor.is::<HTMLFieldSetElement>()) |
| { |
| el.check_ancestors_disabled_state_for_form_control(); |
| } else { |
| el.check_disabled_attribute(); |
| } |
| } |
| |
| fn children_changed(&self, mutation: &ChildrenMutation) { |
| if let Some(s) = self.super_type() { |
| s.children_changed(mutation); |
| } |
| |
| self.update_shadow_tree(CanGc::note()); |
| } |
| |
| fn parse_plain_attribute(&self, local_name: &LocalName, value: DOMString) -> AttrValue { |
| match *local_name { |
| local_name!("size") => AttrValue::from_u32(value.into(), DEFAULT_SELECT_SIZE), |
| _ => self |
| .super_type() |
| .unwrap() |
| .parse_plain_attribute(local_name, value), |
| } |
| } |
| } |
| |
| impl FormControl for HTMLSelectElement { |
| fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> { |
| self.form_owner.get() |
| } |
| |
| fn set_form_owner(&self, form: Option<&HTMLFormElement>) { |
| self.form_owner.set(form); |
| } |
| |
| fn to_element(&self) -> &Element { |
| self.upcast::<Element>() |
| } |
| } |
| |
| impl Validatable for HTMLSelectElement { |
| fn as_element(&self) -> &Element { |
| self.upcast() |
| } |
| |
| fn validity_state(&self) -> DomRoot<ValidityState> { |
| self.validity_state |
| .or_init(|| ValidityState::new(&self.owner_window(), self.upcast(), CanGc::note())) |
| } |
| |
| fn is_instance_validatable(&self) -> bool { |
| // https://html.spec.whatwg.org/multipage/#enabling-and-disabling-form-controls%3A-the-disabled-attribute%3Abarred-from-constraint-validation |
| // https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation |
| !self.upcast::<Element>().disabled_state() && !is_barred_by_datalist_ancestor(self.upcast()) |
| } |
| |
| fn perform_validation( |
| &self, |
| validate_flags: ValidationFlags, |
| _can_gc: CanGc, |
| ) -> ValidationFlags { |
| let mut failed_flags = ValidationFlags::empty(); |
| |
| // https://html.spec.whatwg.org/multipage/#suffering-from-being-missing |
| // https://html.spec.whatwg.org/multipage/#the-select-element%3Asuffering-from-being-missing |
| if validate_flags.contains(ValidationFlags::VALUE_MISSING) && self.Required() { |
| let placeholder = self.get_placeholder_label_option(); |
| let is_value_missing = !self |
| .list_of_options() |
| .any(|e| e.Selected() && placeholder != Some(e)); |
| failed_flags.set(ValidationFlags::VALUE_MISSING, is_value_missing); |
| } |
| |
| failed_flags |
| } |
| } |
| |
| impl Activatable for HTMLSelectElement { |
| fn as_element(&self) -> &Element { |
| self.upcast() |
| } |
| |
| fn is_instance_activatable(&self) -> bool { |
| true |
| } |
| |
| fn activation_behavior(&self, _event: &Event, _target: &EventTarget, can_gc: CanGc) { |
| let Some(selected_value) = self.show_menu() else { |
| // The user did not select a value |
| return; |
| }; |
| |
| self.SetSelectedIndex(selected_value as i32, can_gc); |
| self.send_update_notifications(); |
| } |
| } |
| |
| enum Choice3<I, J, K> { |
| First(I), |
| Second(J), |
| Third(K), |
| } |
| |
| impl<I, J, K, T> Iterator for Choice3<I, J, K> |
| where |
| I: Iterator<Item = T>, |
| J: Iterator<Item = T>, |
| K: Iterator<Item = T>, |
| { |
| type Item = T; |
| |
| fn next(&mut self) -> Option<T> { |
| match *self { |
| Choice3::First(ref mut i) => i.next(), |
| Choice3::Second(ref mut j) => j.next(), |
| Choice3::Third(ref mut k) => k.next(), |
| } |
| } |
| |
| fn size_hint(&self) -> (usize, Option<usize>) { |
| match *self { |
| Choice3::First(ref i) => i.size_hint(), |
| Choice3::Second(ref j) => j.size_hint(), |
| Choice3::Third(ref k) => k.size_hint(), |
| } |
| } |
| } |