Merge lp:~wallyworld/lazr-js/picker-entry-extra-title-links into lp:lazr-js

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: 218
Merged at revision: 211
Proposed branch: lp:~wallyworld/lazr-js/picker-entry-extra-title-links
Merge into: lp:lazr-js
Diff against target: 283 lines (+189/-15)
3 files modified
src-js/lazrjs/picker/picker.js (+93/-11)
src-js/lazrjs/picker/tests/picker.js (+84/-0)
src-py/lazr/js/build.py (+12/-4)
To merge this branch: bzr merge lp:~wallyworld/lazr-js/picker-entry-extra-title-links
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+63069@code.launchpad.net

Commit message

[r=sinzui][bug=791053] Enhance the picker to display an alternate title entry and optionally linkify the title/alt titles.

Description of the change

For the disclosure feature work, enhancements are required to the picker - extra information needs to be rendered so that the user can be confident that they are choosing the correct entry.

This branch provides the infrastructure to allow picker implementations to:
1. Render an alternate title for each picker entry - rendered in parentheses after the main title
2. Optionally provide hrefs for title/alt title which cause these to be rendered as anchors and the links are opened in new windows.
3. Optionally render badges (small images) to the right of the picker entry title.

Building the tarball to test this branch revealed a bug introduced to build.py. A change was made to allow the combo build to be invoked from the source directory rather than the egg. This broke Launchpad's jsbuild so a fix was done to cater for both scenarios.

== Implementation ==

Modify the picker rendering javascript. The title and description rendering is broken out to separate functions. The description is not rendered any differently but may be for subsequent work.

There's one lint error - I'm not sure exactly what I need to change to fix it.

== Demo ==

See screenshot : http://people.canonical.com/~ianb/person-picker-links.png
This example shows just the alternate titles having links.

== Tests ==

Add new tests to picker/tests/picker.js

== Lint ==

jslint: 2 files to lint.
jslint: No problem found in '/home/ian/projects/lazrjs-branches/devel/src-js/lazrjs/picker/picker.js'.

jslint: Lint found in '/home/ian/projects/lazrjs-branches/devel/src-js/lazrjs/picker/tests/picker.js':
Line 158 character 52: Script URL.
            Assert.areEqual(link_node.get('href'), 'javascript:void(0)');

To post a comment you must log in.
211. By Ian Booth

Fix escaping issue

212. By Ian Booth

Fix typo

213. By Ian Booth

Add support for picker item badges

214. By Ian Booth

Add results badge style

215. By Ian Booth

Fix build and remove styling from badges

216. By Ian Booth

Fix tests

217. By Ian Booth

Add css class to badge images

218. By Ian Booth

Tidy up

Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (3.9 KiB)

Hi Ian.

Thank you for providing this branch. I see a few trivial issues I would like you to fix. I also have some reservation about the action link, but I think user testing is more important than my own musings.

> === modified file 'src-js/lazrjs/picker/picker.js'
> --- src-js/lazrjs/picker/picker.js 2011-01-14 15:50:06 +0000
> +++ src-js/lazrjs/picker/picker.js 2011-06-03 02:06:33 +0000
> @@ -317,6 +317,93 @@
> },
>
> /**
> + * Return a node containing the specified text. If a href is provided,
> + * then the text will be linkified with with the given css class. The
> + * link will open in a new window (but the browser can be configured to
> + * open a new tab instead if the user so wishes).
> + * @param text the text to render
> + * @param href the URL of the new window
> + * @param css the style to use when rendering the link
> + */
> + _text_or_link: function(text, href, css) {
> + var result;
> + if (href) {
> + result=Y.Node.create(

whitespace around operators.

+ "<a class='"+css+"' href=javascript:void(0)></a>");

whitespace around operators.

> + result.set('text', text);
> + Y.on('click', function(e) {
> + e.halt();
> + window.open(href);
> + }, result);
> + } else {
> + result = document.createTextNode(text);
> + }
> + return result;
> + },
> +
> + /**
> + * Render a node containing the title part of the picker entry.
> + * The title will consist of some main text with some optional alternate
> + * text which will be rendered in parentheses after the main text. The
> + * title/alt_title text may separately be turned into a link with user
> + * specified URLs.
> + * @param data a json data object with the details to render
> + */

