← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rharding/launchpad/inline_editor into lp:launchpad

 

Richard Harding has proposed merging lp:~rharding/launchpad/inline_editor into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rharding/launchpad/inline_editor/+merge/84528

= Summary =
With the addition of the resizing_textarea, this change is meant to force the inline editor to use it for all resizing needs. This remove the duplicate resizing code in the two modules.

== Proposed Fix ==
Make the resizing_textarea a dependency of inline editor and auto plug the input element from inline editor with the resizing textarea

== Implementation Details ==
This required a few updates on the resizing_textarea to keep full functionality. It needed to support a "one line" mode where it limited the height to 1em as the inline editor did. It also required updates to make sure the width of the textarea is adjusted as window resizes and such occur.

== Tests ==
bin/test -cvt test_resizing_textarea.html
bin/test -cvt test_inline_edit.html

== Demo and Q/A ==
Existing resizing textarea plugin: should work without an inline editor by submitting a new bug, clicking that you need to submit a new one, and then placing lots of content into the description field. It should shrink/grow as required.

New inline editor functions:
- single line test can be done from the PPA view by editing the PPA name. It should start out as a single line size, grow as required by input, and shrink back if you edit down. All other inline edit functions should not be interfered with such as cancel, enter key accepts, etc.
- multi line test can be done via the PPA description edit. It should again, show a multi-line textarea, that grows and shrinks as required. The toolbar buttons on the control should work as usual.

== Lint ==
Left known lint errors due to long referenced url and due to the large block of html used in testing in test_inline_edit.js that's pre-existing.

Linting changed files:
  lib/lp/app/javascript/formwidgets/resizing_textarea.js
  lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js
  lib/lp/app/javascript/inlineedit/editor.js
  lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js

./lib/lp/app/javascript/inlineedit/editor.js
     709: Line exceeds 78 characters.
./lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js
       7: Unexpected '\'.
       7: Unclosed string.
       7: Stopping.  (0% scanned).
-- 
https://code.launchpad.net/~rharding/launchpad/inline_editor/+merge/84528
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/inline_editor into lp:launchpad.
=== modified file 'lib/lp/app/javascript/formwidgets/resizing_textarea.js'
--- lib/lp/app/javascript/formwidgets/resizing_textarea.js	2011-11-23 20:48:19 +0000
+++ lib/lp/app/javascript/formwidgets/resizing_textarea.js	2011-12-06 12:49:28 +0000
@@ -30,10 +30,21 @@
  */
 ResizingTextarea.ATTRS = {
     /**
-     * Min height to allow the textarea to shrink to in px
+     * Get the current elements height. This is READ ONLY.
+     *
+     * @property height
+     */
+    height: {
+        getter: function () {
+            return this.t_area.getStyle('height');
+        }
+    },
+
+    /**
+     * Min height to allow the textarea to shrink to in px.
      *
      * We check if there's a css rule for existing height and make that the
-     * min height in case it's there
+     * min height in case it's there.
      *
      * @property min_height
      */
@@ -59,8 +70,18 @@
     },
 
     /**
-     * Should we bypass animating changes in height
-     * Mainly used to turn off for testing to prevent needing to set timeouts
+     * one_line says that at the minimum, it should show up as a single line
+     * textarea. This is used by tools such as the inline edit widget.
+     *
+     * @property one_line
+     */
+    one_line: {
+        value: false
+    },
+
+    /**
+     * Should we bypass animating changes in height?
+     * Mainly used to turn off for testing to prevent needing to set timeouts.
      *
      * @property skip_animations
      */
