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

Proposed by Richard Harding
Status: Superseded
Proposed branch: lp:~rharding/launchpad/bugfix_891735
Merge into: lp:launchpad
Diff against target: 475 lines (+431/-1)
4 files modified
lib/lp/app/javascript/formwidgets/resizing_textarea.js (+231/-0)
lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.html (+32/-0)
lib/lp/app/javascript/formwidgets/tests/test_resizing_textarea.js (+158/-0)
lib/lp/bugs/javascript/filebug_dupefinder.js (+10/-1)
To merge this branch: bzr merge lp:~rharding/launchpad/bugfix_891735
Reviewer Review Type Date Requested Status
Aaron Bentley Pending
Review via email: mp+83068@code.launchpad.net

This proposal has been superseded by a proposal from 2011-11-23.

Commit message

Add new lp.app.formwidget.resizing_textarea to implement an auto resizing textarea widget for all form use
Add this new widget to the new bug process on the +filebug url
Add tests for the use cases of this working through the common settings and use cases

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.

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.

== 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 ability for webkit browsers to resize the textarea 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 :

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 :

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.

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-23 19:42:29 +0000
@@ -0,0 +1,231 @@
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 *
31 */
32ResizingTextarea.ATTRS = {
33 /**
34 * Min height to allow the textarea to shrink to in px
35 *
36 * We check if there's a css rule for existing height and make that the min
37 * height in case it's there
38 *
39 * @property min_height
40 *
41 */
42 min_height: {
43 value: 10,
44
45 valueFn: function () {
46 var target = this.get("host"),
47 css_height = target.getStyle('height');
48
49 return css_height !== undefined ?
50 this._clean_size(css_height) : undefined;
51 }
52 },
53
54 /**
55 * Max height to allow the textarea to grow to in px
56 *
57 * @property max_height
58 *
59 */
60 max_height: {
61 value: 450
62 },
63
64 /**
65 * Should we bypass animating changes in height
66 * Mainly used to turn off for testing to prevent needing to set timeouts
67 *
68 * @property skip_animations
69 *
70 */
71 skip_animations: {value: false}
72};
73
74Y.extend(ResizingTextarea, Y.Plugin.Base, {
75
76 // special css we add to clones to make sure they're hidden from view
77 CLONE_CSS: {
78 position: 'absolute',
79 top: -9999,
80 left: -9999,
81 opacity: 0,
82 overflow: 'hidden',
83 resize: 'none'
84 },
85
86 /**
87 * Helper function to turn the string from getComputedStyle to int
88 *
89 */
90 _clean_size: function (val) {
91 return parseInt(val.replace('px', ''), 10);
92 },
93
94 // used to track if we're growing/shrinking for each event fired
95 _prev_scroll_height: 0,
96
97 _bind_events: function () {
98 // look at adjusting the size on any value change event including
99 // pasting and such
100 this.t_area.on('valueChange', function(e) {
101 this._run_change(e.newVal);
102 }, this);
103 },
104
105 /**
106 * This is the entry point for the event of change
107 *
108 * Here we update the clone and resize based on the update
109 *
110 */
111 _run_change: function (new_value) {
112 // we need to update the clone with the content so it resizes
113 this.clone.set('text', new_value);
114 this.resize();
115 },
116
117 /**
118 * Given a node, setup a clone so that we can use it for sizing
119 *
120 * We need to copy this, move it way off the screen and setup some css we
121 * use to make sure that we match the original as best as possible.
122 *
123 * This clone is then checked for the size to use
124 *
125 */
126 _setup_clone: function (node) {
127 var clone = node.cloneNode(true);
128
129 clone.setStyles(this.CLONE_CSS);
130 // remove attributes so we don't accidentally grab this node in the
131 // future
132 clone.removeAttribute('id');
133 clone.removeAttribute('name');
134 clone.generateID();
135 clone.setAttrs({
136 'tabIndex': -1,
137 'height': 'auto'
138 });
139 Y.one('body').append(clone);
140
141 return clone;
142 },
143
144 /**
145 * We need to apply some special css to our target we want to resize
146 *
147 */
148 _setup_css: function () {
149 // don't let this text area be resized in the browser, it'll mess with our
150 // calcs and we'll be fighting the whole time for the right size
151 this.t_area.setStyle('resize', 'none');
152 this.t_area.setStyle('overflow', 'hidden');
153
154 // we want to add some animation to our adjusting of the size, using
155 // css animation to smooth all height changes
156 if (!this.get('skip_animations')) {
157 this.t_area.setStyle('transition', 'height 0.3s ease');
158 this.t_area.setStyle('-webkit-transition', 'height 0.3s ease');
159 this.t_area.setStyle('-moz-transition', 'height 0.3s ease');
160 }
161 },
162
163 initializer : function(cfg) {
164 this.t_area = this.get("host");
165 this._setup_css(this.t_area);
166
167 // we need to setup the clone of this node so we can check how big it
168 // is, but way off the screen so you don't see it
169 this.clone = this._setup_clone(this.t_area);
170
171 // we want to start out saying we're at our minimum size
172 this._prev_scroll_height = this.get('min_height');
173
174 this._bind_events();
175
176 // initial sizing in case there's existing content to match to
177 this.resize();
178 },
179
180 /**
181 * Adjust the size of the textarea as needed
182 *
183 * @method resize
184 *
185 */
186 resize: function() {
187 var scroll_height = this.clone.get('scrollHeight');
188 console.log('scroll', scroll_height);
189 console.log('prev', this._prev_scroll_height);
190 console.log('min', this.get('min_height'));
191 console.log('max', this.get('max_height'));
192
193
194
195
196 // only update the height if we've changed
197 if (this._prev_scroll_height !== scroll_height) {
198 new_height = Math.max(this.get('min_height'),
199 Math.min(scroll_height, this.get('max_height')));
200
201 this.t_area.setStyle('height', new_height);
202
203 // check if the changes above require us to change our overflow setting
204 // to allow for a scrollbar now that our max size has been reached
205 this.set_overflow();
206
207 this._prev_scroll_height = scroll_height;
208 }
209 },
210
211 /**
212 * Check if we're larger than the max_height setting and enable scrollbar
213 *
214 * @method set_overflow
215 *
216 */
217 set_overflow: function() {
218 var overflow = "hidden";
219 if (this.clone.get('scrollHeight') >= this.get('max_height')) {
220 overflow = "auto";
221 }
222 this.t_area.setStyle('overflow', overflow);
223 }
224});
225
226// add onto the formwidget namespace
227module.ResizingTextarea = ResizingTextarea;
228
229}, "0.1", {
230 "requires": ["plugin", "node", "event-valuechange"]
231});
0232
=== 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-23 19:42:29 +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-23 19:42:29 +0000
@@ -0,0 +1,158 @@
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
7var Assert = Y.Assert; // For easy access to isTrue(), etc.
8
9var test_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut viverra nibh. Morbi sit amet tellus accumsan justo rutrum blandit sit amet ac augue. Pellentesque eget diam at purus suscipit venenatis. Proin non neque lacus. Curabitur venenatis tempus sem, vitae porttitor magna fringilla vel. Cras dignissim egestas lacus nec hendrerit. Proin pharetra, felis ac auctor dapibus, neque orci commodo lorem, sit amet posuere erat quam euismod arcu. Nulla pharetra augue at enim tempus faucibus. Sed dictum tristique nisl sed rhoncus. Etiam tristique nisl eget risus blandit iaculis. Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
10
11/**
12 * Helper function to turn the string from getComputedStyle to int
13 *
14 */
15function clean_size(val) {
16 return parseInt(val.replace('px', ''), 10);
17}
18
19/**
20 * Helper to extract the computed height of the element
21 *
22 */
23function get_height(target) {
24 return clean_size(target.getComputedStyle('height'));
25}
26
27/**
28 * In order to update the content we need to change the text, but also to fire
29 * the event that the content has changed since we're modifying it
30 * programatically
31 *
32 */
33function update_content(target, val) {
34 target.set('value', val);
35
36 // instead of hitting the changed event directly, we'll just manually call
37 // into the hook for the event itself
38 target.resizing_textarea._run_change(val);
39}
40
41var suite = new Y.Test.Suite("Resizing Textarea Tests");
42
43suite.add(new Y.Test.Case({
44
45 name: 'resizing_textarea',
46
47 test_initial_resizable: function() {
48 var target = Y.one('#init');
49
50 Assert.areEqual('Initial text', target.get('value'));
51
52 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
53 skip_animations: true
54 });
55
56 // get the current sizes so we can pump text into it and make sure it
57 // grows
58 var orig_height = get_height(target);
59 update_content(target, test_text);
60
61 var new_height = get_height(target);
62 Assert.isTrue(new_height > orig_height,
63 "The height should increase with content");
64
65 },
66
67 test_max_height: function () {
68 var target = Y.one('#with_defaults');
69
70 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
71 skip_animations: true,
72 max_height: 200,
73 min_height: 100
74 });
75
76 var min_height = get_height(target);
77 Assert.isTrue(min_height === 100,
78 "The height should be no smaller than 100px");
79
80 update_content(target, test_text);
81
82 var new_height = get_height(target);
83 Assert.isTrue(new_height === 200,
84 "The height should only get to 200px");
85 },
86
87 test_removing_content: function () {
88 var target = Y.one('#shrinkage');
89
90 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
91 skip_animations: true,
92 min_height: 100
93 });
94
95 update_content(target, test_text);
96 var max_height = get_height(target);
97 Assert.isTrue(max_height > 100,
98 "The height should be larger than our min with content");
99
100 update_content(target, "shrink");
101
102 var min_height = get_height(target);
103 Assert.isTrue(min_height === 100,
104 "The height should shrink back to our min");
105 },
106
107 test_multiple: function () {
108 var target = Y.all('.test_multiple');
109
110 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
111 skip_animations: true,
112 min_height: 100
113 });
114
115 target.each(function (n) {
116 var min_height = get_height(n);
117 Assert.isTrue(min_height === 100,
118 "The height of the node should be 100");
119 });
120
121 // now set the content in the first one and check it's unique
122 update_content(Y.one('.first'), test_text);
123
124 var first = Y.one('.first'),
125 second = Y.one('.second');
126
127 var first_height = get_height(first);
128 Assert.isTrue(first_height > 100,
129 "The height of the first should now be > 100");
130
131 var second_height = get_height(second);
132 Assert.isTrue(second_height === 100,
133 "The height of the second should still be == 100: " + get_height(second));
134 },
135
136 test_css_height_preset: function () {
137 var target = Y.one('#css_height');
138
139 target.plug(Y.lp.app.formwidgets.ResizingTextarea, {
140 skip_animations: true,
141 });
142
143 var current_height = get_height(target);
144 Assert.isTrue(current_height === 120,
145 "The height should match the css property at 120px");
146 }
147}));
148
149var yconsole = new Y.Console({
150 newestOnTop: false
151});
152yconsole.render('#log');
153
154Y.on('load', function (e) {
155 Y.lp.testing.Runner.run(suite);
156});
157
158});
0159
=== 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-23 19:42:29 +0000
@@ -197,6 +197,13 @@
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
202 Y.one("#bug-reporting-form textarea").plug(
203 Y.lp.app.formwidgets.ResizingTextarea, {
204 min_height: 300
205 });
206
200 // Copy the value from the search field into the title field207 // Copy the value from the search field into the title field
201 // on the filebug form.208 // on the filebug form.
202 Y.one('#bug-reporting-form input[name=field.title]').set(209 Y.one('#bug-reporting-form input[name=field.title]').set(
@@ -346,6 +353,7 @@
346 // confuse the view when we submit a bug report.353 // confuse the view when we submit a bug report.
347 search_field.set('name', 'field.search');354 search_field.set('name', 'field.search');
348 search_field.set('id', 'field.search');355 search_field.set('id', 'field.search');
356
349 // Set up the handler for the search form.357 // Set up the handler for the search form.
350 var search_form = Y.one('#filebug-search-form');358 var search_form = Y.one('#filebug-search-form');
351 search_form.on('submit', function(e) {359 search_form.on('submit', function(e) {
@@ -462,6 +470,7 @@
462 config = {on: {success: set_up_dupe_finder,470 config = {on: {success: set_up_dupe_finder,
463 failure: function() {}}};471 failure: function() {}}};
464472
473
465 // Load the filebug form asynchronously. If this fails we474 // Load the filebug form asynchronously. If this fails we
466 // degrade to the standard mode for bug filing, clicking through475 // degrade to the standard mode for bug filing, clicking through
467 // to the second part of the bug filing form.476 // to the second part of the bug filing form.
@@ -487,4 +496,4 @@
487496
488}, "0.1", {"requires": [497}, "0.1", {"requires": [
489 "base", "io", "oop", "node", "event", "json", "lazr.formoverlay",498 "base", "io", "oop", "node", "event", "json", "lazr.formoverlay",
490 "lazr.effects", "lp.app.widgets.expander"]});499 "lazr.effects", "lp.app.widgets.expander", "lp.app.formwidgets.resizing_textarea", "plugin"]});