servo: Merge #4152 - Implements multi line text input for TextArea (from mttr:textview); r=jdm
authorMatthew Rasmus <mattr@zzntd.com>
Fri, 05 Dec 2014 16:25:07 -0700
changeset 335379 adac5e81c572b1910677ddbb0a2fb709d79d5c5d
parent 335378 fd3a8678840b00c68ab27905c331f3febcd5d77c
child 335380 8685d34840de05f9703a7feb03fc23714ccddd5a
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdm
servo: Merge #4152 - Implements multi line text input for TextArea (from mttr:textview); r=jdm Fixes #3918 Can be tested in `tests/html/textarea.html`. Also implemented some content reflecting IDL attributes for HTMLTextAreaElement while I was in there. There are some major problems with TextInput when Multiple is enabled that I haven't addressed here, but I'm prepared to open up a follow-up issue. Source-Repo: https://github.com/servo/servo Source-Revision: a369dcfa01f5ad7634469f3a3b652d7f650129a0
servo/components/layout/construct.rs
servo/components/layout/lib.rs
servo/components/layout/wrapper.rs
servo/components/script/dom/htmltextareaelement.rs
servo/components/script/dom/webidls/HTMLTextAreaElement.webidl
servo/resources/servo.css
servo/tests/html/textarea.html
--- a/servo/components/layout/construct.rs
+++ b/servo/components/layout/construct.rs
@@ -44,16 +44,17 @@ use wrapper::{PostorderNodeMutTraversal,
 use wrapper::{Before, After, Normal};
 
 use gfx::display_list::OpaqueNode;
 use script::dom::element::{HTMLIFrameElementTypeId, HTMLImageElementTypeId};
 use script::dom::element::{HTMLObjectElementTypeId, HTMLInputElementTypeId};
 use script::dom::element::{HTMLTableColElementTypeId, HTMLTableDataCellElementTypeId};
 use script::dom::element::{HTMLTableElementTypeId, HTMLTableHeaderCellElementTypeId};
 use script::dom::element::{HTMLTableRowElementTypeId, HTMLTableSectionElementTypeId};
+use script::dom::element::HTMLTextAreaElementTypeId;
 use script::dom::node::{CommentNodeTypeId, DoctypeNodeTypeId, DocumentFragmentNodeTypeId};
 use script::dom::node::{DocumentNodeTypeId, ElementNodeTypeId, ProcessingInstructionNodeTypeId};
 use script::dom::node::{TextNodeTypeId};
 use script::dom::htmlobjectelement::is_image_data;
 use servo_util::opts;
 use std::collections::DList;
 use std::mem;
 use std::sync::atomic::Relaxed;
@@ -482,17 +483,26 @@ impl<'a> FlowConstructor<'a> {
         // Gather up fragments for the inline flows we might need to create.
         let mut inline_fragment_accumulator = InlineFragmentsAccumulator::new();
         let mut consecutive_siblings = vec!();
         let mut first_fragment = true;
 
         // Special case: If this is generated content, then we need to initialize the accumulator
         // with the fragment corresponding to that content.
         if node.get_pseudo_element_type() != Normal ||
-           node.type_id() == Some(ElementNodeTypeId(HTMLInputElementTypeId)) {
+           node.type_id() == Some(ElementNodeTypeId(HTMLInputElementTypeId)) ||
+           node.type_id() == Some(ElementNodeTypeId(HTMLTextAreaElementTypeId)) {
+            // A TextArea's text contents are displayed through the input text
+            // box, so don't construct them.
+            // TODO Maybe this belongs somewhere else?
+            if node.type_id() == Some(ElementNodeTypeId(HTMLTextAreaElementTypeId)) {
+                for kid in node.children() {
+                    kid.set_flow_construction_result(NoConstructionResult)
+                }
+            }
             let fragment_info = UnscannedTextFragment(UnscannedTextFragmentInfo::new(node));
             let fragment = Fragment::new_from_specific_info(node, fragment_info);
             inline_fragment_accumulator.fragments.push_back(fragment);
             first_fragment = false;
         }
 
         // List of absolute descendants, in tree order.
         let mut abs_descendants = Descendants::new();
--- a/servo/components/layout/lib.rs
+++ b/servo/components/layout/lib.rs
@@ -1,16 +1,16 @@
 /* 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/. */
 
 #![comment = "The Servo Parallel Browser Project"]
 #![license = "MPL"]
 
-#![feature(globs, macro_rules, phase, thread_local, unsafe_destructor)]
+#![feature(globs, macro_rules, phase, thread_local, unsafe_destructor, if_let)]
 
 #![deny(unused_imports)]
 #![deny(unused_variables)]
 #![allow(unrooted_must_root)]
 
 #[phase(plugin, link)]
 extern crate log;
 
--- a/servo/components/layout/wrapper.rs
+++ b/servo/components/layout/wrapper.rs
@@ -34,23 +34,24 @@ use context::SharedLayoutContext;
 use css::node_style::StyledNode;
 use incremental::RestyleDamage;
 use util::{LayoutDataAccess, LayoutDataFlags, LayoutDataWrapper, OpaqueNodeMethods};
 use util::{PrivateLayoutData};
 
 use gfx::display_list::OpaqueNode;
 use script::dom::bindings::codegen::InheritTypes::{ElementCast, HTMLIFrameElementCast};
 use script::dom::bindings::codegen::InheritTypes::{HTMLImageElementCast, HTMLInputElementCast};
-use script::dom::bindings::codegen::InheritTypes::{NodeCast, TextCast};
+use script::dom::bindings::codegen::InheritTypes::{HTMLTextAreaElementCast, NodeCast, TextCast};
 use script::dom::bindings::js::JS;
 use script::dom::element::{Element, HTMLAreaElementTypeId, HTMLAnchorElementTypeId};
 use script::dom::element::{HTMLLinkElementTypeId, LayoutElementHelpers, RawLayoutElementHelpers};
 use script::dom::htmliframeelement::HTMLIFrameElement;
 use script::dom::htmlimageelement::LayoutHTMLImageElementHelpers;
 use script::dom::htmlinputelement::LayoutHTMLInputElementHelpers;
+use script::dom::htmltextareaelement::LayoutHTMLTextAreaElementHelpers;
 use script::dom::node::{DocumentNodeTypeId, ElementNodeTypeId, Node, NodeTypeId};
 use script::dom::node::{LayoutNodeHelpers, RawLayoutNodeHelpers, SharedLayoutData};
 use script::dom::node::{HAS_CHANGED, IS_DIRTY, HAS_DIRTY_SIBLINGS, HAS_DIRTY_DESCENDANTS};
 use script::dom::text::Text;
 use script::layout_interface::LayoutChan;
 use servo_msg::constellation_msg::{PipelineId, SubpageId};
 use servo_util::str::{LengthOrPercentageOrAuto, is_whitespace};
 use std::kinds::marker::ContravariantLifetime;
@@ -178,23 +179,24 @@ impl<'ln> TLayoutNode for LayoutNode<'ln
     fn first_child(&self) -> Option<LayoutNode<'ln>> {
         unsafe {
             self.get_jsmanaged().first_child_ref().map(|node| self.new_with_this_lifetime(&node))
         }
     }
 
     fn text(&self) -> String {
         unsafe {
-            let text_opt: Option<JS<Text>> = TextCast::to_js(self.get_jsmanaged());
-            match text_opt {
-                Some(text) => (*text.unsafe_get()).characterdata().data_for_layout().to_string(),
-                None => match HTMLInputElementCast::to_js(self.get_jsmanaged()) {
-                    Some(input) => input.get_value_for_layout(),
-                    None => panic!("not text!")
-                }
+            if let Some(text) = TextCast::to_js(self.get_jsmanaged()) {
+                (*text.unsafe_get()).characterdata().data_for_layout().to_string()
+            } else if let Some(input) = HTMLInputElementCast::to_js(self.get_jsmanaged()) {
+                input.get_value_for_layout()
+            } else if let Some(area) = HTMLTextAreaElementCast::to_js(self.get_jsmanaged()) {
+                area.get_value_for_layout()
+            } else {
+                panic!("not text!")
             }
         }
     }
 }
 
 impl<'ln> LayoutNode<'ln> {
     /// Creates a new layout node, scoped to the given closure.
     pub unsafe fn with_layout_node<R>(node: JS<Node>, f: <'a> |LayoutNode<'a>| -> R) -> R {
--- a/servo/components/script/dom/htmltextareaelement.rs
+++ b/servo/components/script/dom/htmltextareaelement.rs
@@ -1,66 +1,175 @@
 /* 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 dom::attr::Attr;
+use dom::attr::{Attr, AttrValue};
 use dom::attr::AttrHelpers;
+use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::EventBinding::EventMethods;
 use dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding;
 use dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
-use dom::bindings::codegen::InheritTypes::{HTMLElementCast, NodeCast};
+use dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
+use dom::bindings::codegen::InheritTypes::{ElementCast, HTMLElementCast, NodeCast};
 use dom::bindings::codegen::InheritTypes::{HTMLTextAreaElementDerived, HTMLFieldSetElementDerived};
-use dom::bindings::js::{JSRef, Temporary};
+use dom::bindings::codegen::InheritTypes::{KeyboardEventCast, TextDerived};
+use dom::bindings::js::{JS, JSRef, Temporary};
 use dom::bindings::utils::{Reflectable, Reflector};
-use dom::document::Document;
+use dom::document::{Document, DocumentHelpers};
 use dom::element::{AttributeHandlers, HTMLTextAreaElementTypeId};
+use dom::event::Event;
 use dom::eventtarget::{EventTarget, NodeTargetTypeId};
 use dom::htmlelement::HTMLElement;
-use dom::node::{DisabledStateHelpers, Node, NodeHelpers, ElementNodeTypeId};
+use dom::keyboardevent::KeyboardEvent;
+use dom::node::{DisabledStateHelpers, Node, NodeHelpers, ElementNodeTypeId, document_from_node};
+use textinput::{Multiple, TextInput, TriggerDefaultAction, DispatchInput, Nothing};
 use dom::virtualmethods::VirtualMethods;
 
 use servo_util::str::DOMString;
 use string_cache::Atom;
 
+use std::cell::Cell;
+
 #[dom_struct]
 pub struct HTMLTextAreaElement {
     htmlelement: HTMLElement,
+    textinput: DOMRefCell<TextInput>,
+
+    // https://html.spec.whatwg.org/multipage/forms.html#concept-textarea-dirty
+    value_changed: Cell<bool>,
 }
 
 impl HTMLTextAreaElementDerived for EventTarget {
     fn is_htmltextareaelement(&self) -> bool {
         *self.type_id() == NodeTargetTypeId(ElementNodeTypeId(HTMLTextAreaElementTypeId))
     }
 }
 
+pub trait LayoutHTMLTextAreaElementHelpers {
+    unsafe fn get_value_for_layout(self) -> String;
+}
+
+impl LayoutHTMLTextAreaElementHelpers for JS<HTMLTextAreaElement> {
+    #[allow(unrooted_must_root)]
+    unsafe fn get_value_for_layout(self) -> String {
+        (*self.unsafe_get()).textinput.borrow_for_layout().get_content()
+    }
+}
+
+static DEFAULT_COLS: u32 = 20;
+static DEFAULT_ROWS: u32 = 2;
+
 impl HTMLTextAreaElement {
     fn new_inherited(localName: DOMString, prefix: Option<DOMString>, document: JSRef<Document>) -> HTMLTextAreaElement {
         HTMLTextAreaElement {
-            htmlelement: HTMLElement::new_inherited(HTMLTextAreaElementTypeId, localName, prefix, document)
+            htmlelement: HTMLElement::new_inherited(HTMLTextAreaElementTypeId, localName, prefix, document),
+            textinput: DOMRefCell::new(TextInput::new(Multiple, "".to_string())),
+            value_changed: Cell::new(false),
         }
     }
 
     #[allow(unrooted_must_root)]
     pub fn new(localName: DOMString, prefix: Option<DOMString>, document: JSRef<Document>) -> Temporary<HTMLTextAreaElement> {
         let element = HTMLTextAreaElement::new_inherited(localName, prefix, document);
         Node::reflect_node(box element, document, HTMLTextAreaElementBinding::Wrap)
     }
 }
 
 impl<'a> HTMLTextAreaElementMethods for JSRef<'a, HTMLTextAreaElement> {
+    // TODO A few of these attributes have default values and additional
+    // constraints
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-cols
+    make_uint_getter!(Cols)
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-cols
+    make_uint_setter!(SetCols, "cols")
+
     // http://www.whatwg.org/html/#dom-fe-disabled
     make_bool_getter!(Disabled)
 
     // http://www.whatwg.org/html/#dom-fe-disabled
     make_bool_setter!(SetDisabled, "disabled")
 
+    // https://html.spec.whatwg.org/multipage/forms.html#attr-fe-name
+    make_getter!(Name)
+
+    // https://html.spec.whatwg.org/multipage/forms.html#attr-fe-name
+    make_setter!(SetName, "name")
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-placeholder
+    make_getter!(Placeholder)
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-placeholder
+    make_setter!(SetPlaceholder, "placeholder")
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-required
+    make_bool_getter!(Required)
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-required
+    make_bool_setter!(SetRequired, "required")
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-rows
+    make_uint_getter!(Rows)
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-rows
+    make_uint_setter!(SetRows, "rows")
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-wrap
+    make_getter!(Wrap)
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-wrap
+    make_setter!(SetWrap, "wrap")
+
     // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-type
     fn Type(self) -> DOMString {
         "textarea".to_string()
     }
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-defaultvalue
+    fn DefaultValue(self) -> DOMString {
+        let node: JSRef<Node> = NodeCast::from_ref(self);
+        node.GetTextContent().unwrap()
+    }
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-defaultvalue
+    fn SetDefaultValue(self, value: DOMString) {
+        let node: JSRef<Node> = NodeCast::from_ref(self);
+        node.SetTextContent(Some(value));
+
+        // if the element's dirty value flag is false, then the element's
+        // raw value must be set to the value of the element's textContent IDL attribute
+        if !self.value_changed.get() {
+            self.SetValue(node.GetTextContent().unwrap());
+        }
+    }
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-value
+    fn Value(self) -> DOMString {
+        self.textinput.borrow().get_content()
+    }
+
+    // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-value
+    fn SetValue(self, value: DOMString) {
+        self.textinput.borrow_mut().set_content(value);
+        self.force_relayout();
+    }
+}
+
+trait PrivateHTMLTextAreaElementHelpers {
+    fn force_relayout(self);
+}
+
+impl<'a> PrivateHTMLTextAreaElementHelpers for JSRef<'a, HTMLTextAreaElement> {
+    fn force_relayout(self) {
+        let doc = document_from_node(self).root();
+        let node: JSRef<Node> = NodeCast::from_ref(self);
+        doc.content_changed(node)
+    }
 }
 
 impl<'a> VirtualMethods for JSRef<'a, HTMLTextAreaElement> {
     fn super_type<'a>(&'a self) -> Option<&'a VirtualMethods> {
         let htmlelement: &JSRef<HTMLElement> = HTMLElementCast::from_borrowed_ref(self);
         Some(htmlelement as &VirtualMethods)
     }
 
@@ -102,28 +211,78 @@ impl<'a> VirtualMethods for JSRef<'a, HT
             Some(ref s) => s.bind_to_tree(tree_in_doc),
             _ => (),
         }
 
         let node: JSRef<Node> = NodeCast::from_ref(*self);
         node.check_ancestors_disabled_state_for_form_control();
     }
 
+    fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
+        match name {
+            &atom!("cols") => AttrValue::from_u32(value, DEFAULT_COLS),
+            &atom!("rows") => AttrValue::from_u32(value, DEFAULT_ROWS),
+            _ => self.super_type().unwrap().parse_plain_attribute(name, value),
+        }
+    }
+
     fn unbind_from_tree(&self, tree_in_doc: bool) {
         match self.super_type() {
             Some(ref s) => s.unbind_from_tree(tree_in_doc),
             _ => (),
         }
 
         let node: JSRef<Node> = NodeCast::from_ref(*self);
         if node.ancestors().any(|ancestor| ancestor.is_htmlfieldsetelement()) {
             node.check_ancestors_disabled_state_for_form_control();
         } else {
             node.check_disabled_attribute();
         }
     }
+
+    fn child_inserted(&self, child: JSRef<Node>) {
+        match self.super_type() {
+            Some(s) => {
+                s.child_inserted(child);
+            }
+            _ => (),
+        }
+
+        if child.is_text() {
+            self.SetValue(self.DefaultValue());
+        }
+    }
+
+    // copied and modified from htmlinputelement.rs
+    fn handle_event(&self, event: JSRef<Event>) {
+        match self.super_type() {
+            Some(s) => {
+                s.handle_event(event);
+            }
+            _ => (),
+        }
+
+        if "click" == event.Type().as_slice() && !event.DefaultPrevented() {
+            //TODO: set the editing position for text inputs
+
+            let doc = document_from_node(*self).root();
+            doc.request_focus(ElementCast::from_ref(*self));
+        } else if "keydown" == event.Type().as_slice() && !event.DefaultPrevented() {
+            let keyevent: Option<JSRef<KeyboardEvent>> = KeyboardEventCast::to_ref(event);
+            keyevent.map(|event| {
+                match self.textinput.borrow_mut().handle_keydown(event) {
+                    TriggerDefaultAction => (),
+                    DispatchInput => {
+                        self.force_relayout();
+                        self.value_changed.set(true);
+                    }
+                    Nothing => (),
+                }
+            });
+        }
+    }
 }
 
 impl Reflectable for HTMLTextAreaElement {
     fn reflector<'a>(&'a self) -> &'a Reflector {
         self.htmlelement.reflector()
     }
 }
--- a/servo/components/script/dom/webidls/HTMLTextAreaElement.webidl
+++ b/servo/components/script/dom/webidls/HTMLTextAreaElement.webidl
@@ -2,33 +2,33 @@
 /* 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/. */
 
 // http://www.whatwg.org/html/#htmltextareaelement
 interface HTMLTextAreaElement : HTMLElement {
   //         attribute DOMString autocomplete;
   //         attribute boolean autofocus;
-  //         attribute unsigned long cols;
+           attribute unsigned long cols;
   //         attribute DOMString dirName;
            attribute boolean disabled;
   //readonly attribute HTMLFormElement? form;
   //         attribute DOMString inputMode;
   //         attribute long maxLength;
   //         attribute long minLength;
-  //         attribute DOMString name;
-  //         attribute DOMString placeholder;
+           attribute DOMString name;
+           attribute DOMString placeholder;
   //         attribute boolean readOnly;
-  //         attribute boolean required;
-  //         attribute unsigned long rows;
-  //         attribute DOMString wrap;
+           attribute boolean required;
+           attribute unsigned long rows;
+           attribute DOMString wrap;
 
   readonly attribute DOMString type;
-  //         attribute DOMString defaultValue;
-  //[TreatNullAs=EmptyString] attribute DOMString value;
+           attribute DOMString defaultValue;
+  [TreatNullAs=EmptyString] attribute DOMString value;
   //readonly attribute unsigned long textLength;
 
   //readonly attribute boolean willValidate;
   //readonly attribute ValidityState validity;
   //readonly attribute DOMString validationMessage;
   //boolean checkValidity();
   //boolean reportValidity();
   //void setCustomValidity(DOMString error);
--- a/servo/resources/servo.css
+++ b/servo/resources/servo.css
@@ -1,10 +1,11 @@
-input, select { display: inline-block; }
+input, textarea, select { display: inline-block; }
 input                   { background: white; min-height: 1.0em; padding: 0em; padding-left: 0.25em; padding-right: 0.25em; border: solid lightgrey 1px; color: black; white-space: nowrap; }
+textarea                { background: white; min-height: 1.0em; padding: 0em; padding-left: 0.25em; padding-right: 0.25em; border: solid lightgrey 1px; color: black; white-space: pre; }
 input[type="button"],
 input[type="submit"],
 input[type="reset"]     { background: lightgrey; border-top: solid 1px #EEEEEE; border-left: solid 1px #CCCCCC; border-right: solid 1px #999999; border-bottom: solid 1px #999999; text-align: center; vertical-align: middle; color: black; width: 100%; }
 input[type="hidden"]    { display: none !important }
 input[type="checkbox"],
 input[type="radio"]     { font-family: monospace !important; border: none !important; background: transparent; }
 
 input[type="checkbox"]::before { content: "[ ]"; padding: 0; }
new file mode 100644
--- /dev/null
+++ b/servo/tests/html/textarea.html
@@ -0,0 +1,6 @@
+<html>
+<head></head>
+<body>
+<textarea name="textarea">Write something here
+and maybe here</textarea>
+</body>