aboutsummaryrefslogtreecommitdiffstats
path: root/components/script/dom/htmlinputelement.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/script/dom/htmlinputelement.rs')
-rwxr-xr-xcomponents/script/dom/htmlinputelement.rs2889
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(())
+ }
+ }
+}