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

Proposed by Richard Harding on 2012-09-11
Status: Merged
Approved by: Richard Harding on 2012-09-12
Approved revision: no longer in the source branch.
Merged at revision: 15975
Proposed branch: lp:~rharding/launchpad/productjs
Merge into: lp:launchpad
Prerequisite: lp:~rharding/launchpad/pp_register
Diff against target: 786 lines (+515/-235)
5 files modified
lib/lp/app/javascript/testing/testrunner.js (+3/-1)
lib/lp/registry/javascript/product_views.js (+369/-0)
lib/lp/registry/javascript/tests/test_product_views.html (+58/-0)
lib/lp/registry/javascript/tests/test_product_views.js (+74/-0)
lib/lp/registry/templates/product-new.pt (+11/-234)
To merge this branch: bzr merge lp:~rharding/launchpad/productjs
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-09-11 Approve on 2012-09-11
Review via email: mp+123803@code.launchpad.net

Commit Message

Pull product-new.pt JS into a new module and being some tests around it.

Description of the Change

TBD

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

#39: should be _information_type, not _inforation_type

#66: Is it necessary to prevent invalid characters from being typed? That seems like bad UI to me--if I (not knowing good urls) put in bad characters, but nothing happens, I feel like the app or my keyboard is broken. Better maybe to have a dynamic warning that comes up saying it's invalid and what the invalid character is? Or throw away the dynamics and just up the warning after typing ends/submission occurs and validation fails?

#266: Why are we writing our own render method? This will break renderUI and syncUI calls down the road if anyone wants to add that, right? Wouldn't all of this render code be better set in renderUI and called by the render cycle?

#291: Given they all say the same thing (barring the one one line #330) I'm not sure these comments help much? They're not telling me anything that I can't see just from looking--I tend to err on no comment are better than unneeded ones. If you have these b/c of auto-docs of some sort, I would prefer "Lazy load the search results node" etc over "the found node" since if I'm scanning auto-docs I'm probably not pulling up the function.

review: Needs Information
Richard Harding (rharding) wrote :

#39: You are correct. The next step is to add in code to try to figure out how to work around adding information type as a feature flag so this isn't currently processed.

#66 This was from the existing code. I just kept the regex into place. I'm not 100% sure on the best way, but that would be a follow up UX fix for submitting a project that might require some discussion/testing.

#266 I've attempted to keep with the Y.View api in YUI 3.5.1 as at some point this might be a View object vs just JS that runs on the page.

It doesn't involve renderUI and bindUI because those are specific to the Widget interface and this isn't a Widget. A Widget is a much smaller more reusable bit of JS/HTML combo while this is more a large View with events and code that occurs within it.

#291 Yea, I guess that not all node selections are done via ATTRS. I only pulled in the ones that were used in more than one place and noted that they're made ATTRS so that we don't redo the DOM lookup when it's not required. This is because I'm working on breaking apart the old code that one as one giant JS block into a series of calls I can work on splitting and testing as they break apart.

j.c.sackett (jcsackett) wrote :

> #39: You are correct. The next step is to add in code to try to figure out how
> to work around adding information type as a feature flag so this isn't
> currently processed.

Thank you.

> #66 This was from the existing code. I just kept the regex into place. I'm not
> 100% sure on the best way, but that would be a follow up UX fix for submitting
> a project that might require some discussion/testing.

Ah, yes, I missed that.

> #266 I've attempted to keep with the Y.View api in YUI 3.5.1 as at some point
> this might be a View object vs just JS that runs on the page.
>
> It doesn't involve renderUI and bindUI because those are specific to the
> Widget interface and this isn't a Widget. A Widget is a much smaller more
> reusable bit of JS/HTML combo while this is more a large View with events and
> code that occurs within it.

Ah, okay. I saw `render` and `bindUI` and thought `Widget`.

> #291 Yea, I guess that not all node selections are done via ATTRS. I only
> pulled in the ones that were used in more than one place and noted that
> they're made ATTRS so that we don't redo the DOM lookup when it's not
> required. This is because I'm working on breaking apart the old code that one
> as one giant JS block into a series of calls I can work on splitting and
> testing as they break apart.

Ok, thanks.

review: Approve
Richard Harding (rharding) wrote :

I couldn't land this due to a timeout error when running the test suite on ec2. Deryck found that flattening the namespace allowed the tests to pass.