I worry that this presupposes every user of lazrjs using display names and
Ids like Lp does. I think we may need to revisit this. I would not be
surprised if other lazr stakeholders would want this to follow the Web UI
guidelines; an action link:
    action >
or an action button
    [action button]

These are also the same guidelines that define the green we use of actions
that do not leave the current page/context.

> + _renderTitleUI: function(data) {
> + var li_title = Y.Node.create(
> + '<span></span>').addClass(C_RESULT_TITLE);
> + var title = this._text_or_link(
> + data.title, data.title_link, data.link_css);
> + li_title.appendChild(title);
> + if (data.alt_title) {
> + var alt_title = this._text_or_link(
> + data.alt_title, data.alt_title_link, data.link_css);
> + li_title.appendChild('&nbsp(')

I think this is an invalid named entity. Entities are terminated with a
semi-colon.

> + .appendChild(alt_title)
> + .appendChild(')');
> + }
> + return li_title;
> + },

...

> === modified file 'src-js/lazrjs/picker/tests/picker.js'
> --- src-js/lazrjs/picker/tests/picker.js 2011-01-14 17:13:05 +0000
> +++ src-js/lazrjs/picker/tests/picker.js 2011-06-03 ...

Read more...

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

Thanks for reviewing.

>
> whitespace around operators.
>
> + "<a class='"+css+"' href=javascript:void(0)></a>");
>
> whitespace around operators.
>

Doh. I should know that by now. Sorry.

>> +
>> + /**
>> + * Render a node containing the title part of the picker entry.
>> + * The title will consist of some main text with some optional alternate
>> + * text which will be rendered in parentheses after the main text. The
>> + * title/alt_title text may separately be turned into a link with user
>> + * specified URLs.
>> + * @param data a json data object with the details to render
>> + */
>
> I worry that this presupposes every user of lazrjs using display names and
> Ids like Lp does. I think we may need to revisit this. I would not be
> surprised if other lazr stakeholders would want this to follow the Web UI
> guidelines; an action link:
> action >
> or an action button
> [action button]
>

I've made it generic - there's no pre assumption about the intent of the
title or alt title. It's treated as simply text. The css style to use
for the links is also user specified. But yes, there's no allowance for
rendering anything other that a link which opens in a new window. We can
always revist and enhance this as required. I did just enough for the
current use case and anything else might have ended up being YAGNI.

> These are also the same guidelines that define the green we use of actions
> that do not leave the current page/context.
>

The css style which colours the rendered link green is passed in from
the lp side. lazr-js has no knowledge of what the user specified style
means.

>> + var alt_title = this._text_or_link(
>> + data.alt_title, data.alt_title_link, data.link_css);
>> + li_title.appendChild('&nbsp(')
>
> I think this is an invalid named entity. Entities are terminated with a
> semi-colon.
>
>> + .appendChild(alt_title)
>> + .appendChild(')');
>> + }
>> + return li_title;
>> + },
>
> ...

You sure about the above comment? The code is just
li_title.appendChild(xxx).appendChild(xxx).appendChild(xxx)
split over a few lines to avoid exceeding 78 chars.

219. By Ian Booth

Lint fixes

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Curtis

I'm really sorry - I totally misinterpreted your comment below.
I've added the missing ; in the correct place. Don't know what I was
smoking there.

