Merge lp:~rharding/launchpad/bugfix_891735 into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 14379
Proposed branch: lp:~rharding/launchpad/bugfix_891735
Merge into: lp:launchpad
Diff against target: 470 lines (+422/-1)
5 files modified
lib/lp/app/javascript/formwidgets/resizing_textarea.js (+209/-0)
lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html (+32/-0)
lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js (+166/-0)
lib/lp/bugs/javascript/filebug_dupefinder.js (+14/-1)
lib/lp/bugs/javascript/tests/test_filebug_dupfinder.html (+1/-0)
To merge this branch: bzr merge lp:~rharding/launchpad/bugfix_891735
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+83217@code.launchpad.net

This proposal supersedes a proposal from 2011-11-22.

Commit message

[r=abentley][bug=891735] create new YUI plugin for auto resizing textarea ui widgets to the formwidget library and add the new widget to the bug report comment textarea

Description of the change

= Summary =
Add a new auto resizing textarea widget for launchpad

== Proposed fix ==
Added a new module under the lp.app.formwidgets namespace called ResizingTextarea. This is a YUI plugin and can be stuck on any textarea YUI node instances.

== Pre-implementation notes ==
Attempted to use a YUI Gallery module that lacked features and performed poorly. Talked with Deryck and we agreed to work on our own module. Deryck pointed me towards the functionality in the inline edit module and to check if we could use that.

I took a peek at the inline editor module, and it does some of the same thing, but it's not modular enough to just use for simple textareas like the initial sample case of the new bug submitting form. It's also less missing some types of changes into the textarea and missing features such as the min and max height code.

Discussed with Deryck and abently and decided that this can proceed, with the understanding that the next goal will be refactoring the inline edit module to use this plugin as a means to manage its own auto resizing behavior. The inline edit will then only need to worry about button, save/cancel events, and such.

== Implementation details ==
The new plugin uses a YUI specific "valueChanged" event for detecting if the content of an input has changed by any means (pasting, key stroke, etc).

It removes the browsers feature for users to resize the textarea via the corner grab point since that can throw off the calculations of the size of the textarea.

It accepts parameters for min and max heights to help duplicate the functionality of defining rows in a textarea.

The adding and removing of rows of space are animated via a css3 transition. It should gracefull degrade on older browsers. There is a flag for turning off all animations. That's used to help keep tests from taking too long to run.

== Tests ==
./bin/test -cvvt test_resizing_textarea.html
$browser lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html

== Demo and Q/A ==
The tests are the main Q/A right now. The single point where it's created is when adding a new bug. So go to:
https://bugs.launchpad.dev/launchpad/+filebug

Enter a summary, the "Further information" textarea should be an instance of ResizingTextarea and as you enter new lines it will start to expand. There's a default 450px height so at that point, the scrollbar should show up and begin to lengthen.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote : Posted in a previous version of this proposal

I'm confused - the bug description editing field already autosizes
(poorly, but it does) - is that not widget already?

Revision history for this message
Deryck Hodge (deryck) wrote : Posted in a previous version of this proposal

The inline editor widget does resizing as one of among a hundred other things. :) The inline editor's resizing is not reusable at all. For one, the multi-line editor which uses text areas is bound to the same widget for editing single-line text inputs. So the current editor widget is poorly designed and broken in lots of little ways.

I suggested Rick add this current work as a way to plug text areas, thinking it would give us a generic widget for re-use across Launchpad for this pattern, and also give us something to change out to for the resizing bits in the inline editor.

I think fixing the inline editor at the same time as adding this widget is too much. The inline editor is a lot work by itself.

Revision history for this message
Aaron Bentley (abentley) wrote :

