diff options
-rw-r--r-- | components/script/dom/htmlinputelement.rs | 34 | ||||
-rw-r--r-- | components/script/dom/htmltextareaelement.rs | 4 | ||||
-rw-r--r-- | components/script/dom/webidls/HTMLInputElement.webidl | 2 | ||||
-rw-r--r-- | components/script/textinput.rs | 52 | ||||
-rw-r--r-- | components/style/attr.rs | 10 | ||||
-rw-r--r-- | tests/unit/script/textinput.rs | 107 |
6 files changed, 196 insertions, 13 deletions
diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index 524f70e1288..554030c288a 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -8,8 +8,8 @@ use dom::attr::{Attr, AttrValue}; use dom::bindings::cell::DOMRefCell; use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods; use dom::bindings::codegen::Bindings::EventBinding::EventMethods; -use dom::bindings::codegen::Bindings::HTMLInputElementBinding; use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; +use dom::bindings::codegen::Bindings::HTMLInputElementBinding; use dom::bindings::codegen::Bindings::KeyboardEventBinding::KeyboardEventMethods; use dom::bindings::global::GlobalRef; use dom::bindings::inheritance::Castable; @@ -32,6 +32,7 @@ use msg::constellation_msg::ScriptMsg as ConstellationMsg; use selectors::states::*; use std::borrow::ToOwned; use std::cell::Cell; +use std::i32; use string_cache::Atom; use textinput::KeyReaction::{DispatchInput, Nothing, RedrawSelection, TriggerDefaultAction}; use textinput::Lines::Single; @@ -116,7 +117,7 @@ impl HTMLInputElement { checked_changed: Cell::new(false), value_changed: Cell::new(false), size: Cell::new(DEFAULT_INPUT_SIZE), - textinput: DOMRefCell::new(TextInput::new(Single, DOMString::new(), chan)), + textinput: DOMRefCell::new(TextInput::new(Single, DOMString::new(), chan, None)), activation_state: DOMRefCell::new(InputActivationState::new()) } } @@ -337,6 +338,21 @@ impl HTMLInputElementMethods for HTMLInputElement { // https://html.spec.whatwg.org/multipage/#dom-input-formtarget make_setter!(SetFormTarget, "formtarget"); + // https://html.spec.whatwg.org/multipage/#dom-input-maxlength + fn MaxLength(&self) -> i32 { + match self.textinput.borrow().max_length { + Some(max_length) => max_length as i32, + None => i32::MAX + } + } + + // https://html.spec.whatwg.org/multipage/#dom-input-maxlength + fn SetMaxLength(&self, max_length: i32) { + if max_length > 0 { + self.textinput.borrow_mut().max_length = Some(max_length as usize) + } + } + // https://html.spec.whatwg.org/multipage/#dom-input-indeterminate fn Indeterminate(&self) -> bool { self.upcast::<Element>().get_state().contains(IN_INDETERMINATE_STATE) @@ -511,6 +527,7 @@ impl VirtualMethods for HTMLInputElement { fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) { self.super_type().unwrap().attribute_mutated(attr, mutation); + match attr.local_name() { &atom!("disabled") => { let disabled_state = match mutation { @@ -581,6 +598,18 @@ impl VirtualMethods for HTMLInputElement { self.radio_group_updated( mutation.new_value(attr).as_ref().map(|name| name.as_atom())); }, + &atom!("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::UInt"), + } + } &atom!("placeholder") => { // FIXME(ajeffrey): Should we do in-place mutation of the placeholder? let mut placeholder = self.placeholder.borrow_mut(); @@ -599,6 +628,7 @@ impl VirtualMethods for HTMLInputElement { &atom!("name") => AttrValue::from_atomic(value), &atom!("size") => AttrValue::from_limited_u32(value, DEFAULT_INPUT_SIZE), &atom!("type") => AttrValue::from_atomic(value), + &atom!("maxlength") => AttrValue::from_i32(value, i32::MAX), _ => self.super_type().unwrap().parse_plain_attribute(name, value), } } diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index d4e1729f662..94057d4eee0 100644 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -30,6 +30,7 @@ use script_task::ScriptTaskEventCategory::InputEvent; use script_task::{CommonScriptMsg, Runnable}; use selectors::states::*; use std::cell::Cell; +use std::i32; use string_cache::Atom; use textinput::{KeyReaction, Lines, TextInput}; use util::str::DOMString; @@ -89,6 +90,7 @@ impl<'a> RawLayoutHTMLTextAreaElementHelpers for &'a HTMLTextAreaElement { static DEFAULT_COLS: u32 = 20; static DEFAULT_ROWS: u32 = 2; +static DEFAULT_MAX_LENGTH: i32 = i32::MAX; impl HTMLTextAreaElement { fn new_inherited(localName: DOMString, @@ -99,7 +101,7 @@ impl HTMLTextAreaElement { htmlelement: HTMLElement::new_inherited_with_state(IN_ENABLED_STATE, localName, prefix, document), - textinput: DOMRefCell::new(TextInput::new(Lines::Multiple, DOMString::new(), chan)), + textinput: DOMRefCell::new(TextInput::new(Lines::Multiple, DOMString::new(), chan, None)), cols: Cell::new(DEFAULT_COLS), rows: Cell::new(DEFAULT_ROWS), value_changed: Cell::new(false), diff --git a/components/script/dom/webidls/HTMLInputElement.webidl b/components/script/dom/webidls/HTMLInputElement.webidl index a5472818afe..b2b7992f5e0 100644 --- a/components/script/dom/webidls/HTMLInputElement.webidl +++ b/components/script/dom/webidls/HTMLInputElement.webidl @@ -25,7 +25,7 @@ interface HTMLInputElement : HTMLElement { // attribute DOMString inputMode; //readonly attribute HTMLElement? list; // attribute DOMString max; - // attribute long maxLength; + attribute long maxLength; // attribute DOMString min; // attribute long minLength; // attribute boolean multiple; diff --git a/components/script/textinput.rs b/components/script/textinput.rs index aa970e27464..b5bd6421127 100644 --- a/components/script/textinput.rs +++ b/components/script/textinput.rs @@ -11,6 +11,7 @@ use msg::constellation_msg::{Key, KeyModifiers}; use std::borrow::ToOwned; use std::cmp::{max, min}; use std::default::Default; +use std::usize; use util::mem::HeapSizeOf; use util::str::DOMString; @@ -41,6 +42,7 @@ pub struct TextInput<T: ClipboardProvider> { multiline: bool, #[ignore_heap_size_of = "Can't easily measure this generic type"] clipboard_provider: T, + pub max_length: Option<usize> } /// Resulting action to be taken by the owner of a text input that is handling an event. @@ -107,13 +109,14 @@ fn is_printable_key(key: Key) -> bool { impl<T: ClipboardProvider> TextInput<T> { /// Instantiate a new text input control - pub fn new(lines: Lines, initial: DOMString, clipboard_provider: T) -> TextInput<T> { + pub fn new(lines: Lines, initial: DOMString, clipboard_provider: T, max_length: Option<usize>) -> TextInput<T> { let mut i = TextInput { lines: vec!(), edit_point: Default::default(), selection_begin: None, multiline: lines == Lines::Multiple, - clipboard_provider: clipboard_provider + clipboard_provider: clipboard_provider, + max_length: max_length }; i.set_content(initial); i @@ -133,7 +136,7 @@ impl<T: ClipboardProvider> TextInput<T> { } /// Insert a string at the current editing point - fn insert_string<S: Into<String>>(&mut self, s: S) { + pub fn insert_string<S: Into<String>>(&mut self, s: S) { if self.selection_begin.is_none() { self.selection_begin = Some(self.edit_point); } @@ -170,8 +173,40 @@ impl<T: ClipboardProvider> TextInput<T> { }) } + fn selection_len(&self) -> usize { + if let Some((begin, end)) = self.get_sorted_selection() { + let prefix = &self.lines[begin.line][0..begin.index]; + let suffix = &self.lines[end.line][end.index..]; + let lines_prefix = &self.lines[..begin.line]; + let lines_suffix = &self.lines[end.line + 1..]; + + self.len() - (prefix.chars().count() + + suffix.chars().count() + + lines_prefix.iter().fold(0, |m, i| m + i.chars().count() + 1) + + lines_suffix.iter().fold(0, |m, i| m + i.chars().count() + 1)) + } else { + 0 + } + } + pub fn replace_selection(&mut self, insert: DOMString) { if let Some((begin, end)) = self.get_sorted_selection() { + let allowed_to_insert_count = if let Some(max_length) = self.max_length { + let len_after_selection_replaced = self.len() - self.selection_len(); + if len_after_selection_replaced > max_length { + // If, after deleting the selection, the len is still greater than the max + // length, then don't delete/insert anything + return + } + + max_length - len_after_selection_replaced + } else { + usize::MAX + }; + + let last_char_to_insert = min(allowed_to_insert_count, insert.chars().count()); + let chars_to_insert = (&insert[0 .. last_char_to_insert]).to_owned(); + self.clear_selection(); let new_lines = { @@ -181,13 +216,14 @@ impl<T: ClipboardProvider> TextInput<T> { let lines_suffix = &self.lines[end.line + 1..]; let mut insert_lines = if self.multiline { - insert.split('\n').map(DOMString::from).collect() + chars_to_insert.split('\n').map(|s| DOMString::from(s.to_owned())).collect() } else { - vec!(insert) + vec!(DOMString::from(chars_to_insert)) }; // FIXME(ajeffrey): effecient append for DOMStrings let mut new_line = prefix.to_owned(); + new_line.push_str(&insert_lines[0]); insert_lines[0] = DOMString::from(new_line); @@ -434,6 +470,12 @@ impl<T: ClipboardProvider> TextInput<T> { } } + pub fn len(&self) -> usize { + self.lines.iter().fold(0, |m, l| { + m + l.len() + 1 + }) - 1 + } + /// Get the current contents of the text input. Multiple lines are joined by \n. pub fn get_content(&self) -> DOMString { let mut content = "".to_owned(); diff --git a/components/style/attr.rs b/components/style/attr.rs index 625eeee3a5c..e50071e3b3f 100644 --- a/components/style/attr.rs +++ b/components/style/attr.rs @@ -5,7 +5,7 @@ use cssparser::RGBA; use std::ops::Deref; use string_cache::{Atom, Namespace}; -use util::str::{DOMString, LengthOrPercentageOrAuto, parse_unsigned_integer, parse_legacy_color, parse_length}; +use util::str::{DOMString, LengthOrPercentageOrAuto, parse_integer, parse_unsigned_integer, parse_legacy_color, parse_length}; use util::str::{split_html_space_chars, str_join}; use values::specified::{Length}; @@ -17,6 +17,7 @@ pub enum AttrValue { String(DOMString), TokenList(DOMString, Vec<Atom>), UInt(DOMString, u32), + Int(DOMString, i32), Atom(Atom), Length(DOMString, Option<Length>), Color(DOMString, Option<RGBA>), @@ -52,6 +53,12 @@ impl AttrValue { AttrValue::UInt(string, result) } + // https://html.spec.whatwg.org/multipage/infrastructure.html#limited-to-only-non-negative-numbers + pub fn from_i32(string: DOMString, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + AttrValue::Int(string, result) + } + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers-greater-than-zero pub fn from_limited_u32(string: DOMString, default: u32) -> AttrValue { let result = parse_unsigned_integer(string.chars()).unwrap_or(default); @@ -165,6 +172,7 @@ impl Deref for AttrValue { AttrValue::UInt(ref value, _) | AttrValue::Length(ref value, _) | AttrValue::Color(ref value, _) | + AttrValue::Int(ref value, _) | AttrValue::Dimension(ref value, _) => &value, AttrValue::Atom(ref value) => &value, } diff --git a/tests/unit/script/textinput.rs b/tests/unit/script/textinput.rs index 3d62f1f32ed..25ff3e827a0 100644 --- a/tests/unit/script/textinput.rs +++ b/tests/unit/script/textinput.rs @@ -13,11 +13,111 @@ use msg::constellation_msg::CONTROL; use msg::constellation_msg::SUPER; use msg::constellation_msg::{Key, KeyModifiers}; use script::clipboard_provider::DummyClipboardContext; -use script::textinput::{TextInput, Selection, Lines, Direction}; +use script::textinput::{TextInput, TextPoint, Selection, Lines, Direction}; use util::str::DOMString; fn text_input(lines: Lines, s: &str) -> TextInput<DummyClipboardContext> { - TextInput::new(lines, DOMString::from(s), DummyClipboardContext::new("")) + TextInput::new(lines, DOMString::from(s), DummyClipboardContext::new(""), None) +} + +#[test] +fn test_textinput_when_inserting_multiple_lines_over_a_selection_respects_max_length() { + let mut textinput = TextInput::new( + Lines::Multiple, + DOMString::from("hello\nworld"), + DummyClipboardContext::new(""), + Some(17) + ); + + textinput.edit_point = TextPoint { line: 0, index: 1 }; + textinput.adjust_horizontal(3, Selection::Selected); + textinput.adjust_vertical(1, Selection::Selected); + + // Selection is now "hello\n + // ------ + // world" + // ---- + + textinput.insert_string("cruel\nterrible\nbad".to_string()); + + assert_eq!(textinput.get_content(), "hcruel\nterrible\nd"); +} + +#[test] +fn test_textinput_when_inserting_multiple_lines_still_respects_max_length() { + let mut textinput = TextInput::new( + Lines::Multiple, + DOMString::from("hello\nworld"), + DummyClipboardContext::new(""), + Some(17) + ); + + textinput.edit_point = TextPoint { line: 1, index: 0 }; + + textinput.insert_string("cruel\nterrible".to_string()); + + assert_eq!(textinput.get_content(), "hello\ncruel\nworld"); +} + +#[test] +fn test_textinput_when_content_is_already_longer_than_max_length_and_theres_no_selection_dont_insert_anything() { + let mut textinput = TextInput::new( + Lines::Single, + DOMString::from("abc"), + DummyClipboardContext::new(""), + Some(1) + ); + + textinput.insert_char('a'); + + assert_eq!(textinput.get_content(), "abc"); +} + +#[test] +fn test_multi_line_textinput_with_maxlength_doesnt_allow_appending_characters_when_input_spans_lines() { + let mut textinput = TextInput::new( + Lines::Multiple, + DOMString::from("abc\nd"), + DummyClipboardContext::new(""), + Some(5) + ); + + textinput.insert_char('a'); + + assert_eq!(textinput.get_content(), "abc\nd"); +} + +#[test] +fn test_single_line_textinput_with_max_length_doesnt_allow_appending_characters_when_replacing_a_selection() { + let mut textinput = TextInput::new( + Lines::Single, + DOMString::from("abcde"), + DummyClipboardContext::new(""), + Some(5) + ); + + textinput.edit_point = TextPoint { line: 0, index: 1 }; + textinput.adjust_horizontal(3, Selection::Selected); + + // Selection is now "abcde" + // --- + + textinput.replace_selection(DOMString::from("too long")); + + assert_eq!(textinput.get_content(), "atooe"); +} + +#[test] +fn test_single_line_textinput_with_max_length_doesnt_allow_appending_characters_after_max_length_is_reached() { + let mut textinput = TextInput::new( + Lines::Single, + DOMString::from("a"), + DummyClipboardContext::new(""), + Some(1) + ); + + textinput.insert_char('b'); + assert_eq!(textinput.get_content(), "a"); } #[test] @@ -199,7 +299,8 @@ fn test_clipboard_paste() { let mut textinput = TextInput::new(Lines::Single, DOMString::from("defg"), - DummyClipboardContext::new("abc")); + DummyClipboardContext::new("abc"), + None); assert_eq!(textinput.get_content(), "defg"); assert_eq!(textinput.edit_point.index, 0); textinput.handle_keydown_aux(Key::V, MODIFIERS); |