In order to keep some structure, but with a smaller namespace, I've tried to setup a standard that drops the lp. since that's just something all our code has. It does mean we have to watch for collisions with the raw YUI library, but that's well documented.

What I am going for is a nesting based on the 'type' of code in question. In this case, I'm writing up code for an html view. This might have also been a model, utils, etc.

I also didn't nest within the registry namespace by doing registry.product. Instead, the name of the module is registry_product. In this way, there can be many modules that add code to the views.registry namespace. This module adds a class NewProduct. So the full name is Y.views.registry.NewProduct. I like that a lot better than the previous Y.lp.registry.product.views.New.

If there was a model that went with this to be used, it would be in Y.models.registry.Product. This takes us from 5 levels deep to 3 for most use cases I can think of and tries to keep it there.

Deryck Hodge (deryck) wrote :

I appreciate you making these changes, Rick, and I like the general approach you're taking. I agree that we drop the "lp" namespace. I'll follow up about this to the dev list. I. however, don't care for the "views" namespace. It's a bit generic, and could lead to conflict later, should the YUI team ever chose to use that for something related to their view code. I would vote for a couple of variations of the following...

Either using the app name for the first name space followed by the class; something like Y.registry.NewProduct. Or else dropping the app label as well, and doing something like Y.product.NewProduct, or Y.product.new. Or something like that. I'm flexible about the exact naming, but I'd like it to have some correlation to our python code and avoid overly general terms like "views." Let me know if that helps or if you need more clarification from me.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/javascript/testing/testrunner.js'
2--- lib/lp/app/javascript/testing/testrunner.js 2012-06-25 17:34:07 +0000
3+++ lib/lp/app/javascript/testing/testrunner.js 2012-09-18 15:14:29 +0000
4@@ -65,7 +65,9 @@
5 var suite_name = suite_node.get("text");
6 Y.use("lp.testing.runner", suite_name, function(y) {
7 var module = y, parts = suite_name.split(".");
8- while (parts.length > 0) { module = module[parts.shift()]; }
9+ while (parts.length > 0) {
10+ module = module[parts.shift()];
11+ }
12 y.lp.testing.Runner.run(module.suite);
13 });
14 });
15
16=== added file 'lib/lp/registry/javascript/product_views.js'
17--- lib/lp/registry/javascript/product_views.js 1970-01-01 00:00:00 +0000
18+++ lib/lp/registry/javascript/product_views.js 2012-09-18 15:14:29 +0000
19@@ -0,0 +1,369 @@
20+/* Copyright 2012 Canonical Ltd. This software is licensed under the
21+ * GNU Affero General Public License version 3 (see the file LICENSE).
22+ *
23+ * Support the UI for registering a new project.
24+ *
25+ * @module views.registry
26+ * @requires base, node, effects, lp.app.choice
27+ */
28+YUI.add('registry.product-views', function (Y) {
29+ var ns = Y.namespace('registry.views');
30+
31+ /**
32+ * Handle setting up the JS for the new project View
33+ *
34+ * @class registry.views.NewProduct
35+ * @extends Y.Base
36+ */
37+ ns.NewProduct = Y.Base.create('new-product-view', Y.Base, [], {
38+ // These two regexps serve slightly different purposes. The first
39+ // finds the leftmost run of valid url characters for the autofill
40+ // operation. The second validates the entire string, used for
41+ // explicit entry into the URL field. These are simple enough to keep
42+ // in sync so it doesn't bother me that we repeat most of it. Note
43+ // that while both ignore case, only the first one should be global in
44+ // order to utilize the RegExp.lastIndex behavior of .exec().
45+ valid_urls: new RegExp('^[a-z0-9][-.+a-z0-9]*', 'ig'),
46+ valid_char: new RegExp('^[a-z0-9][-.+a-z0-9]*$', 'i'),
47+
48+ /**
49+ * Process binding the UI for the information type choice widget.
50+ *
51+ * @method _bind_information-type
52+ * @private
53+ */
54+ _bind_information_type: function () {
55+ Y.lp.app.choice.addPopupChoiceForRadioButtons(
56+ 'information_type', LP.cache.information_type_data, true);
57+ },
58+
59+ /**
60+ * Bind the url/name field interaction
61+ *
62+ * @method _bind_name_field
63+ * @private
64+ */
65+ _bind_name_field: function () {
66+ var url_field = Y.one('input[id="field.name"]');
67+ var name_field = Y.one('input[id="field.displayname"]');
68+ name_field.on('keyup', this._url_autofill, this);
69+
70+ // Explicitly typing into the URL field disables autofilling.
71+ url_field.on('keyup', function(e) {
72+ if (url_field.get('value') === '') {
73+ // The user cleared the URL field; turn on autofill.
74+ name_field.on('keyup', this._url_autofill);
75+ } else {
76+ /* Honor the user's URL; turn off autofill. */
77+ name_field.detach('keyup', this._url_autofill);
78+ }
79+ }, this);
80+
81+ // Prevent invalid characters from being input into the URL field.
82+ url_field.on('keypress', function(e) {
83+ // Handling key events is madness. For a glimpse, see
84+ // http://unixpapa.com/js/key.html
85+ //
86+ // Additional spice for the insanity stew is given by the
87+ // rhino book, page 428. This code is basically a rip and
88+ // remix of those two texts.
89+ var event = e || window.event;
90+ var code = e.charCode || e.keyCode;
91+
92+ if (/* Check for special characters. */
93+ e.which === 0 || e.which === null ||
94+ /* Check for function keys (Firefox only). */
95+ e.charCode === 0 ||
96+ /* Check for ctrl or alt held down. */
97+ e.ctrlKey || e.altKey ||
98+ /* Check for ASCII control character */
99+ 32 > code)
100+ {
101+ return true;
102+ }
103+ var char = String.fromCharCode(code);
104+ var new_value = url_field.get('value') + char;
105+ if (new_value.search(this.valid_char) >= 0) {
106+ /* The character is valid. */
107+ return true;
108+ }
109+ e.preventDefault();
110+ e.returnValue = false;
111+ return false;
112+ }, this);
113+ },
114+
115+ /**
116+ * When the 'No' button is clicked, we swap in the registration
117+ * details for the search results. It really doesn't look good
118+ * to leave the search results there.
119+ *
120+ * @method _complete_registration
121+ * @param {Event}
122+ * @private
123+ */
124+ _complete_registration: function(ev) {
125+ var that = this;
126+ ev.halt();
127+ var step_title = Y.one('#step-title');
128+ var expander = Y.one('#search-results-expander');
129+
130+ /* Slide in the search results and hide them under a link. */
131+ expander.removeClass('hidden');
132+ expander.on('click', function(e) {
133+ e.halt();
134+
135+ var arrow = Y.one('#search-results-arrow');
136+ if (arrow.getAttribute('src') === '/@@/treeCollapsed') {
137+ // The search results are currently hidden. Slide them
138+ // out and turn the arrow to point downward.
139+ arrow.setAttribute('src', '/@@/treeExpanded');
140+ arrow.setAttribute('title', 'Hide search results');
141+ arrow.setAttribute('alt', 'Hide search results');
142+ Y.lp.ui.effects.slide_out(that.get('search_results')).run();
143+ that._show_separator(true);
144+ }
145+ else {
146+ // The search results are currently displayed. Slide them
147+ // in and turn the arrow to point rightward.
148+ arrow.setAttribute('src', '/@@/treeCollapsed');
149+ arrow.setAttribute('title', 'Show search results');
150+ arrow.setAttribute('alt', 'Show search results');
151+ Y.lp.ui.effects.slide_in(that.get('search_results')).run();
152+ that._show_separator(false);
153+ }
154+ }, this);
155+
156+ // Hide the 'No' button, but slide out the search results, so the
157+ // user has a clue that Something Is Happening.
158+ this.get('details_buttons').addClass('hidden');
159+
160+ // Slide out the registration details widgets, but add an 'end'
161+ // event handler so that the height style left by lp.ui.effects is
162+ // removed when the animation is done. We're never going to slide
163+ // the form widgets back in, and the height style causes the
164+ // licence widget to dive under the Complete Registration button.
165+ // See bug 391138 for details.
166+ var anim = Y.lp.ui.effects.slide_out(this.get('form_widgets'));
167+ anim.on('end', function() {
168+ that.get('form_widgets').setStyle('height', null);
169+ });
170+ anim.run();
171+
172+ // Toggle the visibility of the various other widgets.
173+ this.get('form_actions').removeClass('hidden');
174+ this.get('title').removeClass('hidden');
175+
176+ // Set the H2 title to something more appropriate for the
177+ // selected task.
178+ step_title.set('innerHTML', 'Step 2 (of 2) Registration details');
179+
180+ var reset_height = this.get('search_results').getComputedStyle('height');
181+ this.get('search_results').setStyle('height', reset_height);
182+ Y.lp.ui.effects.slide_in(this.get('search_results')).run();
183+
184+ // Append a special marker to the hidden state widget. See
185+ // ProjectAddStepTwo.search_results_count() for details.
186+ var steps = this.get('marker').getAttribute('value');
187+ if (0 > steps.search(new RegExp('hidesearch'))) {
188+ this.get('marker').setAttribute('value', steps + "|hidesearch");
189+ }
190+ },
191+
192+ /**
193+ * Handle the reveals when there are search results.
194+ *
195+ * @method _show_separator
196+ * @param {Boolean}
197+ * @private
198+ */
199+ _show_separator: function (flag) {
200+ var separator = Y.one('#registration-separator');
201+ if (!separator) {
202+ // The separator is not on the page, because there were no
203+ // search results.
204+ return;
205+ }
206+ if (flag) {
207+ separator.removeClass('hidden');
208+ } else {
209+ separator.addClass('hidden');
210+ }
211+ },
212+
213+ /**
214+ * Generate a url for the project based on the name.
215+ *
216+ * @method _url_autofill
217+ * @param {Event}
218+ * @private
219+ */
220+ _url_autofill: function (e) {
221+ var url_field = Y.one('input[id="field.name"]');
222+ var name_value = e.target.get('value');
223+ if (name_value === '') {
224+ /* When Name is empty, clear URL. */
225+ url_field.set('value', '');
226+ } else {
227+ // Fill the URL field with as much of the left part of the
228+ // string as matches the regexp. If the regexp doesn't
229+ // match (say because there's illegal stuff at the front),
230+ // don't change the current URL field. We have to reset
231+ // lastIndex each time we get here so that search begins
232+ // at the front of the string.
233+ this.valid_urls.lastIndex = 0;
234+ var match = this.valid_urls.exec(name_value);
235+ if (match) {
236+ var slice = name_value.slice(0, this.valid_urls.lastIndex);
237+ url_field.set('value', slice);
238+ }
239+ }
240+ },
241+
242+ /**
243+ * Bind the UI interactions that will be tracked through the View
244+ * lifecycle.
245+ *
246+ * @method bindUI
247+ */
248+ bindUI: function () {
249+ if (Y.one('input[name="field.information_type"]')) {
250+ this._bind_information_type();
251+ }
252+
253+ if(Y.one('input[id="field.name"]')) {
254+ this._bind_name_field();
255+ }
256+
257+ if (this.get('details_buttons')) {
258+ this.get('details_buttons').on('click',
259+ this._complete_registration,
260+ this);
261+ }
262+ },
263+
264+ /**
265+ * Standard YUI init.
266+ *
267+ * @method initialize
268+ * @param {Object}
269+ */
270+ initialize: function (cfg) {
271+ // The details button is only visible when JavaScript is enabled, but
272+ // the H3 separator is only visible when JavaScript is disabled.
273+ // Neither is displayed on the step 1 page.
274+ this._show_separator(false);
275+ },
276+
277+ /**
278+ * Render the view by binding to the current DOM.
279+ *
280+ * @method render
281+ */
282+ render: function () {
283+ this.bindUI();
284+
285+ if (this.get('details_buttons')) {
286+ this.get('details_buttons').removeClass('hidden');
287+ }
288+
289+ // If there are search results, hide the registration details.
290+ if (this.get('search_results')) {
291+ this.get('form_widgets').addClass('hidden');
292+ this.get('form_actions').addClass('hidden');
293+ this.get('title').addClass('hidden');
294+ }
295+
296+ // If we've been here before (e.g. there was an error in
297+ // submitting step 2), jump to continuing the registration.
298+ var marker = this.get('marker');
299+ if (marker && marker.getAttribute('value').search(/hidesearch/) >= 0) {
300+ this._complete_registration(null);
301+ }
302+ }
303+ }, {
304+ ATTRS: {
305+ /**
306+ * Lazy load the found node for use through out the View.
307+ *
308+ * @attribute details_buttons
309+ * @default Node
310+ * @type Node
311+ */
312+ details_buttons: {
313+ valueFn: function (val) {
314+ return Y.one('#registration-details-buttons');
315+ }
316+ },
317+
318+ /**
319+ * Lazy load the found node for use through out the View.
320+ *
321+ * @attribute form_actions
322+ * @default Node
323+ * @type Node
324+ */
325+ form_actions: {
326+ valueFn: function (va) {
327+ return Y.one('#launchpad-form-actions');
328+ }
329+ },
330+
331+ /**
332+ * Lazy load the found node for use through out the View.
333+ *
334+ * @attribute form_widgets
335+ * @default Node
336+ * @type Node
337+ */
338+ form_widgets: {
339+ valueFn: function (val) {
340+ return Y.one('#launchpad-form-widgets');
341+ }
342+ },
343+
344+ /**
345+ * Lazy load the found node for use through out the View.
346+ * This is the magic hidden widget used by the MultiStepView.
347+ *
348+ * @attribute marker
349+ * @default Node
350+ * @type Node
351+ */
352+ marker: {
353+ valueFn: function (val) {
354+ return Y.one(Y.DOM.byId('field.__visited_steps__'));
355+ }
356+ },
357+
358+ /**
359+ * Lazy load the found node for use through out the View.
360+ *
361+ * @attribute search_results
362+ * @default Node
363+ * @type Node
364+ */
365+ search_results: {
366+ valueFn: function (val) {
367+ return Y.one('#search-results');
368+ }
369+ },
370+
371+ /**
372+ * Lazy load the found node for use through out the View.
373+ *
374+ * @attribute title
375+ * @default Node
376+ * @type Node
377+ */
378+ title: {
379+ valueFn: function (val) {
380+ return Y.one('#registration-details-title');
381+ }
382+ }
383+ }
384+ });
385+
386+}, '0.1', {
387+ 'requires': ['base', 'node', 'lp.ui.effects', 'lp.app.choice']
388+});
389
390=== added file 'lib/lp/registry/javascript/tests/test_product_views.html'
391--- lib/lp/registry/javascript/tests/test_product_views.html 1970-01-01 00:00:00 +0000
392+++ lib/lp/registry/javascript/tests/test_product_views.html 2012-09-18 15:14:29 +0000
393@@ -0,0 +1,58 @@
394+<!DOCTYPE html>
395+<!--
396+Copyright 2012 Canonical Ltd. This software is licensed under the
397+GNU Affero General Public License version 3 (see the file LICENSE).
398+-->
399+
400+<html>
401+ <head>
402+ <title>Product New Tests</title>
403+
404+ <!-- YUI and test setup -->
405+ <script type="text/javascript"
406+ src="../../../../../build/js/yui/yui/yui.js">
407+ </script>
408+ <link rel="stylesheet"
409+ href="../../../../../build/js/yui/console/assets/console-core.css" />
410+ <link rel="stylesheet"
411+ href="../../../../../build/js/yui/console/assets/skins/sam/console.css" />
412+ <link rel="stylesheet"
413+ href="../../../../../build/js/yui/test/assets/skins/sam/test.css" />
414+
415+ <script type="text/javascript"
416+ src="../../../../../build/js/lp/app/testing/testrunner.js"></script>
417+
418+ <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
419+
420+ <!-- Dependencies -->
421+ <script type="text/javascript" src="../../../../../build/js/lp/app/client.js"></script>
422+ <script type="text/javascript" src="../../../../../build/js/lp/app/choice.js"></script>
423+ <script type="text/javascript" src="../../../../../build/js/lp/app/ellipsis.js"></script>
424+ <script type="text/javascript" src="../../../../../build/js/lp/app/expander.js"></script>
425+ <script type="text/javascript" src="../../../../../build/js/lp/app/errors.js"></script>
426+ <script type="text/javascript" src="../../../../../build/js/lp/app/lp.js"></script>
427+ <script type="text/javascript" src="../../../../../build/js/lp/app/anim/anim.js"></script>
428+ <script type="text/javascript" src="../../../../../build/js/lp/app/extras/extras.js"></script>
429+ <script type="text/javascript" src="../../../../../build/js/lp/app/choiceedit/choiceedit.js"></script>
430+ <script type="text/javascript" src="../../../../../build/js/lp/app/effects/effects.js"></script>
431+ <script type="text/javascript" src="../../../../../build/js/lp/app/formoverlay/formoverlay.js"></script>
432+ <script type="text/javascript" src="../../../../../build/js/lp/app/formwidgets/resizing_textarea.js"></script>
433+ <script type="text/javascript" src="../../../../../build/js/lp/app/inlineedit/editor.js"></script>
434+ <script type="text/javascript" src="../../../../../build/js/lp/app/testing/helpers.js"></script>
435+ <script type="text/javascript" src="../../../../../build/js/lp/app/overlay/overlay.js"></script>
436+ <script type="text/javascript" src="../../../../../build/js/lp/app/ui/ui.js"></script>
437+
438+ <!-- The module under test. -->
439+ <script type="text/javascript" src="../product_views.js"></script>
440+
441+ <!-- The test suite -->
442+ <script type="text/javascript" src="test_product_views.js"></script>
443+
444+ </head>
445+ <body class="yui3-skin-sam">
446+ <ul id="suites">
447+ <li>registry.product-views.test</li>
448+ </ul>
449+ <div id="testdom"></div>
450+ </body>
451+</html>
452
453=== added file 'lib/lp/registry/javascript/tests/test_product_views.js'
454--- lib/lp/registry/javascript/tests/test_product_views.js 1970-01-01 00:00:00 +0000
455+++ lib/lp/registry/javascript/tests/test_product_views.js 2012-09-18 15:14:29 +0000
456@@ -0,0 +1,74 @@
457+/* Copyright (c) 2012 Canonical Ltd. All rights reserved. */
458+
459+YUI.add('registry.product-views.test', function (Y) {
460+ var tests = Y.namespace('registry.product-views.test');
461+
462+ var ns = Y.namespace('registry.views');
463+ tests.suite = new Y.Test.Suite('registry.product-views Tests');
464+
465+ tests.suite.add(new Y.Test.Case({
466+ name: 'registry.product-views.new_tests',
467+
468+ setUp: function () {},
469+ tearDown: function () {
470+ Y.one('#testdom').empty();
471+ },
472+
473+ _setup_url_fields: function () {
474+ Y.one('#testdom').setContent(
475+ '<input type="text" id="field.name" name="field.name" />' +
476+ '<input type="text" id="field.displayname" name="field.displayname" />'
477+ );
478+ },
479+
480+ test_library_exists: function () {
481+ Y.Assert.isObject(ns.NewProduct,
482+ "Could not locate the registry.views.NewProduct module");
483+ },
484+
485+ test_url_autofill_sync: function () {
486+ this._setup_url_fields();
487+ var view = new ns.NewProduct();
488+ view.render();
489+
490+ var name_field = Y.one('input[id="field.displayname"]');
491+ name_field.set('value', 'test');
492+ name_field.simulate('keyup');
493+
494+ Y.Assert.areEqual(
495+ 'test',
496+ Y.one('input[name="field.name"]').get('value'),
497+ 'The url field should be updated based on the display name');
498+ },
499+
500+ test_url_autofill_disables: function () {
501+ this._setup_url_fields();
502+ var view = new ns.NewProduct();
503+ view.render();
504+
505+ var name_field = Y.one('input[id="field.displayname"]');
506+ var url_field = Y.one('input[id="field.name"]');
507+ name_field.set('value', 'test');
508+ name_field.simulate('keyup');
509+ Y.Assert.areEqual( 'test', url_field.get('value'),
510+ 'The url field should be updated based on the display name');
511+
512+ // Now setting the url field manually should detach the event for
513+ // the sync.
514+ url_field.set('value', 'test2');
515+ url_field.simulate('keyup');
516+
517+ // Changing the value back should fail horribly.
518+ name_field.set('value', 'test');
519+ name_field.simulate('keyup');
520+
521+ Y.Assert.areEqual(
522+ 'test2',
523+ url_field.get('value'),
524+ 'The url field should not be updated.');
525+ }
526+ }));
527+
528+}, '0.1', {
529+ requires: ['test', 'event-simulate', 'console', 'registry.product-views']
530+});
531
532=== modified file 'lib/lp/registry/templates/product-new.pt'
533--- lib/lp/registry/templates/product-new.pt 2012-09-12 13:31:07 +0000
534+++ lib/lp/registry/templates/product-new.pt 2012-09-18 15:14:29 +0000
535@@ -9,240 +9,17 @@
536 <div metal:fill-slot="main">
537
538 <script type="text/javascript">
539-/*
540- * When step 2 of this wizard has search results, we want to hide the
541- * details widgets until the user states that the project they are
542- * registering is not a duplicate.
543- */
544-LPJS.use('node', 'lp.ui.effects', 'lp.app.choice', function(Y) {
545- Y.on('domready', function() {
546- // Setup the information choice widget.
547- if (Y.one('input[name="field.information_type"]')) {
548- Y.lp.app.choice.addPopupChoiceForRadioButtons(
549- 'information_type', LP.cache.information_type_data, true);
550- }
551-
552- /* These two regexps serve slightly different purposes. The first
553- * finds the leftmost run of valid url characters for the autofill
554- * operation. The second validates the entire string, used for
555- * explicit entry into the URL field. These are simple enough to keep
556- * in sync so it doesn't bother me that we repeat most of it. Note
557- * that while both ignore case, only the first one should be global in
558- * order to utilize the RegExp.lastIndex behavior of .exec().
559- */
560- var valid_urls = new RegExp('^[a-z0-9][-.+a-z0-9]*', 'ig');
561- var valid_char = new RegExp('^[a-z0-9][-.+a-z0-9]*$', 'i');
562-
563- /* Handle key presses in the Name field for autofilling the URL
564- * field. By ensuring that field.name exists, we only do this when
565- * we're on step 1 of the wizard.
566- *
567- * XXX BarryWarsaw 12-May-2009
568- * http://yuilibrary.com/projects/yui3/ticket/2423101
569- * Note that we have to use the more verbose way of getting field.name
570- * because Y.one() doesn't like the dots in the Zope field names.
571- */
572- var url_field = Y.one(Y.DOM.byId('field.name'));
573- if (url_field) {
574- var name_field = Y.one(Y.DOM.byId('field.displayname'));
575- function autofill(e) {
576- var name_value = name_field.get('value');
577- if (name_value == '') {
578- /* When Name is empty, clear URL. */
579- url_field.set('value', '');
580- }
581- else {
582- /* Fill the URL field with as much of the left part of the
583- * string as matches the regexp. If the regexp doesn't
584- * match (say because there's illegal stuff at the front),
585- * don't change the current URL field. We have to reset
586- * lastIndex each time we get here so that search begins
587- * at the front of the string.
588- */
589- valid_urls.lastIndex = 0;
590- var match = valid_urls.exec(name_value);
591- if (match) {
592- var slice = name_value.slice(0, valid_urls.lastIndex);
593- url_field.set('value', slice);
594- }
595- }
596- }
597- name_field.on('keyup', autofill);
598- /* Prevent invalid characters from being input into the URL field.
599- */
600- url_field.on('keypress', function(e) {
601- /* Handling key events is madness. For a glimpse, see
602- * http://unixpapa.com/js/key.html
603- *
604- * Additional spice for the insanity stew is given by the
605- * rhino book, page 428. This code is basically a rip and
606- * remix of those two texts.
607- */
608- var event = e || window.event;
609- var code = e.charCode || e.keyCode;
610-
611- if (/* Check for special characters. */
612- e.which == 0 || e.which == null ||
613- /* Check for function keys (Firefox only). */
614- e.charCode == 0 ||
615- /* Check for ctrl or alt held down. */
616- e.ctrlKey || e.altKey ||
617- /* Check for ASCII control character */
618- 32 > code)
619- {
620- return true;
621- }
622- var char = String.fromCharCode(code);
623- var new_value = url_field.get('value') + char;
624- if (new_value.search(valid_char) >= 0) {
625- /* The character is valid. */
626- return true;
627- }
628- e.preventDefault();
629- e.returnValue = false;
630- return false;
631- });
632- /* Explicitly typing into the URL field disables autofilling. */
633- url_field.on('keyup', function(e) {
634- if (url_field.get('value') == '') {
635- /* The user cleared the URL field; turn on autofill. */
636- name_field.on('keyup', autofill);
637- }
638- else {
639- /* Honor the user's URL; turn off autofill. */
640- name_field.detach('keyup', autofill);
641- }
642- });
643- }
644-
645- /* Handle the reveals when there are search results. */
646- var details_buttons = Y.one('#registration-details-buttons');
647- var form_actions = Y.one('#launchpad-form-actions');
648- var form_widgets = Y.one('#launchpad-form-widgets');
649- var search_results = Y.one('#search-results');
650- var step_title = Y.one('#step-title');
651- var title = Y.one('#registration-details-title');
652-
653- /* This is the magic hidden widget used by the MultiStepView. */
654- var marker = Y.one(Y.DOM.byId('field.__visited_steps__'));
655-
656- var separator = Y.one('#registration-separator');
657- function show_separator(flag) {
658- if (!separator) {
659- /* The separator is not on the page, because there were no
660- * search results.
661- */
662- return;
663- }
664- if (flag) {
665- separator.removeClass('hidden');
666- }
667- else {
668- separator.addClass('hidden');
669- }
670- }
671-
672- /* When the 'No' button is clicked, we swap in the registration
673- * details for the search results. It really doesn't look good
674- * to leave the search results there.
675- */
676- function complete_registration(e) {
677- var expander = Y.one('#search-results-expander');
678-
679- /* Slide in the search results and hide them under a link. */
680- expander.removeClass('hidden');
681- expander.on('click', function(e) {
682- e.preventDefault();
683-
684- var arrow = Y.one('#search-results-arrow');
685- if (arrow.getAttribute('src') == '/@@/treeCollapsed') {
686- /* The search results are currently hidden. Slide them
687- * out and turn the arrow to point downward.
688- */
689- arrow.setAttribute('src', '/@@/treeExpanded');
690- arrow.setAttribute('title', 'Hide search results');
691- arrow.setAttribute('alt', 'Hide search results');
692- Y.lp.ui.effects.slide_out(search_results).run();
693- show_separator(true);
694- }
695- else {
696- /* The search results are currently displayed. Slide them
697- * in and turn the arrow to point rightward.
698- */
699- arrow.setAttribute('src', '/@@/treeCollapsed');
700- arrow.setAttribute('title', 'Show search results');
701- arrow.setAttribute('alt', 'Show search results');
702- Y.lp.ui.effects.slide_in(search_results).run();
703- show_separator(false);
704- }
705- });
706-
707- /* Hide the 'No' button, but slide out the search results, so the
708- * user has a clue that Something Is Happening.
709- */
710- details_buttons.addClass('hidden');
711-
712- /* Slide out the registration details widgets, but add an 'end'
713- * event handler so that the height style left by lp.ui.effects is
714- * removed when the animation is done. We're never going to slide
715- * the form widgets back in, and the height style causes the
716- * licence widget to dive under the Complete Registration button.
717- * See bug 391138 for details.
718- */
719- var anim = Y.lp.ui.effects.slide_out(form_widgets);
720- anim.on('end', function() {
721- form_widgets.setStyle('height', null);
722- });
723- anim.run();
724-
725- /* Toggle the visibility of the various other widgets. */
726- form_actions.removeClass('hidden');
727- title.removeClass('hidden');
728-
729- /* Set the H2 title to something more appropriate for the
730- * selected task.
731- */
732- step_title.set('innerHTML', 'Step 2 (of 2) Registration details');
733-
734- var reset_height = search_results.getComputedStyle('height');
735- search_results.setStyle('height', reset_height);
736- Y.lp.ui.effects.slide_in(search_results).run();
737-
738- /* Append a special marker to the hidden state widget. See
739- * ProjectAddStepTwo.search_results_count() for details.
740- */
741- var steps = marker.getAttribute('value');
742- if (0 > steps.search(new RegExp('hidesearch'))) {
743- marker.setAttribute('value', steps + "|hidesearch");
744- }
745- }
746-
747- /* The details button is only visible when JavaScript is enabled, but
748- * the H3 separator is only visible when JavaScript is disabled.
749- * Neither is displayed on the step 1 page.
750- */
751- show_separator(false);
752-
753- if (details_buttons) {
754- details_buttons.removeClass('hidden');
755- details_buttons.on('click', complete_registration);
756- }
757-
758- /* If there are search results, hide the registration details. */
759- if (search_results) {
760- form_widgets.addClass('hidden');
761- form_actions.addClass('hidden');
762- title.addClass('hidden');
763- }
764-
765- /* Finally, if we've been here before (e.g. there was an error in
766- * submitting step 2), jump to continuing the registration.
767- */
768- if (marker.getAttribute('value').search(/hidesearch/) >= 0) {
769- complete_registration(null);
770- }
771- })
772-});
773+ /*
774+ * When step 2 of this wizard has search results, we want to hide the
775+ * details widgets until the user states that the project they are
776+ * registering is not a duplicate.
777+ */
778+ LPJS.use('views.registry_product', function(Y) {
779+ Y.on('domready', function() {
780+ var view = new Y.views.registry.NewProduct();
781+ view.render();
782+ });
783+ });
784 </script>
785
786 <div id="staging-message" style="background: #e0f0d0;