Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for input element's maxlength attr #7323

Merged
merged 14 commits into from Dec 3, 2015
Next

Adds support for input element's maxlength attr

  • Loading branch information
samfoo committed Dec 3, 2015
commit d26c555e2a2fe0e10b9237e1ccf48d96665828c7
@@ -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),
}
}
@@ -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),
@@ -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;
@@ -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();
@@ -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,
}
@@ -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);
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.