>
>>> + var alt_title = this._text_or_link(
>>> + data.alt_title, data.alt_title_link, data.link_css);
>>> + li_title.appendChild('&nbsp(')
>>
>> I think this is an invalid named entity. Entities are terminated with a
>> semi-colon.
>>
>>> + .appendChild(alt_title)
>>> + .appendChild(')');
>>> + }
>>> + return li_title;
>>> + },
>>
>> ...
>
>
> You sure about the above comment? The code is just
> li_title.appendChild(xxx).appendChild(xxx).appendChild(xxx)
> split over a few lines to avoid exceeding 78 chars.
>
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src-js/lazrjs/picker/picker.js'
2--- src-js/lazrjs/picker/picker.js 2011-01-14 15:50:06 +0000
3+++ src-js/lazrjs/picker/picker.js 2011-06-03 06:39:38 +0000
4@@ -317,6 +317,93 @@
5 },
6
7 /**
8+ * Return a node containing the specified text. If a href is provided,
9+ * then the text will be linkified with with the given css class. The
10+ * link will open in a new window (but the browser can be configured to
11+ * open a new tab instead if the user so wishes).
12+ * @param text the text to render
13+ * @param href the URL of the new window
14+ * @param css the style to use when rendering the link
15+ */
16+ _text_or_link: function(text, href, css) {
17+ var result;
18+ if (href) {
19+ result=Y.Node.create(
20+ "<a class = '" + css + "' href=javascript:void(0)></a>");
21+ result.set('text', text);
22+ Y.on('click', function(e) {
23+ e.halt();
24+ window.open(href);
25+ }, result);
26+ } else {
27+ result = document.createTextNode(text);
28+ }
29+ return result;
30+ },
31+
32+ /**
33+ * Render a node containing the title part of the picker entry.
34+ * The title will consist of some main text with some optional alternate
35+ * text which will be rendered in parentheses after the main text. The
36+ * title/alt_title text may separately be turned into a link with user
37+ * specified URLs.
38+ * @param data a json data object with the details to render
39+ */
40+ _renderTitleUI: function(data) {
41+ var li_title = Y.Node.create(
42+ '<span></span>').addClass(C_RESULT_TITLE);
43+ var title = this._text_or_link(
44+ data.title, data.title_link, data.link_css);
45+ li_title.appendChild(title);
46+ if (data.alt_title) {
47+ var alt_title = this._text_or_link(
48+ data.alt_title, data.alt_title_link, data.link_css);
49+ li_title.appendChild('&nbsp(')
50+ .appendChild(alt_title)
51+ .appendChild(')');
52+ }
53+ return li_title;
54+ },
55+
56+ /**
57+ * Render a node containing the badges part of the picker entry.
58+ * The badges are small images which are displayed next to the title. The
59+ * display of badges is optional.
60+ * @param data a json data object with the details to render
61+ */
62+ _renderBadgesUI: function(data) {
63+ if (data.badges) {
64+ var badges = Y.Node.create('<div></div>').addClass('badge');
65+ Y.each(data.badges, function(badge_info) {
66+ var badge_url = badge_info.url;
67+ var badge_alt = badge_info.alt;
68+ var badge = Y.Node.create('<img></img>')
69+ .addClass('badge')
70+ .set('src', badge_url)
71+ .set('alt', badge_alt);
72+ badges.appendChild(badge);
73+ });
74+ return badges;
75+ }
76+ return null;
77+ },
78+
79+ /**
80+ * Render a node containing the description part of the picker entry.
81+ * @param data a json data object with the details to render
82+ */
83+ _renderDescriptionUI: function(data) {
84+ var li_desc = Y.Node.create(
85+ '<div><br /></div>').addClass(C_RESULT_DESCRIPTION);
86+ if (data.description) {
87+ li_desc.replaceChild(
88+ document.createTextNode(data.description),
89+ li_desc.one('br'));
90+ }
91+ return li_desc;
92+ },
93+
94+ /**
95 * Update the UI based on the results attribute.
96 *
97 * @method _syncResultsUI
98@@ -330,19 +417,12 @@
99 this._results_box.set('innerHTML', '');
100
101 Y.Array.each(results, function(data, i) {
102+ // Sort out the badges div.
103+ var li_badges = this._renderBadgesUI(data);
104 // Sort out the title span.
105- var li_title = Y.Node.create(
106- '<span></span>').addClass(C_RESULT_TITLE);
107- li_title.appendChild(
108- document.createTextNode(data.title));
109+ var li_title = this._renderTitleUI(data);
110 // Sort out the description div.
111- var li_desc = Y.Node.create(
112- '<div><br /></div>').addClass(C_RESULT_DESCRIPTION);
113- if (data.description) {
114- li_desc.replaceChild(
115- document.createTextNode(data.description),
116- li_desc.one('br'));
117- }
118+ var li_desc = this._renderDescriptionUI(data);
119 // Put the list item together.
120 var li = Y.Node.create('<li></li>').addClass(
121 i % 2 ? Y.lazr.ui.CSS_ODD : Y.lazr.ui.CSS_EVEN);
122@@ -353,6 +433,8 @@
123 li.appendChild(
124 Y.Node.create('<img />').set('src', data.image));
125 }
126+ if (li_badges !== null)
127+ li.appendChild(li_badges);
128 li.appendChild(li_title);
129 li.appendChild(li_desc);
130 // Attach handlers.
131
132=== modified file 'src-js/lazrjs/picker/tests/picker.js'
133--- src-js/lazrjs/picker/tests/picker.js 2011-01-14 17:13:05 +0000
134+++ src-js/lazrjs/picker/tests/picker.js 2011-06-03 06:39:38 +0000
135@@ -109,6 +109,90 @@
136 'Unexpected description value.');
137 },
138
139+ test_alternate_title_text: function () {
140+ this.picker.render();
141+ this.picker.set('results', [
142+ {
143+ css: 'yui3-blah-blue',
144+ value: 'jschmo',
145+ title: 'Joe Schmo',
146+ description: 'joe@example.com',
147+ alt_title: 'Another Joe'
148+ }
149+ ]);
150+ var bb = this.picker.get('boundingBox');
151+ var li = bb.one('.yui3-picker-results li');
152+ var title_el = li.one('.yui3-picker-result-title');
153+ Assert.isNotNull(title_el, "Missing title element");
154+ Assert.areEqual(
155+ 'Joe Schmo\u00a0(Another Joe)', title_el.get('text'),
156+ 'Unexpected title value.');
157+ },
158+
159+ test_title_links: function () {
160+ this.picker.render();
161+ this.picker.set('results', [
162+ {
163+ css: 'yui3-blah-blue',
164+ value: 'jschmo',
165+ title: 'Joe Schmo',
166+ description: 'joe@example.com',
167+ alt_title: 'Joe Again <foo></foo>',
168+ title_link: 'http://somewhere.com',
169+ alt_title_link: 'http://somewhereelse.com',
170+ link_css: 'cool-style'
171+ }
172+ ]);
173+
174+ function check_link(picker, link_selector, title) {
175+ var bb = picker.get('boundingBox');
176+ var link_clicked = false;
177+ var link_node = bb.one(link_selector);
178+
179+ Assert.areEqual(title, link_node.get('text'));
180+ Assert.areEqual('javascript:void(0)', link_node.get('href'));
181+
182+ Y.on('click', function(e) {
183+ link_clicked = true;
184+ }, link_node);
185+ simulate(bb, link_selector, 'click');
186+ Assert.isTrue(link_clicked,
187+ link_selector + ' link was not clicked');
188+ }
189+ check_link(
190+ this.picker, '.cool-style:nth-child(1)', 'Joe Schmo');
191+ check_link(
192+ this.picker, '.cool-style:nth-child(2)', 'Joe Again <foo></foo>');
193+ },
194+
195+ test_title_badges: function () {
196+ this.picker.render();
197+ var badge_info = [
198+ {url: '../../lazr/assets/skins/sam/search.png', alt: 'alt 1'},
199+ {url: '../../lazr/assets/skins/sam/spinner.png', alt: 'alt 2'}];
200+ this.picker.set('results', [
201+ {
202+ badges: badge_info,
203+ css: 'yui3-blah-blue',
204+ value: 'jschmo',
205+ title: 'Joe Schmo',
206+ description: 'joe@example.com'
207+ }
208+ ]);
209+ var bb = this.picker.get('boundingBox');
210+ var li = bb.one('.yui3-picker-results li');
211+ for (var i=0; i<badge_info.length; i++) {
212+ var img_node = li.one(
213+ 'div.badge img.badge:nth-child(' + (i + 1) + ')');
214+ Assert.areEqual(
215+ badge_info[i].url, img_node.getAttribute('src'),
216+ 'Unexpected badge url');
217+ Assert.areEqual(
218+ badge_info[i].alt, img_node.get('alt'),
219+ 'Unexpected badge alt text');
220+ }
221+ },
222+
223 test_results_display_escaped: function () {
224 this.picker.render();
225 this.picker.set('results', [
226
227=== modified file 'src-py/lazr/js/build.py'
228--- src-py/lazr/js/build.py 2011-05-20 14:54:05 +0000
229+++ src-py/lazr/js/build.py 2011-06-03 06:39:38 +0000
230@@ -13,10 +13,13 @@
231 from glob import glob
232
233 import cssutils
234+import pkg_resources
235
236 HERE = os.path.dirname(__file__)
237 BUILD_DIR = os.path.normpath(os.path.join(HERE, '..', '..', '..', 'build'))
238-SRC_DIR = os.path.normpath(os.path.join(HERE, '..', '..', '..'))
239+DEFAULT_SRC_DIR = os.path.normpath(os.path.join(HERE, '..', '..', '..'))
240+PKG_SRC_DIR = pkg_resources.resource_filename(
241+ pkg_resources.Requirement.parse("lazr-js"), "lazrjs")
242
243 ESCAPE_STAR_PROPERTY_RE = re.compile(r'\*([a-zA-Z0-9_-]+):')
244 UNESCAPE_STAR_PROPERTY_RE = re.compile(r'([a-zA-Z0-9_-]+)_ie_star_hack:')
245@@ -186,7 +189,7 @@
246
247 class Builder:
248
249- def __init__(self, name='lazr', build_dir=BUILD_DIR, src_dir=SRC_DIR,
250+ def __init__(self, name='lazr', build_dir=BUILD_DIR, src_dir=PKG_SRC_DIR,
251 extra_files=None, exclude_regex='', file_type='raw',
252 copy_yui_to=BUILD_DIR):
253 """Create a new Builder.
254@@ -210,6 +213,11 @@
255 self.name = name
256 self.build_dir = build_dir
257 self.src_dir = src_dir
258+ # We need to support the case where this is being invoked directly
259+ # from source rather than a package. If this is the case, the package
260+ # src directory won't exist.
261+ if not os.path.exists(src_dir):
262+ self.src_dir = DEFAULT_SRC_DIR
263 self.built_files = []
264 self.skins = {}
265 if extra_files is None:
266@@ -411,7 +419,7 @@
267
268 def copy_yui_package(self):
269 """Copy the contents of our 'yui' dir to self.copy_yui_to."""
270- yui_dirs = glob(os.path.join(SRC_DIR, 'yui') + '/*')
271+ yui_dirs = glob(os.path.join(self.src_dir, 'yui') + '/*')
272 for dir in yui_dirs:
273 dir_name = os.path.split(dir)[-1]
274 dst_dir = os.path.join(self.copy_yui_to, dir_name)
275@@ -450,7 +458,7 @@
276 '-b', '--builddir', dest='build_dir', default=BUILD_DIR,
277 help=('The directory that should contain built files.'))
278 parser.add_option(
279- '-s', '--srcdir', dest='src_dir', default=SRC_DIR,
280+ '-s', '--srcdir', dest='src_dir', default=PKG_SRC_DIR,
281 help=('The directory containing the src files.'))
282 parser.add_option(
283 '-x', '--exclude', dest='exclude', default='',

Subscribers

People subscribed via source and target branches