diff options
Diffstat (limited to 'components/script/dom/htmlinputelement.rs')
-rwxr-xr-x | components/script/dom/htmlinputelement.rs | 2889 |
1 files changed, 2214 insertions, 675 deletions
diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index baff4983504..aefbea6f30e 100755 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -1,78 +1,234 @@ /* 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 http://mozilla.org/MPL/2.0/. */ - -use caseless::compatibility_caseless_match_str; -use dom::activation::{Activatable, ActivationSource, synthetic_click_activation}; -use dom::attr::Attr; -use dom::bindings::cell::DOMRefCell; -use dom::bindings::codegen::Bindings::EventBinding::EventMethods; -use dom::bindings::codegen::Bindings::FileListBinding::FileListMethods; -use dom::bindings::codegen::Bindings::HTMLInputElementBinding; -use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; -use dom::bindings::codegen::Bindings::KeyboardEventBinding::KeyboardEventMethods; -use dom::bindings::codegen::Bindings::MouseEventBinding::MouseEventMethods; -use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; -use dom::bindings::error::{Error, ErrorResult}; -use dom::bindings::inheritance::Castable; -use dom::bindings::js::{JS, LayoutJS, MutNullableJS, Root, RootedReference}; -use dom::bindings::str::DOMString; -use dom::document::Document; -use dom::element::{AttributeMutation, Element, LayoutElementHelpers, RawLayoutElementHelpers}; -use dom::event::{Event, EventBubbles, EventCancelable}; -use dom::eventtarget::EventTarget; -use dom::file::File; -use dom::filelist::FileList; -use dom::globalscope::GlobalScope; -use dom::htmlelement::HTMLElement; -use dom::htmlfieldsetelement::HTMLFieldSetElement; -use dom::htmlformelement::{FormControl, FormDatum, FormDatumValue, FormSubmitter, HTMLFormElement}; -use dom::htmlformelement::{ResetFrom, SubmittedFrom}; -use dom::keyboardevent::KeyboardEvent; -use dom::mouseevent::MouseEvent; -use dom::node::{Node, NodeDamage, UnbindContext}; -use dom::node::{document_from_node, window_from_node}; -use dom::nodelist::NodeList; -use dom::validation::Validatable; -use dom::validitystate::ValidationFlags; -use dom::virtualmethods::VirtualMethods; + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::dom::activation::Activatable; +use crate::dom::attr::Attr; +use crate::dom::bindings::cell::DomRefCell; +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::reflector::DomObject; +use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; +use crate::dom::bindings::str::{DOMString, USVString}; +use crate::dom::compositionevent::CompositionEvent; +use crate::dom::document::Document; +use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; +use crate::dom::event::{Event, EventBubbles, EventCancelable}; +use crate::dom::eventtarget::EventTarget; +use crate::dom::file::File; +use crate::dom::filelist::FileList; +use crate::dom::globalscope::GlobalScope; +use crate::dom::htmldatalistelement::HTMLDataListElement; +use crate::dom::htmlelement::HTMLElement; +use crate::dom::htmlfieldsetelement::HTMLFieldSetElement; +use crate::dom::htmlformelement::{ + FormControl, FormDatum, FormDatumValue, FormSubmitter, HTMLFormElement, +}; +use crate::dom::htmlformelement::{ResetFrom, SubmittedFrom}; +use crate::dom::keyboardevent::KeyboardEvent; +use crate::dom::mouseevent::MouseEvent; +use crate::dom::node::{document_from_node, window_from_node}; +use crate::dom::node::{ + BindContext, CloneChildrenFlag, Node, NodeDamage, ShadowIncluding, UnbindContext, +}; +use crate::dom::nodelist::NodeList; +use crate::dom::textcontrol::{TextControlElement, TextControlSelection}; +use crate::dom::validation::{is_barred_by_datalist_ancestor, Validatable}; +use crate::dom::validitystate::{ValidationFlags, ValidityState}; +use crate::dom::virtualmethods::VirtualMethods; +use crate::realms::enter_realm; +use crate::script_runtime::JSContext as SafeJSContext; +use crate::textinput::KeyReaction::{ + DispatchInput, Nothing, RedrawSelection, TriggerDefaultAction, +}; +use crate::textinput::Lines::Single; +use crate::textinput::{Direction, SelectionDirection, TextInput, UTF16CodeUnits, UTF8Bytes}; +use chrono::naive::{NaiveDate, NaiveDateTime}; +use chrono::{Datelike, Weekday}; use dom_struct::dom_struct; -use html5ever_atoms::LocalName; -use ipc_channel::ipc::{self, IpcSender}; -use mime_guess; -use net_traits::{CoreResourceMsg, IpcSend}; +use embedder_traits::FilterPattern; +use encoding_rs::Encoding; +use html5ever::{LocalName, Prefix}; +use js::jsapi::{ + ClippedTime, DateGetMsecSinceEpoch, Handle, JSObject, JS_ClearPendingException, NewDateObject, + NewUCRegExpObject, ObjectIsDate, RegExpFlag_Unicode, RegExpFlags, +}; +use js::jsval::UndefinedValue; +use js::rust::jsapi_wrapped::{ExecuteRegExpNoStatics, ObjectIsRegExp}; +use js::rust::{HandleObject, MutableHandleObject}; +use msg::constellation_msg::InputMethodType; use net_traits::blob_url_store::get_blob_origin; -use net_traits::filemanager_thread::{FileManagerThreadMsg, FilterPattern}; +use net_traits::filemanager_thread::FileManagerThreadMsg; +use net_traits::{CoreResourceMsg, IpcSend}; +use profile_traits::ipc; use script_layout_interface::rpc::TextIndexResponse; -use script_traits::ScriptMsg as ConstellationMsg; +use script_traits::ScriptToConstellationChan; use servo_atoms::Atom; -use std::borrow::ToOwned; +use std::borrow::Cow; use std::cell::Cell; +use std::f64; use std::ops::Range; +use std::ptr; +use std::ptr::NonNull; use style::attr::AttrValue; -use style::element_state::*; -use style::str::split_commas; -use textinput::{SelectionDirection, TextInput}; -use textinput::KeyReaction::{DispatchInput, Nothing, RedrawSelection, TriggerDefaultAction}; -use textinput::Lines::Single; +use style::element_state::ElementState; +use style::str::{split_commas, str_join}; +use unicode_bidi::{bidi_class, BidiClass}; +use url::Url; const DEFAULT_SUBMIT_VALUE: &'static str = "Submit"; const DEFAULT_RESET_VALUE: &'static str = "Reset"; const PASSWORD_REPLACEMENT_CHAR: char = '●'; -#[derive(JSTraceable, PartialEq, Copy, Clone)] +#[derive(Clone, Copy, JSTraceable, PartialEq)] #[allow(dead_code)] -#[derive(HeapSizeOf)] -enum InputType { - InputSubmit, - InputReset, - InputButton, - InputText, - InputFile, - InputImage, - InputCheckbox, - InputRadio, - InputPassword +#[derive(MallocSizeOf)] +pub enum InputType { + Button, + Checkbox, + Color, + Date, + DatetimeLocal, + Email, + File, + Hidden, + Image, + Month, + Number, + Password, + Radio, + Range, + Reset, + Search, + Submit, + Tel, + Text, + Time, + Url, + Week, +} + +impl InputType { + // Note that Password is not included here since it is handled + // slightly differently, with placeholder characters shown rather + // than the underlying value. + fn is_textual(&self) -> bool { + match *self { + InputType::Color | + InputType::Date | + InputType::DatetimeLocal | + InputType::Email | + InputType::Hidden | + InputType::Month | + InputType::Number | + InputType::Range | + InputType::Search | + InputType::Tel | + InputType::Text | + InputType::Time | + InputType::Url | + InputType::Week => true, + + _ => false, + } + } + + fn is_textual_or_password(&self) -> bool { + self.is_textual() || *self == InputType::Password + } + + // https://html.spec.whatwg.org/multipage/#has-a-periodic-domain + fn has_periodic_domain(&self) -> bool { + *self == InputType::Time + } + + fn to_str(&self) -> &str { + match *self { + InputType::Button => "button", + InputType::Checkbox => "checkbox", + InputType::Color => "color", + InputType::Date => "date", + InputType::DatetimeLocal => "datetime-local", + InputType::Email => "email", + InputType::File => "file", + InputType::Hidden => "hidden", + InputType::Image => "image", + InputType::Month => "month", + InputType::Number => "number", + InputType::Password => "password", + InputType::Radio => "radio", + InputType::Range => "range", + InputType::Reset => "reset", + InputType::Search => "search", + InputType::Submit => "submit", + InputType::Tel => "tel", + InputType::Text => "text", + InputType::Time => "time", + InputType::Url => "url", + InputType::Week => "week", + } + } + + pub fn as_ime_type(&self) -> Option<InputMethodType> { + match *self { + InputType::Color => Some(InputMethodType::Color), + InputType::Date => Some(InputMethodType::Date), + InputType::DatetimeLocal => Some(InputMethodType::DatetimeLocal), + InputType::Email => Some(InputMethodType::Email), + InputType::Month => Some(InputMethodType::Month), + InputType::Number => Some(InputMethodType::Number), + InputType::Password => Some(InputMethodType::Password), + InputType::Search => Some(InputMethodType::Search), + InputType::Tel => Some(InputMethodType::Tel), + InputType::Text => Some(InputMethodType::Text), + InputType::Time => Some(InputMethodType::Time), + InputType::Url => Some(InputMethodType::Url), + InputType::Week => Some(InputMethodType::Week), + _ => None, + } + } +} + +impl<'a> From<&'a Atom> for InputType { + fn from(value: &Atom) -> InputType { + match value.to_ascii_lowercase() { + atom!("button") => InputType::Button, + atom!("checkbox") => InputType::Checkbox, + atom!("color") => InputType::Color, + atom!("date") => InputType::Date, + atom!("datetime-local") => InputType::DatetimeLocal, + atom!("email") => InputType::Email, + atom!("file") => InputType::File, + atom!("hidden") => InputType::Hidden, + atom!("image") => InputType::Image, + atom!("month") => InputType::Month, + atom!("number") => InputType::Number, + atom!("password") => InputType::Password, + atom!("radio") => InputType::Radio, + atom!("range") => InputType::Range, + atom!("reset") => InputType::Reset, + atom!("search") => InputType::Search, + atom!("submit") => InputType::Submit, + atom!("tel") => InputType::Tel, + atom!("text") => InputType::Text, + atom!("time") => InputType::Time, + atom!("url") => InputType::Url, + atom!("week") => InputType::Week, + _ => Self::default(), + } + } +} + +impl Default for InputType { + fn default() -> InputType { + InputType::Text + } } #[derive(Debug, PartialEq)] @@ -83,212 +239,916 @@ enum ValueMode { Filename, } +#[derive(Debug, PartialEq)] +enum StepDirection { + Up, + Down, +} + #[dom_struct] pub struct HTMLInputElement { htmlelement: HTMLElement, input_type: Cell<InputType>, checked_changed: Cell<bool>, - placeholder: DOMRefCell<DOMString>, - value_changed: Cell<bool>, + placeholder: DomRefCell<DOMString>, size: Cell<u32>, maxlength: Cell<i32>, minlength: Cell<i32>, - #[ignore_heap_size_of = "#7193"] - textinput: DOMRefCell<TextInput<IpcSender<ConstellationMsg>>>, - activation_state: DOMRefCell<InputActivationState>, + #[ignore_malloc_size_of = "#7193"] + textinput: DomRefCell<TextInput<ScriptToConstellationChan>>, // https://html.spec.whatwg.org/multipage/#concept-input-value-dirty-flag value_dirty: Cell<bool>, - - filelist: MutNullableJS<FileList>, - form_owner: MutNullableJS<HTMLFormElement>, + // not specified explicitly, but implied by the fact that sanitization can't + // happen until after all of step/min/max/value content attributes have + // been added + sanitization_flag: Cell<bool>, + + filelist: MutNullableDom<FileList>, + form_owner: MutNullableDom<HTMLFormElement>, + labels_node_list: MutNullableDom<NodeList>, + validity_state: MutNullableDom<ValidityState>, } #[derive(JSTraceable)] -#[must_root] -#[derive(HeapSizeOf)] -struct InputActivationState { +pub struct InputActivationState { indeterminate: bool, checked: bool, - checked_changed: bool, - checked_radio: Option<JS<HTMLInputElement>>, - // In case mutability changed - was_mutable: bool, + checked_radio: Option<DomRoot<HTMLInputElement>>, // In case the type changed old_type: InputType, -} - -impl InputActivationState { - fn new() -> InputActivationState { - InputActivationState { - indeterminate: false, - checked: false, - checked_changed: false, - checked_radio: None, - was_mutable: false, - old_type: InputType::InputText - } - } + // 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; +#[allow(non_snake_case)] impl HTMLInputElement { - fn new_inherited(local_name: LocalName, prefix: Option<DOMString>, document: &Document) -> HTMLInputElement { - let chan = document.window().upcast::<GlobalScope>().constellation_chan().clone(); + fn new_inherited( + local_name: LocalName, + prefix: Option<Prefix>, + document: &Document, + ) -> HTMLInputElement { + let chan = document + .window() + .upcast::<GlobalScope>() + .script_to_constellation_chan() + .clone(); HTMLInputElement { - htmlelement: - HTMLElement::new_inherited_with_state(IN_ENABLED_STATE | IN_READ_WRITE_STATE, - local_name, prefix, document), - input_type: Cell::new(InputType::InputText), - placeholder: DOMRefCell::new(DOMString::new()), + htmlelement: HTMLElement::new_inherited_with_state( + ElementState::IN_ENABLED_STATE | ElementState::IN_READ_WRITE_STATE, + local_name, + prefix, + document, + ), + input_type: Cell::new(Default::default()), + placeholder: DomRefCell::new(DOMString::new()), checked_changed: Cell::new(false), - value_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(Single, - DOMString::new(), - chan, - None, - None, - SelectionDirection::None)), - activation_state: DOMRefCell::new(InputActivationState::new()), + textinput: DomRefCell::new(TextInput::new( + Single, + DOMString::new(), + chan, + None, + None, + SelectionDirection::None, + )), value_dirty: Cell::new(false), - filelist: MutNullableJS::new(None), + sanitization_flag: Cell::new(true), + filelist: MutNullableDom::new(None), form_owner: Default::default(), + labels_node_list: MutNullableDom::new(None), + validity_state: Default::default(), } } #[allow(unrooted_must_root)] - pub fn new(local_name: LocalName, - prefix: Option<DOMString>, - document: &Document) -> Root<HTMLInputElement> { - Node::reflect_node(box HTMLInputElement::new_inherited(local_name, prefix, document), - document, - HTMLInputElementBinding::Wrap) + pub fn new( + local_name: LocalName, + prefix: Option<Prefix>, + document: &Document, + ) -> DomRoot<HTMLInputElement> { + Node::reflect_node( + Box::new(HTMLInputElement::new_inherited( + local_name, prefix, document, + )), + document, + ) } - pub fn type_(&self) -> Atom { - self.upcast::<Element>() - .get_attribute(&ns!(), &local_name!("type")) - .map_or_else(|| atom!(""), |a| a.value().as_atom().to_owned()) + pub 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 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/#input-type-attr-summary + // 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.get() { - InputType::InputSubmit | - InputType::InputReset | - InputType::InputButton | - InputType::InputImage => ValueMode::Default, - InputType::InputCheckbox | - InputType::InputRadio => ValueMode::DefaultOn, - InputType::InputPassword | - InputType::InputText => ValueMode::Value, - InputType::InputFile => ValueMode::Filename, + 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 fn input_type(&self) -> InputType { + self.input_type.get() + } + + #[inline] + pub fn is_submit_button(&self) -> bool { + let input_type = self.input_type.get(); + input_type == InputType::Submit || input_type == InputType::Image + } + + pub fn disable_sanitization(&self) { + self.sanitization_flag.set(false); + } + + pub fn enable_sanitization(&self) { + self.sanitization_flag.set(true); + let mut textinput = self.textinput.borrow_mut(); + let mut value = textinput.single_line_content().clone(); + self.sanitize_value(&mut value); + textinput.set_content(value); + } + + fn does_readonly_apply(&self) -> bool { + match self.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 => true, + _ => false, + } + } + + fn does_minmaxlength_apply(&self) -> bool { + match self.input_type() { + InputType::Text | + InputType::Search | + InputType::Url | + InputType::Tel | + InputType::Email | + InputType::Password => true, + _ => false, + } + } + + fn does_pattern_apply(&self) -> bool { + match self.input_type() { + InputType::Text | + InputType::Search | + InputType::Url | + InputType::Tel | + InputType::Email | + InputType::Password => true, + _ => false, + } + } + + fn does_multiple_apply(&self) -> bool { + 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 { + match self.input_type() { + InputType::Date | + InputType::Month | + InputType::Week | + InputType::Time | + InputType::DatetimeLocal | + InputType::Number | + InputType::Range => true, + _ => false, + } + } + + fn does_value_as_date_apply(&self) -> bool { + match self.input_type() { + InputType::Date | InputType::Month | InputType::Week | InputType::Time => true, + // surprisingly, spec says false for DateTimeLocal! + _ => false, + } + } + + // https://html.spec.whatwg.org/multipage#concept-input-step + fn allowed_value_step(&self) -> Option<f64> { + if let Some(attr) = self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("step")) + { + if let Ok(step) = DOMString::from(attr.summarize().value).parse_floating_point_number() + { + if step > 0.0 { + return Some(step * self.step_scale_factor()); + } + } + } + self.default_step() + .map(|step| step * self.step_scale_factor()) + } + + // https://html.spec.whatwg.org/multipage#concept-input-min + fn minimum(&self) -> Option<f64> { + if let Some(attr) = self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("min")) + { + if let Ok(min) = self.convert_string_to_number(&DOMString::from(attr.summarize().value)) + { + return Some(min); + } + } + return self.default_minimum(); + } + + // https://html.spec.whatwg.org/multipage#concept-input-max + fn maximum(&self) -> Option<f64> { + if let Some(attr) = self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("max")) + { + if let Ok(max) = self.convert_string_to_number(&DOMString::from(attr.summarize().value)) + { + return Some(max); + } + } + return self.default_maximum(); + } + + // when allowed_value_step and minumum 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 { + if let Some(attr) = self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("min")) + { + let minstr = &DOMString::from(attr.summarize().value); + if let Ok(min) = self.convert_string_to_number(minstr) { + return min; + } + } + if let Some(attr) = self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("value")) + { + if let Ok(value) = + self.convert_string_to_number(&DOMString::from(attr.summarize().value)) + { + return value; + } + } + self.default_step_base().unwrap_or(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-stepdown + // https://html.spec.whatwg.org/multipage/#dom-input-stepup + fn step_up_or_down(&self, n: i32, dir: StepDirection) -> ErrorResult { + // Step 1 + if !self.does_value_as_number_apply() { + return Err(Error::InvalidState); + } + let step_base = self.step_base(); + // Step 2 + let allowed_value_step = match self.allowed_value_step() { + Some(avs) => avs, + None => return Err(Error::InvalidState), + }; + let minimum = self.minimum(); + let maximum = self.maximum(); + if let (Some(min), Some(max)) = (minimum, maximum) { + // Step 3 + if min > max { + return Ok(()); + } + // Step 4 + if let Some(smin) = self.stepped_minimum() { + if smin > max { + return Ok(()); + } + } + } + // Step 5 + let mut value: f64 = self.convert_string_to_number(&self.Value()).unwrap_or(0.0); + + // Step 6 + let valueBeforeStepping = value; + + // Step 7 + 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 + } + }; + } else { + value = value + + match dir { + StepDirection::Down => -f64::from(n) * allowed_value_step, + StepDirection::Up => f64::from(n) * allowed_value_step, + }; + } + + // Step 8 + if let Some(min) = minimum { + if value < min { + value = self.stepped_minimum().unwrap_or(value); + } + } + + // Step 9 + if let Some(max) = maximum { + if value > max { + value = self.stepped_maximum().unwrap_or(value); + } + } + + // Step 10 + match dir { + StepDirection::Down => { + if value > valueBeforeStepping { + return Ok(()); + } + }, + StepDirection::Up => { + if value < valueBeforeStepping { + return Ok(()); + } + }, + } + + // Step 11 + self.SetValueAsNumber(value) + } + + // https://html.spec.whatwg.org/multipage/#concept-input-list + fn suggestions_source_element(&self) -> Option<DomRoot<HTMLElement>> { + 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>() + .map_or(false, |e| e.Id() == list_string) + }); + first_with_id + .as_ref() + .and_then(|el| { + el.downcast::<HTMLDataListElement>() + .map(|data_el| data_el.upcast::<HTMLElement>()) + }) + .map(|el| DomRoot::from_ref(&*el)) + } + + // https://html.spec.whatwg.org/multipage/#suffering-from-being-missing + fn suffers_from_being_missing(&self, value: &DOMString) -> bool { + match self.input_type() { + // https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing + InputType::Checkbox => self.Required() && !self.Checked(), + // https://html.spec.whatwg.org/multipage/#radio-button-state-(type%3Dradio)%3Asuffering-from-being-missing + InputType::Radio => { + let mut is_required = self.Required(); + let mut is_checked = self.Checked(); + for other in radio_group_iter(self, self.radio_group_name().as_ref()) { + is_required = is_required || other.Required(); + is_checked = is_checked || other.Checked(); + } + is_required && !is_checked + }, + // https://html.spec.whatwg.org/multipage/#file-upload-state-(type%3Dfile)%3Asuffering-from-being-missing + InputType::File => { + self.Required() && + self.filelist + .get() + .map_or(true, |files| files.Length() == 0) + }, + // https://html.spec.whatwg.org/multipage/#the-required-attribute%3Asuffering-from-being-missing + _ => { + self.Required() && + self.value_mode() == ValueMode::Value && + self.is_mutable() && + value.is_empty() + }, + } + } + + // 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; + } + + match self.input_type() { + // https://html.spec.whatwg.org/multipage/#url-state-(type%3Durl)%3Asuffering-from-a-type-mismatch + InputType::Url => Url::parse(&value).is_err(), + // https://html.spec.whatwg.org/multipage/#e-mail-state-(type%3Demail)%3Asuffering-from-a-type-mismatch + // https://html.spec.whatwg.org/multipage/#e-mail-state-(type%3Demail)%3Asuffering-from-a-type-mismatch-2 + InputType::Email => { + if self.Multiple() { + !split_commas(&value).all(|s| { + DOMString::from_string(s.to_string()).is_valid_email_address_string() + }) + } else { + !value.is_valid_email_address_string() + } + }, + // Other input types don't suffer from type mismatch + _ => false, + } + } + + // https://html.spec.whatwg.org/multipage/#suffering-from-a-pattern-mismatch + fn suffers_from_pattern_mismatch(&self, value: &DOMString) -> 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 = self.global().get_cx(); + let _ac = enter_realm(self); + rooted!(in(*cx) let mut pattern = ptr::null_mut::<JSObject>()); + + if compile_pattern(cx, &pattern_str, pattern.handle_mut()) { + if self.Multiple() && self.does_multiple_apply() { + !split_commas(&value) + .all(|s| matches_js_regex(cx, pattern.handle(), s).unwrap_or(true)) + } else { + !matches_js_regex(cx, pattern.handle(), &value).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; + } + + match self.input_type() { + // https://html.spec.whatwg.org/multipage/#e-mail-state-(type%3Demail)%3Asuffering-from-bad-input + // https://html.spec.whatwg.org/multipage/#e-mail-state-(type%3Demail)%3Asuffering-from-bad-input-2 + InputType::Email => { + // TODO: Check for input that cannot be converted to punycode. + // Currently we don't support conversion of email values to punycode + // so always return false. + false + }, + // https://html.spec.whatwg.org/multipage/#date-state-(type%3Ddate)%3Asuffering-from-bad-input + InputType::Date => !value.is_valid_date_string(), + // https://html.spec.whatwg.org/multipage/#month-state-(type%3Dmonth)%3Asuffering-from-bad-input + InputType::Month => !value.is_valid_month_string(), + // https://html.spec.whatwg.org/multipage/#week-state-(type%3Dweek)%3Asuffering-from-bad-input + InputType::Week => !value.is_valid_week_string(), + // https://html.spec.whatwg.org/multipage/#time-state-(type%3Dtime)%3Asuffering-from-bad-input + InputType::Time => !value.is_valid_time_string(), + // https://html.spec.whatwg.org/multipage/#local-date-and-time-state-(type%3Ddatetime-local)%3Asuffering-from-bad-input + InputType::DatetimeLocal => value.parse_local_date_and_time_string().is_err(), + // https://html.spec.whatwg.org/multipage/#number-state-(type%3Dnumber)%3Asuffering-from-bad-input + // https://html.spec.whatwg.org/multipage/#range-state-(type%3Drange)%3Asuffering-from-bad-input + InputType::Number | InputType::Range => !value.is_valid_floating_point_number_string(), + // https://html.spec.whatwg.org/multipage/#color-state-(type%3Dcolor)%3Asuffering-from-bad-input + InputType::Color => !value.is_valid_simple_color_string(), + // Other input types don't suffer from bad input + _ => false, + } + } + + // 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 UTF16CodeUnits(value_len) = textinput.utf16_len(); + 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 value_as_number = match self.convert_string_to_number(&value) { + Ok(num) => num, + Err(()) => 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 } } -pub trait LayoutHTMLInputElementHelpers { - #[allow(unsafe_code)] - unsafe fn value_for_layout(self) -> String; - #[allow(unsafe_code)] - unsafe fn size_for_layout(self) -> u32; - #[allow(unsafe_code)] - unsafe fn selection_for_layout(self) -> Option<Range<usize>>; - #[allow(unsafe_code)] - unsafe fn checked_state_for_layout(self) -> bool; - #[allow(unsafe_code)] - unsafe fn indeterminate_state_for_layout(self) -> bool; +pub trait LayoutHTMLInputElementHelpers<'dom> { + fn value_for_layout(self) -> Cow<'dom, str>; + fn size_for_layout(self) -> u32; + fn selection_for_layout(self) -> Option<Range<usize>>; + fn checked_state_for_layout(self) -> bool; + fn indeterminate_state_for_layout(self) -> bool; } #[allow(unsafe_code)] -unsafe fn get_raw_textinput_value(input: LayoutJS<HTMLInputElement>) -> DOMString { - (*input.unsafe_get()).textinput.borrow_for_layout().get_content() +impl<'dom> LayoutDom<'dom, HTMLInputElement> { + fn get_raw_textinput_value(self) -> DOMString { + unsafe { + self.unsafe_get() + .textinput + .borrow_for_layout() + .get_content() + } + } + + fn placeholder(self) -> &'dom str { + unsafe { self.unsafe_get().placeholder.borrow_for_layout() } + } + + fn input_type(self) -> InputType { + unsafe { self.unsafe_get().input_type.get() } + } + + fn textinput_sorted_selection_offsets_range(self) -> Range<UTF8Bytes> { + unsafe { + self.unsafe_get() + .textinput + .borrow_for_layout() + .sorted_selection_offsets_range() + } + } } -impl LayoutHTMLInputElementHelpers for LayoutJS<HTMLInputElement> { - #[allow(unsafe_code)] - unsafe fn value_for_layout(self) -> String { - #[allow(unsafe_code)] - unsafe fn get_raw_attr_value(input: LayoutJS<HTMLInputElement>, default: &str) -> String { - let elem = input.upcast::<Element>(); - let value = (*elem.unsafe_get()) +impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElement> { + fn value_for_layout(self) -> Cow<'dom, str> { + fn get_raw_attr_value<'dom>( + input: LayoutDom<'dom, HTMLInputElement>, + default: &'static str, + ) -> Cow<'dom, str> { + input + .upcast::<Element>() .get_attr_val_for_layout(&ns!(), &local_name!("value")) - .unwrap_or(default); - String::from(value) - } - - match (*self.unsafe_get()).input_type.get() { - InputType::InputCheckbox | InputType::InputRadio => String::new(), - InputType::InputFile | InputType::InputImage => String::new(), - InputType::InputButton => get_raw_attr_value(self, ""), - InputType::InputSubmit => get_raw_attr_value(self, DEFAULT_SUBMIT_VALUE), - InputType::InputReset => get_raw_attr_value(self, DEFAULT_RESET_VALUE), - InputType::InputPassword => { - let text = get_raw_textinput_value(self); + .unwrap_or(default) + .into() + } + + match self.input_type() { + InputType::Checkbox | InputType::Radio => "".into(), + InputType::File | InputType::Image => "".into(), + InputType::Button => get_raw_attr_value(self, ""), + InputType::Submit => get_raw_attr_value(self, DEFAULT_SUBMIT_VALUE), + InputType::Reset => get_raw_attr_value(self, DEFAULT_RESET_VALUE), + InputType::Password => { + let text = self.get_raw_textinput_value(); if !text.is_empty() { - text.chars().map(|_| PASSWORD_REPLACEMENT_CHAR).collect() + text.chars() + .map(|_| PASSWORD_REPLACEMENT_CHAR) + .collect::<String>() + .into() } else { - String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone()) + self.placeholder().into() } }, _ => { - let text = get_raw_textinput_value(self); + let text = self.get_raw_textinput_value(); if !text.is_empty() { - String::from(text) + text.into() } else { - String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone()) + self.placeholder().into() } }, } } - #[allow(unrooted_must_root)] #[allow(unsafe_code)] - unsafe fn size_for_layout(self) -> u32 { - (*self.unsafe_get()).size.get() + fn size_for_layout(self) -> u32 { + unsafe { self.unsafe_get().size.get() } } - #[allow(unrooted_must_root)] - #[allow(unsafe_code)] - unsafe fn selection_for_layout(self) -> Option<Range<usize>> { - if !(*self.unsafe_get()).upcast::<Element>().focus_state() { + fn selection_for_layout(self) -> Option<Range<usize>> { + if !self.upcast::<Element>().focus_state() { return None; } - let textinput = (*self.unsafe_get()).textinput.borrow_for_layout(); + let sorted_selection_offsets_range = self.textinput_sorted_selection_offsets_range(); - match (*self.unsafe_get()).input_type.get() { - InputType::InputPassword => { - let text = get_raw_textinput_value(self); - let sel = textinput.get_absolute_selection_range(); + match self.input_type() { + InputType::Password => { + let text = self.get_raw_textinput_value(); + let sel = UTF8Bytes::unwrap_range(sorted_selection_offsets_range); // Translate indices from the raw value to indices in the replacement value. - let char_start = text[.. sel.start].chars().count(); + let char_start = text[..sel.start].chars().count(); let char_end = char_start + text[sel].chars().count(); let bytes_per_char = PASSWORD_REPLACEMENT_CHAR.len_utf8(); - Some(char_start * bytes_per_char .. char_end * bytes_per_char) - } - InputType::InputText => Some(textinput.get_absolute_selection_range()), - _ => None + Some(char_start * bytes_per_char..char_end * bytes_per_char) + }, + input_type if input_type.is_textual() => { + Some(UTF8Bytes::unwrap_range(sorted_selection_offsets_range)) + }, + _ => None, } } - #[allow(unrooted_must_root)] - #[allow(unsafe_code)] - unsafe fn checked_state_for_layout(self) -> bool { - self.upcast::<Element>().get_state_for_layout().contains(IN_CHECKED_STATE) + fn checked_state_for_layout(self) -> bool { + self.upcast::<Element>() + .get_state_for_layout() + .contains(ElementState::IN_CHECKED_STATE) } - #[allow(unrooted_must_root)] - #[allow(unsafe_code)] - unsafe fn indeterminate_state_for_layout(self) -> bool { - self.upcast::<Element>().get_state_for_layout().contains(IN_INDETERMINATE_STATE) + fn indeterminate_state_for_layout(self) -> bool { + self.upcast::<Element>() + .get_state_for_layout() + .contains(ElementState::IN_INDETERMINATE_STATE) + } +} + +impl TextControlElement for HTMLInputElement { + // https://html.spec.whatwg.org/multipage/#concept-input-apply + fn selection_api_applies(&self) -> bool { + match self.input_type() { + InputType::Text | + InputType::Search | + InputType::Url | + InputType::Tel | + InputType::Password => true, + + _ => false, + } + } + + // 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 { + match self.input_type() { + InputType::Text | + InputType::Search | + InputType::Url | + InputType::Tel | + InputType::Password | + InputType::Email | + InputType::Date | + InputType::Month | + InputType::Week | + InputType::Time | + InputType::DatetimeLocal | + InputType::Number | + InputType::Color => true, + + InputType::Button | + InputType::Checkbox | + InputType::File | + InputType::Hidden | + InputType::Image | + InputType::Radio | + InputType::Range | + InputType::Reset | + InputType::Submit => false, + } + } + + fn set_dirty_value_flag(&self, value: bool) { + self.value_dirty.set(value) } } @@ -318,12 +1178,12 @@ impl HTMLInputElementMethods for HTMLInputElement { make_bool_setter!(SetDisabled, "disabled"); // https://html.spec.whatwg.org/multipage/#dom-fae-form - fn GetForm(&self) -> Option<Root<HTMLFormElement>> { + fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> { self.form_owner() } // https://html.spec.whatwg.org/multipage/#dom-input-files - fn GetFiles(&self) -> Option<Root<FileList>> { + fn GetFiles(&self) -> Option<DomRoot<FileList>> { match self.filelist.get() { Some(ref fl) => Some(fl.clone()), None => None, @@ -338,7 +1198,9 @@ impl HTMLInputElementMethods for HTMLInputElement { // https://html.spec.whatwg.org/multipage/#dom-input-checked fn Checked(&self) -> bool { - self.upcast::<Element>().state().contains(IN_CHECKED_STATE) + self.upcast::<Element>() + .state() + .contains(ElementState::IN_CHECKED_STATE) } // https://html.spec.whatwg.org/multipage/#dom-input-checked @@ -359,16 +1221,9 @@ impl HTMLInputElementMethods for HTMLInputElement { make_limited_uint_setter!(SetSize, "size", DEFAULT_INPUT_SIZE); // https://html.spec.whatwg.org/multipage/#dom-input-type - make_enumerated_getter!(Type, - "type", - "text", - "hidden" | "search" | "tel" | - "url" | "email" | "password" | - "datetime" | "date" | "month" | - "week" | "time" | "datetime-local" | - "number" | "range" | "color" | - "checkbox" | "radio" | "file" | - "submit" | "image" | "reset" | "button"); + fn Type(&self) -> DOMString { + DOMString::from(self.input_type().to_str()) + } // https://html.spec.whatwg.org/multipage/#dom-input-type make_atomic_setter!(SetType, "type"); @@ -377,18 +1232,18 @@ impl HTMLInputElementMethods for HTMLInputElement { fn Value(&self) -> DOMString { match self.value_mode() { ValueMode::Value => self.textinput.borrow().get_content(), - ValueMode::Default => { - self.upcast::<Element>() - .get_attribute(&ns!(), &local_name!("value")) - .map_or(DOMString::from(""), - |a| DOMString::from(a.summarize().value)) - } - ValueMode::DefaultOn => { - self.upcast::<Element>() - .get_attribute(&ns!(), &local_name!("value")) - .map_or(DOMString::from("on"), - |a| DOMString::from(a.summarize().value)) - } + ValueMode::Default => self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("value")) + .map_or(DOMString::from(""), |a| { + DOMString::from(a.summarize().value) + }), + ValueMode::DefaultOn => self + .upcast::<Element>() + .get_attribute(&ns!(), &local_name!("value")) + .map_or(DOMString::from("on"), |a| { + DOMString::from(a.summarize().value) + }), ValueMode::Filename => { let mut path = DOMString::from(""); match self.filelist.get() { @@ -397,26 +1252,40 @@ impl HTMLInputElementMethods for HTMLInputElement { path.push_str("C:\\fakepath\\"); path.push_str(f.name()); path - } + }, None => path, }, None => path, } - } + }, } } // https://html.spec.whatwg.org/multipage/#dom-input-value - fn SetValue(&self, value: DOMString) -> ErrorResult { + fn SetValue(&self, mut value: DOMString) -> ErrorResult { match self.value_mode() { ValueMode::Value => { - self.textinput.borrow_mut().set_content(value); + // Step 3. self.value_dirty.set(true); - } - ValueMode::Default | - ValueMode::DefaultOn => { - self.upcast::<Element>().set_string_attribute(&local_name!("value"), value); - } + + // Step 4. + self.sanitize_value(&mut value); + + let mut textinput = self.textinput.borrow_mut(); + + // Step 5. + if *textinput.single_line_content() != value { + // Steps 1-2 + textinput.set_content(value); + + // Step 5. + textinput.clear_selection_to_limit(Direction::Forward); + } + }, + ValueMode::Default | ValueMode::DefaultOn => { + self.upcast::<Element>() + .set_string_attribute(&local_name!("value"), value); + }, ValueMode::Filename => { if value.is_empty() { let window = window_from_node(self); @@ -425,10 +1294,9 @@ impl HTMLInputElementMethods for HTMLInputElement { } else { return Err(Error::InvalidState); } - } + }, } - self.value_changed.set(true); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); Ok(()) } @@ -439,6 +1307,96 @@ impl HTMLInputElementMethods for HTMLInputElement { // 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<HTMLElement>> { + self.suggestions_source_element() + } + + // https://html.spec.whatwg.org/multipage/#dom-input-valueasdate + #[allow(unsafe_code)] + fn GetValueAsDate(&self, cx: SafeJSContext) -> Option<NonNull<JSObject>> { + self.convert_string_to_naive_datetime(self.Value()) + .map(|dt| unsafe { + let time = ClippedTime { + t: dt.timestamp_millis() as f64, + }; + NonNull::new_unchecked(NewDateObject(*cx, time)) + }) + .ok() + } + + // https://html.spec.whatwg.org/multipage/#dom-input-valueasdate + #[allow(unsafe_code, non_snake_case)] + fn SetValueAsDate(&self, cx: SafeJSContext, value: *mut JSObject) -> ErrorResult { + rooted!(in(*cx) let value = value); + if !self.does_value_as_date_apply() { + return Err(Error::InvalidState); + } + if value.is_null() { + return self.SetValue(DOMString::from("")); + } + 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("Value was not a date".to_string())); + } + if !DateGetMsecSinceEpoch(*cx, Handle::from(value.handle()), &mut msecs) { + return Err(Error::JSFailed); + } + if !msecs.is_finite() { + return self.SetValue(DOMString::from("")); + } + } + // now we make a Rust date out of it so we can use safe code for the + // actual conversion logic + match milliseconds_to_datetime(msecs) { + Ok(dt) => match self.convert_naive_datetime_to_string(dt) { + Ok(converted) => self.SetValue(converted), + _ => self.SetValue(DOMString::from("")), + }, + _ => self.SetValue(DOMString::from("")), + } + } + + // https://html.spec.whatwg.org/multipage/#dom-input-valueasnumber + fn ValueAsNumber(&self) -> f64 { + self.convert_string_to_number(&self.Value()) + .unwrap_or(std::f64::NAN) + } + + // https://html.spec.whatwg.org/multipage/#dom-input-valueasnumber + fn SetValueAsNumber(&self, value: f64) -> ErrorResult { + if value.is_infinite() { + Err(Error::Type("value is not finite".to_string())) + } else if !self.does_value_as_number_apply() { + Err(Error::InvalidState) + } else if value.is_nan() { + self.SetValue(DOMString::from("")) + } else if let Ok(converted) = self.convert_number_to_string(value) { + self.SetValue(converted) + } else { + // The most literal spec-compliant implementation would + // use bignum chrono 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("")) + } + } + // https://html.spec.whatwg.org/multipage/#attr-fe-name make_getter!(Name, "name"); @@ -452,16 +1410,18 @@ impl HTMLInputElementMethods for HTMLInputElement { make_setter!(SetPlaceholder, "placeholder"); // https://html.spec.whatwg.org/multipage/#dom-input-formaction - make_url_or_base_getter!(FormAction, "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-input-formenctype - make_enumerated_getter!(FormEnctype, - "formenctype", - "application/x-www-form-urlencoded", - "text/plain" | "multipart/form-data"); + make_enumerated_getter!( + FormEnctype, + "formenctype", + "application/x-www-form-urlencoded", + "text/plain" | "multipart/form-data" + ); // https://html.spec.whatwg.org/multipage/#dom-input-formenctype make_setter!(SetFormEnctype, "formenctype"); @@ -502,12 +1462,6 @@ impl HTMLInputElementMethods for HTMLInputElement { // 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-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-multiple make_bool_getter!(Multiple, "multiple"); @@ -540,71 +1494,89 @@ impl HTMLInputElementMethods for HTMLInputElement { // https://html.spec.whatwg.org/multipage/#dom-input-indeterminate fn Indeterminate(&self) -> bool { - self.upcast::<Element>().state().contains(IN_INDETERMINATE_STATE) + self.upcast::<Element>() + .state() + .contains(ElementState::IN_INDETERMINATE_STATE) } // https://html.spec.whatwg.org/multipage/#dom-input-indeterminate fn SetIndeterminate(&self, val: bool) { - self.upcast::<Element>().set_state(IN_INDETERMINATE_STATE, val) + self.upcast::<Element>() + .set_state(ElementState::IN_INDETERMINATE_STATE, val) } // https://html.spec.whatwg.org/multipage/#dom-lfe-labels - fn Labels(&self) -> Root<NodeList> { - if self.type_() == atom!("hidden") { - let window = window_from_node(self); - NodeList::empty(&window) + // Different from make_labels_getter because this one + // conditionally returns null. + fn GetLabels(&self) -> Option<DomRoot<NodeList>> { + if self.input_type() == InputType::Hidden { + None } else { - self.upcast::<HTMLElement>().labels() + Some(self.labels_node_list.or_init(|| { + NodeList::new_labels_list( + self.upcast::<Node>().owner_doc().window(), + self.upcast::<HTMLElement>(), + ) + })) } } - // https://html.spec.whatwg.org/multipage/#dom-input-selectionstart - fn SelectionStart(&self) -> u32 { - self.textinput.borrow().get_selection_start() + // 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 SetSelectionStart(&self, start: u32) { - let selection_end = self.SelectionEnd(); - self.textinput.borrow_mut().set_selection_range(start, selection_end); - self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + fn GetSelectionStart(&self) -> Option<u32> { + self.selection().dom_start() + } + + // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart + fn SetSelectionStart(&self, start: Option<u32>) -> ErrorResult { + self.selection().set_dom_start(start) } // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend - fn SelectionEnd(&self) -> u32 { - self.textinput.borrow().get_absolute_insertion_point() as u32 + fn GetSelectionEnd(&self) -> Option<u32> { + self.selection().dom_end() } // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend - fn SetSelectionEnd(&self, end: u32) { - let selection_start = self.SelectionStart(); - self.textinput.borrow_mut().set_selection_range(selection_start, end); - self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + fn SetSelectionEnd(&self, end: Option<u32>) -> ErrorResult { + self.selection().set_dom_end(end) } // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection - fn SelectionDirection(&self) -> DOMString { - DOMString::from(self.textinput.borrow().selection_direction) + fn GetSelectionDirection(&self) -> Option<DOMString> { + self.selection().dom_direction() } // https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection - fn SetSelectionDirection(&self, direction: DOMString) { - self.textinput.borrow_mut().selection_direction = SelectionDirection::from(direction); + 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>) { - let direction = direction.map_or(SelectionDirection::None, |d| SelectionDirection::from(d)); - self.textinput.borrow_mut().selection_direction = direction; - self.textinput.borrow_mut().set_selection_range(start, end); - let window = window_from_node(self); - let _ = window.user_interaction_task_source().queue_event( - &self.upcast(), - atom!("select"), - EventBubbles::Bubbles, - EventCancelable::NotCancelable, - &window); - self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + fn SetSelectionRange(&self, start: u32, end: u32, direction: Option<DOMString>) -> ErrorResult { + self.selection().set_dom_range(start, 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(start), Some(end), selection_mode) } // Select the files based on filepaths passed in, @@ -612,53 +1584,105 @@ impl HTMLInputElementMethods for HTMLInputElement { // used for test purpose. // check-tidy: no specs after this line fn SelectFiles(&self, paths: Vec<DOMString>) { - if self.input_type.get() == InputType::InputFile { + if self.input_type() == InputType::File { self.select_files(Some(paths)); } } + + // https://html.spec.whatwg.org/multipage/#dom-input-stepup + fn StepUp(&self, n: i32) -> ErrorResult { + self.step_up_or_down(n, StepDirection::Up) + } + + // https://html.spec.whatwg.org/multipage/#dom-input-stepdown + fn StepDown(&self, n: i32) -> ErrorResult { + self.step_up_or_down(n, StepDirection::Down) + } + + // https://html.spec.whatwg.org/multipage/#dom-cva-willvalidate + fn WillValidate(&self) -> bool { + self.is_instance_validatable() + } + + // https://html.spec.whatwg.org/multipage/#dom-cva-validity + fn Validity(&self) -> DomRoot<ValidityState> { + self.validity_state() + } + + // https://html.spec.whatwg.org/multipage/#dom-cva-checkvalidity + fn CheckValidity(&self) -> bool { + self.check_validity() + } + + // https://html.spec.whatwg.org/multipage/#dom-cva-reportvalidity + fn ReportValidity(&self) -> bool { + self.report_validity() + } + + // https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage + fn ValidationMessage(&self) -> DOMString { + self.validation_message() + } + + // https://html.spec.whatwg.org/multipage/#dom-cva-setcustomvalidity + fn SetCustomValidity(&self, error: DOMString) { + self.validity_state().set_custom_error_message(error); + } } +fn radio_group_iter<'a>( + elem: &'a HTMLInputElement, + group: Option<&'a Atom>, +) -> impl Iterator<Item = DomRoot<HTMLInputElement>> + 'a { + let owner = elem.form_owner(); + let root = elem + .upcast::<Node>() + .GetRootNode(&GetRootNodeOptions::empty()); + + // If group is None, in_same_group always fails, but we need to always return elem. + root.traverse_preorder(ShadowIncluding::No) + .filter_map(|r| DomRoot::downcast::<HTMLInputElement>(r)) + .filter(move |r| &**r == elem || in_same_group(&r, owner.as_deref(), group, None)) +} -#[allow(unsafe_code)] fn broadcast_radio_checked(broadcaster: &HTMLInputElement, group: Option<&Atom>) { - match group { - None | Some(&atom!("")) => { - // Radio input elements with a missing or empty name are alone in their - // own group. - return; - }, - _ => {}, - } - - //TODO: if not in document, use root ancestor instead of document - let owner = broadcaster.form_owner(); - let doc = document_from_node(broadcaster); - - // This function is a workaround for lifetime constraint difficulties. - fn do_broadcast(doc_node: &Node, broadcaster: &HTMLInputElement, - owner: Option<&HTMLFormElement>, group: Option<&Atom>) { - let iter = doc_node.query_selector_iter(DOMString::from("input[type=radio]")).unwrap() - .filter_map(Root::downcast::<HTMLInputElement>) - .filter(|r| in_same_group(&r, owner, group) && broadcaster != &**r); - for ref r in iter { - if r.Checked() { - r.SetChecked(false); - } + for r in radio_group_iter(broadcaster, group) { + if broadcaster != &*r && r.Checked() { + r.SetChecked(false); } } - - do_broadcast(doc.upcast(), broadcaster, owner.r(), group) } // https://html.spec.whatwg.org/multipage/#radio-button-group -fn in_same_group(other: &HTMLInputElement, owner: Option<&HTMLFormElement>, - group: Option<&Atom>) -> bool { - other.input_type.get() == InputType::InputRadio && - // TODO Both a and b are in the same home subtree. - other.form_owner().r() == owner && - match (other.radio_group_name(), group) { - (Some(ref s1), Some(s2)) => compatibility_caseless_match_str(s1, s2) && s2 != &atom!(""), - _ => false +fn in_same_group( + other: &HTMLInputElement, + owner: Option<&HTMLFormElement>, + group: Option<&Atom>, + tree_root: Option<&Node>, +) -> bool { + if group.is_none() { + // Radio input elements with a missing or empty name are alone in their own group. + return false; + } + + if other.input_type() != InputType::Radio || + other.form_owner().as_deref() != owner || + other.radio_group_name().as_ref() != group + { + return false; + } + + match tree_root { + Some(tree_root) => { + let other_root = other + .upcast::<Node>() + .GetRootNode(&GetRootNodeOptions::empty()); + tree_root == &*other_root + }, + None => { + // Skip check if the tree root isn't provided. + true + }, } } @@ -669,93 +1693,121 @@ impl HTMLInputElement { } } - /// https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set - /// Steps range from 3.1 to 3.7 (specific to HTMLInputElement) - pub fn form_datums(&self, submitter: Option<FormSubmitter>) -> Vec<FormDatum> { + /// <https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set> + /// Steps range from 5.1 to 5.10 (specific to HTMLInputElement) + pub fn form_datums( + &self, + submitter: Option<FormSubmitter>, + encoding: Option<&'static Encoding>, + ) -> Vec<FormDatum> { // 3.1: disabled state check is in get_unclean_dataset - // Step 3.2 - let ty = self.type_(); - // Step 3.4 + // Step 5.2 + let ty = self.Type(); + + // Step 5.4 let name = self.Name(); let is_submitter = match submitter { - Some(FormSubmitter::InputElement(s)) => { - self == s - }, - _ => false + Some(FormSubmitter::InputElement(s)) => self == s, + _ => false, }; - match ty { - // Step 3.1: it's a button but it is not submitter. - atom!("submit") | atom!("button") | atom!("reset") if !is_submitter => return vec![], - // Step 3.1: it's the "Checkbox" or "Radio Button" and whose checkedness is false. - atom!("radio") | atom!("checkbox") => if !self.Checked() || name.is_empty() { + 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![]; }, - atom!("file") => { + + // 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 3.2-3.7 + // Step 5.2-5.7 let name = self.Name(); - let type_ = self.Type(); match self.GetFiles() { Some(fl) => { for f in fl.iter_files() { datums.push(FormDatum { - ty: type_.clone(), + ty: ty.clone(), name: name.clone(), - value: FormDatumValue::File(Root::from_ref(&f)), + 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: type_.clone(), + ty: ty.clone(), name: name.clone(), value: FormDatumValue::String(DOMString::from("")), }) - } + }, } return datums; - } - atom!("image") => return vec![], // Unimplemented - // Step 3.1: it's not the "Image Button" and doesn't have a name attribute. - _ => if name.is_empty() { - return vec![]; - } + }, + + 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: ty.clone(), + name: 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 3.9 + // Step 5.12 vec![FormDatum { - ty: DOMString::from(&*ty), // FIXME(ajeffrey): Convert directly from Atoms to DOMStrings + ty: ty.clone(), name: name, - value: FormDatumValue::String(self.Value()) + value: FormDatumValue::String(self.Value()), }] } // https://html.spec.whatwg.org/multipage/#radio-button-group fn radio_group_name(&self) -> Option<Atom> { - //TODO: determine form owner - self.upcast::<Element>() - .get_attribute(&ns!(), &local_name!("name")) - .map(|name| name.value().as_atom().clone()) + self.upcast::<Element>().get_name().and_then(|name| { + if name == atom!("") { + None + } else { + Some(name) + } + }) } fn update_checked_state(&self, checked: bool, dirty: bool) { - self.upcast::<Element>().set_state(IN_CHECKED_STATE, checked); + self.upcast::<Element>() + .set_state(ElementState::IN_CHECKED_STATE, checked); if dirty { self.checked_changed.set(true); } - if self.input_type.get() == InputType::InputRadio && checked { - broadcast_radio_checked(self, - self.radio_group_name().as_ref()); + if self.input_type() == InputType::Radio && checked { + broadcast_radio_checked(self, self.radio_group_name().as_ref()); } self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); @@ -771,30 +1823,28 @@ impl HTMLInputElement { // https://html.spec.whatwg.org/multipage/#the-input-element:concept-form-reset-control pub fn reset(&self) { - match self.input_type.get() { - InputType::InputRadio | InputType::InputCheckbox => { + match self.input_type() { + InputType::Radio | InputType::Checkbox => { self.update_checked_state(self.DefaultChecked(), false); self.checked_changed.set(false); }, - InputType::InputImage => (), - _ => () + InputType::Image => (), + _ => (), } - - self.SetValue(self.DefaultValue()) - .expect("Failed to reset input value to default."); + self.textinput.borrow_mut().set_content(self.DefaultValue()); self.value_dirty.set(false); - self.value_changed.set(false); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); } fn update_placeholder_shown_state(&self) { - match self.input_type.get() { - InputType::InputText | InputType::InputPassword => {}, - _ => return, + if !self.input_type().is_textual_or_password() { + return; } + let has_placeholder = !self.placeholder.borrow().is_empty(); let has_value = !self.textinput.borrow().is_empty(); let el = self.upcast::<Element>(); + el.set_placeholder_shown_state(has_placeholder && !has_value); } @@ -805,18 +1855,22 @@ impl HTMLInputElement { let origin = get_blob_origin(&window.get_url()); let resource_threads = window.upcast::<GlobalScope>().resource_threads(); - let mut files: Vec<Root<File>> = vec![]; + let mut files: Vec<DomRoot<File>> = vec![]; let mut error = None; let filter = filter_from_accept(&self.Accept()); let target = self.upcast::<EventTarget>(); if self.Multiple() { - let opt_test_paths = opt_test_paths.map(|paths| paths.iter().map(|p| p.to_string()).collect()); + let opt_test_paths = + opt_test_paths.map(|paths| paths.iter().map(|p| p.to_string()).collect()); - let (chan, recv) = ipc::channel().expect("Error initializing channel"); + let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone()) + .expect("Error initializing channel"); let msg = FileManagerThreadMsg::SelectFiles(filter, chan, origin, opt_test_paths); - let _ = resource_threads.send(CoreResourceMsg::ToFileManager(msg)).unwrap(); + let _ = resource_threads + .send(CoreResourceMsg::ToFileManager(msg)) + .unwrap(); match recv.recv().expect("IpcSender side error") { Ok(selected_files) => { @@ -834,13 +1888,16 @@ impl HTMLInputElement { } else { Some(paths[0].to_string()) // neglect other paths } - } + }, None => None, }; - let (chan, recv) = ipc::channel().expect("Error initializing channel"); + let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone()) + .expect("Error initializing channel"); let msg = FileManagerThreadMsg::SelectFile(filter, chan, origin, opt_test_path); - let _ = resource_threads.send(CoreResourceMsg::ToFileManager(msg)).unwrap(); + let _ = resource_threads + .send(CoreResourceMsg::ToFileManager(msg)) + .unwrap(); match recv.recv().expect("IpcSender side error") { Ok(selected) => { @@ -860,23 +1917,374 @@ impl HTMLInputElement { target.fire_bubbling_event(atom!("change")); } } + + // https://html.spec.whatwg.org/multipage/#value-sanitization-algorithm + fn sanitize_value(&self, value: &mut DOMString) { + // if sanitization_flag is false, we are setting content attributes + // on an element we haven't really finished creating; we will + // enable the flag and really sanitize before this element becomes + // observable. + if !self.sanitization_flag.get() { + return; + } + match self.input_type() { + InputType::Text | InputType::Search | InputType::Tel | InputType::Password => { + value.strip_newlines(); + }, + InputType::Url => { + value.strip_newlines(); + value.strip_leading_and_trailing_ascii_whitespace(); + }, + InputType::Date => { + if !value.is_valid_date_string() { + value.clear(); + } + }, + InputType::Month => { + if !value.is_valid_month_string() { + value.clear(); + } + }, + InputType::Week => { + if !value.is_valid_week_string() { + value.clear(); + } + }, + InputType::Color => { + if value.is_valid_simple_color_string() { + value.make_ascii_lowercase(); + } else { + *value = "#000000".into(); + } + }, + InputType::Time => { + if !value.is_valid_time_string() { + value.clear(); + } + }, + InputType::DatetimeLocal => { + if value + .convert_valid_normalized_local_date_and_time_string() + .is_err() + { + value.clear(); + } + }, + InputType::Number => { + if !value.is_valid_floating_point_number_string() { + value.clear(); + } + // Spec says that user agent "may" round the value + // when it's suffering a step mismatch, but WPT tests + // want it unrounded, and this matches other browser + // behavior (typing an unrounded number into an + // integer field box and pressing enter generally keeps + // the number intact but makes the input box :invalid) + }, + // https://html.spec.whatwg.org/multipage/#range-state-(type=range):value-sanitization-algorithm + InputType::Range => { + if !value.is_valid_floating_point_number_string() { + *value = DOMString::from(self.default_range_value().to_string()); + } + if let Ok(fval) = &value.parse::<f64>() { + let mut fval = *fval; + // comparing max first, because if they contradict + // the spec wants min to be the one that applies + if let Some(max) = self.maximum() { + if fval > max { + fval = max; + } + } + if let Some(min) = self.minimum() { + if fval < min { + fval = min; + } + } + // https://html.spec.whatwg.org/multipage/#range-state-(type=range):suffering-from-a-step-mismatch + // Spec does not describe this in a way that lends itself to + // reproducible handling of floating-point rounding; + // Servo may fail a WPT test because .1 * 6 == 6.000000000000001 + if let Some(allowed_value_step) = self.allowed_value_step() { + let step_base = self.step_base(); + let steps_from_base = (fval - step_base) / allowed_value_step; + if steps_from_base.fract() != 0.0 { + // not an integer number of steps, there's a mismatch + // round the number of steps... + let int_steps = round_halves_positive(steps_from_base); + // and snap the value to that rounded value... + fval = int_steps * allowed_value_step + step_base; + + // but if after snapping we're now outside min..max + // we have to adjust! (adjusting to min last because + // that "wins" over max in the spec) + if let Some(stepped_maximum) = self.stepped_maximum() { + if fval > stepped_maximum { + fval = stepped_maximum; + } + } + if let Some(stepped_minimum) = self.stepped_minimum() { + if fval < stepped_minimum { + fval = stepped_minimum; + } + } + } + } + *value = DOMString::from(fval.to_string()); + }; + }, + InputType::Email => { + if !self.Multiple() { + value.strip_newlines(); + value.strip_leading_and_trailing_ascii_whitespace(); + } else { + let sanitized = str_join( + split_commas(value).map(|token| { + let mut token = DOMString::from_string(token.to_string()); + token.strip_newlines(); + token.strip_leading_and_trailing_ascii_whitespace(); + token + }), + ",", + ); + value.clear(); + value.push_str(sanitized.as_str()); + } + }, + // The following inputs don't have a value sanitization algorithm. + // See https://html.spec.whatwg.org/multipage/#value-sanitization-algorithm + InputType::Button | + InputType::Checkbox | + InputType::File | + InputType::Hidden | + InputType::Image | + InputType::Radio | + InputType::Reset | + InputType::Submit => (), + } + } + + #[allow(unrooted_must_root)] + fn selection(&self) -> TextControlSelection<Self> { + TextControlSelection::new(&self, &self.textinput) + } + + // https://html.spec.whatwg.org/multipage/#implicit-submission + #[allow(unsafe_code)] + fn implicit_submission(&self) { + let doc = document_from_node(self); + 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; + submit_button = node + .query_selector_iter(DOMString::from("input[type=submit]")) + .unwrap() + .filter_map(DomRoot::downcast::<HTMLInputElement>) + .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_mouse_event_not_trusted(DOMString::from("click")); + } + }, + None => { + let inputs = node + .query_selector_iter(DOMString::from("input")) + .unwrap() + .filter_map(DomRoot::downcast::<HTMLInputElement>) + .filter(|input| { + input.form_owner() == owner && + match 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 => true, + _ => false, + } + }); + + if inputs.skip(1).next().is_some() { + // lazily test for > 1 submission-blocking inputs + return; + } + form.submit( + SubmittedFrom::NotFromForm, + FormSubmitter::FormElement(&form), + ); + }, + } + } + + // https://html.spec.whatwg.org/multipage/#concept-input-value-string-number + fn convert_string_to_number(&self, value: &DOMString) -> Result<f64, ()> { + match self.input_type() { + InputType::Date => match value.parse_date_string() { + Ok((year, month, day)) => { + let d = NaiveDate::from_ymd(year, month, day); + let duration = d.signed_duration_since(NaiveDate::from_ymd(1970, 1, 1)); + Ok(duration.num_milliseconds() as f64) + }, + _ => Err(()), + }, + InputType::Month => match value.parse_month_string() { + // This one returns number of months, not milliseconds + // (specification requires this, presumably because number of + // milliseconds is not consistent across months) + // the - 1.0 is because january is 1, not 0 + Ok((year, month)) => Ok(((year - 1970) * 12) as f64 + (month as f64 - 1.0)), + _ => Err(()), + }, + InputType::Week => match value.parse_week_string() { + Ok((year, weeknum)) => { + let d = NaiveDate::from_isoywd(year, weeknum, Weekday::Mon); + let duration = d.signed_duration_since(NaiveDate::from_ymd(1970, 1, 1)); + Ok(duration.num_milliseconds() as f64) + }, + _ => Err(()), + }, + InputType::Time => match value.parse_time_string() { + Ok((hours, minutes, seconds)) => { + Ok((seconds as f64 + 60.0 * minutes as f64 + 3600.0 * hours as f64) * 1000.0) + }, + _ => Err(()), + }, + InputType::DatetimeLocal => match value.parse_local_date_and_time_string() { + // Is this supposed to know the locale's daylight-savings-time rules? + Ok(((year, month, day), (hours, minutes, seconds))) => { + let d = NaiveDate::from_ymd(year, month, day); + let ymd_duration = d.signed_duration_since(NaiveDate::from_ymd(1970, 1, 1)); + let hms_millis = + (seconds + 60.0 * minutes as f64 + 3600.0 * hours as f64) * 1000.0; + Ok(ymd_duration.num_milliseconds() as f64 + hms_millis) + }, + _ => Err(()), + }, + InputType::Number | InputType::Range => value.parse_floating_point_number(), + // min/max/valueAsNumber/stepDown/stepUp do not apply to + // the remaining types + _ => Err(()), + } + } + + // https://html.spec.whatwg.org/multipage/#concept-input-value-string-number + fn convert_number_to_string(&self, value: f64) -> Result<DOMString, ()> { + match self.input_type() { + InputType::Date => { + let datetime = milliseconds_to_datetime(value)?; + Ok(DOMString::from(datetime.format("%Y-%m-%d").to_string())) + }, + InputType::Month => { + // interpret value as months(not millis) in epoch, return monthstring + let year_from_1970 = (value / 12.0).floor(); + let month = (value - year_from_1970 * 12.0).floor() as u32 + 1; // january is 1, not 0 + let year = (year_from_1970 + 1970.0) as u64; + Ok(DOMString::from(format!("{:04}-{:02}", year, month))) + }, + InputType::Week => { + let datetime = milliseconds_to_datetime(value)?; + let year = datetime.iso_week().year(); // not necessarily the same as datetime.year() + let week = datetime.iso_week().week(); + Ok(DOMString::from(format!("{:04}-W{:02}", year, week))) + }, + InputType::Time => { + let datetime = milliseconds_to_datetime(value)?; + Ok(DOMString::from(datetime.format("%H:%M:%S%.3f").to_string())) + }, + InputType::DatetimeLocal => { + let datetime = milliseconds_to_datetime(value)?; + Ok(DOMString::from( + datetime.format("%Y-%m-%dT%H:%M:%S%.3f").to_string(), + )) + }, + InputType::Number | InputType::Range => Ok(DOMString::from(value.to_string())), + // this won't be called from other input types + _ => unreachable!(), + } + } + + // https://html.spec.whatwg.org/multipage/#concept-input-value-string-date + // This does the safe Rust part of conversion; the unsafe JS Date part + // is in GetValueAsDate + fn convert_string_to_naive_datetime(&self, value: DOMString) -> Result<NaiveDateTime, ()> { + match self.input_type() { + InputType::Date => value + .parse_date_string() + .and_then(|(y, m, d)| NaiveDate::from_ymd_opt(y, m, d).ok_or(())) + .map(|date| date.and_hms(0, 0, 0)), + InputType::Time => value.parse_time_string().and_then(|(h, m, s)| { + let whole_seconds = s.floor(); + let nanos = ((s - whole_seconds) * 1e9).floor() as u32; + NaiveDate::from_ymd(1970, 1, 1) + .and_hms_nano_opt(h, m, whole_seconds as u32, nanos) + .ok_or(()) + }), + InputType::Week => value + .parse_week_string() + .and_then(|(iso_year, week)| { + NaiveDate::from_isoywd_opt(iso_year, week, Weekday::Mon).ok_or(()) + }) + .map(|date| date.and_hms(0, 0, 0)), + InputType::Month => value + .parse_month_string() + .and_then(|(y, m)| NaiveDate::from_ymd_opt(y, m, 1).ok_or(())) + .map(|date| date.and_hms(0, 0, 0)), + // does not apply to other types + _ => Err(()), + } + } + + // https://html.spec.whatwg.org/multipage/#concept-input-value-date-string + // This does the safe Rust part of conversion; the unsafe JS Date part + // is in SetValueAsDate + fn convert_naive_datetime_to_string(&self, value: NaiveDateTime) -> Result<DOMString, ()> { + match self.input_type() { + InputType::Date => Ok(DOMString::from(value.format("%Y-%m-%d").to_string())), + InputType::Month => Ok(DOMString::from(value.format("%Y-%m").to_string())), + InputType::Week => { + let year = value.iso_week().year(); // not necessarily the same as value.year() + let week = value.iso_week().week(); + Ok(DOMString::from(format!("{:04}-W{:02}", year, week))) + }, + InputType::Time => Ok(DOMString::from(value.format("%H:%M:%S%.3f").to_string())), + // this won't be called from other input types + _ => unreachable!(), + } + } } impl VirtualMethods for HTMLInputElement { - fn super_type(&self) -> Option<&VirtualMethods> { - Some(self.upcast::<HTMLElement>() as &VirtualMethods) + fn super_type(&self) -> Option<&dyn VirtualMethods> { + Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods) } fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { self.super_type().unwrap().attribute_mutated(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; + // Input was already disabled before. + return; }, AttributeMutation::Removed => false, }; @@ -885,55 +2293,48 @@ impl VirtualMethods for HTMLInputElement { el.set_enabled_state(!disabled_state); el.check_ancestors_disabled_state_for_form_control(); - if self.input_type.get() == InputType::InputText { + if self.input_type().is_textual() { let read_write = !(self.ReadOnly() || el.disabled_state()); el.set_read_write_state(read_write); } + + el.update_sequentially_focusable_status(); }, &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; + // Input was already checked before. + return; }, AttributeMutation::Removed => false, }; self.update_checked_state(checked_state, false); }, &local_name!("size") => { - let size = mutation.new_value(attr).map(|value| { - value.as_uint() - }); + let size = mutation.new_value(attr).map(|value| value.as_uint()); self.size.set(size.unwrap_or(DEFAULT_INPUT_SIZE)); - } + }, &local_name!("type") => { let el = self.upcast::<Element>(); match mutation { AttributeMutation::Set(_) => { - let new_type = match attr.value().as_atom() { - &atom!("button") => InputType::InputButton, - &atom!("submit") => InputType::InputSubmit, - &atom!("reset") => InputType::InputReset, - &atom!("file") => InputType::InputFile, - &atom!("radio") => InputType::InputRadio, - &atom!("checkbox") => InputType::InputCheckbox, - &atom!("password") => InputType::InputPassword, - _ => InputType::InputText, - }; + let new_type = InputType::from(attr.value().as_atom()); // 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.set(new_type); - if new_type == InputType::InputText { + if new_type.is_textual() { let read_write = !(self.ReadOnly() || el.disabled_state()); el.set_read_write_state(read_write); } else { el.set_read_write_state(false); } - if new_type == InputType::InputFile { + if new_type == InputType::File { let window = window_from_node(self); let filelist = FileList::new(&window, vec![]); self.filelist.set(Some(&filelist)); @@ -947,96 +2348,112 @@ impl VirtualMethods for HTMLInputElement { (&ValueMode::Value, false, ValueMode::DefaultOn) => { self.SetValue(old_idl_value) .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(&ns!(), &local_name!("value")) - .map_or(DOMString::from(""), - |a| DOMString::from(a.summarize().value))) - .expect("Failed to set input value on type change to ValueMode::Value."); + self.SetValue( + self.upcast::<Element>() + .get_attribute(&ns!(), &local_name!("value")) + .map_or(DOMString::from(""), |a| { + DOMString::from(a.summarize().value) + }), + ) + .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 => { + (_, _, ValueMode::Filename) + if old_value_mode != ValueMode::Filename => + { self.SetValue(DOMString::from("")) .expect("Failed to set input value on type change to ValueMode::Filename."); } - _ => {} + _ => {}, } // Step 5 - if new_type == InputType::InputRadio { - self.radio_group_updated( - self.radio_group_name().as_ref()); + if new_type == InputType::Radio { + self.radio_group_updated(self.radio_group_name().as_ref()); } - // TODO: Step 6 - value sanitization + // Step 6 + let mut textinput = self.textinput.borrow_mut(); + let mut value = textinput.single_line_content().clone(); + self.sanitize_value(&mut value); + textinput.set_content(value); + + // Steps 7-9 + if !previously_selectable && self.selection_api_applies() { + textinput.clear_selection_to_limit(Direction::Backward); + } }, AttributeMutation::Removed => { - if self.input_type.get() == InputType::InputRadio { - broadcast_radio_checked( - self, - self.radio_group_name().as_ref()); + if self.input_type() == InputType::Radio { + broadcast_radio_checked(self, self.radio_group_name().as_ref()); } - self.input_type.set(InputType::InputText); + self.input_type.set(InputType::default()); 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(); }, - &local_name!("value") if !self.value_changed.get() => { + &local_name!("value") if !self.value_dirty.get() => { let value = mutation.new_value(attr).map(|value| (**value).to_owned()); - self.textinput.borrow_mut().set_content( - value.map_or(DOMString::new(), DOMString::from)); + 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!("name") if self.input_type.get() == InputType::InputRadio => { + &local_name!("name") if self.input_type() == InputType::Radio => { self.radio_group_updated( - mutation.new_value(attr).as_ref().map(|name| name.as_atom())); + mutation.new_value(attr).as_ref().map(|name| name.as_atom()), + ); }, - &local_name!("maxlength") => { - match *attr.value() { - AttrValue::Int(_, value) => { - if value < 0 { - self.textinput.borrow_mut().max_length = None - } else { - self.textinput.borrow_mut().max_length = Some(value as usize) - } - }, - _ => panic!("Expected an AttrValue::Int"), - } + &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(UTF16CodeUnits(value as usize))) + } + }, + _ => panic!("Expected an AttrValue::Int"), }, - &local_name!("minlength") => { - match *attr.value() { - AttrValue::Int(_, value) => { - if value < 0 { - self.textinput.borrow_mut().min_length = None - } else { - self.textinput.borrow_mut().min_length = Some(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(UTF16CodeUnits(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')); + placeholder + .extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r')); } } self.update_placeholder_shown_state(); }, - &local_name!("readonly") if self.input_type.get() == InputType::InputText => { + &local_name!("readonly") if self.input_type().is_textual() => { let el = self.upcast::<Element>(); match mutation { AttributeMutation::Set(_) => { @@ -1044,7 +2461,7 @@ impl VirtualMethods for HTMLInputElement { }, AttributeMutation::Removed => { el.set_read_write_state(!el.disabled_state()); - } + }, } }, &local_name!("form") => { @@ -1057,21 +2474,27 @@ impl VirtualMethods for HTMLInputElement { fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { match name { &local_name!("accept") => AttrValue::from_comma_separated_tokenlist(value.into()), - &local_name!("name") => AttrValue::from_atomic(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), + &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, tree_in_doc: bool) { + fn bind_to_tree(&self, context: &BindContext) { if let Some(ref s) = self.super_type() { - s.bind_to_tree(tree_in_doc); + s.bind_to_tree(context); } - - self.upcast::<Element>().check_ancestors_disabled_state_for_form_control(); + self.upcast::<Element>() + .check_ancestors_disabled_state_for_form_control(); } fn unbind_from_tree(&self, context: &UnbindContext) { @@ -1079,97 +2502,138 @@ impl VirtualMethods for HTMLInputElement { let node = self.upcast::<Node>(); let el = self.upcast::<Element>(); - if node.ancestors().any(|ancestor| ancestor.is::<HTMLFieldSetElement>()) { + if node + .ancestors() + .any(|ancestor| ancestor.is::<HTMLFieldSetElement>()) + { el.check_ancestors_disabled_state_for_form_control(); } else { el.check_disabled_attribute(); } } + // 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) { if let Some(s) = self.super_type() { s.handle_event(event); } if event.type_() == atom!("click") && !event.DefaultPrevented() { - // TODO: Dispatch events for non activatable inputs - // https://html.spec.whatwg.org/multipage/#common-input-element-events + // WHATWG-specified activation behaviors are handled elsewhere; + // this is for all the other things a UI click might do //TODO: set the editing position for text inputs - document_from_node(self).request_focus(self.upcast()); - if (self.input_type.get() == InputType::InputText || - self.input_type.get() == InputType::InputPassword) && + if self.input_type().is_textual_or_password() && // Check if we display a placeholder. Layout doesn't know about this. - !self.textinput.borrow().is_empty() { - if let Some(mouse_event) = event.downcast::<MouseEvent>() { - // dispatch_key_event (document.rs) triggers a click event when releasing - // the space key. There's no nice way to catch this so let's use this for - // now. - if !(mouse_event.ScreenX() == 0 && mouse_event.ScreenY() == 0 && - mouse_event.GetRelatedTarget().is_none()) { - let window = window_from_node(self); - let translated_x = mouse_event.ClientX() + window.PageXOffset(); - let translated_y = mouse_event.ClientY() + window.PageYOffset(); - let TextIndexResponse(index) = window.text_index_query( - self.upcast::<Node>().to_trusted_node_address(), - translated_x, - translated_y - ); - if let Some(i) = index { - self.textinput.borrow_mut().set_edit_point_index(i as usize); - // trigger redraw - self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); - event.PreventDefault(); - } - } - } - } - } else if event.type_() == atom!("keydown") && !event.DefaultPrevented() && - (self.input_type.get() == InputType::InputText || - self.input_type.get() == InputType::InputPassword) { - 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); - match action { - TriggerDefaultAction => { - self.implicit_submission(keyevent.CtrlKey(), - keyevent.ShiftKey(), - keyevent.AltKey(), - keyevent.MetaKey()); - }, - DispatchInput => { - self.value_changed.set(true); - self.update_placeholder_shown_state(); - self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); - event.mark_as_handled(); - } - RedrawSelection => { + !self.textinput.borrow().is_empty() + { + if let Some(mouse_event) = event.downcast::<MouseEvent>() { + // dispatch_key_event (document.rs) triggers a click event when releasing + // the space key. There's no nice way to catch this so let's use this for + // now. + if let Some(point_in_target) = mouse_event.point_in_target() { + let window = window_from_node(self); + let TextIndexResponse(index) = + window.text_index_query(self.upcast::<Node>(), point_in_target); + if let Some(i) = index { + self.textinput.borrow_mut().set_edit_point_index(i as usize); + // trigger redraw self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); - event.mark_as_handled(); + event.PreventDefault(); } - Nothing => (), } } - } else if event.type_() == atom!("keypress") && !event.DefaultPrevented() && - (self.input_type.get() == InputType::InputText || - self.input_type.get() == InputType::InputPassword) { - if event.IsTrusted() { - let window = window_from_node(self); - let _ = window.user_interaction_task_source() - .queue_event(&self.upcast(), - atom!("input"), - EventBubbles::Bubbles, - EventCancelable::NotCancelable, - &window); + } + } 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); + match action { + TriggerDefaultAction => { + self.implicit_submission(); + }, + DispatchInput => { + self.value_dirty.set(true); + self.update_placeholder_shown_state(); + self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + event.mark_as_handled(); + }, + RedrawSelection => { + self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); + event.mark_as_handled(); + }, + Nothing => (), + } + } + } else if event.type_() == atom!("keypress") && + !event.DefaultPrevented() && + self.input_type().is_textual_or_password() + { + if event.IsTrusted() { + let window = window_from_node(self); + let _ = window + .task_manager() + .user_interaction_task_source() + .queue_event( + &self.upcast(), + atom!("input"), + EventBubbles::Bubbles, + EventCancelable::NotCancelable, + &window, + ); + } + } else if (event.type_() == atom!("compositionstart") || + event.type_() == atom!("compositionupdate") || + event.type_() == atom!("compositionend")) && + self.input_type().is_textual_or_password() + { + // TODO: Update DOM on start and continue + // and generally do proper CompositionEvent handling. + if let Some(compositionevent) = event.downcast::<CompositionEvent>() { + if event.type_() == atom!("compositionend") { + let _ = self + .textinput + .borrow_mut() + .handle_compositionend(compositionevent); + self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); } + event.mark_as_handled(); } + } + } + + // https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext + fn cloning_steps( + &self, + copy: &Node, + maybe_doc: Option<&Document>, + clone_children: CloneChildrenFlag, + ) { + if let Some(ref s) = self.super_type() { + s.cloning_steps(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::IN_CHECKED_STATE, self.Checked()); + elem.textinput + .borrow_mut() + .set_content(self.textinput.borrow().get_content()); } } impl FormControl for HTMLInputElement { - fn form_owner(&self) -> Option<Root<HTMLFormElement>> { + fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> { self.form_owner.get() } @@ -1183,13 +2647,73 @@ impl FormControl for HTMLInputElement { } impl Validatable for HTMLInputElement { + fn as_element(&self) -> &Element { + self.upcast() + } + + fn validity_state(&self) -> DomRoot<ValidityState> { + self.validity_state + .or_init(|| ValidityState::new(&window_from_node(self), self.upcast())) + } + fn is_instance_validatable(&self) -> bool { - // https://html.spec.whatwg.org/multipage/#candidate-for-constraint-validation - true + // 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() && self.does_readonly_apply()) || + is_barred_by_datalist_ancestor(self.upcast())) + }, + } } - fn validate(&self, _validate_flags: ValidationFlags) -> bool { - // call stub methods defined in validityState.rs file here according to the flags set in validate_flags - true + + fn perform_validation(&self, validate_flags: ValidationFlags) -> ValidationFlags { + let mut failed_flags = ValidationFlags::empty(); + let value = self.Value(); + + if validate_flags.contains(ValidationFlags::VALUE_MISSING) { + if self.suffers_from_being_missing(&value) { + failed_flags.insert(ValidationFlags::VALUE_MISSING); + } + } + + if validate_flags.contains(ValidationFlags::TYPE_MISMATCH) { + if self.suffers_from_type_mismatch(&value) { + failed_flags.insert(ValidationFlags::TYPE_MISMATCH); + } + } + + if validate_flags.contains(ValidationFlags::PATTERN_MISMATCH) { + if self.suffers_from_pattern_mismatch(&value) { + failed_flags.insert(ValidationFlags::PATTERN_MISMATCH); + } + } + + if validate_flags.contains(ValidationFlags::BAD_INPUT) { + if 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 } } @@ -1199,202 +2723,134 @@ impl Activatable for HTMLInputElement { } fn is_instance_activatable(&self) -> bool { - match self.input_type.get() { + match self.input_type() { // https://html.spec.whatwg.org/multipage/#submit-button-state-%28type=submit%29:activation-behaviour-2 // https://html.spec.whatwg.org/multipage/#reset-button-state-%28type=reset%29:activation-behaviour-2 // https://html.spec.whatwg.org/multipage/#checkbox-state-%28type=checkbox%29:activation-behaviour-2 // https://html.spec.whatwg.org/multipage/#radio-button-state-%28type=radio%29:activation-behaviour-2 - InputType::InputSubmit | InputType::InputReset | InputType::InputFile - | InputType::InputCheckbox | InputType::InputRadio => self.is_mutable(), - _ => false + InputType::Submit | InputType::Reset | InputType::File => self.is_mutable(), + InputType::Checkbox | InputType::Radio => true, + _ => false, } } - // https://html.spec.whatwg.org/multipage/#run-pre-click-activation-steps - #[allow(unsafe_code)] - fn pre_click_activation(&self) { - let mut cache = self.activation_state.borrow_mut(); - let ty = self.input_type.get(); - cache.old_type = ty; - cache.was_mutable = self.is_mutable(); - if cache.was_mutable { - match ty { - // https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior - // InputType::InputSubmit => (), // No behavior defined - // https://html.spec.whatwg.org/multipage/#reset-button-state-(type=reset):activation-behavior - // InputType::InputSubmit => (), // No behavior defined - InputType::InputCheckbox => { - /* - https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):pre-click-activation-steps - cache current values of `checked` and `indeterminate` - we may need to restore them later - */ - cache.indeterminate = self.Indeterminate(); - cache.checked = self.Checked(); - cache.checked_changed = self.checked_changed.get(); - self.SetIndeterminate(false); - self.SetChecked(!cache.checked); - }, - // https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):pre-click-activation-steps - InputType::InputRadio => { - //TODO: if not in document, use root ancestor instead of document - let owner = self.form_owner(); - let doc = document_from_node(self); - let doc_node = doc.upcast::<Node>(); - let group = self.radio_group_name();; - - // Safe since we only manipulate the DOM tree after finding an element - let checked_member = doc_node.query_selector_iter(DOMString::from("input[type=radio]")) - .unwrap() - .filter_map(Root::downcast::<HTMLInputElement>) - .find(|r| { - in_same_group(&*r, owner.r(), group.as_ref()) && - r.Checked() - }); - cache.checked_radio = checked_member.r().map(JS::from_ref); - cache.checked_changed = self.checked_changed.get(); - self.SetChecked(true); - } - _ => () - } + // https://dom.spec.whatwg.org/#eventtarget-legacy-pre-activation-behavior + fn legacy_pre_activation_behavior(&self) -> Option<InputActivationState> { + let ty = self.input_type(); + match ty { + InputType::Checkbox => { + let was_checked = self.Checked(); + let was_indeterminate = self.Indeterminate(); + self.SetIndeterminate(false); + self.SetChecked(!was_checked); + return Some(InputActivationState { + checked: was_checked, + indeterminate: was_indeterminate, + checked_radio: None, + old_type: InputType::Checkbox, + }); + }, + InputType::Radio => { + let checked_member = + radio_group_iter(self, self.radio_group_name().as_ref()).find(|r| r.Checked()); + let was_checked = self.Checked(); + self.SetChecked(true); + return Some(InputActivationState { + checked: was_checked, + indeterminate: false, + checked_radio: checked_member.as_deref().map(DomRoot::from_ref), + old_type: InputType::Radio, + }); + }, + _ => (), } + return None; } - // https://html.spec.whatwg.org/multipage/#run-canceled-activation-steps - fn canceled_activation(&self) { - let cache = self.activation_state.borrow(); - let ty = self.input_type.get(); - if cache.old_type != ty { - // Type changed, abandon ship - // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414 - return; - } - match ty { - // https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior - // InputType::InputSubmit => (), // No behavior defined - // https://html.spec.whatwg.org/multipage/#reset-button-state-(type=reset):activation-behavior - // InputType::InputReset => (), // No behavior defined - // https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):canceled-activation-steps - InputType::InputCheckbox => { - // We want to restore state only if the element had been changed in the first place - if cache.was_mutable { - self.SetIndeterminate(cache.indeterminate); - self.SetChecked(cache.checked); - self.checked_changed.set(cache.checked_changed); + // https://dom.spec.whatwg.org/#eventtarget-legacy-canceled-activation-behavior + fn legacy_canceled_activation_behavior(&self, cache: Option<InputActivationState>) { + // Step 1 + let ty = self.input_type(); + let cache = match cache { + Some(cache) => { + if cache.old_type != ty { + // Type changed, abandon ship + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414 + return; } + cache }, - // https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):canceled-activation-steps - InputType::InputRadio => { - // We want to restore state only if the element had been changed in the first place - if cache.was_mutable { - match cache.checked_radio.r() { - Some(o) => { - // Avoiding iterating through the whole tree here, instead - // we can check if the conditions for radio group siblings apply - if in_same_group(&o, self.form_owner().r(), self.radio_group_name().as_ref()) { - o.SetChecked(true); - } else { - self.SetChecked(false); - } - }, - None => self.SetChecked(false) - }; - self.checked_changed.set(cache.checked_changed); + None => { + return; + }, + }; + + match ty { + // Step 2 + InputType::Checkbox => { + self.SetIndeterminate(cache.indeterminate); + self.SetChecked(cache.checked); + }, + // Step 3 + InputType::Radio => { + if let Some(ref o) = cache.checked_radio { + let tree_root = self + .upcast::<Node>() + .GetRootNode(&GetRootNodeOptions::empty()); + // Avoiding iterating through the whole tree here, instead + // we can check if the conditions for radio group siblings apply + if in_same_group( + &o, + self.form_owner().as_deref(), + self.radio_group_name().as_ref(), + Some(&*tree_root), + ) { + o.SetChecked(true); + } else { + self.SetChecked(false); + } + } else { + self.SetChecked(false); } - } - _ => () + }, + _ => (), } } // https://html.spec.whatwg.org/multipage/#run-post-click-activation-steps fn activation_behavior(&self, _event: &Event, _target: &EventTarget) { - let ty = self.input_type.get(); - if self.activation_state.borrow().old_type != ty || !self.is_mutable() { - // Type changed or input is immutable, abandon ship - // https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414 - return; - } + let ty = self.input_type(); match ty { - InputType::InputSubmit => { + InputType::Submit => { // https://html.spec.whatwg.org/multipage/#submit-button-state-(type=submit):activation-behavior // FIXME (Manishearth): support document owners (needs ability to get parent browsing context) // Check if document owner is fully active self.form_owner().map(|o| { - o.submit(SubmittedFrom::NotFromForm, - FormSubmitter::InputElement(self.clone())) + o.submit( + SubmittedFrom::NotFromForm, + FormSubmitter::InputElement(self), + ) }); }, - InputType::InputReset => { + InputType::Reset => { // https://html.spec.whatwg.org/multipage/#reset-button-state-(type=reset):activation-behavior // FIXME (Manishearth): support document owners (needs ability to get parent browsing context) // Check if document owner is fully active - self.form_owner().map(|o| { - o.reset(ResetFrom::NotFromForm) - }); + self.form_owner().map(|o| o.reset(ResetFrom::NotFromForm)); }, - InputType::InputCheckbox | InputType::InputRadio => { + InputType::Checkbox | InputType::Radio => { // https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):activation-behavior // https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):activation-behavior // Check if document owner is fully active + if !self.upcast::<Node>().is_connected() { + return (); + } let target = self.upcast::<EventTarget>(); target.fire_bubbling_event(atom!("input")); target.fire_bubbling_event(atom!("change")); }, - InputType::InputFile => self.select_files(None), - _ => () - } - } - - // https://html.spec.whatwg.org/multipage/#implicit-submission - #[allow(unsafe_code)] - fn implicit_submission(&self, ctrl_key: bool, shift_key: bool, alt_key: bool, meta_key: bool) { - let doc = document_from_node(self); - 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; - submit_button = node.query_selector_iter(DOMString::from("input[type=submit]")).unwrap() - .filter_map(Root::downcast::<HTMLInputElement>) - .find(|r| r.form_owner() == owner); - match submit_button { - Some(ref button) => { - if button.is_instance_activatable() { - synthetic_click_activation(button.as_element(), - ctrl_key, - shift_key, - alt_key, - meta_key, - ActivationSource::NotFromClick) - } - } - None => { - let inputs = node.query_selector_iter(DOMString::from("input")).unwrap() - .filter_map(Root::downcast::<HTMLInputElement>) - .filter(|input| { - input.form_owner() == owner && match input.type_() { - atom!("text") | atom!("search") | atom!("url") | atom!("tel") | - atom!("email") | atom!("password") | atom!("datetime") | - atom!("date") | atom!("month") | atom!("week") | atom!("time") | - atom!("datetime-local") | atom!("number") - => true, - _ => false - } - }); - - if inputs.skip(1).next().is_some() { - // lazily test for > 1 submission-blocking inputs - return; - } - form.submit(SubmittedFrom::NotFromForm, - FormSubmitter::FormElement(&form)); - } + InputType::File => self.select_files(None), + _ => (), } } } @@ -1416,3 +2872,86 @@ fn filter_from_accept(s: &DOMString) -> Vec<FilterPattern> { filter } + +fn round_halves_positive(n: f64) -> f64 { + // WHATWG specs about input steps say to round to the nearest step, + // rounding halves always to positive infinity. + // This differs from Rust's .round() in the case of -X.5. + if n.fract() == -0.5 { + n.ceil() + } else { + n.round() + } +} + +fn milliseconds_to_datetime(value: f64) -> Result<NaiveDateTime, ()> { + let seconds = (value / 1000.0).floor(); + let milliseconds = value - (seconds * 1000.0); + let nanoseconds = milliseconds * 1e6; + NaiveDateTime::from_timestamp_opt(seconds as i64, nanoseconds as u32).ok_or(()) +} + +// 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) -> bool { + // First check if pattern compiles... + if new_js_regex(cx, pattern_str, out_regex) { + // ...and if it does make pattern that matches only the entirety of string + let pattern_str = format!("^(?:{})$", pattern_str); + new_js_regex(cx, &pattern_str, out_regex) + } else { + false + } +} + +#[allow(unsafe_code)] +fn new_js_regex(cx: SafeJSContext, pattern: &str, mut out_regex: MutableHandleObject) -> bool { + let pattern: Vec<u16> = pattern.encode_utf16().collect(); + unsafe { + out_regex.set(NewUCRegExpObject( + *cx, + pattern.as_ptr(), + pattern.len(), + RegExpFlags { + flags_: RegExpFlag_Unicode, + }, + )); + if out_regex.is_null() { + JS_ClearPendingException(*cx); + return false; + } + } + true +} + +#[allow(unsafe_code)] +fn matches_js_regex(cx: SafeJSContext, regex_obj: HandleObject, value: &str) -> 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, + &mut rval.handle_mut(), + ); + + if ok { + Ok(!rval.is_null()) + } else { + JS_ClearPendingException(*cx); + Err(()) + } + } +} |