Rick will be refactoring the inline editor, so the functionality duplication should be short-term.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'lib/lp/app/javascript/formwidgets/resizing_textarea.js'
--- lib/lp/app/javascript/formwidgets/resizing_textarea.js 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/formwidgets/resizing_textarea.js 2011-11-24 03:57:28 +0000
@@ -0,0 +1,209 @@
1/**
2 * Copyright 2011 Canonical Ltd. This software is licensed under the
3 * GNU Affero General Public License version 3 (see the file LICENSE).
4 *
5 * Auto Resizing Textarea Widget.
6 *
7 * Usage:
8 * Y.one('#myid').plug(ResizingTextarea);
9 * Y.one('#settings').plug(ResizingTextarea, {
10 * min_height: 100
11 * });
12 *
13 * Y.all('textarea').plug(ResizingTextarea);
14 *
15 * @module lp.app.formwidgets
16 * @submodule resizing_textarea
17 */
18YUI.add('lp.app.formwidgets.resizing_textarea', function(Y) {
19
20var module = Y.namespace("lp.app.formwidgets"),
21 ResizingTextarea = function(cfg) {
22 ResizingTextarea.superclass.constructor.apply(this, arguments);
23 };
24
25ResizingTextarea.NAME = "resizing_textarea";
26ResizingTextarea.NS = "resizing_textarea";
27
28/**
29 * ATTRS you can set on initialization to determine how we size the textarea
30 */
31ResizingTextarea.ATTRS = {
32 /**
33 * Min height to allow the textarea to shrink to in px
34 *
35 * We check if there's a css rule for existing height and make that the
36 * min height in case it's there
37 *
38 * @property min_height
39 */
40 min_height: {
41 value: 10,
42
43 valueFn: function () {
44 var target = this.get("host");
45 var css_height = target.getStyle('height');
46
47 return !Y.Lang.isUndefined(css_height) ?
48 this._clean_size(css_height) : undefined;
49 }
50 },
51
52 /**
53 * Max height to allow the textarea to grow to in px
54 *
55 * @property max_height
56 */
57 max_height: {
58 value: 450
59 },
60
61 /**
62 * Should we bypass animating changes in height
63 * Mainly used to turn off for testing to prevent needing to set timeouts
64 *
65 * @property skip_animations
66 */
67 skip_animations: {value: false}
68};
69
70Y.extend(ResizingTextarea, Y.Plugin.Base, {
71
72 // special css we add to clones to make sure they're hidden from view
73 CLONE_CSS: {
74 position: 'absolute',
75 top: -9999,
76 left: -9999,
77 opacity: 0,
78 overflow: 'hidden',
79 resize: 'none'
80 },
81
82 /**
83 * Helper function to turn the string from getComputedStyle to int
84 */
85 _clean_size: function (val) {
86 return parseInt(val.replace('px', ''), 10);
87 },
88
89 // used to track if we're growing/shrinking for each event fired
90 _prev_scroll_height: 0,
91
92 /**
93 * This is the entry point for the event of change
94 *
95 * Here we update the clone and resize based on the update
96 */
97 _run_change: function (new_value) {
98 // we need to update the clone with the content so it resizes
99 this.clone.set('text', new_value);
100 this.resize();
101 },
102
103 /**
104 * Given a node, setup a clone so that we can use it for sizing
105 *
106 * We need to copy this, move it way off the screen and setup some css we
107 * use to make sure that we match the original as best as possible.
108 *
109 * This clone is then checked for the size to use
110 */
111 _setup_clone: function (node) {
112 var clone = node.cloneNode(true);
113
114 clone.setStyles(this.CLONE_CSS);
115 // remove attributes so we don't accidentally grab this node in the
116 // future
117 clone.removeAttribute('id');
118 clone.removeAttribute('name');
119 clone.generateID();
120 clone.setAttrs({
121 'tabIndex': -1,
122 'height': 'auto'
123 });
124 Y.one('body').append(clone);
125
126 return clone;
127 },
128
129 /**
130 * We need to apply some special css to our target we want to resize
131 */
132 _setup_css: function () {
133 // don't let this text area be resized in the browser, it'll mess with
134 // our calcs and we'll be fighting the whole time for the right size
135 this.t_area.setStyle('resize', 'none');
136 this.t_area.setStyle('overflow', 'hidden');
137
138 // we want to add some animation to our adjusting of the size, using
139 // css animation to smooth all height changes
140 if (!this.get('skip_animations')) {
141 this.t_area.setStyle('transition', 'height 0.3s ease');
142 this.t_area.setStyle('-webkit-transition', 'height 0.3s ease');
143 this.t_area.setStyle('-moz-transition', 'height 0.3s ease');
144 }
145 },
146
147 initializer : function(cfg) {
148 this.t_area = this.get("host");
149 this._setup_css(this.t_area);
150
151 // we need to setup the clone of this node so we can check how big it
152 // is, but way off the screen so you don't see it
153 this.clone = this._setup_clone(this.t_area);
154
155 // look at adjusting the size on any value change event including
156 // pasting and such
157 this.t_area.on('valueChange', function(e) {
158 this._run_change(e.newVal);
159 }, this);
160
161 // initial sizing in case there's existing content to match to
162 this.resize();
163 },
164
165 /**
166 * Adjust the size of the textarea as needed
167 *
168 * @method resize
169 */
170 resize: function() {
171 var scroll_height = this.clone.get('scrollHeight');
172
173 // only update the height if we've changed
174 if (this._prev_scroll_height !== scroll_height) {
175 new_height = Math.max(
176 this.get('min_height'),
177 Math.min(scroll_height, this.get('max_height')));
178
179 this.t_area.setStyle('height', new_height);
180
181 // check if the changes above require us to change our overflow
182 // setting to allow for a scrollbar now that our max size has been
183 // reached
184 this.set_overflow();
185
186 this._prev_scroll_height = scroll_height;
187 }
188 },
189
190 /**
191 * Check if we're larger than the max_height setting and enable scrollbar
192 *
193 * @method set_overflow
194 */
195 set_overflow: function() {
196 var overflow = "hidden";
197 if (this.clone.get('scrollHeight') >= this.get('max_height')) {
198 overflow = "auto";
199 }
200 this.t_area.setStyle('overflow', overflow);
201 }
202});
203
204// add onto the formwidget namespace
205module.ResizingTextarea = ResizingTextarea;
206
207}, "0.1", {
208 "requires": ["plugin", "node", "event-valuechange"]
209});
0210
=== added file 'lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html'
--- lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html 2011-11-24 03:57:28 +0000
@@ -0,0 +1,32 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html>
4 <head>
5 <title>Resizing Textarea Plugin</title>
6
7 <!-- YUI and test setup -->
8 <script type="text/javascript"
9 src="../../../../../canonical/launchpad/icing/yui/yui/yui.js">
10 </script>
11 <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
12 <script type="text/javascript"
13 src="../../../../app/javascript/testing/testrunner.js"></script>
14
15 <!-- The module under test -->
16 <script type="text/javascript" src="../resizing_textarea.js"></script>
17
18 <!-- The test suite -->
19 <script type="text/javascript" src="test_resizing_textarea.js"></script>
20
21</head>
22<body class="yui3-skin-sam">
23 <!-- We're going to test interacting with dom elements, so let's have some -->
24 <textarea id="init">Initial text</textarea>
25 <textarea id="with_defaults">has defaults</textarea>
26 <textarea id="shrinkage"></textarea>
27 <textarea id="multiple1" class="test_multiple first"></textarea>
28 <textarea id="multiple2" class="test_multiple second"></textarea>
29 <textarea id="css_height" style="height: 120px;"></textarea>
30</body>
31
32</html>
033
=== added file 'lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js'
--- lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js 1970-01-01 00:00:00 +0000
+++ lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js 2011-11-24 03:57:28 +0000
@@ -0,0 +1,166 @@
1/* Copyright (c) 2009, Canonical Ltd. All rights reserved. */
2
3YUI().use('lp.testing.runner', 'test', 'console', 'node', 'event',
4 'node-event-simulate', 'event-valuechange', 'plugin',
5 'lp.app.formwidgets.resizing_textarea', function(Y) {
6
7
8var test_text = ["Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
9 "Maecenas ut viverra nibh. Morbi sit amet tellus accumsan justo rutrum",
10 "blandit sit amet ac augue. Pellentesque eget diam at purus suscipit",
11 "venenatis. Proin non neque lacus. Curabitur venenatis tempus sem, vitae",
12 "porttitor magna fringilla vel. Cras dignissim egestas lacus nec",
13 "hendrerit. Proin pharetra, felis ac auctor dapibus, neque orci commodo ",
14 "lorem, sit amet posuere erat quam euismod arcu. Nulla pharetra augue at",
15 "enim tempus faucibus. Sed dictum tristique nisl sed rhoncus. Etiam ",
16 "tristique nisl eget risus blandit iaculis. Lorem ipsum dolor sit amet,",
17 "consectetur adipiscing elit."].join("");
18
19/**
20 * Helper function to turn the string from getComputedStyle to int
21 *
22 */
23function clean_size(val) {
24 return parseInt(val.replace('px', ''), 10);
25}
26
27/**
28 * Helper to extract the computed height of the element
29 *
30 */
31function get_height(target) {
32 return clean_size(target.getComputedStyle('height'));
33}
34
35/**
36 * In order to update the content we need to change the text, but also to fire
37 * the event that the content has changed since we're modifying it
38 * programatically
39 *
40 */
41function update_content(target, val) {
42 target.set('value', val);
43
44 // instead of hitting the changed event directly, we'll just manually call
45 // into the hook for the event itself
46 target.resizing_textarea._run_change(val);
47}
48
49var suite = new Y.Test.Suite("Resizing Textarea Tests");
50
51suite.add(new Y.Test.Case({
52
53 name: 'resizing_textarea',
54
55 test_initial_resizable: function() {
56 var target = Y.one('#init');
57
58 Y.Assert.areEqual('Initial text', target.get('value'));
59
60 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
61 skip_animations: true
62 });
63
64 // get the current sizes so we can pump text into it and make sure it
65 // grows
66 var orig_height = get_height(target);
67 update_content(target, test_text);
68
69 var new_height = get_height(target);
70 Y.Assert.isTrue(new_height > orig_height,
71 "The height should increase with content");
72
73 },
74
75 test_max_height: function () {
76 var target = Y.one('#with_defaults');
77
78 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
79 skip_animations: true,
80 max_height: 200,
81 min_height: 100
82 });
83
84 var min_height = get_height(target);
85 Y.Assert.areSame(100, min_height,
86 "The height should be no smaller than 100px");
87
88 update_content(target, test_text);
89
90 var new_height = get_height(target);
91 Y.Assert.areSame(200, new_height,
92 "The height should only get to 200px");
93 },
94
95 test_removing_content: function () {
96 var target = Y.one('#shrinkage');
97
98 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
99 skip_animations: true,
100 min_height: 100
101 });
102
103 update_content(target, test_text);
104 var max_height = get_height(target);
105 Y.Assert.isTrue(max_height > 100,
106 "The height should be larger than our min with content");
107
108 update_content(target, "shrink");
109
110 var min_height = get_height(target);
111 Y.Assert.areSame(100, min_height,
112 "The height should shrink back to our min");
113 },
114
115 test_multiple: function () {
116 var target = Y.all('.test_multiple');
117
118 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
119 skip_animations: true,
120 min_height: 100
121 });
122
123 target.each(function (node) {
124 var min_height = get_height(node);
125 Y.Assert.areSame(100, min_height,
126 "The height of the node should be 100");
127 });
128
129 // now set the content in the first one and check it's unique
130 update_content(Y.one('.first'), test_text);
131
132 var first = Y.one('.first');
133 var second = Y.one('.second');
134
135 var first_height = get_height(first);
136 Y.Assert.isTrue(first_height > 100,
137 "The height of the first should now be > 100");
138
139 var second_height = get_height(second);
140 Y.Assert.areSame(100, second_height,
141 "The height of the second should still be 100");
142 },
143
144 test_css_height_preset: function () {
145 var target = Y.one('#css_height');
146
147 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
148 skip_animations: true
149 });
150
151 var current_height = get_height(target);
152 Y.Assert.areSame(120, current_height,
153 "The height should match the css property at 120px");
154 }
155}));
156
157var yconsole = new Y.Console({
158 newestOnTop: false
159});
160yconsole.render('#log');
161
162Y.on('load', function (e) {
163 Y.lp.testing.Runner.run(suite);
164});
165
166});
0167
=== modified file 'lib/lp/bugs/javascript/filebug_dupefinder.js'
--- lib/lp/bugs/javascript/filebug_dupefinder.js 2011-07-18 15:07:40 +0000
+++ lib/lp/bugs/javascript/filebug_dupefinder.js 2011-11-24 03:57:28 +0000
@@ -197,6 +197,17 @@
197 show_bug_reporting_form();197 show_bug_reporting_form();
198 }198 }
199199
200 // now we need to wire up the text expander after we load our textarea
201 // onto the page, but only if we find one on the page. In testing we
202 // don't have one. We want to just fail silently.
203 var textarea = Y.one("#bug-reporting-form textarea");
204 if (textarea) {
205 textarea.plug(
206 Y.lp.app.formwidgets.ResizingTextarea, {
207 min_height: 300
208 });
209 }
210
200 // Copy the value from the search field into the title field211 // Copy the value from the search field into the title field
201 // on the filebug form.212 // on the filebug form.
202 Y.one('#bug-reporting-form input[name=field.title]').set(213 Y.one('#bug-reporting-form input[name=field.title]').set(
@@ -346,6 +357,7 @@
346 // confuse the view when we submit a bug report.357 // confuse the view when we submit a bug report.
347 search_field.set('name', 'field.search');358 search_field.set('name', 'field.search');
348 search_field.set('id', 'field.search');359 search_field.set('id', 'field.search');
360
349 // Set up the handler for the search form.361 // Set up the handler for the search form.
350 var search_form = Y.one('#filebug-search-form');362 var search_form = Y.one('#filebug-search-form');
351 search_form.on('submit', function(e) {363 search_form.on('submit', function(e) {
@@ -487,4 +499,5 @@
487499
488}, "0.1", {"requires": [500}, "0.1", {"requires": [
489 "base", "io", "oop", "node", "event", "json", "lazr.formoverlay",501 "base", "io", "oop", "node", "event", "json", "lazr.formoverlay",
490 "lazr.effects", "lp.app.widgets.expander"]});502 "lazr.effects", "lp.app.widgets.expander",
503 "lp.app.formwidgets.resizing_textarea", "plugin"]});
491504
=== modified file 'lib/lp/bugs/javascript/tests/test_filebug_dupfinder.html'
--- lib/lp/bugs/javascript/tests/test_filebug_dupfinder.html 2011-07-08 06:06:15 +0000
+++ lib/lp/bugs/javascript/tests/test_filebug_dupfinder.html 2011-11-24 03:57:28 +0000
@@ -18,6 +18,7 @@
18 <!-- Some required dependencies -->18 <!-- Some required dependencies -->
19 <script type="text/javascript" src="../../../app/javascript/effects/effects.js"></script>19 <script type="text/javascript" src="../../../app/javascript/effects/effects.js"></script>
20 <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>20 <script type="text/javascript" src="../../../app/javascript/overlay/overlay.js"></script>
21 <script type="text/javascript" src="../../../app/javascript/formwidgets/resizing_textarea.js"></script>
2122
22 <!-- The module under test -->23 <!-- The module under test -->
23 <script type="text/javascript"24 <script type="text/javascript"