/* 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, RefCell, RefMut}; use std::ptr::NonNull; use std::{f64, ptr}; use dom_struct::dom_struct; use embedder_traits::{EmbedderControlRequest, InputMethodRequest, RgbColor, SelectedFile}; use encoding_rs::Encoding; use fonts::{ByteIndex, TextByteRange}; use html5ever::{LocalName, Prefix, local_name}; use js::context::JSContext; use js::jsapi::{ ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject, NewUCRegExpObject, ObjectIsDate, RegExpFlag_UnicodeSets, RegExpFlags, }; use js::jsval::UndefinedValue; use js::rust::wrappers::{CheckRegExpSyntax, ExecuteRegExpNoStatics, ObjectIsRegExp}; use js::rust::{HandleObject, MutableHandleObject}; use layout_api::{ScriptSelection, SharedSelection}; use script_bindings::codegen::GenericBindings::AttrBinding::AttrMethods; use script_bindings::domstring::parse_floating_point_number; use servo_base::generic_channel::GenericSender; use servo_base::text::Utf16CodeUnitLength; use style::attr::AttrValue; use style::str::split_commas; use stylo_atoms::Atom; use stylo_dom::ElementState; use time::OffsetDateTime; use unicode_bidi::{BidiClass, bidi_class}; use webdriver::error::ErrorStatus; use crate::clipboard_provider::EmbedderClipboardProvider; 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::EventBinding::EventMethods; use crate::dom::bindings::codegen::Bindings::FileListBinding::FileListMethods; use crate::dom::bindings::codegen::Bindings::HTMLFormElementBinding::SelectionMode; use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; use crate::dom::bindings::error::{Error, ErrorResult}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::clipboardevent::{ClipboardEvent, ClipboardEventType}; use crate::dom::compositionevent::CompositionEvent; use crate::dom::document::Document; use crate::dom::document_embedder_controls::ControlElement; use crate::dom::element::{AttributeMutation, Element}; use crate::dom::event::Event; use crate::dom::eventtarget::EventTarget; use crate::dom::filelist::FileList; use crate::dom::globalscope::GlobalScope; use crate::dom::html::htmldatalistelement::HTMLDataListElement; use crate::dom::html::htmlelement::HTMLElement; use crate::dom::html::htmlfieldsetelement::HTMLFieldSetElement; use crate::dom::html::htmlformelement::{ FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, SubmittedFrom, }; use crate::dom::htmlinputelement::radio_input_type::{ broadcast_radio_checked, perform_radio_group_validation, }; use crate::dom::input_element::input_type::InputType; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::node::{ BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext, }; use crate::dom::nodelist::NodeList; use crate::dom::textcontrol::{TextControlElement, TextControlSelection}; use crate::dom::types::{FocusEvent, MouseEvent}; use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor}; use crate::dom::validitystate::{ValidationFlags, ValidityState}; use crate::dom::virtualmethods::VirtualMethods; use crate::realms::enter_realm; use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; use crate::textinput::{ClipboardEventFlags, IsComposing, KeyReaction, Lines, TextInput}; pub(crate) mod button_input_type; pub(crate) mod checkbox_input_type; pub(crate) mod color_input_type; pub(crate) mod date_input_type; pub(crate) mod datetime_local_input_type; pub(crate) mod email_input_type; pub(crate) mod file_input_type; pub(crate) mod hidden_input_type; pub(crate) mod image_input_type; pub(crate) mod input_type; pub(crate) mod month_input_type; pub(crate) mod number_input_type; pub(crate) mod password_input_type; pub(crate) mod radio_input_type; pub(crate) mod range_input_type; pub(crate) mod reset_input_type; pub(crate) mod search_input_type; pub(crate) mod submit_input_type; pub(crate) mod tel_input_type; pub(crate) mod text_input_type; pub(crate) mod text_input_widget; pub(crate) mod text_value_widget; pub(crate) mod time_input_type; pub(crate) mod url_input_type; pub(crate) mod week_input_type; #[derive(Debug, PartialEq)] enum ValueMode { /// Value, /// Default, /// DefaultOn, /// Filename, } #[derive(Debug, PartialEq)] enum StepDirection { Up, Down, } #[dom_struct] pub(crate) struct HTMLInputElement { htmlelement: HTMLElement, input_type: DomRefCell, /// Whether or not the [`InputType`] for this [`HTMLInputElement`] renders as /// textual input. This is cached so that it can be read during layout. is_textual_or_password: Cell, /// checked_changed: Cell, placeholder: DomRefCell, size: Cell, maxlength: Cell, minlength: Cell, #[no_trace] textinput: DomRefCell>, /// value_dirty: Cell, /// A [`SharedSelection`] that is shared with layout. This can be updated dyanmnically /// and layout should reflect the new value after a display list update. #[no_trace] #[conditional_malloc_size_of] shared_selection: SharedSelection, form_owner: MutNullableDom, labels_node_list: MutNullableDom, validity_state: MutNullableDom, #[no_trace] pending_webdriver_response: RefCell>, } #[derive(JSTraceable)] pub(crate) struct InputActivationState { indeterminate: bool, checked: bool, checked_radio: Option>, was_radio: bool, was_checkbox: bool, // was_mutable is implied: pre-activation would return None if it wasn't } static DEFAULT_INPUT_SIZE: u32 = 20; static DEFAULT_MAX_LENGTH: i32 = -1; static DEFAULT_MIN_LENGTH: i32 = -1; #[expect(non_snake_case)] impl HTMLInputElement { fn new_inherited( local_name: LocalName, prefix: Option, document: &Document, ) -> HTMLInputElement { let embedder_sender = document .window() .as_global_scope() .script_to_embedder_chan() .clone(); HTMLInputElement { htmlelement: HTMLElement::new_inherited_with_state( ElementState::ENABLED | ElementState::READWRITE, local_name, prefix, document, ), input_type: DomRefCell::new(InputType::new_text()), is_textual_or_password: Cell::new(true), placeholder: DomRefCell::new(DOMString::new()), checked_changed: Cell::new(false), maxlength: Cell::new(DEFAULT_MAX_LENGTH), minlength: Cell::new(DEFAULT_MIN_LENGTH), size: Cell::new(DEFAULT_INPUT_SIZE), textinput: DomRefCell::new(TextInput::new( Lines::Single, DOMString::new(), EmbedderClipboardProvider { embedder_sender, webview_id: document.webview_id(), }, )), value_dirty: Cell::new(false), shared_selection: Default::default(), form_owner: Default::default(), labels_node_list: MutNullableDom::new(None), validity_state: Default::default(), pending_webdriver_response: Default::default(), } } 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(HTMLInputElement::new_inherited( local_name, prefix, document, )), document, proto, ) } pub(crate) fn auto_directionality(&self) -> Option { match *self.input_type() { InputType::Text(_) | InputType::Search(_) | InputType::Url(_) | InputType::Email(_) => { let value: String = self.Value().to_string(); Some(HTMLInputElement::directionality_from_value(&value)) }, _ => None, } } pub(crate) fn directionality_from_value(value: &str) -> String { if HTMLInputElement::is_first_strong_character_rtl(value) { "rtl".to_owned() } else { "ltr".to_owned() } } fn is_first_strong_character_rtl(value: &str) -> bool { for ch in value.chars() { return match bidi_class(ch) { BidiClass::L => false, BidiClass::AL => true, BidiClass::R => true, _ => continue, }; } false } // https://html.spec.whatwg.org/multipage/#dom-input-value /// fn value_mode(&self) -> ValueMode { match *self.input_type() { InputType::Submit(_) | InputType::Reset(_) | InputType::Button(_) | InputType::Image(_) | InputType::Hidden(_) => ValueMode::Default, InputType::Checkbox(_) | InputType::Radio(_) => ValueMode::DefaultOn, InputType::Color(_) | InputType::Date(_) | InputType::DatetimeLocal(_) | InputType::Email(_) | InputType::Month(_) | InputType::Number(_) | InputType::Password(_) | InputType::Range(_) | InputType::Search(_) | InputType::Tel(_) | InputType::Text(_) | InputType::Time(_) | InputType::Url(_) | InputType::Week(_) => ValueMode::Value, InputType::File(_) => ValueMode::Filename, } } #[inline] pub(crate) fn input_type(&self) -> Ref<'_, InputType> { self.input_type.borrow() } /// pub(crate) fn is_nontypeable(&self) -> bool { matches!( *self.input_type(), InputType::Button(_) | InputType::Checkbox(_) | InputType::Color(_) | InputType::File(_) | InputType::Hidden(_) | InputType::Image(_) | InputType::Radio(_) | InputType::Range(_) | InputType::Reset(_) | InputType::Submit(_) ) } #[inline] pub(crate) fn is_submit_button(&self) -> bool { matches!( *self.input_type(), InputType::Submit(_) | InputType::Image(_) ) } fn does_minmaxlength_apply(&self) -> bool { matches!( *self.input_type(), InputType::Text(_) | InputType::Search(_) | InputType::Url(_) | InputType::Tel(_) | InputType::Email(_) | InputType::Password(_) ) } fn does_pattern_apply(&self) -> bool { matches!( *self.input_type(), InputType::Text(_) | InputType::Search(_) | InputType::Url(_) | InputType::Tel(_) | InputType::Email(_) | InputType::Password(_) ) } fn does_multiple_apply(&self) -> bool { matches!(*self.input_type(), InputType::Email(_)) } // valueAsNumber, step, min, and max all share the same set of // input types they apply to fn does_value_as_number_apply(&self) -> bool { matches!( *self.input_type(), InputType::Date(_) | InputType::Month(_) | InputType::Week(_) | InputType::Time(_) | InputType::DatetimeLocal(_) | InputType::Number(_) | InputType::Range(_) ) } fn does_value_as_date_apply(&self) -> bool { matches!( *self.input_type(), InputType::Date(_) | InputType::Month(_) | InputType::Week(_) | InputType::Time(_) ) } /// fn allowed_value_step(&self) -> Option { // Step 1. If the attribute does not apply, then there is no allowed value step. // NOTE: The attribute applies iff there is a default step let default_step = self.default_step()?; // Step 2. Otherwise, if the attribute is absent, then the allowed value step // is the default step multiplied by the step scale factor. let Some(attribute) = self.upcast::().get_attribute(&local_name!("step")) else { return Some(default_step * self.step_scale_factor()); }; // Step 3. Otherwise, if the attribute's value is an ASCII case-insensitive match // for the string "any", then there is no allowed value step. if attribute.value().eq_ignore_ascii_case("any") { return None; } // Step 4. Otherwise, if the rules for parsing floating-point number values, when they // are applied to the attribute's value, return an error, zero, or a number less than zero, // then the allowed value step is the default step multiplied by the step scale factor. let Some(parsed_value) = parse_floating_point_number(&attribute.value()).filter(|value| *value > 0.0) else { return Some(default_step * self.step_scale_factor()); }; // Step 5. Otherwise, the allowed value step is the number returned by the rules for parsing // floating-point number values when they are applied to the attribute's value, // multiplied by the step scale factor. Some(parsed_value * self.step_scale_factor()) } /// fn minimum(&self) -> Option { self.upcast::() .get_attribute(&local_name!("min")) .and_then(|attribute| self.convert_string_to_number(&attribute.value())) .or_else(|| self.default_minimum()) } /// fn maximum(&self) -> Option { self.upcast::() .get_attribute(&local_name!("max")) .and_then(|attribute| self.convert_string_to_number(&attribute.value())) .or_else(|| self.default_maximum()) } /// when allowed_value_step and minimum both exist, this is the smallest /// value >= minimum that lies on an integer step fn stepped_minimum(&self) -> Option { match (self.minimum(), self.allowed_value_step()) { (Some(min), Some(allowed_step)) => { let step_base = self.step_base(); // how many steps is min from step_base? let nsteps = (min - step_base) / allowed_step; // count that many integer steps, rounded +, from step_base Some(step_base + (allowed_step * nsteps.ceil())) }, (_, _) => None, } } /// when allowed_value_step and maximum both exist, this is the smallest /// value <= maximum that lies on an integer step fn stepped_maximum(&self) -> Option { match (self.maximum(), self.allowed_value_step()) { (Some(max), Some(allowed_step)) => { let step_base = self.step_base(); // how many steps is max from step_base? let nsteps = (max - step_base) / allowed_step; // count that many integer steps, rounded -, from step_base Some(step_base + (allowed_step * nsteps.floor())) }, (_, _) => None, } } /// fn default_minimum(&self) -> Option { match *self.input_type() { InputType::Range(_) => Some(0.0), _ => None, } } /// fn default_maximum(&self) -> Option { match *self.input_type() { InputType::Range(_) => Some(100.0), _ => None, } } /// fn default_range_value(&self) -> f64 { let min = self.minimum().unwrap_or(0.0); let max = self.maximum().unwrap_or(100.0); if max < min { min } else { min + (max - min) * 0.5 } } /// fn default_step(&self) -> Option { match *self.input_type() { InputType::Date(_) => Some(1.0), InputType::Month(_) => Some(1.0), InputType::Week(_) => Some(1.0), InputType::Time(_) => Some(60.0), InputType::DatetimeLocal(_) => Some(60.0), InputType::Number(_) => Some(1.0), InputType::Range(_) => Some(1.0), _ => None, } } /// fn step_scale_factor(&self) -> f64 { match *self.input_type() { InputType::Date(_) => 86400000.0, InputType::Month(_) => 1.0, InputType::Week(_) => 604800000.0, InputType::Time(_) => 1000.0, InputType::DatetimeLocal(_) => 1000.0, InputType::Number(_) => 1.0, InputType::Range(_) => 1.0, _ => unreachable!(), } } /// fn step_base(&self) -> f64 { // Step 1. If the element has a min content attribute, and the result of applying // the algorithm to convert a string to a number to the value of the min content attribute // is not an error, then return that result. if let Some(minimum) = self .upcast::() .get_attribute(&local_name!("min")) .and_then(|attribute| self.convert_string_to_number(&attribute.value())) { return minimum; } // Step 2. If the element has a value content attribute, and the result of applying the // algorithm to convert a string to a number to the value of the value content attribute // is not an error, then return that result. if let Some(value) = self .upcast::() .get_attribute(&local_name!("value")) .and_then(|attribute| self.convert_string_to_number(&attribute.value())) { return value; } // Step 3. If a default step base is defined for this element given its type attribute's state, then return it. if let Some(default_step_base) = self.default_step_base() { return default_step_base; } // Step 4. Return zero. 0.0 } /// fn default_step_base(&self) -> Option { match *self.input_type() { InputType::Week(_) => Some(-259200000.0), _ => None, } } /// /// /// fn step_up_or_down(&self, n: i32, dir: StepDirection, can_gc: CanGc) -> ErrorResult { // Step 1. If the stepDown() and stepUp() methods do not apply, as defined for the // input element's type attribute's current state, then throw an "InvalidStateError" DOMException. if !self.does_value_as_number_apply() { return Err(Error::InvalidState(None)); } let step_base = self.step_base(); // Step 2. If the element has no allowed value step, then throw an "InvalidStateError" DOMException. let Some(allowed_value_step) = self.allowed_value_step() else { return Err(Error::InvalidState(None)); }; // Step 3. If the element has a minimum and a maximum and the minimum is greater than the maximum, // then return. let minimum = self.minimum(); let maximum = self.maximum(); if let (Some(min), Some(max)) = (minimum, maximum) { if min > max { return Ok(()); } // Step 4. If the element has a minimum and a maximum and there is no value greater than or equal to the // element's minimum and less than or equal to the element's maximum that, when subtracted from the step // base, is an integral multiple of the allowed value step, then return. if let Some(stepped_minimum) = self.stepped_minimum() { if stepped_minimum > max { return Ok(()); } } } // Step 5. If applying the algorithm to convert a string to a number to the string given // by the element's value does not result in an error, then let value be the result of // that algorithm. Otherwise, let value be zero. let mut value: f64 = self .convert_string_to_number(&self.Value().str()) .unwrap_or(0.0); // Step 6. Let valueBeforeStepping be value. let valueBeforeStepping = value; // Step 7. If value subtracted from the step base is not an integral multiple of the allowed value step, // then set value to the nearest value that, when subtracted from the step base, is an integral multiple // of the allowed value step, and that is less than value if the method invoked was the stepDown() method, // and more than value otherwise. if (value - step_base) % allowed_value_step != 0.0 { value = match dir { StepDirection::Down => // step down a fractional step to be on a step multiple { let intervals_from_base = ((value - step_base) / allowed_value_step).floor(); intervals_from_base * allowed_value_step + step_base }, StepDirection::Up => // step up a fractional step to be on a step multiple { let intervals_from_base = ((value - step_base) / allowed_value_step).ceil(); intervals_from_base * allowed_value_step + step_base }, }; } // Otherwise (value subtracted from the step base is an integral multiple of the allowed value step): else { // Step 7.1 Let n be the argument. // Step 7.2 Let delta be the allowed value step multiplied by n. // Step 7.3 If the method invoked was the stepDown() method, negate delta. // Step 7.4 Let value be the result of adding delta to value. value += match dir { StepDirection::Down => -f64::from(n) * allowed_value_step, StepDirection::Up => f64::from(n) * allowed_value_step, }; } // Step 8. If the element has a minimum, and value is less than that minimum, then set value to the smallest // value that, when subtracted from the step base, is an integral multiple of the allowed value step, and that // is more than or equal to that minimum. if let Some(min) = minimum { if value < min { value = self.stepped_minimum().unwrap_or(value); } } // Step 9. If the element has a maximum, and value is greater than that maximum, then set value to the largest // value that, when subtracted from the step base, is an integral multiple of the allowed value step, and that // is less than or equal to that maximum. if let Some(max) = maximum { if value > max { value = self.stepped_maximum().unwrap_or(value); } } // Step 10. If either the method invoked was the stepDown() method and value is greater than // valueBeforeStepping, or the method invoked was the stepUp() method and value is less than // valueBeforeStepping, then return. match dir { StepDirection::Down => { if value > valueBeforeStepping { return Ok(()); } }, StepDirection::Up => { if value < valueBeforeStepping { return Ok(()); } }, } // Step 11. Let value as string be the result of running the algorithm to convert a number to a string, // as defined for the input element's type attribute's current state, on value. // Step 12. Set the value of the element to value as string. self.SetValueAsNumber(value, can_gc) } /// fn suggestions_source_element(&self) -> Option> { let list_string = self .upcast::() .get_string_attribute(&local_name!("list")); if list_string.is_empty() { return None; } let ancestor = self .upcast::() .GetRootNode(&GetRootNodeOptions::empty()); let first_with_id = &ancestor .traverse_preorder(ShadowIncluding::No) .find(|node| { node.downcast::() .is_some_and(|e| e.Id() == list_string) }); first_with_id .as_ref() .and_then(|el| el.downcast::()) .map(DomRoot::from_ref) } /// fn suffers_from_being_missing(&self, value: &DOMString) -> bool { self.input_type() .as_specific() .suffers_from_being_missing(self, value) } /// fn suffers_from_type_mismatch(&self, value: &DOMString) -> bool { if value.is_empty() { return false; } self.input_type() .as_specific() .suffers_from_type_mismatch(self, value) } /// fn suffers_from_pattern_mismatch(&self, value: &DOMString, can_gc: CanGc) -> bool { // https://html.spec.whatwg.org/multipage/#the-pattern-attribute%3Asuffering-from-a-pattern-mismatch // https://html.spec.whatwg.org/multipage/#the-pattern-attribute%3Asuffering-from-a-pattern-mismatch-2 let pattern_str = self.Pattern(); if value.is_empty() || pattern_str.is_empty() || !self.does_pattern_apply() { return false; } // Rust's regex is not compatible, we need to use mozjs RegExp. let cx = GlobalScope::get_cx(); let _ac = enter_realm(self); rooted!(in(*cx) let mut pattern = ptr::null_mut::()); if compile_pattern(cx, &pattern_str.str(), pattern.handle_mut(), can_gc) { if self.Multiple() && self.does_multiple_apply() { !split_commas(&value.str()) .all(|s| matches_js_regex(cx, pattern.handle(), s, can_gc).unwrap_or(true)) } else { !matches_js_regex(cx, pattern.handle(), &value.str(), can_gc).unwrap_or(true) } } else { // Element doesn't suffer from pattern mismatch if pattern is invalid. false } } /// fn suffers_from_bad_input(&self, value: &DOMString) -> bool { if value.is_empty() { return false; } self.input_type() .as_specific() .suffers_from_bad_input(value) } // https://html.spec.whatwg.org/multipage/#suffering-from-being-too-long /// fn suffers_from_length_issues(&self, value: &DOMString) -> ValidationFlags { // https://html.spec.whatwg.org/multipage/#limiting-user-input-length%3A-the-maxlength-attribute%3Asuffering-from-being-too-long // https://html.spec.whatwg.org/multipage/#setting-minimum-input-length-requirements%3A-the-minlength-attribute%3Asuffering-from-being-too-short let value_dirty = self.value_dirty.get(); let textinput = self.textinput.borrow(); let edit_by_user = !textinput.was_last_change_by_set_content(); if value.is_empty() || !value_dirty || !edit_by_user || !self.does_minmaxlength_apply() { return ValidationFlags::empty(); } let mut failed_flags = ValidationFlags::empty(); let Utf16CodeUnitLength(value_len) = textinput.len_utf16(); let min_length = self.MinLength(); let max_length = self.MaxLength(); if min_length != DEFAULT_MIN_LENGTH && value_len < (min_length as usize) { failed_flags.insert(ValidationFlags::TOO_SHORT); } if max_length != DEFAULT_MAX_LENGTH && value_len > (max_length as usize) { failed_flags.insert(ValidationFlags::TOO_LONG); } failed_flags } /// * /// * /// * fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags { if value.is_empty() || !self.does_value_as_number_apply() { return ValidationFlags::empty(); } let Some(value_as_number) = self.convert_string_to_number(&value.str()) else { return ValidationFlags::empty(); }; let mut failed_flags = ValidationFlags::empty(); let min_value = self.minimum(); let max_value = self.maximum(); // https://html.spec.whatwg.org/multipage/#has-a-reversed-range let has_reversed_range = match (min_value, max_value) { (Some(min), Some(max)) => self.input_type().has_periodic_domain() && min > max, _ => false, }; if has_reversed_range { // https://html.spec.whatwg.org/multipage/#the-min-and-max-attributes:has-a-reversed-range-3 if value_as_number > max_value.unwrap() && value_as_number < min_value.unwrap() { failed_flags.insert(ValidationFlags::RANGE_UNDERFLOW); failed_flags.insert(ValidationFlags::RANGE_OVERFLOW); } } else { // https://html.spec.whatwg.org/multipage/#the-min-and-max-attributes%3Asuffering-from-an-underflow-2 if let Some(min_value) = min_value { if value_as_number < min_value { failed_flags.insert(ValidationFlags::RANGE_UNDERFLOW); } } // https://html.spec.whatwg.org/multipage/#the-min-and-max-attributes%3Asuffering-from-an-overflow-2 if let Some(max_value) = max_value { if value_as_number > max_value { failed_flags.insert(ValidationFlags::RANGE_OVERFLOW); } } } // https://html.spec.whatwg.org/multipage/#the-step-attribute%3Asuffering-from-a-step-mismatch if let Some(step) = self.allowed_value_step() { // TODO: Spec has some issues here, see https://github.com/whatwg/html/issues/5207. // Chrome and Firefox parse values as decimals to get exact results, // we probably should too. let diff = (self.step_base() - value_as_number) % step / value_as_number; if diff.abs() > 1e-12 { failed_flags.insert(ValidationFlags::STEP_MISMATCH); } } failed_flags } /// Whether this input type renders as a basic text input widget. pub(crate) fn is_textual_or_password(&self) -> bool { self.is_textual_or_password.get() } fn may_have_embedder_control(&self) -> bool { let el = self.upcast::(); matches!(*self.input_type(), InputType::Color(_)) && !el.disabled_state() } fn handle_key_reaction(&self, action: KeyReaction, event: &Event, can_gc: CanGc) { match action { KeyReaction::TriggerDefaultAction => { self.implicit_submission(can_gc); event.mark_as_handled(); }, KeyReaction::DispatchInput(text, is_composing, input_type) => { if event.IsTrusted() { self.textinput.borrow().queue_input_event( self.upcast(), text, is_composing, input_type, ); } self.value_dirty.set(true); self.update_placeholder_shown_state(); self.upcast::().dirty(NodeDamage::Other); event.mark_as_handled(); }, KeyReaction::RedrawSelection => { self.maybe_update_shared_selection(); event.mark_as_handled(); }, KeyReaction::Nothing => (), } } /// Return a string that represents the contents of the element in its displayed shadow DOM. fn value_for_shadow_dom(&self) -> DOMString { let input_type = &*self.input_type(); match input_type { InputType::Checkbox(_) | InputType::Radio(_) | InputType::Image(_) | InputType::Hidden(_) | InputType::Range(_) => input_type.as_specific().value_for_shadow_dom(self), _ => { if let Some(attribute_value) = self .upcast::() .get_attribute(&local_name!("value")) .map(|attribute| attribute.Value()) { return attribute_value; } input_type.as_specific().value_for_shadow_dom(self) }, } } fn textinput_mut(&self) -> RefMut<'_, TextInput> { self.textinput.borrow_mut() } } impl<'dom> LayoutDom<'dom, HTMLInputElement> { /// Textual input, specifically text entry and domain specific input has /// a default preferred size. /// /// /// // FIXME(stevennovaryo): Implement the calculation of default preferred size // for domain specific input widgets correctly. // FIXME(#4378): Implement the calculation of average character width for // textual input correctly. pub(crate) fn size_for_layout(self) -> u32 { self.unsafe_get().size.get() } pub(crate) fn selection_for_layout(self) -> Option { if !self.unsafe_get().is_textual_or_password.get() { return None; } Some(self.unsafe_get().shared_selection.clone()) } } impl TextControlElement for HTMLInputElement { /// fn selection_api_applies(&self) -> bool { matches!( *self.input_type(), InputType::Text(_) | InputType::Search(_) | InputType::Url(_) | InputType::Tel(_) | InputType::Password(_) ) } // https://html.spec.whatwg.org/multipage/#concept-input-apply // // Defines input types to which the select() IDL method applies. These are a superset of the // types for which selection_api_applies() returns true. // // Types omitted which could theoretically be included if they were // rendered as a text control: file fn has_selectable_text(&self) -> bool { self.is_textual_or_password() && !self.textinput.borrow().get_content().is_empty() } fn has_uncollapsed_selection(&self) -> bool { self.textinput.borrow().has_uncollapsed_selection() } fn set_dirty_value_flag(&self, value: bool) { self.value_dirty.set(value) } fn select_all(&self) { self.textinput.borrow_mut().select_all(); self.maybe_update_shared_selection(); } fn maybe_update_shared_selection(&self) { let offsets = self.textinput.borrow().sorted_selection_offsets_range(); let (start, end) = (offsets.start.0, offsets.end.0); let range = TextByteRange::new(ByteIndex(start), ByteIndex(end)); let enabled = self.is_textual_or_password() && self.upcast::().focus_state(); let mut shared_selection = self.shared_selection.borrow_mut(); if range == shared_selection.range && enabled == shared_selection.enabled { return; } *shared_selection = ScriptSelection { range, character_range: self .textinput .borrow() .sorted_selection_character_offsets_range(), enabled, }; self.owner_window().layout().set_needs_new_display_list(); } fn is_password_field(&self) -> bool { matches!(*self.input_type(), InputType::Password(_)) } fn placeholder_text<'a>(&'a self) -> Ref<'a, DOMString> { self.placeholder.borrow() } fn value_text(&self) -> DOMString { self.Value() } } impl HTMLInputElementMethods for HTMLInputElement { // https://html.spec.whatwg.org/multipage/#dom-input-accept make_getter!(Accept, "accept"); // https://html.spec.whatwg.org/multipage/#dom-input-accept make_setter!(SetAccept, "accept"); // https://html.spec.whatwg.org/multipage/#dom-input-alpha make_bool_getter!(Alpha, "alpha"); // https://html.spec.whatwg.org/multipage/#dom-input-alpha make_bool_setter!(SetAlpha, "alpha"); // https://html.spec.whatwg.org/multipage/#dom-input-alt make_getter!(Alt, "alt"); // https://html.spec.whatwg.org/multipage/#dom-input-alt make_setter!(SetAlt, "alt"); // https://html.spec.whatwg.org/multipage/#dom-input-dirName make_getter!(DirName, "dirname"); // https://html.spec.whatwg.org/multipage/#dom-input-dirName make_setter!(SetDirName, "dirname"); // 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"); /// fn GetForm(&self) -> Option> { self.form_owner() } /// fn GetFiles(&self) -> Option> { self.input_type() .as_specific() .get_files() .as_ref() .cloned() } /// fn SetFiles(&self, files: Option<&FileList>) { if let Some(files) = files { self.input_type().as_specific().set_files(files) } } // https://html.spec.whatwg.org/multipage/#dom-input-defaultchecked make_bool_getter!(DefaultChecked, "checked"); // https://html.spec.whatwg.org/multipage/#dom-input-defaultchecked make_bool_setter!(SetDefaultChecked, "checked"); /// fn Checked(&self) -> bool { self.upcast::() .state() .contains(ElementState::CHECKED) } /// fn SetChecked(&self, checked: bool, can_gc: CanGc) { self.update_checked_state(checked, true, can_gc); self.value_changed(can_gc); } // https://html.spec.whatwg.org/multipage/#attr-input-colorspace make_enumerated_getter!( ColorSpace, "colorspace", "limited-srgb" | "display-p3", missing => "limited-srgb", invalid => "limited-srgb" ); // https://html.spec.whatwg.org/multipage/#attr-input-colorspace make_setter!(SetColorSpace, "colorspace"); // https://html.spec.whatwg.org/multipage/#dom-input-readonly make_bool_getter!(ReadOnly, "readonly"); // https://html.spec.whatwg.org/multipage/#dom-input-readonly make_bool_setter!(SetReadOnly, "readonly"); // https://html.spec.whatwg.org/multipage/#dom-input-size make_uint_getter!(Size, "size", DEFAULT_INPUT_SIZE); // https://html.spec.whatwg.org/multipage/#dom-input-size make_limited_uint_setter!(SetSize, "size", DEFAULT_INPUT_SIZE); /// fn Type(&self) -> DOMString { DOMString::from(self.input_type().as_str()) } // https://html.spec.whatwg.org/multipage/#dom-input-type make_atomic_setter!(SetType, "type"); /// fn Value(&self) -> DOMString { match self.value_mode() { ValueMode::Value => self.textinput.borrow().get_content(), ValueMode::Default => self .upcast::() .get_attribute(&local_name!("value")) .map_or(DOMString::from(""), |a| { DOMString::from(a.summarize().value) }), ValueMode::DefaultOn => self .upcast::() .get_attribute(&local_name!("value")) .map_or(DOMString::from("on"), |a| { DOMString::from(a.summarize().value) }), ValueMode::Filename => { let mut path = DOMString::from(""); match self.input_type().as_specific().get_files() { Some(ref fl) => match fl.Item(0) { Some(ref f) => { path.push_str("C:\\fakepath\\"); path.push_str(&f.name().str()); path }, None => path, }, None => path, } }, } } /// fn SetValue(&self, mut value: DOMString, can_gc: CanGc) -> ErrorResult { match self.value_mode() { ValueMode::Value => { { // Step 3. Set the element's dirty value flag to true. self.value_dirty.set(true); // Step 4. Invoke the value sanitization algorithm, if the element's type // attribute's current state defines one. self.sanitize_value(&mut value); let mut textinput = self.textinput.borrow_mut(); // Step 5. If the element's value (after applying the value sanitization algorithm) // is different from oldValue, and the element has a text entry cursor position, // move the text entry cursor position to the end of the text control, // unselecting any selected text and resetting the selection direction to "none". if textinput.get_content() != value { // Step 2. Set the element's value to the new value. textinput.set_content(value); textinput.clear_selection_to_end(); } } // Additionally, update the placeholder shown state in another // scope to prevent the borrow checker issue. This is normally // being done in the attributed mutated. self.update_placeholder_shown_state(); self.maybe_update_shared_selection(); }, ValueMode::Default | ValueMode::DefaultOn => { self.upcast::() .set_string_attribute(&local_name!("value"), value, can_gc); }, ValueMode::Filename => { if value.is_empty() { let window = self.owner_window(); let fl = FileList::new(&window, vec![], can_gc); self.input_type().as_specific().set_files(&fl) } else { return Err(Error::InvalidState(None)); } }, } self.value_changed(can_gc); self.upcast::().dirty(NodeDamage::Other); Ok(()) } // https://html.spec.whatwg.org/multipage/#dom-input-defaultvalue make_getter!(DefaultValue, "value"); // https://html.spec.whatwg.org/multipage/#dom-input-defaultvalue make_setter!(SetDefaultValue, "value"); // https://html.spec.whatwg.org/multipage/#dom-input-min make_getter!(Min, "min"); // https://html.spec.whatwg.org/multipage/#dom-input-min make_setter!(SetMin, "min"); /// fn GetList(&self) -> Option> { self.suggestions_source_element() } // https://html.spec.whatwg.org/multipage/#dom-input-valueasdate #[expect(unsafe_code)] fn GetValueAsDate(&self, cx: SafeJSContext) -> Option> { self.input_type() .as_specific() .convert_string_to_naive_datetime(self.Value()) .map(|date_time| unsafe { let time = ClippedTime { t: (date_time - OffsetDateTime::UNIX_EPOCH).whole_milliseconds() as f64, }; NonNull::new_unchecked(NewDateObject(*cx, time)) }) } // https://html.spec.whatwg.org/multipage/#dom-input-valueasdate #[expect(non_snake_case)] #[expect(unsafe_code)] fn SetValueAsDate( &self, cx: SafeJSContext, value: *mut JSObject, can_gc: CanGc, ) -> ErrorResult { rooted!(in(*cx) let value = value); if !self.does_value_as_date_apply() { return Err(Error::InvalidState(None)); } if value.is_null() { return self.SetValue(DOMString::from(""), can_gc); } let mut msecs: f64 = 0.0; // We need to go through unsafe code to interrogate jsapi about a Date. // To minimize the amount of unsafe code to maintain, this just gets the milliseconds, // which we then reinflate into a NaiveDate for use in safe code. unsafe { let mut isDate = false; if !ObjectIsDate(*cx, Handle::from(value.handle()), &mut isDate) { return Err(Error::JSFailed); } if !isDate { return Err(Error::Type(c"Value was not a date".to_owned())); } if !DateGetMsecSinceEpoch(*cx, Handle::from(value.handle()), &mut msecs) { return Err(Error::JSFailed); } if !msecs.is_finite() { return self.SetValue(DOMString::from(""), can_gc); } } let Ok(date_time) = OffsetDateTime::from_unix_timestamp_nanos((msecs * 1e6) as i128) else { return self.SetValue(DOMString::from(""), can_gc); }; self.SetValue( self.input_type() .as_specific() .convert_datetime_to_dom_string(date_time), can_gc, ) } /// fn ValueAsNumber(&self) -> f64 { self.convert_string_to_number(&self.Value().str()) .unwrap_or(f64::NAN) } /// fn SetValueAsNumber(&self, value: f64, can_gc: CanGc) -> ErrorResult { if value.is_infinite() { Err(Error::Type(c"value is not finite".to_owned())) } else if !self.does_value_as_number_apply() { Err(Error::InvalidState(None)) } else if value.is_nan() { self.SetValue(DOMString::from(""), can_gc) } else if let Some(converted) = self.convert_number_to_string(value) { self.SetValue(converted, can_gc) } else { // The most literal spec-compliant implementation would use bignum types so // overflow is impossible, but just setting an overflow to the empty string // matches Firefox's behavior. For example, try input.valueAsNumber=1e30 on // a type="date" input. self.SetValue(DOMString::from(""), can_gc) } } // https://html.spec.whatwg.org/multipage/#attr-fe-name make_getter!(Name, "name"); // https://html.spec.whatwg.org/multipage/#attr-fe-name make_atomic_setter!(SetName, "name"); // https://html.spec.whatwg.org/multipage/#dom-input-placeholder make_getter!(Placeholder, "placeholder"); // https://html.spec.whatwg.org/multipage/#dom-input-placeholder make_setter!(SetPlaceholder, "placeholder"); // https://html.spec.whatwg.org/multipage/#dom-input-formaction make_form_action_getter!(FormAction, "formaction"); // https://html.spec.whatwg.org/multipage/#dom-input-formaction make_setter!(SetFormAction, "formaction"); // https://html.spec.whatwg.org/multipage/#dom-fs-formenctype make_enumerated_getter!( FormEnctype, "formenctype", "application/x-www-form-urlencoded" | "text/plain" | "multipart/form-data", invalid => "application/x-www-form-urlencoded" ); // https://html.spec.whatwg.org/multipage/#dom-input-formenctype make_setter!(SetFormEnctype, "formenctype"); // https://html.spec.whatwg.org/multipage/#dom-fs-formmethod make_enumerated_getter!( FormMethod, "formmethod", "get" | "post" | "dialog", invalid => "get" ); // https://html.spec.whatwg.org/multipage/#dom-fs-formmethod make_setter!(SetFormMethod, "formmethod"); // https://html.spec.whatwg.org/multipage/#dom-input-formtarget make_getter!(FormTarget, "formtarget"); // https://html.spec.whatwg.org/multipage/#dom-input-formtarget make_setter!(SetFormTarget, "formtarget"); // https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate make_bool_getter!(FormNoValidate, "formnovalidate"); // https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate make_bool_setter!(SetFormNoValidate, "formnovalidate"); // https://html.spec.whatwg.org/multipage/#dom-input-max make_getter!(Max, "max"); // https://html.spec.whatwg.org/multipage/#dom-input-max make_setter!(SetMax, "max"); // https://html.spec.whatwg.org/multipage/#dom-input-maxlength make_int_getter!(MaxLength, "maxlength", DEFAULT_MAX_LENGTH); // https://html.spec.whatwg.org/multipage/#dom-input-maxlength make_limited_int_setter!(SetMaxLength, "maxlength", DEFAULT_MAX_LENGTH); // https://html.spec.whatwg.org/multipage/#dom-input-minlength make_int_getter!(MinLength, "minlength", DEFAULT_MIN_LENGTH); // https://html.spec.whatwg.org/multipage/#dom-input-minlength make_limited_int_setter!(SetMinLength, "minlength", DEFAULT_MIN_LENGTH); // https://html.spec.whatwg.org/multipage/#dom-input-multiple make_bool_getter!(Multiple, "multiple"); // https://html.spec.whatwg.org/multipage/#dom-input-multiple make_bool_setter!(SetMultiple, "multiple"); // https://html.spec.whatwg.org/multipage/#dom-input-pattern make_getter!(Pattern, "pattern"); // https://html.spec.whatwg.org/multipage/#dom-input-pattern make_setter!(SetPattern, "pattern"); // https://html.spec.whatwg.org/multipage/#dom-input-required make_bool_getter!(Required, "required"); // https://html.spec.whatwg.org/multipage/#dom-input-required make_bool_setter!(SetRequired, "required"); // https://html.spec.whatwg.org/multipage/#dom-input-src make_url_getter!(Src, "src"); // https://html.spec.whatwg.org/multipage/#dom-input-src make_url_setter!(SetSrc, "src"); // https://html.spec.whatwg.org/multipage/#dom-input-step make_getter!(Step, "step"); // https://html.spec.whatwg.org/multipage/#dom-input-step make_setter!(SetStep, "step"); // https://html.spec.whatwg.org/multipage/#dom-input-usemap make_getter!(UseMap, "usemap"); // https://html.spec.whatwg.org/multipage/#dom-input-usemap make_setter!(SetUseMap, "usemap"); /// fn Indeterminate(&self) -> bool { self.upcast::() .state() .contains(ElementState::INDETERMINATE) } /// fn SetIndeterminate(&self, val: bool) { self.upcast::() .set_state(ElementState::INDETERMINATE, val) } // https://html.spec.whatwg.org/multipage/#dom-lfe-labels // Different from make_labels_getter because this one // conditionally returns null. fn GetLabels(&self, can_gc: CanGc) -> Option> { if matches!(*self.input_type(), InputType::Hidden(_)) { None } else { Some(self.labels_node_list.or_init(|| { NodeList::new_labels_list( self.upcast::().owner_doc().window(), self.upcast::(), can_gc, ) })) } } /// fn Select(&self) { self.selection().dom_select(); } /// fn GetSelectionStart(&self) -> Option { self.selection().dom_start().map(|start| start.0 as u32) } /// fn SetSelectionStart(&self, start: Option) -> ErrorResult { self.selection() .set_dom_start(start.map(Utf16CodeUnitLength::from)) } /// fn GetSelectionEnd(&self) -> Option { self.selection().dom_end().map(|end| end.0 as u32) } /// fn SetSelectionEnd(&self, end: Option) -> ErrorResult { self.selection() .set_dom_end(end.map(Utf16CodeUnitLength::from)) } /// fn GetSelectionDirection(&self) -> Option { self.selection().dom_direction() } /// fn SetSelectionDirection(&self, direction: Option) -> ErrorResult { self.selection().set_dom_direction(direction) } /// fn SetSelectionRange(&self, start: u32, end: u32, direction: Option) -> ErrorResult { self.selection().set_dom_range( Utf16CodeUnitLength::from(start), Utf16CodeUnitLength::from(end), direction, ) } /// fn SetRangeText(&self, replacement: DOMString) -> ErrorResult { self.selection() .set_dom_range_text(replacement, None, None, Default::default()) } /// fn SetRangeText_( &self, replacement: DOMString, start: u32, end: u32, selection_mode: SelectionMode, ) -> ErrorResult { self.selection().set_dom_range_text( replacement, Some(Utf16CodeUnitLength::from(start)), Some(Utf16CodeUnitLength::from(end)), selection_mode, ) } /// Select the files based on filepaths passed in, enabled by /// `dom_testing_html_input_element_select_files_enabled`, used for test purpose. fn SelectFiles(&self, paths: Vec) { self.input_type() .as_specific() .select_files(self, Some(paths)); } /// fn StepUp(&self, n: i32, can_gc: CanGc) -> ErrorResult { self.step_up_or_down(n, StepDirection::Up, can_gc) } /// fn StepDown(&self, n: i32, can_gc: CanGc) -> ErrorResult { self.step_up_or_down(n, StepDirection::Down, can_gc) } /// fn WillValidate(&self) -> bool { self.is_instance_validatable() } /// fn Validity(&self, can_gc: CanGc) -> DomRoot { self.validity_state(can_gc) } /// fn CheckValidity(&self, cx: &mut JSContext) -> bool { self.check_validity(cx) } /// fn ReportValidity(&self, cx: &mut JSContext) -> bool { self.report_validity(cx) } /// fn ValidationMessage(&self) -> DOMString { self.validation_message() } /// fn SetCustomValidity(&self, error: DOMString, can_gc: CanGc) { self.validity_state(can_gc).set_custom_error_message(error); } } impl HTMLInputElement { /// /// Steps range from 5.1 to 5.10 (specific to HTMLInputElement) pub(crate) fn form_datums( &self, submitter: Option, encoding: Option<&'static Encoding>, ) -> Vec { // 3.1: disabled state check is in get_unclean_dataset // Step 5.2 let ty = self.Type(); // Step 5.4 let name = self.Name(); let is_submitter = match submitter { Some(FormSubmitterElement::Input(s)) => self == s, _ => false, }; match *self.input_type() { // Step 5.1: it's a button but it is not submitter. InputType::Submit(_) | InputType::Button(_) | InputType::Reset(_) if !is_submitter => { return vec![]; }, // Step 5.1: it's the "Checkbox" or "Radio Button" and whose checkedness is false. InputType::Radio(_) | InputType::Checkbox(_) => { if !self.Checked() || name.is_empty() { return vec![]; } }, InputType::File(_) => { let mut datums = vec![]; // Step 5.2-5.7 let name = self.Name(); match self.GetFiles() { Some(fl) => { for f in fl.iter_files() { datums.push(FormDatum { ty: ty.clone(), name: name.clone(), value: FormDatumValue::File(DomRoot::from_ref(f)), }); } }, None => { datums.push(FormDatum { // XXX(izgzhen): Spec says 'application/octet-stream' as the type, // but this is _type_ of element rather than content right? ty, name, value: FormDatumValue::String(DOMString::from("")), }) }, } return datums; }, InputType::Image(_) => return vec![], // Unimplemented // Step 5.10: it's a hidden field named _charset_ InputType::Hidden(_) => { if name.to_ascii_lowercase() == "_charset_" { return vec![FormDatum { ty, name, value: FormDatumValue::String(match encoding { None => DOMString::from("UTF-8"), Some(enc) => DOMString::from(enc.name()), }), }]; } }, // Step 5.1: it's not the "Image Button" and doesn't have a name attribute. _ => { if name.is_empty() { return vec![]; } }, } // Step 5.12 vec![FormDatum { ty, name, value: FormDatumValue::String(self.Value()), }] } /// fn radio_group_name(&self) -> Option { self.upcast::() .get_name() .filter(|name| !name.is_empty()) } fn update_checked_state(&self, checked: bool, dirty: bool, can_gc: CanGc) { self.upcast::() .set_state(ElementState::CHECKED, checked); if dirty { self.checked_changed.set(true); } if matches!(*self.input_type(), InputType::Radio(_)) && checked { broadcast_radio_checked(self, self.radio_group_name().as_ref(), can_gc); } self.upcast::().dirty(NodeDamage::Other); } // https://html.spec.whatwg.org/multipage/#concept-fe-mutable pub(crate) fn is_mutable(&self) -> bool { // https://html.spec.whatwg.org/multipage/#the-input-element:concept-fe-mutable // https://html.spec.whatwg.org/multipage/#the-readonly-attribute:concept-fe-mutable !(self.upcast::().disabled_state() || self.ReadOnly()) } /// : /// /// > The reset algorithm for input elements is to set its user validity, dirty value /// > flag, and dirty checkedness flag back to false, set the value of the element to /// > the value of the value content attribute, if there is one, or the empty string /// > otherwise, set the checkedness of the element to true if the element has a checked /// > content attribute and false if it does not, empty the list of selected files, and /// > then invoke the value sanitization algorithm, if the type attribute's current /// > state defines one. pub(crate) fn reset(&self, can_gc: CanGc) { self.value_dirty.set(false); // We set the value and sanitize all in one go. let mut value = self.DefaultValue(); self.sanitize_value(&mut value); self.textinput.borrow_mut().set_content(value); let input_type = &*self.input_type(); if matches!(input_type, InputType::Radio(_) | InputType::Checkbox(_)) { self.update_checked_state(self.DefaultChecked(), false, can_gc); self.checked_changed.set(false); } if matches!(input_type, InputType::File(_)) { input_type.as_specific().set_files(&FileList::new( &self.owner_window(), vec![], can_gc, )); } self.value_changed(can_gc); } /// /// Used by WebDriver to clear the input element. pub(crate) fn clear(&self, can_gc: CanGc) { // Step 1. Reset dirty value and dirty checkedness flags. self.value_dirty.set(false); self.checked_changed.set(false); // Step 2. Set value to empty string. self.textinput.borrow_mut().set_content(DOMString::from("")); // Step 3. Set checkedness based on presence of content attribute. self.update_checked_state(self.DefaultChecked(), false, can_gc); // Step 4. Empty selected files if self.input_type().as_specific().get_files().is_some() { let window = self.owner_window(); let filelist = FileList::new(&window, vec![], can_gc); self.input_type().as_specific().set_files(&filelist); } // Step 5. Invoke the value sanitization algorithm iff the type attribute's // current state defines one. { let mut textinput = self.textinput.borrow_mut(); let mut value = textinput.get_content(); self.sanitize_value(&mut value); textinput.set_content(value); } self.value_changed(can_gc); } fn update_placeholder_shown_state(&self) { if !self.input_type().is_textual_or_password() { self.upcast::().set_placeholder_shown_state(false); } else { let has_placeholder = !self.placeholder.borrow().is_empty(); let has_value = !self.textinput.borrow().is_empty(); self.upcast::() .set_placeholder_shown_state(has_placeholder && !has_value); } } pub(crate) fn select_files_for_webdriver( &self, test_paths: Vec, response_sender: GenericSender>, ) { let mut stored_sender = self.pending_webdriver_response.borrow_mut(); assert!(stored_sender.is_none()); *stored_sender = Some(PendingWebDriverResponse { response_sender, expected_file_count: test_paths.len(), }); self.input_type() .as_specific() .select_files(self, Some(test_paths)); } /// fn sanitize_value(&self, value: &mut DOMString) { self.input_type().as_specific().sanitize_value(self, value); } #[cfg_attr(crown, expect(crown::unrooted_must_root))] fn selection(&self) -> TextControlSelection<'_, Self> { TextControlSelection::new(self, &self.textinput) } /// fn implicit_submission(&self, can_gc: CanGc) { let doc = self.owner_document(); let node = doc.upcast::(); let owner = self.form_owner(); let form = match owner { None => return, Some(ref f) => f, }; if self.upcast::().click_in_progress() { return; } let submit_button = node .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .filter(|input| matches!(*input.input_type(), InputType::Submit(_))) .find(|r| r.form_owner() == owner); match submit_button { Some(ref button) => { if button.is_instance_activatable() { // spec does not actually say to set the not trusted flag, // but we can get here from synthetic keydown events button .upcast::() .fire_synthetic_pointer_event_not_trusted(atom!("click"), can_gc); } }, None => { let mut inputs = node .traverse_preorder(ShadowIncluding::No) .filter_map(DomRoot::downcast::) .filter(|input| { input.form_owner() == owner && matches!( *input.input_type(), InputType::Text(_) | InputType::Search(_) | InputType::Url(_) | InputType::Tel(_) | InputType::Email(_) | InputType::Password(_) | InputType::Date(_) | InputType::Month(_) | InputType::Week(_) | InputType::Time(_) | InputType::DatetimeLocal(_) | InputType::Number(_) ) }); if inputs.nth(1).is_some() { // lazily test for > 1 submission-blocking inputs return; } form.submit( SubmittedFrom::NotFromForm, FormSubmitterElement::Form(form), can_gc, ); }, } } /// fn convert_string_to_number(&self, value: &str) -> Option { self.input_type() .as_specific() .convert_string_to_number(value) } /// fn convert_number_to_string(&self, value: f64) -> Option { self.input_type() .as_specific() .convert_number_to_string(value) } fn update_related_validity_states(&self, can_gc: CanGc) { match *self.input_type() { InputType::Radio(_) => { perform_radio_group_validation(self, self.radio_group_name().as_ref(), can_gc) }, _ => { self.validity_state(can_gc) .perform_validation_and_update(ValidationFlags::all(), can_gc); }, } } #[expect(unsafe_code)] fn value_changed(&self, can_gc: CanGc) { self.maybe_update_shared_selection(); self.update_related_validity_states(can_gc); // TODO https://github.com/servo/servo/issues/43253 let mut cx = unsafe { script_bindings::script_runtime::temp_cx() }; let cx = &mut cx; self.input_type().as_specific().update_shadow_tree(cx, self); } /// fn show_the_picker_if_applicable(&self) { // FIXME: Implement most of this algorithm // Step 2. If element is not mutable, then return. if !self.is_mutable() { return; } // Step 6. Otherwise, the user agent should show the relevant user interface for selecting a value for element, // in the way it normally would when the user interacts with the control. self.input_type() .as_specific() .show_the_picker_if_applicable(self); } pub(crate) fn handle_color_picker_response(&self, response: Option, can_gc: CanGc) { if let InputType::Color(ref color_input_type) = *self.input_type() { color_input_type.handle_color_picker_response(self, response, can_gc) } } pub(crate) fn handle_file_picker_response( &self, response: Option>, can_gc: CanGc, ) { if let InputType::File(ref file_input_type) = *self.input_type() { file_input_type.handle_file_picker_response(self, response, can_gc) } } fn handle_focus_event(&self, event: &FocusEvent) { let event_type = event.upcast::().type_(); if *event_type == *"blur" { self.owner_document() .embedder_controls() .hide_embedder_control(self.upcast()); } else if *event_type == *"focus" { let input_type = &*self.input_type(); let Ok(input_method_type) = input_type.try_into() else { return; }; self.owner_document() .embedder_controls() .show_embedder_control( ControlElement::Ime(DomRoot::from_ref(self.upcast())), EmbedderControlRequest::InputMethod(InputMethodRequest { input_method_type, text: self.Value().to_string(), insertion_point: self.GetSelectionEnd(), multiline: false, // We follow chromium's heuristic to show the virtual keyboard only if user had interacted before. allow_virtual_keyboard: self.owner_window().has_sticky_activation(), }), None, ); } else { unreachable!("Got unexpected FocusEvent {event_type:?}"); } } fn handle_mouse_event(&self, mouse_event: &MouseEvent) { if mouse_event.upcast::().DefaultPrevented() { return; } // Only respond to mouse events if we are displayed as text input or a password. If the // placeholder is displayed, also don't do any interactive mouse event handling. if !self.input_type().is_textual_or_password() || self.textinput.borrow().is_empty() { return; } let node = self.upcast(); if self .textinput .borrow_mut() .handle_mouse_event(node, mouse_event) { self.maybe_update_shared_selection(); } } } impl VirtualMethods for HTMLInputElement { fn super_type(&self) -> Option<&dyn VirtualMethods> { Some(self.upcast::() as &dyn VirtualMethods) } fn attribute_mutated(&self, cx: &mut JSContext, attr: &Attr, mutation: AttributeMutation) { let could_have_had_embedder_control = self.may_have_embedder_control(); self.super_type() .unwrap() .attribute_mutated(cx, attr, mutation); match *attr.local_name() { local_name!("disabled") => { let disabled_state = match mutation { AttributeMutation::Set(None, _) => true, AttributeMutation::Set(Some(_), _) => { // Input was already disabled before. return; }, AttributeMutation::Removed => false, }; let el = self.upcast::(); el.set_disabled_state(disabled_state); el.set_enabled_state(!disabled_state); el.check_ancestors_disabled_state_for_form_control(); if self.input_type().is_textual() { let read_write = !(self.ReadOnly() || el.disabled_state()); el.set_read_write_state(read_write); } }, local_name!("checked") if !self.checked_changed.get() => { let checked_state = match mutation { AttributeMutation::Set(None, _) => true, AttributeMutation::Set(Some(_), _) => { // Input was already checked before. return; }, AttributeMutation::Removed => false, }; self.update_checked_state(checked_state, false, CanGc::from_cx(cx)); }, local_name!("size") => { let size = mutation.new_value(attr).map(|value| value.as_uint()); self.size.set(size.unwrap_or(DEFAULT_INPUT_SIZE)); }, local_name!("type") => { match mutation { AttributeMutation::Set(..) => { // https://html.spec.whatwg.org/multipage/#input-type-change let (old_value_mode, old_idl_value) = (self.value_mode(), self.Value()); let previously_selectable = self.selection_api_applies(); *self.input_type.borrow_mut() = InputType::new_from_atom(attr.value().as_atom()); self.is_textual_or_password .set(self.input_type().is_textual_or_password()); let element = self.upcast::(); if self.input_type().is_textual() { let read_write = !(self.ReadOnly() || element.disabled_state()); element.set_read_write_state(read_write); } else { element.set_read_write_state(false); } let new_value_mode = self.value_mode(); match (&old_value_mode, old_idl_value.is_empty(), new_value_mode) { // Step 1 (&ValueMode::Value, false, ValueMode::Default) | (&ValueMode::Value, false, ValueMode::DefaultOn) => { self.SetValue(old_idl_value, CanGc::from_cx(cx)) .expect("Failed to set input value on type change to a default ValueMode."); }, // Step 2 (_, _, ValueMode::Value) if old_value_mode != ValueMode::Value => { self.SetValue( self.upcast::() .get_attribute(&local_name!("value")) .map_or(DOMString::from(""), |a| { DOMString::from(a.summarize().value) }), CanGc::from_cx(cx), ) .expect( "Failed to set input value on type change to ValueMode::Value.", ); self.value_dirty.set(false); }, // Step 3 (_, _, ValueMode::Filename) if old_value_mode != ValueMode::Filename => { self.SetValue(DOMString::from(""), CanGc::from_cx(cx)) .expect("Failed to set input value on type change to ValueMode::Filename."); }, _ => {}, } // Step 5 self.input_type() .as_specific() .signal_type_change(self, CanGc::from_cx(cx)); // Step 6 let mut textinput = self.textinput.borrow_mut(); let mut value = textinput.get_content(); self.sanitize_value(&mut value); textinput.set_content(value); self.upcast::().dirty(NodeDamage::Other); // Steps 7-9 if !previously_selectable && self.selection_api_applies() { textinput.clear_selection_to_start(); } }, AttributeMutation::Removed => { self.input_type() .as_specific() .signal_type_change(self, CanGc::from_cx(cx)); *self.input_type.borrow_mut() = InputType::new_text(); self.is_textual_or_password .set(self.input_type().is_textual_or_password()); let element = self.upcast::(); let read_write = !(self.ReadOnly() || element.disabled_state()); element.set_read_write_state(read_write); }, } self.update_placeholder_shown_state(); self.input_type() .as_specific() .update_placeholder_contents(cx, self); }, local_name!("value") if !self.value_dirty.get() => { // This is only run when the `value` or `defaultValue` attribute is set. It // has a different behavior than `SetValue` which is triggered by setting the // value property in script. let value = mutation.new_value(attr).map(|value| (**value).to_owned()); let mut value = value.map_or(DOMString::new(), DOMString::from); self.sanitize_value(&mut value); self.textinput.borrow_mut().set_content(value); self.update_placeholder_shown_state(); }, local_name!("maxlength") => match *attr.value() { AttrValue::Int(_, value) => { let mut textinput = self.textinput.borrow_mut(); if value < 0 { textinput.set_max_length(None); } else { textinput.set_max_length(Some(Utf16CodeUnitLength(value as usize))) } }, _ => panic!("Expected an AttrValue::Int"), }, local_name!("minlength") => match *attr.value() { AttrValue::Int(_, value) => { let mut textinput = self.textinput.borrow_mut(); if value < 0 { textinput.set_min_length(None); } else { textinput.set_min_length(Some(Utf16CodeUnitLength(value as usize))) } }, _ => panic!("Expected an AttrValue::Int"), }, local_name!("placeholder") => { { let mut placeholder = self.placeholder.borrow_mut(); placeholder.clear(); if let AttributeMutation::Set(..) = mutation { placeholder .extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r')); } } self.update_placeholder_shown_state(); self.input_type() .as_specific() .update_placeholder_contents(cx, self); }, local_name!("readonly") => { if self.input_type().is_textual() { let el = self.upcast::(); match mutation { AttributeMutation::Set(..) => { el.set_read_write_state(false); }, AttributeMutation::Removed => { el.set_read_write_state(!el.disabled_state()); }, } } }, local_name!("form") => { self.form_attribute_mutated(mutation, CanGc::from_cx(cx)); }, _ => { self.input_type() .as_specific() .attribute_mutated(cx, self, attr, mutation); }, } self.value_changed(CanGc::from_cx(cx)); if could_have_had_embedder_control && !self.may_have_embedder_control() { self.owner_document() .embedder_controls() .hide_embedder_control(self.upcast()); } } fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { match *name { local_name!("accept") => AttrValue::from_comma_separated_tokenlist(value.into()), local_name!("size") => AttrValue::from_limited_u32(value.into(), DEFAULT_INPUT_SIZE), local_name!("type") => AttrValue::from_atomic(value.into()), local_name!("maxlength") => { AttrValue::from_limited_i32(value.into(), DEFAULT_MAX_LENGTH) }, local_name!("minlength") => { AttrValue::from_limited_i32(value.into(), DEFAULT_MIN_LENGTH) }, _ => self .super_type() .unwrap() .parse_plain_attribute(name, value), } } 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_ancestors_disabled_state_for_form_control(); self.input_type() .as_specific() .bind_to_tree(cx, self, context); self.value_changed(CanGc::from_cx(cx)); } fn unbind_from_tree(&self, cx: &mut JSContext, context: &UnbindContext) { let form_owner = self.form_owner(); self.super_type().unwrap().unbind_from_tree(cx, context); let node = self.upcast::(); let el = self.upcast::(); if node .ancestors() .any(|ancestor| ancestor.is::()) { el.check_ancestors_disabled_state_for_form_control(); } else { el.check_disabled_attribute(); } self.input_type().as_specific().unbind_from_tree( self, form_owner, context, CanGc::from_cx(cx), ); self.validity_state(CanGc::from_cx(cx)) .perform_validation_and_update(ValidationFlags::all(), CanGc::from_cx(cx)); } // This represents behavior for which the UIEvents spec and the // DOM/HTML specs are out of sync. // Compare: // https://w3c.github.io/uievents/#default-action /// fn handle_event(&self, event: &Event, can_gc: CanGc) { if let Some(mouse_event) = event.downcast::() { self.handle_mouse_event(mouse_event); event.mark_as_handled(); } else if event.type_() == atom!("keydown") && !event.DefaultPrevented() && self.input_type().is_textual_or_password() { if let Some(keyevent) = event.downcast::() { // This can't be inlined, as holding on to textinput.borrow_mut() // during self.implicit_submission will cause a panic. let action = self.textinput.borrow_mut().handle_keydown(keyevent); self.handle_key_reaction(action, event, can_gc); } } else if (event.type_() == atom!("compositionstart") || event.type_() == atom!("compositionupdate") || event.type_() == atom!("compositionend")) && self.input_type().is_textual_or_password() { if let Some(compositionevent) = event.downcast::() { if event.type_() == atom!("compositionend") { let action = self .textinput .borrow_mut() .handle_compositionend(compositionevent); self.handle_key_reaction(action, event, can_gc); self.upcast::().dirty(NodeDamage::Other); self.update_placeholder_shown_state(); } else if event.type_() == atom!("compositionupdate") { let action = self .textinput .borrow_mut() .handle_compositionupdate(compositionevent); self.handle_key_reaction(action, event, can_gc); self.upcast::().dirty(NodeDamage::Other); self.update_placeholder_shown_state(); } else if event.type_() == atom!("compositionstart") { // Update placeholder state when composition starts self.update_placeholder_shown_state(); } event.mark_as_handled(); } } else if let Some(clipboard_event) = event.downcast::() { let reaction = self .textinput .borrow_mut() .handle_clipboard_event(clipboard_event); let flags = reaction.flags; if flags.contains(ClipboardEventFlags::FireClipboardChangedEvent) { self.owner_document().event_handler().fire_clipboard_event( None, ClipboardEventType::Change, can_gc, ); } if flags.contains(ClipboardEventFlags::QueueInputEvent) { self.textinput.borrow().queue_input_event( self.upcast(), reaction.text, IsComposing::NotComposing, reaction.input_type, ); } if !flags.is_empty() { event.mark_as_handled(); self.upcast::().dirty(NodeDamage::ContentOrHeritage); } } else if let Some(event) = event.downcast::() { self.handle_focus_event(event) } self.value_changed(can_gc); if let Some(super_type) = self.super_type() { super_type.handle_event(event, can_gc); } } /// fn cloning_steps( &self, cx: &mut JSContext, copy: &Node, maybe_doc: Option<&Document>, clone_children: CloneChildrenFlag, ) { if let Some(s) = self.super_type() { s.cloning_steps(cx, copy, maybe_doc, clone_children); } let elem = copy.downcast::().unwrap(); elem.value_dirty.set(self.value_dirty.get()); elem.checked_changed.set(self.checked_changed.get()); elem.upcast::() .set_state(ElementState::CHECKED, self.Checked()); elem.textinput .borrow_mut() .set_content(self.textinput.borrow().get_content()); self.value_changed(CanGc::from_cx(cx)); } } impl FormControl for HTMLInputElement { fn form_owner(&self) -> Option> { self.form_owner.get() } fn set_form_owner(&self, form: Option<&HTMLFormElement>) { self.form_owner.set(form); } fn to_element(&self) -> &Element { self.upcast::() } } impl Validatable for HTMLInputElement { fn as_element(&self) -> &Element { self.upcast() } fn validity_state(&self, can_gc: CanGc) -> DomRoot { self.validity_state .or_init(|| ValidityState::new(&self.owner_window(), self.upcast(), can_gc)) } fn is_instance_validatable(&self) -> bool { // https://html.spec.whatwg.org/multipage/#hidden-state-(type%3Dhidden)%3Abarred-from-constraint-validation // https://html.spec.whatwg.org/multipage/#button-state-(type%3Dbutton)%3Abarred-from-constraint-validation // https://html.spec.whatwg.org/multipage/#reset-button-state-(type%3Dreset)%3Abarred-from-constraint-validation // 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-readonly-attribute%3Abarred-from-constraint-validation // https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation match *self.input_type() { InputType::Hidden(_) | InputType::Button(_) | InputType::Reset(_) => false, _ => { !(self.upcast::().disabled_state() || self.ReadOnly() || is_barred_by_datalist_ancestor(self.upcast())) }, } } fn perform_validation( &self, validate_flags: ValidationFlags, can_gc: CanGc, ) -> ValidationFlags { let mut failed_flags = ValidationFlags::empty(); let value = self.Value(); if validate_flags.contains(ValidationFlags::VALUE_MISSING) && self.suffers_from_being_missing(&value) { failed_flags.insert(ValidationFlags::VALUE_MISSING); } if validate_flags.contains(ValidationFlags::TYPE_MISMATCH) && self.suffers_from_type_mismatch(&value) { failed_flags.insert(ValidationFlags::TYPE_MISMATCH); } if validate_flags.contains(ValidationFlags::PATTERN_MISMATCH) && self.suffers_from_pattern_mismatch(&value, can_gc) { failed_flags.insert(ValidationFlags::PATTERN_MISMATCH); } if validate_flags.contains(ValidationFlags::BAD_INPUT) && self.suffers_from_bad_input(&value) { failed_flags.insert(ValidationFlags::BAD_INPUT); } if validate_flags.intersects(ValidationFlags::TOO_LONG | ValidationFlags::TOO_SHORT) { failed_flags |= self.suffers_from_length_issues(&value); } if validate_flags.intersects( ValidationFlags::RANGE_UNDERFLOW | ValidationFlags::RANGE_OVERFLOW | ValidationFlags::STEP_MISMATCH, ) { failed_flags |= self.suffers_from_range_issues(&value); } failed_flags & validate_flags } } impl Activatable for HTMLInputElement { fn as_element(&self) -> &Element { self.upcast() } fn is_instance_activatable(&self) -> bool { match *self.input_type() { // https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):input-activation-behavior // https://html.spec.whatwg.org/multipage/#reset-button-state-(type=reset):input-activation-behavior // https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file):input-activation-behavior // https://html.spec.whatwg.org/multipage/#image-button-state-(type=image):input-activation-behavior // // Although they do not have implicit activation behaviors, `type=button` is an activatable input event. InputType::Submit(_) | InputType::Reset(_) | InputType::File(_) | InputType::Image(_) | InputType::Button(_) => self.is_mutable(), // https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):input-activation-behavior // https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):input-activation-behavior // https://html.spec.whatwg.org/multipage/#color-state-(type=color):input-activation-behavior InputType::Checkbox(_) | InputType::Radio(_) | InputType::Color(_) => true, _ => false, } } /// fn legacy_pre_activation_behavior(&self, can_gc: CanGc) -> Option { let activation_state = self .input_type() .as_specific() .legacy_pre_activation_behavior(self, can_gc); if activation_state.is_some() { self.value_changed(can_gc); } activation_state } /// fn legacy_canceled_activation_behavior( &self, cache: Option, can_gc: CanGc, ) { // Step 1 let ty = self.input_type(); let cache = match cache { Some(cache) => { if (cache.was_radio && !matches!(*ty, InputType::Radio(_))) || (cache.was_checkbox && !matches!(*ty, InputType::Checkbox(_))) { // Type changed, abandon ship // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414 return; } cache }, None => { return; }, }; // Step 2 and 3 ty.as_specific() .legacy_canceled_activation_behavior(self, cache, can_gc); self.value_changed(can_gc); } /// fn activation_behavior(&self, event: &Event, target: &EventTarget, can_gc: CanGc) { self.input_type() .as_specific() .activation_behavior(self, event, target, can_gc) } } /// This is used to compile JS-compatible regex provided in pattern attribute /// that matches only the entirety of string. /// fn compile_pattern( cx: SafeJSContext, pattern_str: &str, out_regex: MutableHandleObject, can_gc: CanGc, ) -> bool { // First check if pattern compiles... if check_js_regex_syntax(cx, pattern_str, can_gc) { // ...and if it does make pattern that matches only the entirety of string let pattern_str = format!("^(?:{})$", pattern_str); let flags = RegExpFlags { flags_: RegExpFlag_UnicodeSets, }; new_js_regex(cx, &pattern_str, flags, out_regex, can_gc) } else { false } } #[expect(unsafe_code)] /// Check if the pattern by itself is valid first, and not that it only becomes /// valid once we add ^(?: and )$. fn check_js_regex_syntax(cx: SafeJSContext, pattern: &str, _can_gc: CanGc) -> bool { let pattern: Vec = pattern.encode_utf16().collect(); unsafe { rooted!(in(*cx) let mut exception = UndefinedValue()); let valid = CheckRegExpSyntax( *cx, pattern.as_ptr(), pattern.len(), RegExpFlags { flags_: RegExpFlag_UnicodeSets, }, exception.handle_mut(), ); if !valid { JS_ClearPendingException(*cx); return false; } // TODO(cybai): report `exception` to devtools // exception will be `undefined` if the regex is valid exception.is_undefined() } } #[expect(unsafe_code)] pub(crate) fn new_js_regex( cx: SafeJSContext, pattern: &str, flags: RegExpFlags, mut out_regex: MutableHandleObject, _can_gc: CanGc, ) -> bool { let pattern: Vec = pattern.encode_utf16().collect(); unsafe { out_regex.set(NewUCRegExpObject( *cx, pattern.as_ptr(), pattern.len(), flags, )); if out_regex.is_null() { JS_ClearPendingException(*cx); return false; } } true } #[expect(unsafe_code)] fn matches_js_regex( cx: SafeJSContext, regex_obj: HandleObject, value: &str, _can_gc: CanGc, ) -> Result { let mut value: Vec = value.encode_utf16().collect(); unsafe { let mut is_regex = false; assert!(ObjectIsRegExp(*cx, regex_obj, &mut is_regex)); assert!(is_regex); rooted!(in(*cx) let mut rval = UndefinedValue()); let mut index = 0; let ok = ExecuteRegExpNoStatics( *cx, regex_obj, value.as_mut_ptr(), value.len(), &mut index, true, rval.handle_mut(), ); if ok { Ok(!rval.is_null()) } else { JS_ClearPendingException(*cx); Err(()) } } } /// When WebDriver asks the [`HTMLInputElement`] to do some asynchronous actions, such /// as selecting files, this stores the details necessary to complete the response when /// the action is complete. #[derive(MallocSizeOf)] struct PendingWebDriverResponse { /// An [`IpcSender`] to use to send the reply when the response is ready. response_sender: GenericSender>, /// The number of files expected to be selected when the selection process is done. expected_file_count: usize, } impl PendingWebDriverResponse { fn finish(self, number_files_selected: usize) { if number_files_selected == self.expected_file_count { let _ = self.response_sender.send(Ok(false)); } else { // If not all files are found the WebDriver specification says to return // the InvalidArgument error. let _ = self.response_sender.send(Err(ErrorStatus::InvalidArgument)); } } }