@@ -69,9 +90,10 @@
 
 Y.extend(ResizingTextarea, Y.Plugin.Base, {
 
-    // special css we add to clones to make sure they're hidden from view
+    // Special css we add to clones to make sure they're hidden from view.
     CLONE_CSS: {
         position: 'absolute',
+        height: '1em',
         top: -9999,
         left: -9999,
         opacity: 0,
@@ -79,38 +101,43 @@
         resize: 'none'
     },
 
+    // Used to track if we're growing/shrinking for each event fired.
+    _prev_scroll_height: 0,
+
     /**
-     * Helper function to turn the string from getComputedStyle to int
+     * Helper function to turn the string from getComputedStyle to int.
+     *
+     * Deals with the case where we pass in a value with a px at the end. For
+     * instance, if you pass the max size from a computed style call, it'll
+     * have xxpx which we want to just pull off.
      */
     _clean_size: function (val) {
-        return parseInt(val.replace('px', ''), 10);
-    },
-
-    // used to track if we're growing/shrinking for each event fired
-    _prev_scroll_height: 0,
-
-    /**
-     * This is the entry point for the event of change
-     *
-     * Here we update the clone and resize based on the update
-     */
-    _run_change: function (new_value) {
-        // we need to update the clone with the content so it resizes
-        this.clone.set('text', new_value);
-        this.resize();
-    },
-
-    /**
-     * Given a node, setup a clone so that we can use it for sizing
-     *
-     * We need to copy this, move it way off the screen and setup some css we
-     * use to make sure that we match the original as best as possible.
-     *
-     * This clone is then checked for the size to use
-     */
-    _setup_clone: function (node) {
-        var clone = node.cloneNode(true);
-
+        if (Y.Lang.isString(val) && val.indexOf("px") === -1) {
+            val.replace('px', '');
+        }
+        return parseInt(val, 10);
+    },
+
+    /**
+     * Is this input a single line of text?
+     *
+     * In order to tell, we create a one line clone of our master element and
+     * see how tall it is.
+     */
+    _get_one_line_height: function (node) {
+        var clone_one = this._prep_clone(node);
+        // Clear any input so we know it's only one line tall.
+        clone_one.set('value', 'one');
+        node.get('parentNode').appendChild(clone_one);
+        return clone_one.get('scrollHeight');
+    },
+
+    /**
+     * Common modifications we make to a textarea node in order to clone it
+     * without side effects.
+     */
+    _prep_clone: function (node) {
+        var clone= node.cloneNode(true);
         clone.setStyles(this.CLONE_CSS);
         // remove attributes so we don't accidentally grab this node in the
         // future
@@ -118,25 +145,50 @@
         clone.removeAttribute('name');
         clone.generateID();
         clone.setAttrs({
-            'tabIndex': -1,
-            'height': 'auto'
+            'tabIndex': -1
         });
-        Y.one('body').append(clone);
 
         return clone;
     },
 
     /**
-     * We need to apply some special css to our target we want to resize
+     * This is the entry point for the event of change.
+     *
+     * Here we update the clone and resize based on the update.
+     */
+    _run_change: function (new_value) {
+        // we need to update the clone with the content so it resizes
+        this.clone.set('value', new_value);
+        this.resize();
+    },
+
+    /**
+     * Given a node, setup a clone so that we can use it for sizing.
+     *
+     * We need to copy this, move it way off the screen and setup some css we
+     * use to make sure that we match the original as best as possible.
+     *
+     * This clone is then checked for the size to use.
+     */
+    _setup_clone: function (node) {
+        this.clone = this._prep_clone(node);
+        this._update_clone_width();
+
+        node.get('parentNode').append(this.clone);
+        return this.clone;
+    },
+
+    /**
+     * We need to apply some special css to our target we want to resize.
      */
     _setup_css: function () {
-        // don't let this text area be resized in the browser, it'll mess with
-        // our calcs and we'll be fighting the whole time for the right size
+        // Don't let this text area be resized in the browser, it'll mess with
+        // our calcs and we'll be fighting the whole time for the right size.
         this.t_area.setStyle('resize', 'none');
         this.t_area.setStyle('overflow', 'hidden');
 
-        // we want to add some animation to our adjusting of the size, using
-        // css animation to smooth all height changes
+        // We want to add some animation to our adjusting of the size, using
+        // css animation to smooth all height changes.
         if (!this.get('skip_animations')) {
             this.t_area.setStyle('transition', 'height 0.3s ease');
             this.t_area.setStyle('-webkit-transition', 'height 0.3s ease');
@@ -144,43 +196,84 @@
         }
     },
 
+    /**
+     * Update the css width of the clone node.
+     *
+     * In the process of page dom manipulation, the width might change based
+     * on other nodes showing up and forcing changes due to padding/etc.
+     *
+     * We'll play safe and just always recalc the width for the clone before
+     * we check it's scroll height.
+     *
+     */
+    _update_clone_width: function () {
+        this.clone.setStyle('width', this.t_area.getComputedStyle('width'));
+    },
+
     initializer : function(cfg) {
+        var that = this;
         this.t_area = this.get("host");
+
+        // We need to clean the px of any defaults passed in.
+        this.set('min_height', this._clean_size(this.get('min_height')));
+        this.set('max_height', this._clean_size(this.get('max_height')));
+
         this._setup_css(this.t_area);
 
-        // we need to setup the clone of this node so we can check how big it
-        // is, but way off the screen so you don't see it
-        this.clone = this._setup_clone(this.t_area);
+        // We need to setup the clone of this node so we can check how big it
+        // is, but way off the screen so you don't see it.
+        this._setup_clone(this.t_area);
 
-        // look at adjusting the size on any value change event including
-        // pasting and such
+        // Look at adjusting the size on any value change event including
+        // pasting and such.
         this.t_area.on('valueChange', function(e) {
             this._run_change(e.newVal);
         }, this);
 
-        // initial sizing in case there's existing content to match to
+        // We also want to handle adjusting if the user resizes their browser.
+        Y.on('windowresize', function(e) {
+            that._run_change(that.t_area.get('value'));
+        }, this);
+
+        // Init the single line height info.
+        if (this.get('one_line')) {
+            this._one_line_height = this._get_one_line_height(this.t_area);
+        }
+
+        // Initial sizing in case there's existing content to match to.
         this.resize();
     },
 
     /**
-     * Adjust the size of the textarea as needed
+     * Adjust the size of the textarea as needed.
      *
      * @method resize
      */
     resize: function() {
+        // We need to update the clone width in case the node's width has
+        // changed.
+        this._update_clone_width();
+
         var scroll_height = this.clone.get('scrollHeight');
 
-        // only update the height if we've changed
-        if (this._prev_scroll_height !== scroll_height) {
+        if (this.get('one_line') && this._one_line_height >= scroll_height) {
+            // Force height if we're only one line and the one_line attr
+            // is set.
+            this.t_area.setStyles({
+                height: '1em',
+                overflow: 'hidden'
+            });
+        } else if (this._prev_scroll_height !== scroll_height) {
+            // Only update the height if we've changed.
             new_height = Math.max(
                 this.get('min_height'),
                 Math.min(scroll_height, this.get('max_height')));
 
             this.t_area.setStyle('height', new_height);
 
-            // check if the changes above require us to change our overflow
+            // Check if the changes above require us to change our overflow
             // setting to allow for a scrollbar now that our max size has been
-            // reached
+            // reached.
             this.set_overflow();
 
             this._prev_scroll_height = scroll_height;
@@ -188,12 +281,13 @@
     },
 
     /**
-     * Check if we're larger than the max_height setting and enable scrollbar
+     * Check if we're larger than the max_height setting and enable scrollbar.
      *
      * @method set_overflow
      */
     set_overflow: function() {
         var overflow = "hidden";
+
         if (this.clone.get('scrollHeight') >= this.get('max_height')) {
             overflow = "auto";
         }
@@ -205,5 +299,5 @@
 module.ResizingTextarea = ResizingTextarea;
 
 }, "0.1", {
-    "requires": ["plugin", "node", "event-valuechange"]
+    "requires": ["plugin", "node", "event-valuechange", "event-resize"]
 });

=== modified file 'lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html'
--- lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html	2011-11-23 19:34:12 +0000
+++ lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html	2011-12-06 12:49:28 +0000
@@ -21,12 +21,14 @@
 </head>
 <body class="yui3-skin-sam">
     <!-- We're going to test interacting with dom elements, so let's have some -->
-    <textarea id="init">Initial text</textarea>
-    <textarea id="with_defaults">has defaults</textarea>
-    <textarea id="shrinkage"></textarea>
-    <textarea id="multiple1" class="test_multiple first"></textarea>
-    <textarea id="multiple2" class="test_multiple second"></textarea>
-    <textarea id="css_height" style="height: 120px;"></textarea>
+    <textarea id="init" style="width: auto;">Initial text</textarea>
+    <textarea id="with_defaults" style="width: auto;">has defaults</textarea>
+    <textarea id="shrinkage" style="width: auto;"></textarea>
+    <textarea id="multiple1" class="test_multiple first" style="width: auto;"></textarea>
+    <textarea id="multiple2" class="test_multiple second" style="width: auto;"></textarea>
+    <textarea id="css_height" style="height: 120px; width: auto;"></textarea>
+    <textarea id="one_line_sample" style="height: 1em; overflow: hidden; resize: none; width: auto;"></textarea>
+    <textarea id="one_line" style="height: 1em; width: auto;"></textarea>
 </body>
 
 </html>

=== modified file 'lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js'
--- lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js	2011-11-23 20:48:19 +0000
+++ lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js	2011-12-06 12:49:28 +0000
@@ -17,7 +17,7 @@
     "consectetur adipiscing elit."].join("");
 
 /**
- * Helper function to turn the string from getComputedStyle to int
+ * Helper function to turn the string from getComputedStyle to int.
  *
  */
 function clean_size(val) {
@@ -25,7 +25,7 @@
 }
 
 /**
- * Helper to extract the computed height of the element
+ * Helper to extract the computed height of the element.
  *
  */
 function get_height(target) {
@@ -35,14 +35,14 @@
 /**
  * In order to update the content we need to change the text, but also to fire
  * the event that the content has changed since we're modifying it
- * programatically
+ * programatically.
  *
  */
 function update_content(target, val) {
     target.set('value', val);
 
-    // instead of hitting the changed event directly, we'll just manually call
-    // into the hook for the event itself
+    // Instead of hitting the changed event directly, we'll just manually call
+    // into the hook for the event itself.
     target.resizing_textarea._run_change(val);
 }
 
@@ -61,8 +61,8 @@
             skip_animations: true
         });
 
-        // get the current sizes so we can pump text into it and make sure it
-        // grows
+        // Get the current sizes so we can pump text into it and make sure it
+        // grows.
         var orig_height = get_height(target);
         update_content(target, test_text);
 
@@ -126,7 +126,7 @@
                 "The height of the node should be 100");
         });
 
-        // now set the content in the first one and check it's unique
+        // Now set the content in the first one and check it's unique.
         update_content(Y.one('.first'), test_text);
 
         var first = Y.one('.first');
@@ -151,6 +151,32 @@
         var current_height = get_height(target);
         Y.Assert.areSame(120, current_height,
             "The height should match the css property at 120px");
+    },
+
+    "setting oneline to true should size to 1em": function () {
+        // Passing a one line in the cfg should limit the height to 1em even
+        // though a normal textarea would be two lines tall.
+        var sample_height = get_height(Y.one('#one_line_sample')),
+            target = Y.one('#one_line');
+
+        target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
+            skip_animations: true,
+            one_line: true
+        });
+
+        var initial_height = get_height(target);
+        Y.Assert.areSame(sample_height, initial_height,
+            "The initial height should be 1em");
+
+        // After adding a bunch of text and removing it, we should be back at
+        // one em height.
+        update_content(target, test_text);
+        Y.Assert.isTrue(get_height(target) > initial_height,
+            'Verify that we did change the height');
+
+        update_content(target, "");
+        Y.Assert.areSame(sample_height, get_height(target),
+            "The updated final height should be 1em");
     }
 }));
 

