Files
servo/components/script/dom/html/input_element/mod.rs
Martin Robinson 45972e07ab script: Rewrite the layout DOM wrappers (#44114)
This change reworks the layout DOM wrappers so that they are simpler and
easier to reason about. The main changes here:

**Combine layout wrappers into one interface:**

 - `LayoutNode`/`ThreadSafeLayoutNode` is combined into `LayoutNode`:
   The idea here is that `LayoutNode` is always thread-safe when used in
   layout as long as no `unsafe` calls are used. These interfaces
   only expose what is necessary for layout.
 - `LayoutElement`/`ThreadSafeLayoutElement` is combined into
   `LayoutElement`: See above.

**Expose two new interfaces to be used *only* with `stylo` and
`selectors`:**

`DangerousStyleNode` and `DangerousStyleElement`. `stylo`
and `selectors` have a different way of ensuring safety that is
incompatible with Servo's layout (access all of the DOM tree anywhere,
but ensure that writing only happens from a single-thread). These types
only implement things like `TElement`, `TNode` and are not intended to
be used by layout at all.

All traits and implementations are moved to files that are named after
the struct or trait inside them, in order to better understand what one
is looking at.

The main goals here are:

 - Make it easier to reason about the safe use of the DOM APIs.
 - Remove the interdependencies between the `stylo` and `selectors`
   interface implementations and the layout interface. This helps
   with the first point as well and makes it simpler to know where
   a method is implemented.
 - Reduce the amount of code.
 - Make it possible to eliminate `TrustedNodeAddress` in the future.
 - Document and bring the method naming up to modern Rust conventions.

This is a lot of code changes, but is very well tested by the WPT tests.
Unfortunately, it is difficult to make a change like this iteratively.
In addition, this new design comes with new documentation at
servo/book#225.

Testing: This should not change behavior so should be covered by
existing
WPT tests.

Signed-off-by: Martin Robinson <mrobinson@fastmail.fm>
2026-04-12 04:22:06 +00:00

2632 lines
100 KiB
Rust

/* 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 {
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-value>
Value,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default>
Default,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default-on>
DefaultOn,
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-filename>
Filename,
}
#[derive(Debug, PartialEq)]
enum StepDirection {
Up,
Down,
}
#[dom_struct]
pub(crate) struct HTMLInputElement {
htmlelement: HTMLElement,
input_type: DomRefCell<InputType>,
/// <https://html.spec.whatwg.org/multipage/#concept-input-checked-dirty-flag>
checked_changed: Cell<bool>,
placeholder: DomRefCell<DOMString>,
size: Cell<u32>,
maxlength: Cell<i32>,
minlength: Cell<i32>,
#[no_trace]
textinput: DomRefCell<TextInput<EmbedderClipboardProvider>>,
/// <https://html.spec.whatwg.org/multipage/#concept-input-value-dirty-flag>
value_dirty: Cell<bool>,
/// 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<HTMLFormElement>,
labels_node_list: MutNullableDom<NodeList>,
validity_state: MutNullableDom<ValidityState>,
#[no_trace]
pending_webdriver_response: RefCell<Option<PendingWebDriverResponse>>,
}
#[derive(JSTraceable)]
pub(crate) struct InputActivationState {
indeterminate: bool,
checked: bool,
checked_radio: Option<DomRoot<HTMLInputElement>>,
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<Prefix>,
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()),
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<Prefix>,
document: &Document,
proto: Option<HandleObject>,
) -> DomRoot<HTMLInputElement> {
Node::reflect_node_with_proto(
cx,
Box::new(HTMLInputElement::new_inherited(
local_name, prefix, document,
)),
document,
proto,
)
}
pub(crate) fn auto_directionality(&self) -> Option<String> {
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
/// <https://html.spec.whatwg.org/multipage/#concept-input-apply>
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()
}
/// <https://w3c.github.io/webdriver/#dfn-non-typeable-form-control>
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(_)
)
}
/// <https://html.spec.whatwg.org/multipage#concept-input-step>
fn allowed_value_step(&self) -> Option<f64> {
// 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::<Element>().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())
}
/// <https://html.spec.whatwg.org/multipage#concept-input-min>
fn minimum(&self) -> Option<f64> {
self.upcast::<Element>()
.get_attribute(&local_name!("min"))
.and_then(|attribute| self.convert_string_to_number(&attribute.value()))
.or_else(|| self.default_minimum())
}
/// <https://html.spec.whatwg.org/multipage#concept-input-max>
fn maximum(&self) -> Option<f64> {
self.upcast::<Element>()
.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<f64> {
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<f64> {
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,
}
}
/// <https://html.spec.whatwg.org/multipage#concept-input-min-default>
fn default_minimum(&self) -> Option<f64> {
match *self.input_type() {
InputType::Range(_) => Some(0.0),
_ => None,
}
}
/// <https://html.spec.whatwg.org/multipage#concept-input-max-default>
fn default_maximum(&self) -> Option<f64> {
match *self.input_type() {
InputType::Range(_) => Some(100.0),
_ => None,
}
}
/// <https://html.spec.whatwg.org/multipage#concept-input-value-default-range>
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
}
}
/// <https://html.spec.whatwg.org/multipage#concept-input-step-default>
fn default_step(&self) -> Option<f64> {
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,
}
}
/// <https://html.spec.whatwg.org/multipage#concept-input-step-scale>
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!(),
}
}
/// <https://html.spec.whatwg.org/multipage#concept-input-min-zero>
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::<Element>()
.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::<Element>()
.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
}
/// <https://html.spec.whatwg.org/multipage#concept-input-step-default-base>
fn default_step_base(&self) -> Option<f64> {
match *self.input_type() {
InputType::Week(_) => Some(-259200000.0),
_ => None,
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-stepup>
///
/// <https://html.spec.whatwg.org/multipage/#dom-input-stepdown>
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)
}
/// <https://html.spec.whatwg.org/multipage/#concept-input-list>
fn suggestions_source_element(&self) -> Option<DomRoot<HTMLDataListElement>> {
let list_string = self
.upcast::<Element>()
.get_string_attribute(&local_name!("list"));
if list_string.is_empty() {
return None;
}
let ancestor = self
.upcast::<Node>()
.GetRootNode(&GetRootNodeOptions::empty());
let first_with_id = &ancestor
.traverse_preorder(ShadowIncluding::No)
.find(|node| {
node.downcast::<Element>()
.is_some_and(|e| e.Id() == list_string)
});
first_with_id
.as_ref()
.and_then(|el| el.downcast::<HTMLDataListElement>())
.map(DomRoot::from_ref)
}
/// <https://html.spec.whatwg.org/multipage/#suffering-from-being-missing>
fn suffers_from_being_missing(&self, value: &DOMString) -> bool {
self.input_type()
.as_specific()
.suffers_from_being_missing(self, value)
}
/// <https://html.spec.whatwg.org/multipage/#suffering-from-a-type-mismatch>
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)
}
/// <https://html.spec.whatwg.org/multipage/#suffering-from-a-pattern-mismatch>
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::<JSObject>());
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
}
}
/// <https://html.spec.whatwg.org/multipage/#suffering-from-bad-input>
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
/// <https://html.spec.whatwg.org/multipage/#suffering-from-being-too-short>
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
}
/// * <https://html.spec.whatwg.org/multipage/#suffering-from-an-underflow>
/// * <https://html.spec.whatwg.org/multipage/#suffering-from-an-overflow>
/// * <https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch>
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.
///
/// TODO(#38251): This should eventually only include `text`, `password`, `url`, `tel`,
/// and `email`, but the others do not yet have a custom shadow DOM implementation.
pub(crate) fn renders_as_text_input_widget(&self) -> bool {
matches!(
*self.input_type(),
InputType::Date(_) |
InputType::DatetimeLocal(_) |
InputType::Email(_) |
InputType::Month(_) |
InputType::Number(_) |
InputType::Password(_) |
InputType::Search(_) |
InputType::Tel(_) |
InputType::Text(_) |
InputType::Time(_) |
InputType::Url(_) |
InputType::Week(_)
)
}
fn may_have_embedder_control(&self) -> bool {
let el = self.upcast::<Element>();
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::<Node>().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::<Element>()
.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<EmbedderClipboardProvider>> {
self.textinput.borrow_mut()
}
}
impl<'dom> LayoutDom<'dom, HTMLInputElement> {
/// Textual input, specifically text entry and domain specific input has
/// a default preferred size.
///
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-domain-specific-widgets>
// 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<SharedSelection> {
Some(self.unsafe_get().shared_selection.clone())
}
}
impl TextControlElement for HTMLInputElement {
/// <https://html.spec.whatwg.org/multipage/#concept-input-apply>
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.renders_as_text_input_widget() && !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.renders_as_text_input_widget() && self.upcast::<Element>().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<crate::DomTypeHolder> 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");
/// <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-input-files>
fn GetFiles(&self) -> Option<DomRoot<FileList>> {
self.input_type()
.as_specific()
.get_files()
.as_ref()
.cloned()
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-files>
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");
/// <https://html.spec.whatwg.org/multipage/#dom-input-checked>
fn Checked(&self) -> bool {
self.upcast::<Element>()
.state()
.contains(ElementState::CHECKED)
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-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);
/// <https://html.spec.whatwg.org/multipage/#dom-input-type>
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");
/// <https://html.spec.whatwg.org/multipage/#dom-input-value>
fn Value(&self) -> DOMString {
match self.value_mode() {
ValueMode::Value => self.textinput.borrow().get_content(),
ValueMode::Default => self
.upcast::<Element>()
.get_attribute(&local_name!("value"))
.map_or(DOMString::from(""), |a| {
DOMString::from(a.summarize().value)
}),
ValueMode::DefaultOn => self
.upcast::<Element>()
.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,
}
},
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-value>
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::<Element>()
.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::<Node>().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");
/// <https://html.spec.whatwg.org/multipage/#dom-input-list>
fn GetList(&self) -> Option<DomRoot<HTMLDataListElement>> {
self.suggestions_source_element()
}
// https://html.spec.whatwg.org/multipage/#dom-input-valueasdate
#[expect(unsafe_code)]
fn GetValueAsDate(&self, cx: SafeJSContext) -> Option<NonNull<JSObject>> {
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,
)
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-valueasnumber>
fn ValueAsNumber(&self) -> f64 {
self.convert_string_to_number(&self.Value().str())
.unwrap_or(f64::NAN)
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-valueasnumber>
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");
/// <https://html.spec.whatwg.org/multipage/#dom-input-indeterminate>
fn Indeterminate(&self) -> bool {
self.upcast::<Element>()
.state()
.contains(ElementState::INDETERMINATE)
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-indeterminate>
fn SetIndeterminate(&self, val: bool) {
self.upcast::<Element>()
.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<DomRoot<NodeList>> {
if matches!(*self.input_type(), InputType::Hidden(_)) {
None
} else {
Some(self.labels_node_list.or_init(|| {
NodeList::new_labels_list(
self.upcast::<Node>().owner_doc().window(),
self.upcast::<HTMLElement>(),
can_gc,
)
}))
}
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-select>
fn Select(&self) {
self.selection().dom_select();
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart>
fn GetSelectionStart(&self) -> Option<u32> {
self.selection().dom_start().map(|start| start.0 as u32)
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart>
fn SetSelectionStart(&self, start: Option<u32>) -> ErrorResult {
self.selection()
.set_dom_start(start.map(Utf16CodeUnitLength::from))
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend>
fn GetSelectionEnd(&self) -> Option<u32> {
self.selection().dom_end().map(|end| end.0 as u32)
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend>
fn SetSelectionEnd(&self, end: Option<u32>) -> ErrorResult {
self.selection()
.set_dom_end(end.map(Utf16CodeUnitLength::from))
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection>
fn GetSelectionDirection(&self) -> Option<DOMString> {
self.selection().dom_direction()
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection>
fn SetSelectionDirection(&self, direction: Option<DOMString>) -> ErrorResult {
self.selection().set_dom_direction(direction)
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange>
fn SetSelectionRange(&self, start: u32, end: u32, direction: Option<DOMString>) -> ErrorResult {
self.selection().set_dom_range(
Utf16CodeUnitLength::from(start),
Utf16CodeUnitLength::from(end),
direction,
)
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext>
fn SetRangeText(&self, replacement: DOMString) -> ErrorResult {
self.selection()
.set_dom_range_text(replacement, None, None, Default::default())
}
/// <https://html.spec.whatwg.org/multipage/#dom-textarea/input-setrangetext>
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<DOMString>) {
self.input_type()
.as_specific()
.select_files(self, Some(paths));
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-stepup>
fn StepUp(&self, n: i32, can_gc: CanGc) -> ErrorResult {
self.step_up_or_down(n, StepDirection::Up, can_gc)
}
/// <https://html.spec.whatwg.org/multipage/#dom-input-stepdown>
fn StepDown(&self, n: i32, can_gc: CanGc) -> ErrorResult {
self.step_up_or_down(n, StepDirection::Down, 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, can_gc: CanGc) -> DomRoot<ValidityState> {
self.validity_state(can_gc)
}
/// <https://html.spec.whatwg.org/multipage/#dom-cva-checkvalidity>
fn CheckValidity(&self, cx: &mut JSContext) -> bool {
self.check_validity(cx)
}
/// <https://html.spec.whatwg.org/multipage/#dom-cva-reportvalidity>
fn ReportValidity(&self, cx: &mut JSContext) -> bool {
self.report_validity(cx)
}
/// <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, can_gc: CanGc) {
self.validity_state(can_gc).set_custom_error_message(error);
}
}
impl HTMLInputElement {
/// <https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set>
/// Steps range from 5.1 to 5.10 (specific to HTMLInputElement)
pub(crate) fn form_datums(
&self,
submitter: Option<FormSubmitterElement>,
encoding: Option<&'static Encoding>,
) -> Vec<FormDatum> {
// 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()),
}]
}
/// <https://html.spec.whatwg.org/multipage/#radio-button-group>
fn radio_group_name(&self) -> Option<Atom> {
self.upcast::<Element>()
.get_name()
.filter(|name| !name.is_empty())
}
fn update_checked_state(&self, checked: bool, dirty: bool, can_gc: CanGc) {
self.upcast::<Element>()
.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::<Node>().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::<Element>().disabled_state() || self.ReadOnly())
}
/// <https://html.spec.whatwg.org/multipage/#the-input-element:concept-form-reset-control>:
///
/// > 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);
}
/// <https://w3c.github.io/webdriver/#ref-for-dfn-clear-algorithm-3>
/// 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::<Element>().set_placeholder_shown_state(false);
} else {
let has_placeholder = !self.placeholder.borrow().is_empty();
let has_value = !self.textinput.borrow().is_empty();
self.upcast::<Element>()
.set_placeholder_shown_state(has_placeholder && !has_value);
}
}
pub(crate) fn select_files_for_webdriver(
&self,
test_paths: Vec<DOMString>,
response_sender: GenericSender<Result<bool, ErrorStatus>>,
) {
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));
}
/// <https://html.spec.whatwg.org/multipage/#value-sanitization-algorithm>
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)
}
/// <https://html.spec.whatwg.org/multipage/#implicit-submission>
fn implicit_submission(&self, can_gc: CanGc) {
let doc = self.owner_document();
let node = doc.upcast::<Node>();
let owner = self.form_owner();
let form = match owner {
None => return,
Some(ref f) => f,
};
if self.upcast::<Element>().click_in_progress() {
return;
}
let submit_button = node
.traverse_preorder(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLInputElement>)
.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::<Node>()
.fire_synthetic_pointer_event_not_trusted(atom!("click"), can_gc);
}
},
None => {
let mut inputs = node
.traverse_preorder(ShadowIncluding::No)
.filter_map(DomRoot::downcast::<HTMLInputElement>)
.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,
);
},
}
}
/// <https://html.spec.whatwg.org/multipage/#concept-input-value-string-number>
fn convert_string_to_number(&self, value: &str) -> Option<f64> {
self.input_type()
.as_specific()
.convert_string_to_number(value)
}
/// <https://html.spec.whatwg.org/multipage/#concept-input-value-string-number>
fn convert_number_to_string(&self, value: f64) -> Option<DOMString> {
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);
}
/// <https://html.spec.whatwg.org/multipage/#show-the-picker,-if-applicable>
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<RgbColor>, 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<Vec<SelectedFile>>,
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::<Event>().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::<Event>().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::<HTMLElement>() 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::<Element>();
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());
let element = self.upcast::<Element>();
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::<Element>()
.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::<Node>().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();
let el = self.upcast::<Element>();
let read_write = !(self.ReadOnly() || el.disabled_state());
el.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::<Element>();
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::<Element>()
.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, context: &UnbindContext, can_gc: CanGc) {
let form_owner = self.form_owner();
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();
}
self.input_type()
.as_specific()
.unbind_from_tree(self, form_owner, context, can_gc);
self.validity_state(can_gc)
.perform_validation_and_update(ValidationFlags::all(), can_gc);
}
// 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
/// <https://dom.spec.whatwg.org/#action-versus-occurance>
fn handle_event(&self, event: &Event, can_gc: CanGc) {
if let Some(mouse_event) = event.downcast::<MouseEvent>() {
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::<KeyboardEvent>() {
// 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::<CompositionEvent>() {
if event.type_() == atom!("compositionend") {
let action = self
.textinput
.borrow_mut()
.handle_compositionend(compositionevent);
self.handle_key_reaction(action, event, can_gc);
self.upcast::<Node>().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::<Node>().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::<ClipboardEvent>() {
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::<Node>().dirty(NodeDamage::ContentOrHeritage);
}
} else if let Some(event) = event.downcast::<FocusEvent>() {
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);
}
}
/// <https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext>
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::<HTMLInputElement>().unwrap();
elem.value_dirty.set(self.value_dirty.get());
elem.checked_changed.set(self.checked_changed.get());
elem.upcast::<Element>()
.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<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 HTMLInputElement {
fn as_element(&self) -> &Element {
self.upcast()
}
fn validity_state(&self, can_gc: CanGc) -> DomRoot<ValidityState> {
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::<Element>().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,
}
}
/// <https://dom.spec.whatwg.org/#eventtarget-legacy-pre-activation-behavior>
fn legacy_pre_activation_behavior(&self, can_gc: CanGc) -> Option<InputActivationState> {
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
}
/// <https://dom.spec.whatwg.org/#eventtarget-legacy-canceled-activation-behavior>
fn legacy_canceled_activation_behavior(
&self,
cache: Option<InputActivationState>,
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);
}
/// <https://html.spec.whatwg.org/multipage/#input-activation-behavior>
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.
/// <https://html.spec.whatwg.org/multipage/#compiled-pattern-regular-expression>
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<u16> = 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<u16> = 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<bool, ()> {
let mut value: Vec<u16> = 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<Result<bool, ErrorStatus>>,
/// 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));
}
}
}