=== modified file 'lib/lp/app/javascript/inlineedit/editor.js'
--- lib/lp/app/javascript/inlineedit/editor.js	2011-08-09 14:18:02 +0000
+++ lib/lp/app/javascript/inlineedit/editor.js	2011-12-06 12:49:28 +0000
@@ -1,6 +1,6 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
-YUI.add('lazr.editor', function(Y) {
+YUI.add('lazr.editor', function (Y) {
 
 /**
  * Edit any on-screen text in-place.
@@ -33,8 +33,7 @@
     CLICK = 'click',
     ACCEPT_EMPTY = 'accept_empty',
     MULTILINE = 'multiline',
-    SCROLLBAR_LEGROOM = 30,
-    MULTILINE_MIN_LINES = 2,
+    MULTILINE_MIN_SIZE = 60,
 
     TOP_BUTTONS = 'top_buttons',
     BOTTOM_BUTTONS = 'bottom_buttons',
@@ -62,13 +61,12 @@
 
     SAVE = 'save',
     CANCEL = 'cancel',
-    SHRINK = 'shrink',
-    RESIZED = 'resized';
+    SHRINK = 'shrink';
 
 // To strip the 'px' unit suffix off widget sizes.
 var strip_px = /px$/;
 
-var InlineEditor = function() {
+var InlineEditor = function () {
     InlineEditor.superclass.constructor.apply(this, arguments);
 };
 
@@ -160,7 +158,9 @@
      */
     submit_button: {
         value: null,
-        setter: function(v) { return this._setNode(v); }
+        setter: function (v) {
+            return this._setNode(v);
+        }
     },
 
     /**
@@ -172,7 +172,7 @@
      */
     cancel_button: {
         value: null,
-        setter: function(v) { return this._setNode(v); }
+        setter: function (v) { return this._setNode(v); }
     },
 
     /**
@@ -184,7 +184,7 @@
      */
     top_buttons: {
         value: null,
-        setter: function(v) { return this._setNode(v); }
+        setter: function (v) { return this._setNode(v); }
     },
 
     /**
@@ -196,7 +196,7 @@
      */
     bottom_buttons: {
         value: null,
-        setter: function(v) { return this._setNode(v); }
+        setter: function (v) { return this._setNode(v); }
     },
 
     /**
@@ -207,7 +207,7 @@
      */
     error_message: {
         value: null,
-        setter: function(v) { return this._setNode(v); }
+        setter: function (v) { return this._setNode(v); }
     },
 
     /**
@@ -219,16 +219,21 @@
      */
     value: {
         value: '',
-        validator: function(v) { return v !== null; }
+        validator: function (v) {
+            return v !== null;
+        },
+        getter: function (val) {
+            return Y.Lang.trim(val);
+        }
     },
 
     /**
-     * When not null, this overrides the initial value for the <input> field.
-     * Otherwise, the initial value is taken from the widget's <span>
-     * element.  The caller must provide this because when accept_empty is
-     * true, the editor widget has no way to distinguish whether a value of ''
-     * means that no value was given, or whether the empty string is a valid
-     * user supplied value.
+     * When not null, this overrides the initial value for the <input>
+     * field.  Otherwise, the initial value is taken from the widget's
+     * <span> element.  The caller must provide this because when
+     * accept_empty is true, the editor widget has no way to distinguish
+     * whether a value of '' means that no value was given, or whether the
+     * empty string is a valid user supplied value.
      */
     initial_value_override: {
         value: null
@@ -255,8 +260,8 @@
      * easier to line up the button boxes correctly.  But doing it this
      * way happens to be useful for testing.
      *
-     * If you are creating a multi-line editor then using 'size' will lead to
-     * some strange layout results.  You are better off sepecifying the
+     * If you are creating a multi-line editor then using 'size' will lead
+     * to some strange layout results.  You are better off specifying the
      * widget "width" attribute for such widgets.
      *
      * @attribute size
@@ -264,20 +269,22 @@
      */
     size: {
         value: null,
-        validator: function(v) { return this._validateSize(v); }
+        validator: function (v) {
+            return this._validateSize(v);
+        }
     },
 
     /**
-     * Determines which sets of buttons should be shown in multi-line mode:
-     * "top", "bottom", or "both".
+     * Determines which sets of buttons should be shown in multi-line
+     * mode: "top", "bottom", or "both".
      *
      * @attribute buttons
      * @default "both"
      */
     buttons: {
         value: B_BOTH,
-        validator: function(v) {
-            return (v == B_TOP || v == B_BOTTOM || v == B_BOTH);
+        validator: function (v) {
+            return (v === B_TOP || v === B_BOTTOM || v === B_BOTH);
         }
     }
 };
@@ -292,7 +299,7 @@
      * @param v {Node|String|HTMLElement} The node element or selector
      * @return {Node} The Node, if found.  null otherwise.
      */
-    _setNode: function(v) {
+    _setNode: function (v) {
         return v ? Y.one(v) : null;
     },
 
@@ -304,7 +311,7 @@
      * @param val {String} the input to validate
      * @return {Boolean} true if the input is ok.
      */
-    validate: function(val) {
+    validate: function (val) {
         if (!this.get(ACCEPT_EMPTY) && val === '') {
             this.showError("Empty input is unacceptable!");
             return false;
@@ -316,12 +323,12 @@
     },
 
     /**
-     * Save the editor's current input.  Validates the input, and, if it is
-     * valid, clears any errors before calling _saveData().
+     * Save the editor's current input.  Validates the input, and, if it
+     * is valid, clears any errors before calling _saveData().
      *
      * @method save
      */
-    save: function() {
+    save: function () {
         // We don't want to save any whitespace characters.
         var input = Y.Lang.trim(this.getInput());
 
@@ -332,9 +339,9 @@
     },
 
     /**
-     * The default save() operation.  Writes the input field's value to the
-     * editor's 'value' attribute.  Fires the 'save' event after everything
-     * is complete.
+     * The default save() operation.  Writes the input field's value to
+     * the editor's 'value' attribute.  Fires the 'save' event after
+     * everything is complete.
      *
      * This method will only be called if the editor's input has been
      * validated.
@@ -343,7 +350,7 @@
      * @param data {ANY} The data to be saved.
      * @protected
      */
-    _saveData: function(data) {
+    _saveData: function (data) {
         this.set(VALUE, data);
         this.fire(SAVE);
     },
@@ -354,168 +361,10 @@
      *
      * @method cancel
      */
-    cancel: function() {
+    cancel: function () {
         this.fire(CANCEL);
     },
 
-    /**
-     * Record initial input box size, if available and not already recorded.
-     *
-     * Sets initial_scroll_width and initial_scroll_height, which are used
-     * later to detect horizontal overflow and determine the right minimum
-     * input box height, respectively.
-     *
-     * @method _recordInitialSize
-     * @protected
-     */
-    _recordInitialSize: function() {
-        if (this.alter_ego && !this.initial_scroll_width) {
-            /* Get initial internal size from the alter_ego.  It needs
-             * to be empty so that we don't get the initial text's size.
-             */
-            var text = this.alter_ego.get(VALUE);
-            this.alter_ego.set(VALUE, '');
-            var width = this.alter_ego.get('scrollWidth');
-            if (width > 0) {
-                this.initial_scroll_width = width;
-                this.initial_scroll_height = this.alter_ego.get('scrollHeight');
-            }
-            this.alter_ego.set(VALUE, text);
-        }
-    },
-
-    /**
-     * Reset initial input box size.
-     *
-     * Clears the editor's notions about how much text will fit in the
-     * input box without scrolling.
-     */
-    _forgetInitialSize: function() {
-        this.initial_scroll_width = undefined;
-        this.initial_scroll_height = undefined;
-    },
-
-    /**
-     * Handle window resizes.
-     *
-     * @method _windowResize
-     * @param e {Event.Facade} An Event Facade object.
-     * @protected
-     */
-    _windowResize: function(e) {
-        if (this.get('visible')) {
-            this._forgetInitialSize();
-            this.updateSize(false);
-        }
-    },
-
-    /**
-     * Handle events that may have changed the input text.
-     *
-     * Resizes the widget vertically to fit the text snugly.  When lines
-     * are removed, produces an animated shrinking effect.
-     *
-     * @method _onChange
-     * @param e {Event.Facade} An Event Facade object.
-     * @protected
-     */
-    _onChange: function(e) {
-        this.updateSize(true);
-    },
-
-    /**
-     * Fire the 'resized' event.
-     *
-     * @method _resized
-     * @protected
-     */
-    _resized: function() {
-        this.fire(RESIZED);
-    },
-
-    /**
-     * Animated smooth shrinking effect when lines are removed.
-     *
-     * Fires the 'resized' effect when done.
-     *
-     * @method _shrink
-     * @protected
-     */
-    _shrink: function() {
-        var anim = new Y.Anim({
-            node: this.get(INPUT_EL),
-            to: { height: this._target_height },
-            duration: 0.1
-        });
-        anim.on('end', Y.bind(this._resized, this));
-        anim.run();
-    },
-
-    /**
-     * Refresh size immediately, without animation.
-     *
-     * @method _updateNow
-     */
-    _updateNow: function() {
-        this.updateSize(false);
-    },
-
-    /**
-     * Adjust vertical input box size to fit its contents snugly.
-     *
-     * Increases are immediate, but decreases produce an animated
-     * shrinking effect if requested.  (Doing the same while growing
-     * would probably inconvenience the user).
-     *
-     * @method updateSize
-     * @param animate {Bool} Whether shrinking should be animated.
-     */
-    updateSize: function(animate) {
-        this._recordInitialSize();
-
-        var input = this.get(INPUT_EL);
-        this.alter_ego.set(VALUE, input.get(VALUE));
-
-        var new_height = this.alter_ego.get('scrollHeight');
-        if (new_height === 0) {
-            return;
-        }
-
-        if (input.get('scrollWidth') > this.initial_scroll_width) {
-            // Text is wider than the widget.  There's probably a scroll bar.
-            new_height += SCROLLBAR_LEGROOM;
-            input.setStyle('overflow', 'auto');
-        } else {
-            // Text fits in the widget.  Suppress scrollbars.
-            input.setStyle('overflow', HIDDEN);
-        }
-
-        if (this.get(MULTILINE)) {
-            // Show deeper text area to let user know that multiple
-            // lines are allowed.
-            var min_height = this.initial_scroll_height * MULTILINE_MIN_LINES;
-            if (new_height < min_height) {
-                new_height = min_height;
-            }
-            // The multi-line editor has to dynamically resize,
-            // in case the widget is fluid.
-            var box_width = this.get(BOUNDING_BOX).get('offsetWidth');
-            var new_width = box_width - 29;
-            input.setStyle('width', new_width + 'px');
-        }
-
-        this._target_height = new_height;
-        current_height = input.getStyle('height').replace(strip_px, '');
-
-        if (animate && (new_height < current_height)) {
-            // Animate shrinking.
-            this._shrink();
-        } else if (new_height != current_height) {
-            // Resize instantly.
-            input.setStyle('height', new_height + 'px');
-            this._resized();
-        }
-    },
 
     /**
      * The default cancel() operation.  Resets the current input field.
@@ -524,7 +373,7 @@
      * @param e {Event.Facade} An Event Facade object.
      * @protected
      */
-    _defaultCancel: function(e) {
+    _defaultCancel: function (e) {
         this.reset();
     },
 
@@ -534,7 +383,7 @@
      *
      * @method reset
      */
-    reset: function() {
+    reset: function () {
         this.setInput(this.get(VALUE));
         this.clearErrors();
     },
@@ -544,7 +393,7 @@
      *
      * @method focus
      */
-    focus: function() {
+    focus: function () {
         this.get(INPUT_EL).focus();
     },
 
@@ -554,7 +403,7 @@
      * @method showError
      * @param msg A string or HTMLElement to be displayed.
      */
-    showError: function(msg) {
+    showError: function (msg) {
         this.hideLoadingSpinner();
         this.get(ERROR_MSG).set('innerHTML', msg);
         this.set(IN_ERROR, true);
@@ -566,7 +415,7 @@
      *
      * @method clearErrors
      */
-    clearErrors: function() {
+    clearErrors: function () {
         this.set(IN_ERROR, false);
     },
 
@@ -576,7 +425,7 @@
      * @method hasErrors
      * @return Boolean
      */
-    hasErrors: function() {
+    hasErrors: function () {
         return this.get(IN_ERROR);
     },
 
@@ -586,7 +435,7 @@
      * @method initializer
      * @protected
      */
-    initializer: function(cfg) {
+    initializer: function (cfg) {
         /**
          * Fires when the user presses the 'Submit' button.
          *
@@ -603,15 +452,14 @@
         this.publish(CANCEL, { defaultFn: this._defaultCancel });
 
         /**
-         * Fires after the input box has been resized vertically (which
-         * may involve asynchronous animation).
-         *
-         * @event shrink
+         * Store the cfg so we can pass things to composed elements like
+         * the ResizingTextarea. In this way we can change settings of
+         * that plugin while creating and dealing with this widget
          */
-        this.publish(RESIZED);
+        this.cfg = Y.Lang.isUndefined(cfg) ? {} : cfg;
     },
 
-    _removeElement: function(content_box, element) {
+    _removeElement: function (content_box, element) {
         if (element) {
             content_box.removeChild(element);
         }
@@ -623,7 +471,7 @@
      * @method destructor
      * @private
      */
-    destructor: function() {
+    destructor: function () {
         var box = this.get(CONTENT_BOX);
         this._removeElement(box, this.get(ERROR_MSG));
         this._removeElement(box, this.get(TOP_BUTTONS));
@@ -638,7 +486,7 @@
      * @protected
      * @param parent {Node} The parent node that will hold the buttons.
      */
-    _renderSingleLineButtons: function(parent) {
+    _renderSingleLineButtons: function (parent) {
         var button_box = createNode('<span></span>')
             .addClass(C_BTNBOX);
         this._renderOKCancel(button_box);
@@ -654,15 +502,17 @@
      * @protected
      * @param parent {Node} The parent node that will hold the buttons.
      */
-    _renderTopButtons: function(parent) {
+    _renderTopButtons: function (parent) {
         var button_bar = createNode('<div></div>')
             .addClass(C_BTNBOX);
 
         // Firefox needs a text node in order to calculate the line-height
-        // and thus vertically center the buttons in the div.  Apparently it
-        // can't use the button elements themselves to do this.
+        // and thus vertically center the buttons in the div.
+        // Apparently it can't use the button elements themselves to
+        // do this.
         var label = button_bar.appendChild(
-            createNode('<div class="bg-top-label">&nbsp;</div>'));
+            createNode('<div class="bg-top-label">&nbsp;</div>')
+        );
 
         this._renderOKCancel(label);
         parent.appendChild(button_bar);
@@ -670,22 +520,24 @@
     },
 
     /**
-     * Create a box to hold the OK and Cancel buttons around the bottom of a
-     * multi-line editor.
+     * Create a box to hold the OK and Cancel buttons around the bottom of
+     * a multi-line editor.
      *
      * @method _renderBottomButtons
      * @protected
      * @param parent {Node} The parent node that will hold the buttons.
      */
-    _renderBottomButtons: function(parent) {
+    _renderBottomButtons: function (parent) {
         var button_bar = createNode('<div></div>')
             .addClass(C_BTNBOX);
 
         // Firefox needs a text node in order to calculate the line-height
-        // and thus vertically center the buttons in the div.  Apparently it
-        // can't use the button elements themselves to do this.
+        // and thus vertically center the buttons in the div.
+        // Apparently it can't use the button elements themselves to
+        // do this.
         var label = button_bar.appendChild(
-            createNode('<div class="bg-bottom-label">&nbsp</div>'));
+            createNode('<div class="bg-bottom-label">&nbsp</div>')
+        );
 
         this._renderOKCancel(label);
         parent.appendChild(button_bar);
@@ -700,7 +552,7 @@
      * @param parent {Node} The parent node that the buttons should be
      * appended to.
      */
-    _renderOKCancel: function(parent) {
+    _renderOKCancel: function (parent) {
         var ok = createNode(InlineEditor.SUBMIT_TEMPLATE)
                 .addClass(C_SUBMIT);
         var cancel = createNode(InlineEditor.CANCEL_TEMPLATE)
@@ -717,7 +569,7 @@
      * @method render
      * @protected
      */
-    renderUI: function() {
+    renderUI: function () {
         var bounding_box = this.get(BOUNDING_BOX);
         var content = this.get(CONTENT_BOX);
         var multiline = this.get(MULTILINE);
@@ -727,7 +579,7 @@
         }
 
         if (multiline) {
-            if (buttons == B_TOP || buttons == B_BOTH) {
+            if (buttons === B_TOP || buttons === B_BOTH) {
                 this._renderTopButtons(content);
             }
         }
@@ -735,9 +587,10 @@
         this._initInput();
 
         if (multiline) {
-            if (buttons == B_BOTTOM || buttons == B_BOTH) {
+            if (buttons === B_BOTTOM || buttons === B_BOTH) {
                 this._renderBottomButtons(content);
             }
+
             bounding_box.addClass(C_MULTILINE);
         } else {
             this._renderSingleLineButtons(content);
@@ -747,20 +600,20 @@
         this._initErrorMsg();
     },
 
-    _makeInputBox: function() {
+    _makeInputBox: function () {
         var box = createNode(InlineEditor.INPUT_TEMPLATE),
             size = this.get(SIZE);
 
         if (size) {
             if (Y.Lang.isNumber(size)) {
-                size = size + 'ex';
+                size = size + 'px';
             }
             box.setStyle('width', size);
         }
-        box.setStyle('height', '1em');
         box.setStyle('overflow', HIDDEN);
         box.addClass(C_INPUT);
         this.get(CONTENT_BOX).appendChild(box);
+
         return box;
     },
 
@@ -771,17 +624,9 @@
      * @method _initInput
      * @protected
      */
-    _initInput: function() {
+    _initInput: function () {
         if (!this.get(INPUT_EL)) {
             this.set(INPUT_EL, this._makeInputBox());
-
-            // Create invisible alter ego for sizing purposes.
-            this.alter_ego = this._makeInputBox();
-            this.alter_ego.setStyles({
-                'visibility': HIDDEN,
-                'position': 'absolute',
-                'left': '-1000px'
-            });
         }
     },
 
@@ -792,7 +637,7 @@
      * @method _initErrorMsg
      * @protected
      */
-    _initErrorMsg: function() {
+    _initErrorMsg: function () {
         var cb = this.get(CONTENT_BOX),
             msg = this.get(ERROR_MSG);
 
@@ -806,20 +651,22 @@
         msg.addClass(C_ERROR_HIDDEN);
     },
 
-    showLoadingSpinner: function(e) {
+    showLoadingSpinner: function (e) {
         // The multi-line editor submit icon should change to a spinner.
         if (this.get(MULTILINE)) {
             this.get(TOP_BUTTONS).one('.' + C_SUBMIT).setStyle(
-                'display', 'none');
+                'display', 'none'
+            );
             this.get(TOP_BUTTONS).one('.' + C_CANCEL).setStyle(
-                'display', 'none');
+                'display', 'none'
+            );
             var span = Y.Node.create('<span></span>');
             span.addClass(LOADING);
             e.target.get('parentNode').appendChild(span);
         }
     },
 
-    hideLoadingSpinner: function() {
+    hideLoadingSpinner: function () {
         // Remove the spinner from the multi-line editor.
         if (this.get(MULTILINE)) {
             var spinner = this.get(TOP_BUTTONS).one('.' + LOADING);
@@ -840,72 +687,51 @@
      * @method bindUI
      * @protected
      */
-    bindUI: function() {
+    bindUI: function () {
         this.after('in_errorChange', this._afterInErrorChange);
 
-        this._bindButtons(C_SUBMIT, function(e) {
+        this._bindButtons(C_SUBMIT, function (e) {
             e.preventDefault();
             this.showLoadingSpinner(e);
             this.save();
         });
-        this._bindButtons(C_CANCEL, function(e) {
+        this._bindButtons(C_CANCEL, function (e) {
             e.preventDefault();
             this.cancel();
         });
 
+        // hook up the resizing textarea to handle those changes
+        var cfg = this.cfg;
+        var input = this.get(INPUT_EL);
+
         if (!this.get(MULTILINE)) {
+            // if this is not a multi-line, make sure it starts out as a
+            // nice single row textarea then
+            cfg.one_line = true;
+
             // 'down:13' is the decimal value of the Firefox DOM_VK_RETURN
             // symbol or U+000D.
             // https://developer.mozilla.org/en/DOM/Event/UIEvent/KeyEvent#Specification
             Y.on('key', this.save, this.get(INPUT_EL), 'down:13', this);
+        } else {
+            cfg.min_height = MULTILINE_MIN_SIZE;
         }
 
-        // 'down:27' is the decimal value of the Firefox DOM_VK_ESCAPE symbol
-        // or U+001B.
+        this.get(INPUT_EL).plug(
+            Y.lp.app.formwidgets.ResizingTextarea,
+            cfg
+        );
+
+        // 'down:27' is the decimal value of the Firefox DOM_VK_ESCAPE
+        // symbol or U+001B.
         Y.on('key', this.cancel, this.get(INPUT_EL), 'down:27', this);
-
-        // Update size right after rendering.  This is the first time we'll
-        // be able to get the actual required size.
-        this.get(INPUT_EL).on('focus', Y.bind(this._updateNow, this));
-
-        var change_handler = Y.bind(this._onChange, this);
-
-        // Various events that can change how we render the input field.
-        Y.on('keyup', change_handler, this.get(INPUT_EL), Y, this);
-        Y.on('keypress', change_handler, this.get(INPUT_EL), Y, this);
-        Y.on('cut', change_handler, this.get(INPUT_EL), Y, this);
-        Y.on('paste', change_handler, this.get(INPUT_EL), Y, this);
-
-        // Listen for window resizes when necessary.
-        this._original_show = this.show;
-        this.show = this._extendedShow;
-        this._original_hide = this.hide;
-        this.hide = this._extendedHide;
     },
 
-    _bindButtons: function(button_class, method) {
+    _bindButtons: function (button_class, method) {
         var box = this.get(CONTENT_BOX);
         box.all('.' + button_class).on(CLICK, Y.bind(method, this));
     },
 
-    _extendedShow: function() {
-        // Editor may have ignored resize events while hidden.
-        if (!this._resize_handler) {
-            this._resize_handler = Y.on(
-                'resize', Y.bind(this._windowResize, this), Y.one(window), Y);
-        }
-        this._forgetInitialSize();
-        return this._original_show.apply(this, arguments[0]);
-    },
-
-    _extendedHide: function() {
-        if (this._resize_handler) {
-            this._resize_handler.detach();
-            this._resize_handler = null;
-        }
-        return this._original_hide.apply(this, arguments[0]);
-    },
-
     /**
      * Render the control's value to the INPUT control.  Normally, this is
      * taken from the `value` attribute, which gets initialized from the
@@ -916,7 +742,7 @@
      * @method syncUI
      * @protected
      */
-    syncUI: function() {
+    syncUI: function () {
         var value = this.get(INITIAL_VALUE_OVERRIDE);
         if (value === null || value === undefined) {
             value = this.get(VALUE);
@@ -929,7 +755,7 @@
     /**
      * A convenience method to fetch the control's input Element.
      */
-    getInput: function() {
+    getInput: function () {
         return this.get(INPUT_EL).get(VALUE);
     },
 
@@ -940,20 +766,25 @@
      * @method setInput
      * @param value New text to set as input box contents.
      */
-    setInput: function(value) {
+    setInput: function (value) {
         this.get(INPUT_EL).set(VALUE, value);
-        this.updateSize(false);
+
+        // we don't handle size updates, but the ResizingTextarea does.
+        // Normally, an event is caught on the change of the input, but if
+        // we programatically set the input, it won't always catch
+        this.get(INPUT_EL).resizing_textarea._run_change(value);
     },
 
     /**
-     * Hook to run after the 'in_error' attribute has changed.  Calls hooks
-     * to hide or show the UI's error message using _uiShowErrorMsg().
+     * Hook to run after the 'in_error' attribute has changed.  Calls
+     * hooks to hide or show the UI's error message using
+     * _uiShowErrorMsg().
      *
      * @method _afterInErrorChange
      * @param e {Event.Facade} An attribute change event instance.
      * @protected
      */
-    _afterInErrorChange: function(e) {
+    _afterInErrorChange: function (e) {
         this._uiShowErrorMsg(e.newVal);
     },
 
@@ -965,7 +796,7 @@
      * or hidden.
      * @protected
      */
-    _uiShowErrorMsg: function(show) {
+    _uiShowErrorMsg: function (show) {
         var emsg = this.get(ERROR_MSG),
             cb   = this.get(CONTENT_BOX);
 
@@ -985,7 +816,7 @@
      * @method _uiSetWaiting
      * @protected
      */
-    _uiSetWaiting: function() {
+    _uiSetWaiting: function () {
         this.get(INPUT_EL).set('disabled', true);
         this.get(BOUNDING_BOX).addClass(C_WAITING);
 
@@ -997,7 +828,7 @@
      * @method _uiClearWaiting
      * @protected
      */
-    _uiClearWaiting: function() {
+    _uiClearWaiting: function () {
         this.get(INPUT_EL).set('disabled', false);
         this.get(BOUNDING_BOX).removeClass(C_WAITING);
     },
@@ -1009,7 +840,7 @@
      * @param val {ANY} the value to validate
      * @protected
      */
-    _validateSize: function(val) {
+    _validateSize: function (val) {
         if (Y.Lang.isNumber(val)) {
             return (val >= 0);
         }
@@ -1040,7 +871,7 @@
  * @constructor
  * @extends Widget
  */
-var EditableText = function() {
+var EditableText = function () {
     EditableText.superclass.constructor.apply(this, arguments);
 };
 
@@ -1055,7 +886,7 @@
      * @type Node
      */
     trigger: {
-        setter: function(node) {
+        setter: function (node) {
             if (this.get(RENDERED)) {
                 this._bindTrigger(node);
             }
@@ -1071,10 +902,10 @@
      * @type Node
      */
     text: {
-        setter: function(v) {
+        setter: function (v) {
             return Y.Node.one(v);
         },
-        validator: function(v) {
+        validator: function (v) {
             return Y.Node.one(v);
         }
     },
@@ -1091,19 +922,19 @@
      * @readOnly
      */
     value: {
-        getter: function() {
+        getter: function () {
             var text_node = this.get(TEXT);
             var ptags = text_node.all('p');
             if (Y.Lang.isValue(ptags) && ptags.size()) {
                 var lines = [];
-                ptags.each(function(ptag) {
+                ptags.each(function (ptag) {
                     lines = lines.concat([ptag.get('text'), '\n\n']);
                 });
                 var content = lines.join("");
                 // Remove trailing whitespace.
                 return content.replace(/\s+$/,'');
             } else {
-                return this.get(TEXT).get('text');
+                return Y.Lang.trim(this.get(TEXT).get('text'));
             }
         },
         readOnly: true
@@ -1118,7 +949,7 @@
      */
     accept_empty: {
         value: false,
-        getter: function() {
+        getter: function () {
             if (this.editor) {
                 return this.editor.get(ACCEPT_EMPTY);
             }
@@ -1175,7 +1006,7 @@
      * @param e {Event} Click event facade.
      * @protected
      */
-    _triggerEdit: function(e) {
+    _triggerEdit: function (e) {
         e.preventDefault();
         this.show_editor();
         var cancel = this._editor_bb.one('.' + C_CANCEL);
@@ -1187,7 +1018,7 @@
             to: { left: -7 }
         });
         var self = this;
-        anim.on('end', function(e) {
+        anim.on('end', function (e) {
             self.editor.focus();
         });
         anim.run();
@@ -1200,7 +1031,7 @@
      *
      * @method show_editor
      */
-    show_editor: function() {
+    show_editor: function () {
         // Make sure that the cancel button starts back under the edit.
         var bounding_box = this.get(BOUNDING_BOX);
         bounding_box.one('.' + C_CANCEL).setStyle('left', 0);
@@ -1217,7 +1048,7 @@
      *
      * @method hide_editor
      */
-    hide_editor: function() {
+    hide_editor: function () {
         var box = this.get(BOUNDING_BOX);
         box.removeClass(C_EDIT_MODE);
         this.editor.hide();
@@ -1229,7 +1060,7 @@
      * @method _uiAnimateSave
      * @protected
      */
-    _uiAnimateSave: function() {
+    _uiAnimateSave: function () {
         this._uiAnimateFlash(Y.lp.anim.green_flash);
     },
 
@@ -1239,7 +1070,7 @@
      * @method _uiAnimateCancel
      * @protected.
      */
-    _uiAnimateCancel: function() {
+    _uiAnimateCancel: function () {
         this._uiAnimateFlash(Y.lp.anim.red_flash);
     },
 
@@ -1250,7 +1081,7 @@
      * @param flash_fn {Function} A lp.anim flash-in function.
      * @protected
      */
-    _uiAnimateFlash: function(flash_fn) {
+    _uiAnimateFlash: function (flash_fn) {
         var anim = flash_fn({ node: this.get(TEXT) });
         anim.run();
     },
@@ -1265,7 +1096,7 @@
      * @method initializer
      * @protected
      */
-    initializer: function(cfg) {
+    initializer: function (cfg) {
         this.editor = cfg.editor ? cfg.editor : this._makeEditor(cfg);
         this.editor.hide();
         // Make sure the editor appears as a child of our contentBox.
@@ -1279,8 +1110,8 @@
         // editor.
         this.on('accept_emptyChange', this._afterAcceptEmptyChange);
 
-        // We might want to cancel the render event, depending on the user's
-        // browser.
+        // We might want to cancel the render event, depending on the
+        // user's browser.
         this.on('render', this._onRender);
     },
 
@@ -1291,7 +1122,7 @@
      * @method destructor
      * @protected
      */
-    destructor: function() {
+    destructor: function () {
         if (this._click_handler) {
             this._click_handler.detach();
         }
@@ -1315,11 +1146,12 @@
      * @param e {Event.Facade} The event object.
      * @protected
      */
-    _onRender: function(e) {
+    _onRender: function (e) {
         // The webkit UA property will be set to the value '1' if the
         // browser uses the KHTML engine.
         //
-        // See http://developer.yahoo.com/yui/3/api/UA.html#property_webkit
+        // See
+        // http://developer.yahoo.com/yui/3/api/UA.html#property_webkit
         if (Y.UA.webkit === 1) {
             // Our editor is really broken in KHTML, so just prevent the
             // render event from firing and modifying the DOM.  This
@@ -1336,15 +1168,16 @@
     },
 
     /**
-     * Create an inline editor instance if one wasn't supplied by the user.
-     * Saves the editor's bounding box so that it can be removed later.
+     * Create an inline editor instance if one wasn't supplied by the
+     * user. Saves the editor's bounding box so that it can be removed
+     * later.
      *
      * @method _makeEditor
-     * @param cfg {Object} The current user configuration, usually supplied
-     * to the initializer function.
+     * @param cfg {Object} The current user configuration, usually
+     * supplied to the initializer function.
      * @protected
      */
-    _makeEditor: function(cfg) {
+    _makeEditor: function (cfg) {
         var editor_cfg = Y.merge(cfg, {
             value: this.get(VALUE)
         });
@@ -1368,7 +1201,7 @@
      * @method renderUI
      * @protected
      */
-    renderUI: function() {
+    renderUI: function () {
         // Just in case the user didn't assign the correct classes.
         this.get(TEXT).addClass(C_TEXT);
         this.get(TRIGGER).addClass(C_TRIGGER);
@@ -1380,7 +1213,7 @@
      * @method bindUI
      * @protected
      */
-    bindUI: function() {
+    bindUI: function () {
         // XXX: mars 2008-12-19
         // I should be able to use this.after('editor:save') here, but
         // the event model is broken: the listener will fire *before*
@@ -1401,11 +1234,11 @@
                 var edit_text = this.get(TEXT);
                 var control_hover_class = 'edit-controls-hover';
                 var text_hover_class = C_TEXT + '-hover';
-                edit_controls.on('mouseover', function(e) {
+                edit_controls.on('mouseover', function (e) {
                     edit_controls.addClass(control_hover_class);
                     edit_text.addClass(text_hover_class);
                 });
-                edit_controls.on('mouseout', function(e) {
+                edit_controls.on('mouseout', function (e) {
                     edit_controls.removeClass(control_hover_class);
                     edit_text.removeClass(text_hover_class);
                 });
@@ -1420,9 +1253,9 @@
      * @method syncUI
      * @protected
      */
-    syncUI: function() {
-        // We only want to grab the editor's current value if we've finished
-        // our own rendering phase.
+    syncUI: function () {
+        // We only want to grab the editor's current value if we've
+        // finished our own rendering phase.
         if (this.get(RENDERED)) {
             var text = this.get(TEXT),
                 val  = this.editor.get(VALUE);
@@ -1443,7 +1276,7 @@
      * @param node {Node} The node instance to bind to.
      * @protected
      */
-    _bindTrigger: function(node) {
+    _bindTrigger: function (node) {
         // Clean up the existing handler, to prevent event listener leaks.
         if (this._click_handler) {
             this._click_handler.detach();
@@ -1454,15 +1287,15 @@
 
     /**
      * Function to run after the user clicks 'save' on the inline editor.
-     * Syncs the UI, hides the editor, and animates a successful text change.
-     * This also resets the initial_value_override so that we do not continue
-     * to override the value when syncUI is called.
+     * Syncs the UI, hides the editor, and animates a successful text
+     * change.  This also resets the initial_value_override so that we do
+     * not continue to override the value when syncUI is called.
      *
      * @method _afterSave
      * @param e {Event.Custom} The editor widget's "save" event.
      * @protected
      */
-    _afterSave: function(e) {
+    _afterSave: function (e) {
         this.editor.hideLoadingSpinner();
         this.syncUI();
         this.hide_editor();
@@ -1478,7 +1311,7 @@
      * @param e {Event.Custom} The editor's "cancel" event.
      * @protected
      */
-    _afterCancel: function(e) {
+    _afterCancel: function (e) {
         this.hide_editor();
         this._uiAnimateCancel();
     },
@@ -1491,7 +1324,7 @@
      * @param e {Event} Change event for the 'accept_empty' attribute.
      * @protected
      */
-    _afterAcceptEmptyChange: function(e) {
+    _afterAcceptEmptyChange: function (e) {
         this.editor.set(ACCEPT_EMPTY, e.newVal);
     },
 
@@ -1501,7 +1334,7 @@
      *
      * @method renderer
      */
-    renderer: function() {
+    renderer: function () {
         if (this.editor.get(MULTILINE) && (Y.UA.ie || Y.UA.opera)) {
             return;
         }
@@ -1515,4 +1348,5 @@
 
 }, "0.2", {"skinnable": true,
            "requires": ["oop", "anim", "event", "node", "widget",
-                        "lp.anim", "lazr.base"]});
+                        "lp.anim", "lazr.base",
+                        "lp.app.formwidgets.resizing_textarea"]});

=== modified file 'lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html'
--- lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html	2011-08-10 08:43:17 +0000
+++ lib/lp/app/javascript/inlineedit/tests/test_inline_edit.html	2011-12-06 12:49:28 +0000
@@ -17,6 +17,7 @@
   <script type="text/javascript" src="../../anim/anim.js"></script>
   <script type="text/javascript" src="../../lazr/lazr.js"></script>
   <script type="text/javascript" src="../../extras/extras.js"></script>
+  <script type="text/javascript" src="../../formwidgets/resizing_textarea.js"></script>
 
   <!-- The test suite -->
   <script type="text/javascript" src="test_inline_edit.js"></script>

=== modified file 'lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js'
--- lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js	2011-07-08 05:12:39 +0000
+++ lib/lp/app/javascript/inlineedit/tests/test_inline_edit.js	2011-12-06 12:49:28 +0000
@@ -1,21 +1,25 @@
 /* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
 
 YUI().use('lp.testing.runner', 'test', 'console', 'node', 'lazr.editor',
-           'event', 'event-simulate', 'plugin', function(Y) {
+          'lp.app.formwidgets.resizing_textarea', 'event', 'event-simulate',
+          'plugin', function(Y) {
 
-var SAMPLE_HTML = "                                                                           \
- <h1>Single-line editing</h1>                                                                 \
-  <div id='editable_single_text'>                                                             \
-    <span id='single_text' class='yui3-editable_text-text'>Some editable inline text.</span>  \
-    <button id='single_edit' class='yui3-editable_text-trigger'>Edit this</button>            \
-  </div>                                                                                      \
-  <hr />                                                                                      \
-  <h1>Multi-line editing</h1>                                                                 \
-  <div id='editable_multi_text'>                                                              \
-    <button id='multi_edit' class='yui3-editable_text-trigger'>Edit this</button>             \
-    <span id='multi_text' class='yui3-editable_text-text'>                                    \
-      <p>Some editable multi-line text.</p></span>                                            \
-  </div>                                                                                      \
+var SAMPLE_HTML = "                                                        \
+<h1>Single-line editing</h1>                                               \
+ <div id='editable_single_text'>                                           \
+   <span id='single_text'                                                  \
+    class='yui3-editable_text-text'>Some editable inline text.</span>      \
+   <button id='single_edit' class='yui3-editable_text-trigger'>            \
+            Edit this</button>                                             \
+ </div>                                                                    \
+ <hr />                                                                    \
+ <h1>Multi-line editing</h1>                                               \
+ <div id='editable_multi_text'>                                            \
+   <button id='multi_edit' class='yui3-editable_text-trigger'>             \
+        Edit this</button>                                                 \
+   <span id='multi_text' class='yui3-editable_text-text'>                  \
+     <p>Some editable multi-line text.</p></span>                          \
+ </div>                                                                    \
 ";
 
 var Assert = Y.Assert;  // For easy access to isTrue(), etc.
@@ -501,7 +505,7 @@
         this.editor.render();
         var input = this.editor.get('input_field');
         Assert.areEqual(
-            this.expected_size + 'ex',
+            this.expected_size + 'px',
             input.getStyle('width'),
             "The editor's input field size should have been set from the " +
             "'size' attribute.");
@@ -876,7 +880,7 @@
         Assert.areNotEqual(
             multi_height,
             single_height,
-            "Multi-line and single-line editors should have different sizes.");
+            "Multi-line and single-line should have different sizes.");
         Assert.isTrue(
             multi_height > single_height,
             "Multi-line editor should start out larger.");
@@ -900,265 +904,6 @@
 }));
 
 suite.add(new Y.Test.Case({
-
-    name: "Editor input sizing",
-
-    setUp: function() {
-        this.expected_size = 23;
-        this.editor = make_editor({
-            contentBox: '#editable_single_text',
-            size: this.expected_size
-        });
-        this.editor.render();
-
-        this.long_line = 'mm mmmmm mmmmmmm mm mmm mm mmmmmmm mmmmmmmm mm mmm m';
-        this.long_word = 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm';
-        this.short_line = 'hi mom';
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    test_size_attribute_passthrough: function() {
-        // create a new editor with the input size set to 23 characters.
-        Assert.areEqual(
-            this.expected_size,
-            this.editor.get('size'),
-            "The inline editor widget should have received a size " +
-            "from the EditableText widget.");
-    },
-
-    test_long_text_wraps: function() {
-        var editor = this.editor,
-            input = editor.get('input_field'),
-            original_height = input.getStyle('height');
-
-        editor.setInput(this.long_line);
-        var new_height = input.getStyle('height');
-
-        Assert.areNotEqual(
-            original_height,
-            new_height,
-            "Inserting a long text should grow the input area.");
-        Assert.areEqual(
-            'hidden',
-            input.getStyle('overflow'),
-            "Scrollbars should be hidden when no unbreakable lines present.");
-        Assert.isTrue(
-            parse_size(new_height) > parse_size(original_height),
-            "A grown input area should be larger than before.");
-    },
-
-    test_long_words_scroll: function() {
-        var editor = this.editor,
-            input = editor.get('input_field'),
-            original_height = input.getStyle('height');
-
-        editor.setInput(this.long_word);
-        var new_height = input.getStyle('height');
-
-        Assert.areNotEqual(
-            original_height,
-            new_height,
-            "Long words should add legroom for a horizontal scrollbar.");
-        Assert.isTrue(
-            parse_size(new_height) > parse_size(original_height),
-            "A grown input area should be larger than before.");
-    },
-
-    test_resize_on_growth: function() {
-        var editor = this.editor,
-            input = editor.get('input_field');
-
-        var test = this;
-        var resized = false;
-        editor.on('ieditor:resized', function() {
-            resized = true;
-        });
-        input.set('value', this.long_line);
-        editor.updateSize();
-        Assert.isTrue(resized, "Editor resize event was not fired.");
-    },
-
-    test_resize_on_shrinkage: function() {
-        var editor = this.editor,
-            input = editor.get('input_field');
-
-        editor.setInput(this.long_line);
-
-        var test = this;
-        var resized = false;
-        editor.on('ieditor:resized', function() {
-            resized = true;
-        });
-        input.set('value', this.short_line);
-
-        editor.updateSize();
-        Assert.isTrue(resized, "Editor resize event was not fired.");
-    },
-
-    test_long_text_unwraps: function() {
-        var editor = this.editor,
-            input = editor.get('input_field');
-
-        editor.setInput(this.short_line);
-        var original_height = input.getStyle('height');
-
-        editor.setInput(this.long_line);
-        editor.setInput(this.short_line);
-        var new_height = input.getStyle('height');
-
-        Assert.areEqual(
-            original_height,
-            new_height,
-            "Removing lines of text should shrink the input area.");
-    },
-
-    test_long_words_unscroll: function() {
-        var editor = this.editor,
-            input = editor.get('input_field');
-
-	editor.setInput(this.short_line);
-	var original_height = input.getStyle('height');
-
-        editor.setInput(this.long_word);
-	editor.setInput(this.short_line);
-        var new_height = input.getStyle('height');
-
-        Assert.areEqual(
-            original_height,
-            new_height,
-            "Removing long words should remove legroom for scrollbar.");
-        Assert.areEqual(
-            'hidden',
-            input.getStyle('overflow'),
-            "Scrollbars should be hidden when long lines are removed.");
-    }
-}));
-
-suite.add(new Y.Test.Case({
-    name: "Window resizing",
-
-    setUp: function() {
-        this.small_size = 5;
-        this.large_size = 40;
-        this.short_line = 'i';
-        // A line that fits in small_size but not in large_size.
-        this.long_line = 'x xx x xx x xx x xx';
-        // A word that fits in small_size but not in large_size.
-        this.long_word = 'xxxxxxxxxxxxxxxxxxx';
-        this.editor = make_editor();
-        this.editor.render();
-    },
-
-    tearDown: function() {
-        cleanup_widget(this.editor);
-    },
-
-    // Pretend that a window resize has changed the editor's width to
-    // the given number of columns.  Also lets you set new contents for
-    // the input box.  Returns resulting input box height in pixels.
-    _pretendResize: function(new_size, new_text) {
-        var text = (new_text ? new_text : this.editor.getInput());
-        var content_box = this.editor.get('contentBox');
-
-        var old_input = this.editor.get('input_field');
-        content_box.removeChild(old_input);
-        this.editor.set('input_field', null);
-
-        var old_alter_ego = this.editor.alter_ego;
-        content_box.removeChild(old_alter_ego);
-        this.editor.alter_ego = null;
-
-        this.editor.set('size', new_size);
-        this.editor._initInput();
-
-        this.editor.setInput(text);
-        this.editor._windowResize();
-        return this._getInputBoxHeight();
-    },
-
-    _getInputBoxHeight: function() {
-        var input = this.editor.get('input_field');
-        return parse_size(input.getStyle('height'));
-    },
-
-    // Helper: assert lower < higher, and print helpful message.
-    _assertLower: function(lower, higher, failure_text) {
-        if (!(lower < higher)) {
-            // Log values in separate statements to avoid infinite recursion
-            // during ill-typed attempts at string concatenation.
-            Y.log("Expected the first of these to be lower than the second:");
-            Y.log(lower);
-            Y.log(higher);
-        }
-        Assert.isTrue(lower < higher, failure_text);
-    },
-
-    test_resize_might_not_change_layout: function() {
-        var roomy = this._pretendResize(this.large_size, this.short_line);
-        var tight = this._pretendResize(this.small_size);
-        Assert.areEqual(
-            roomy,
-            tight,
-            "If there's enough room, resizing should not affect height.");
-    },
-
-    test_undersize_adds_lines: function() {
-        var roomy = this._pretendResize(this.large_size, this.long_line);
-        var tight = this._pretendResize(this.small_size);
-        this._assertLower(
-            roomy,
-            tight,
-            "Undersizing a long line should break it.");
-    },
-
-    test_oversize_removes_lines: function() {
-        var tight = this._pretendResize(this.small_size, this.long_line);
-        var roomy = this._pretendResize(this.large_size);
-        this._assertLower(
-            roomy,
-            tight,
-            "Oversizing a long line should unbreak it.");
-    },
-
-    test_undersize_adds_scrollbar: function() {
-        // Actually, a scrollbar and/or more lines.  The spec leaves it
-        // all up to the browser, but either way we'll see a higher
-        // input box.
-        var roomy = this._pretendResize(this.large_size, this.long_word);
-        var tight = this._pretendResize(this.small_size);
-        this._assertLower(
-            roomy,
-            tight,
-            "Undersizing a long word should require a taller input box.");
-    },
-
-    test_oversize_removes_scrollbar: function() {
-        var tight = this._pretendResize(this.small_size, this.long_word);
-        var roomy = this._pretendResize(this.large_size);
-        this._assertLower(
-            roomy,
-            tight,
-            "Oversizing a long word should reduce input box height.");
-    },
-
-    test_resize_works_while_hidden: function() {
-        var roomy = this._pretendResize(this.large_size, this.long_line);
-        this.editor.hide();
-        var tight = this._pretendResize(this.small_size);
-        this.editor.show();
-        this.editor.updateSize();
-        Assert.areNotEqual(
-            roomy,
-            tight,
-            "Editors should notice window resizes even while hidden.");
-    }
-}));
-
-
-suite.add(new Y.Test.Case({
     name: "EditableText text value",
 
     setUp: function() {