Merge lp:~cjwatson/launchpad/yui-autocomplete into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18435
Proposed branch: lp:~cjwatson/launchpad/yui-autocomplete
Merge into: lp:launchpad
Diff against target: 2140 lines (+64/-1928)
11 files modified
lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css (+0/-193)
lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css (+0/-286)
lib/lp/app/javascript/autocomplete/autocomplete.js (+0/-788)
lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html (+0/-46)
lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js (+0/-575)
lib/lp/bugs/javascript/bug_tags_entry.js (+55/-25)
lib/lp/bugs/javascript/tests/test_bug_tags_entry.html (+0/-2)
lib/lp/bugs/javascript/tests/test_bug_tags_entry.js (+6/-8)
lib/lp/registry/javascript/tests/test_structural_subscription.html (+0/-2)
lib/lp/registry/javascript/tests/test_structural_subscription.js (+2/-2)
lib/lp/scripts/utilities/js/combinecss.py (+1/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/yui-autocomplete
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+327804@code.launchpad.net

Commit message

Switch to the YUI autocomplete widget.

Description of the change

While in some ways it's a bit odd to be adding substantial new YUI dependencies at this point, I think it's worth it. The YUI widget is IMO more flexible and better-designed than our in-tree one, and in particular it has rather cleaner support for multiple data sources. My eventual aim is to be able to autocomplete over vocabularies as well, particularly for Git branch names.

It's not totally clear to me why we had our own autocomplete widget in the first place, since it appears that the YUI one already existed when it was created; but maybe the YUI version was less capable at the time or something.

orderedPhraseMatch should probably eventually end up somewhere more common than lib/lp/bugs/, but I'm leaving that until there are other users.

It's possible that I've missed some compatibility issues, but it looks pretty close in both Firefox and Chrome.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

Looks fine to me. Better, even. Nice find.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed directory 'lib/lp/app/javascript/autocomplete'
2=== removed directory 'lib/lp/app/javascript/autocomplete/assets'
3=== removed file 'lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css'
4--- lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css 2011-06-29 14:56:15 +0000
5+++ lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css 1970-01-01 00:00:00 +0000
6@@ -1,193 +0,0 @@
7-/*
8-Copyright (c) 2009, Canonical Ltd. All rights reserved.
9-Licensed under the GNU Affero General Public License:
10-http://www.gnu.org/licenses/agpl.txt
11-*/
12-
13-/*
14- * Make the z-index a bit higher than the lazr.Overlay, which is at
15- * z-index 999, so we can use autocomplete widgets inside overlays without
16- * hassle.
17- */
18-.yui3-autocomplete { position: absolute; z-index: 1050; }
19-.yui3-autocomplete-hidden { display: none; }
20-
21-
22-/*
23-* Bring in the NodeMenuNav core CSS from YUI 3.0.0pr2.
24-*
25-* This saves us from jumping through hoops to pull the individual files
26-* from YUI itself.
27-*
28-* This will have to change once the YUI Loader starts bringing in the CSS
29-* file dependencies.
30-*/
31-.yui3-menu .yui3-menu {
32-
33- position: absolute;
34- z-index: 1;
35-
36-}
37-
38-
39-.yui3-menu .yui3-shim {
40-
41- /*
42- Styles for the <iframe> shim used to prevent <select> elements from poking through
43- submenus in IE < 7. Note: For peformance, creation of the <iframe> shim for each submenu
44- is deferred until it is initially made visible by the user.
45- */
46-
47- position: absolute;
48- top: 0;
49- left: 0;
50- z-index: -1;
51- opacity: 0;
52- filter: alpha(opacity=0); /* For IE since it doesn't implement the CSS3 "opacity" property. */
53- border: none;
54- margin: 0;
55- padding: 0;
56- height: 100%;
57- width: 100%;
58-
59-}
60-
61-.yui3-menu-hidden {
62-
63- /*
64- Position hidden menus outside the viewport boundaries to prevent them from
65- triggering scrollbars on the viewport.
66- */
67-
68- top: -10000px;
69- left: -10000px;
70-
71- /*
72- Using "visibility:hidden" over "display" none because:
73-
74- 1) As the "position" property for submenus is set to "absolute", they are out of
75- the document flow and take up no space. Therefore, from that perspective use of
76- "display:none" is redundant.
77-
78- 2) According to MSDN use of "display:none" is more expensive:
79- "Display is the more expensive of the two CSS properties, so if you are
80- making elements appear and disappear often, visibility will be faster."
81- (See http://msdn.microsoft.com/en-us/library/bb264005(VS.85).aspx)
82- */
83-
84- visibility: hidden;
85-
86-}
87-
88-.yui3-menu li {
89-
90- list-style-type: none;
91-
92-}
93-
94-.yui3-menu ul,
95-.yui3-menu li {
96-
97- margin: 0;
98- padding: 0;
99-
100-}
101-
102-.yui3-menu-label,
103-.yui3-menuitem-content {
104-
105- text-align: left;
106- white-space: nowrap;
107- display: block;
108-
109-}
110-
111-.yui3-menu-horizontal li {
112-
113- float: left;
114- width: auto;
115-
116-}
117-
118-.yui3-menu-horizontal li li {
119-
120- float: none;
121-
122-}
123-
124-.yui3-menu-horizontal ul {
125-
126- /*
127- Use of "zoom" sets the "hasLayout" property to "true" in IE (< 8). When "hasLayout" is
128- set to "true", an element can clear its floated descendents. For more:
129- http://msdn.microsoft.com/en-gb/library/ms533776(VS.85).aspx
130- */
131-
132- *zoom: 1;
133-
134-}
135-
136-.yui3-menu-horizontal ul ul {
137-
138- /*
139- No need to clear <ul>s of submenus of horizontal menus since <li>s of submenus
140- aren't floated.
141- */
142-
143- *zoom: normal;
144-
145-}
146-
147-.yui3-menu-horizontal>.yui3-menu-content>ul:after {
148-
149- /* Self-clearing solution for Opera, Webkit, Gecko and IE > 7 */
150-
151- content: "";
152- display: block;
153- clear: both;
154- line-height: 0;
155- font-size: 0;
156- visibility: hidden;
157-
158-}
159-
160-
161-/*
162- The following two rules are for IE 7. Triggering "hasLayout" (via use of "zoom") prevents
163- first-tier submenus from hiding when the mouse is moving from an menu label in a root menu to
164- its corresponding submenu.
165-*/
166-
167-.yui3-menu-content {
168-
169- *zoom: 1;
170-
171-}
172-
173-
174-.yui3-menu-hidden .yui3-menu-content {
175-
176- *zoom: normal;
177-
178-}
179-
180-
181-/*
182- The following two rules are for IE 6 (Standards Mode and Quirks Mode) and IE 7 (Quirks Mode
183- only). Triggering "hasLayout" (via use of "zoom") fixes a bug in IE where mousing mousing off
184- the text node of menuitem or menu label will incorrectly trigger the mouseout event.
185-*/
186-
187-.yui3-menuitem-content,
188-.yui3-menu-label {
189-
190- _zoom: 1;
191-
192-}
193-
194-.yui3-menu-hiden .yui3-menuitem-content,
195-.yui3-menu-hiden .yui3-menu-label {
196-
197- _zoom: normal;
198-
199-}
200
201=== removed directory 'lib/lp/app/javascript/autocomplete/assets/skins'
202=== removed directory 'lib/lp/app/javascript/autocomplete/assets/skins/sam'
203=== removed file 'lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css'
204--- lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css 2012-01-06 11:08:30 +0000
205+++ lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css 1970-01-01 00:00:00 +0000
206@@ -1,286 +0,0 @@
207-/*
208-Copyright (c) 2009, Canonical Ltd. All rights reserved.
209-Licensed under the GNU Affero General Public License:
210-http://www.gnu.org/licenses/agpl.txt
211-*/
212-
213-.yui3-skin-sam .yui3-autocomplete-content { background-color: #fff; }
214-.yui3-skin-sam .yui3-autocomplete-list {
215- margin: 0;
216- padding: 0 0.3em 0 0.3em;
217- border: 1px solid black;
218-}
219-.yui3-skin-sam .yui3-autocomplete-list .item { list-style-type: none; }
220-.yui3-skin-sam .yui3-autocomplete-list .item .matching-text { font-weight: bold; }
221-.yui3-skin-sam .yui3-autocomplete-list .yui3-menuitem-content { padding: 0 0.3em; }
222-
223-
224-/*
225-* Bring in the NodeMenuNav skin CSS from YUI 3.0.0pr2.
226-*
227-* This saves us from jumping through hoops to pull the individual files
228-* from YUI itself.
229-*
230-* This will have to change once the YUI Loader starts bringing in the CSS
231-* file dependencies.
232-*/
233-.yui3-skin-sam .yui3-menu-content,
234-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content {
235-
236- font-size: 93%; /* 12px */
237- line-height: 1.5; /* 18px */
238- *line-height: 1.45; /* For IE */
239- border: solid 1px #808080;
240- background: #fff;
241- padding: 3px 0;
242-
243-}
244-
245-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content {
246-
247- font-size: 100%;
248-
249-}
250-
251-/* Horizontal menus */
252-
253-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-content {
254-
255- line-height: 2; /* ~24px */
256- *line-height: 1.9; /* For IE */
257- background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
258- padding: 0;
259-
260-}
261-
262-
263-.yui3-skin-sam .yui3-menu ul,
264-.yui3-skin-sam .yui3-menu ul ul {
265-
266- margin-top: 3px;
267- padding-top: 3px;
268- border-top: solid 1px #ccc;
269-
270-}
271-
272-.yui3-skin-sam .yui3-menu ul.first-of-type {
273-
274- border: 0;
275- margin: 0;
276- padding: 0;
277-
278-}
279-
280-.yui3-skin-sam .yui3-menu-horizontal ul {
281-
282- padding: 0;
283- margin: 0;
284- border: 0;
285-
286-}
287-
288-
289-.yui3-skin-sam .yui3-menu li,
290-.yui3-skin-sam .yui3-menu .yui3-menu li {
291-
292- /*
293- For and IE 6 (Strict Mode and Quirks Mode) and IE 7 (Quirks Mode only): Used to collapse
294- superfluous white space between <li> elements that is triggered by the "display" property
295- of the <a> elements being set to "block" by node-menunav-core.css file.
296- */
297-
298- _border-bottom: solid 1px #fff;
299-
300-}
301-
302-.yui3-skin-sam .yui3-menu-horizontal li {
303-
304- _border-bottom: 0;
305-
306-}
307-
308-.yui3-skin-sam .yui3-menubuttonnav li {
309-
310- border-right: solid 1px #ccc;
311-
312-}
313-
314-.yui3-skin-sam .yui3-splitbuttonnav li {
315-
316- border-right: solid 1px #808080;
317-
318-}
319-
320-.yui3-skin-sam .yui3-menubuttonnav li li,
321-.yui3-skin-sam .yui3-splitbuttonnav li li {
322-
323- border-right: 0;
324-
325-}
326-
327-
328-/* Menuitems and menu labels */
329-
330-
331-.yui3-skin-sam .yui3-menu-label,
332-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label,
333-.yui3-skin-sam .yui3-menuitem-content,
334-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-content {
335-
336- padding: 0 20px;
337- color: #000;
338- text-decoration: none;
339- cursor: default;
340-
341- /*
342- Necessary specify values for border, position and margin to override values specified in
343- the selectors that follow.
344- */
345-
346- float: none;
347- border: 0;
348- margin: 0;
349-
350-}
351-
352-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label,
353-.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-content {
354-
355- padding: 0 10px;
356- border-style: solid;
357- border-color: #808080;
358- border-width: 1px 0;
359- margin: -1px 0;
360-
361- float: left; /* Ensures that menu labels clear floated descendents. Also gets negative
362- margins working in IE 7 (Strict Mode). */
363- width: auto;
364-
365-}
366-
367-.yui3-skin-sam .yui3-menu-label,
368-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label {
369-
370- background: url(vertical-menu-submenu-indicator.png) right center no-repeat;
371-
372-}
373-
374-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label {
375-
376- background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
377-
378-}
379-
380-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label,
381-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label {
382-
383- background-image: none;
384-
385-}
386-
387-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label {
388-
389- padding-right: 0;
390-
391-}
392-
393-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label em {
394-
395- font-style: normal;
396- padding-right: 20px;
397- display: block;
398- background: url(horizontal-menu-submenu-indicator.png) right center no-repeat;
399-
400-}
401-
402-
403-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label {
404-
405- padding: 0;
406-
407-}
408-
409-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label a {
410-
411- float: left;
412- width: auto;
413- color: #000;
414- text-decoration: none;
415- cursor: default;
416- padding: 0 5px 0 10px;
417-
418-}
419-
420-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle {
421-
422- padding: 0; /* Override padding applied by the preceding rule. */
423- border-left: solid 1px #ccc;
424- width: 15px;
425- overflow: hidden;
426- text-indent: -1000px;
427- background: url(horizontal-menu-submenu-indicator.png) 3px center no-repeat;
428-
429-}
430-
431-
432-/* Selected menuitem */
433-
434-.yui3-skin-sam .yui3-menu-label-active,
435-.yui3-skin-sam .yui3-menu-label-menuvisible,
436-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-active,
437-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-menuvisible {
438-
439- background-color: #B3D4FF;
440-
441-}
442-
443-.yui3-skin-sam .yui3-menuitem-active .yui3-menuitem-content,
444-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content {
445-
446- background-image: none;
447- background-color: #B3D4FF;
448-
449- /*
450- Undo values set for "border-left-width" and "margin-left" when the root menu has a class of
451- "yui3-menubuttonnav" or "yui3-splitbuttonnav" applied.
452- */
453-
454- border-left-width: 0;
455- margin-left: 0;
456-
457-}
458-
459-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-active,
460-.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,
461-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-menuvisible {
462-
463- border-color: #7D98B8;
464- background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -1700px;
465-
466-}
467-
468-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-active,
469-.yui3-skin-sam .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,
470-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-menuvisible,
471-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-active,
472-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,
473-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
474-
475- border-left-width: 1px;
476- margin-left: -1px;
477-
478-}
479-
480-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
481-
482- border-color: #808080;
483- background: transparent;
484-
485-}
486-
487-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle {
488-
489- border-color: #7D98B8;
490- background: url(horizontal-menu-submenu-toggle.png) left center no-repeat;
491-
492-}
493
494=== removed file 'lib/lp/app/javascript/autocomplete/autocomplete.js'
495--- lib/lp/app/javascript/autocomplete/autocomplete.js 2017-07-21 16:43:02 +0000
496+++ lib/lp/app/javascript/autocomplete/autocomplete.js 1970-01-01 00:00:00 +0000
497@@ -1,788 +0,0 @@
498-/* Copyright 2009 Canonical Ltd. This software is licensed under the
499- * GNU Affero General Public License version 3 (see the file LICENSE). */
500-
501-YUI.add('lp.ui.autocomplete', function(Y) {
502-
503-/**
504- * A simple autocomplete widget.
505- *
506- * @module lp.ui.autocomplete
507- * @namespace lp.ui.autocomplete
508- */
509-
510-Y.namespace('lp.ui.autocomplete');
511-
512-
513-var AUTOCOMP = 'autocomplete',
514- BOUNDING_BOX = 'boundingBox',
515- CONTENT_BOX = 'contentBox',
516-
517- INPUT = 'input',
518- VALUE = 'value',
519- QUERY = 'query',
520- DATA = 'data',
521- MATCHES = 'matches',
522- RENDERED = 'rendered',
523- DELIMITER = 'delimiter',
524-
525- TAB = 9,
526- RETURN = 13,
527- ESCAPE = 27,
528- ARROW_DOWN = 40,
529-
530- getCN = Y.ClassNameManager.getClassName,
531-
532- C_LIST = getCN(AUTOCOMP, 'list');
533-
534-
535-// We need a base class on which to build our autocomplete widget, so we will
536-// make that class capable of positioning itself, too.
537-var AutoCompleteBase = Y.Base.build(
538- "AutoCompleteBase", Y.Widget, [Y.WidgetStack]);
539-
540-
541-/**
542- * A simple autocomplete widget.
543- *
544- * @class AutoComplete
545- */
546-
547-function AutoComplete() {
548- AutoComplete.superclass.constructor.apply(this, arguments);
549-}
550-
551-AutoComplete.NAME = 'autocomplete';
552-
553-AutoComplete.LIST_TEMPLATE = '<ul></ul>';
554-AutoComplete.ITEM_TEMPLATE = '<li class="item yui3-menuitem"></li>';
555-AutoComplete.ITEM_CONTENT_TEMPLATE = (
556- '<a href="#" class="yui3-menuitem-content"></a>');
557-
558-AutoComplete.ATTRS = {
559- /**
560- * The autocomplete data that we will be filtering to find matching
561- * results.
562- *
563- * @attribute data
564- * @type Hash
565- * @default Obj
566- */
567- data: {
568- valueFn: function() { return {}; }
569- },
570-
571- /**
572- * The delimiter to use when splitting the user's current input into
573- * matchable query strings.
574- *
575- * @attribute delimiter
576- * @type String
577- * @default ' '
578- */
579- delimiter: {
580- value: ' '
581- },
582-
583- /**
584- * The current subset of data matching the user's query, ordered by
585- * accuracy. Contains an Array of hash objects; see
586- * <code>filterResults</code>' return type for the details.
587- *
588- * @attribute matches
589- * @type Array
590- * @default []
591- */
592- matches: {
593- valueFn: function() { return []; }
594- },
595-
596- /**
597- * The DOM element we watch for new input. May be set with a Node,
598- * HTMLElement, or CSS selector. Setting this aligns the widget's
599- * position.
600- *
601- * @attribute input
602- * @type Node
603- * @default null
604- */
605- input: {
606- value: null,
607- setter: function(val) {
608- return this._setInput(val);
609- }
610- },
611-
612- /**
613- * The user's current query. Contains a hash of values containing the
614- * current query text, and the query offset. <code>null</code> if the
615- * widget doesn't contain a valid query.
616- *
617- * See the <code>parseQuery</code> method for the hash details.
618- *
619- * @attribute query
620- * @type Object
621- * @default null
622- */
623- query: {
624- value: null
625- }
626-};
627-
628-Y.extend(AutoComplete, AutoCompleteBase, {
629-
630- /**
631- * The <ul> containing the current list of completions. May be null.
632- *
633- * @property _completions
634- * @private
635- */
636- _completions: null,
637-
638- /**
639- * Flag to indicate that the user just completed a string
640- *
641- * @property _last_input_was_completed
642- * @private
643- */
644- _last_input_was_completed: false,
645-
646- /**
647- * Initialize the widget.
648- *
649- * @method initializer
650- * @protected
651- */
652- initializer: function() {
653- // The widget starts out hidden.
654- this.hide();
655- },
656-
657- /**
658- * Destroy the widget.
659- *
660- * @method destructor
661- * @protected
662- */
663- destructor: function() {
664- // Detach our keyboard input listener
665- var input = this.get('INPUT');
666- if (input && this.get(RENDERED)) {
667- input.detach('keydown', this._onInputKeydown);
668- input.detach('keyup', this._onInputKeyup);
669- }
670- },
671-
672- /**
673- * Render the DOM and position the widget.
674- *
675- * @method renderUI
676- * @protected
677- */
678- renderUI: function() {
679- var input = this.get(INPUT);
680- var bounding_box = this.get(BOUNDING_BOX);
681- // Needed by the NodeMenuNav plugin
682- bounding_box.addClass("yui3-menu");
683- // Move ourself into position below the document body. This is
684- // necessary so that the absolute widget positioning code sets
685- // the correct coordinates.
686- Y.one('body').appendChild(bounding_box);
687- this.get(CONTENT_BOX)
688- .setStyle('minWidth', input.get('offsetWidth') + "px")
689- .addClass('yui3-menu-content');
690-
691- // Set the correct absolute coordinates on-screen. Bypass the
692- // Widget.move() function, since it incorrectly positions the element
693- // relative to the viewportal scroll.
694- var iregion = input.get('region');
695- bounding_box.setStyles({
696- 'left': iregion.left + 'px',
697- 'top': iregion.bottom + 'px'
698- });
699- // Disable the browser autocomplete so that it does not conflict.
700- input.setAttribute('autocomplete', 'off');
701- },
702-
703- /**
704- * Render the completions list. Swaps out the existing list if one is
705- * already present.
706- *
707- * @method _renderCompletions
708- * @param query {String} The user's current query, used for formatting.
709- * @protected
710- */
711- _renderCompletions: function(query) {
712- var matches = this.get(MATCHES);
713- if (!this.get(RENDERED) || !matches) {
714- // Skip lots of rendering work, because if there are no matches,
715- // then the autocomplete list will be hidden.
716- return;
717- }
718-
719- var list = Y.Node.create(AutoComplete.LIST_TEMPLATE);
720- list.addClass(C_LIST);
721-
722- var result;
723- var item;
724- var match;
725- var idx;
726- for (idx = 0; idx < matches.length; idx++) {
727- match = matches[idx];
728- result = this.formatResult(match.text, query, match.offset);
729- item = this._renderCompletion(result, idx);
730- list.appendChild(item);
731- }
732-
733- var cbox = this.get(CONTENT_BOX);
734-
735- var box = this.get(BOUNDING_BOX);
736- box.unplug(Y.Plugin.NodeMenuNav);
737-
738- if (this._completions) {
739- cbox.replaceChild(list, this._completions);
740- } else {
741- cbox.appendChild(list);
742- }
743-
744- // Re-plug the MenuNav, so it updates the menu options.
745- box.plug(Y.Plugin.NodeMenuNav);
746- box.setStyle('z-index', '31000');
747-
748- // Highlight the first item.
749- this._selectItem(0, false);
750-
751- this._completions = list;
752- },
753-
754- /**
755- * Render a completion list item.
756- *
757- * @method _renderCompletion
758- * @protected
759- * @param html_content {String} The completion's HTML text content.
760- * @param item_index {NUM} The index of this completion item in the list.
761- * @return {Node} The new list item.
762- */
763- _renderCompletion: function(html_content, item_index) {
764- var item = Y.Node.create(AutoComplete.ITEM_TEMPLATE);
765- item.setAttribute('id', this._makeItemID(item_index));
766-
767- var link = Y.Node.create(AutoComplete.ITEM_CONTENT_TEMPLATE);
768- link.set('innerHTML', html_content);
769- item.appendChild(link);
770-
771- return item;
772- },
773-
774- /**
775- * Generate a new item identifier string for a given item index.
776- *
777- * @method _makeItemID
778- * @protected
779- * @param index {NUM} The index of the item in the matches list.
780- * @return {String} The generated ID.
781- */
782- _makeItemID: function(index) {
783- return 'item' + index;
784- },
785-
786- /**
787- * Retrieve the given item node's index in the match list.
788- *
789- * @method _indexForItem
790- * @protected
791- * @param item {Node} The item node to retrieve the index from.
792- * @return {NUM} The index as an integer, null if the index couldn't
793- * be retrieved.
794- */
795- _indexForItem: function(item) {
796- var id = parseInt(item.getAttribute('id').replace('item', ''), 10);
797- return Y.Lang.isNumber(id) ? id : null;
798- },
799-
800- /**
801- * Bind the widget to the DOM.
802- *
803- * @method bindUI
804- * @protected
805- */
806- bindUI: function() {
807- // Save the handle so we can detach it later.
808- var input = this.get(INPUT);
809- input.on('keydown', this._onInputKeydown, this);
810- input.on('keyup', this._onInputKeyup, this);
811- this.get('contentBox').on('click', this._onListClick, this);
812- },
813-
814- /**
815- * Parse the user's input, returning the specific query string to be
816- * matched. Returns null if the query is empty (with no characters typed
817- * yet).
818- *
819- * @method parseQuery
820- * @public
821- * @param input {String} The textbox input to be parsed.
822- * @param caret_pos {NUM} Optional: the position of the caret. Defaults
823- * to the end of the string.
824- * @return {Object} A hash containing:
825- * <dl>
826- * <dt>text</dt><dd>The query text</dd>
827- * <dt>offset</dt><dd>The starting index of the query in the input</dd>
828- * </dl>.
829- * Returns <code>null</code> if the query couldn't be parsed.
830- */
831- parseQuery: function(input, caret_pos) {
832- if (caret_pos <= 0) {
833- // The caret is as the start of the input field, so no query
834- // is possible.
835- return null;
836- }
837-
838- if (!Y.Lang.isNumber(caret_pos) || (caret_pos > input.length)) {
839- caret_pos = input.length;
840- }
841-
842- var delimiter = this.get(DELIMITER);
843-
844- // Start searches at the character before the cursor in the string.
845- var start = input.lastIndexOf(delimiter, caret_pos - 1);
846- var end = input.indexOf(delimiter, caret_pos - 1);
847-
848- if ((start === end) && (start !== -1)) {
849- // The caret was on the delimiter itself.
850- return null;
851- }
852-
853- if (start === -1) {
854- // There wasn't a delimiter between the caret and the start of the
855- // string.
856- start = 0;
857- } else {
858- // Move one character past the delimiter
859- start++;
860- }
861-
862- if (end === -1) {
863- // There wasn't a delimiter between the caret and the end of the
864- // string.
865- end = input.length;
866- }
867-
868- // Strip any leading whitespace.
869- while ((input[start] === ' ' || input[start] === '\t')
870- && (start <= end)) {
871- start++;
872- }
873-
874- if (start === end) {
875- // The whitespace stripping took us to the end of the input.
876- return null;
877- }
878-
879- var query = {
880- text: input.substring(start, end),
881- offset: start
882- };
883- return query;
884- },
885-
886- /**
887- * Find inputs matching the user query and update the <em>matches<em>
888- * attribute with the result.
889- *
890- * @method findMatches
891- * @public
892- * @param query {String} The user query we want to find matches for.
893- * @return The array of matches, or an empty array if no results were
894- * found.
895- */
896- findMatches: function(query) {
897- var matches = this.filterResults(this.get(DATA), query);
898- this.set(MATCHES, matches);
899- return matches;
900- },
901-
902- /**
903- * Filter the widget's data set down to the matching results.
904- *
905- * The returned list of matches is in order of priority.
906- *
907- * The default implementation puts the matches closest to the front of the
908- * user query first. Matches are case-insensitive.
909- *
910- * @method filterResults
911- * @public
912- * @param results {Array} The data to filter
913- * @param query {String} The user's current query
914- * @return Array of filtered and ordered match objects. Each match object
915- * has the following keys:
916- * <dl>
917- * <dt>text</dt>
918- * <dd>The query text</dd>
919- * <dt>offset</dt>
920- * <dd>The starting index of the query in the input</dd>
921- * </dl>
922- */
923- filterResults: function(data, query) {
924- // Find matches and push them into an array of arrays. The array
925- // is indexed by the start of the match.
926-
927- var midx;
928- var match_string;
929- var start_indicies = [];
930-
931- var lowercase_query = query.toLowerCase();
932-
933- if (data) {
934- Y.Array.each(data, function(match_key) {
935-
936- match_string = match_key.toString();
937- midx = match_string.toLowerCase().indexOf(lowercase_query);
938-
939- if (midx > -1) {
940- if (!start_indicies[midx]) {
941- start_indicies[midx] = [];
942- }
943- start_indicies[midx].push(match_string);
944- }
945- });
946- }
947-
948- // Flatten the array of match indicies. Matches close to the front
949- // of the user query have a higher priority, and come first in the
950- // list of matches. Matches farther toward the end coming later.
951- var matches = [];
952- Y.Array.each(start_indicies, function(match_set, index) {
953- if (match_set) {
954- Y.Array.each(match_set, function(match) {
955- matches.push({text: match, offset: index});
956- });
957- }
958- });
959-
960- return matches;
961- },
962-
963- /**
964- * Format a possible completion for display.
965- *
966- * The returned string will appear as a list item's contents.
967- *
968- * @method formatResult
969- * @public
970- * @param result {String} The result data to format.
971- * @param query {String} The user's current query.
972- * @param offset {NUM} The offset of the matching text in the result.
973- * @return {String} The HTML to be displayed.
974- */
975- formatResult: function(result, query, offset) {
976- return this.markMatchingText(result, query, offset);
977- },
978-
979- /**
980- * Mark the portion of a result that matches the user query.
981- *
982- * @method markMatchingText
983- * @public
984- * @param text {String} The completion result text to be marked.
985- * @param query {String} The user query string.
986- * @param offset {NUM} The offset of the query in the text.
987- * @return {String} The modified text.
988- */
989- markMatchingText: function(text, query, offset) {
990- var start = offset;
991- if (start < 0 || !query) {
992- return text;
993- }
994-
995- var end = start + query.length;
996-
997- var before = text.substring(0, start);
998- var match = text.substring(start, end);
999- var after = text.substring(end);
1000-
1001- // This is ugly, but I can't see a better way to do it at the moment.
1002- match = '<span class="matching-text">' + match + '</span>';
1003-
1004- return before + match + after;
1005- },
1006-
1007- /**
1008- * Complete the user's input using the item currently selected in the
1009- * completions list, or the first item if no list item was picked.
1010- *
1011- * @method completeInput
1012- * @public
1013- */
1014- completeInput: function() {
1015- var active_item = this.getActiveItem();
1016- if (active_item) {
1017- var item_index = this._indexForItem(active_item);
1018- if (item_index !== null) {
1019- this.completeInputUsingItem(item_index);
1020- }
1021- } else {
1022- // Select the first item in the list
1023- this.completeInputUsingItem(0);
1024- }
1025- this.get(INPUT).focus();
1026- this._last_input_was_completed = true;
1027- },
1028-
1029- /**
1030- * Completes the user's input using the specified match number.
1031- *
1032- * @method completeInputUsingItem
1033- * @public
1034- * @param match_num {NUM} The number of the match to select.
1035- */
1036- completeInputUsingItem: function(match_num) {
1037- var matches = this.get(MATCHES);
1038- if (matches.length === 0) {
1039- return;
1040- }
1041-
1042- if (match_num >= matches.length) {
1043- Y.fail("Failed to complete item number " + match_num +
1044- " because there are only " + matches.length + " matches " +
1045- "available.");
1046- return;
1047- }
1048-
1049- var completion_txt = matches[match_num].text;
1050- var query = this.get(QUERY);
1051- var delimiter = this.get(DELIMITER);
1052- var input = this.get(INPUT);
1053- var input_txt = input.get('value');
1054-
1055- // Drop the current query from the input string.
1056- var query_end = query.offset + query.text.length;
1057- var input_head = input_txt.substring(0, query.offset);
1058- var input_tail = input_txt.substring(query_end, input_txt.length);
1059- var tail_delimiter = delimiter;
1060- // Add the delimiter only if it's needed.
1061- if (input_tail.charAt(input_tail.length - 1) === delimiter) {
1062- tail_delimiter = '';
1063- }
1064-
1065- var new_input = [
1066- input_head, completion_txt, input_tail, tail_delimiter].join('');
1067-
1068- input.set(VALUE, new_input);
1069- this.hide();
1070- },
1071-
1072- /**
1073- * Return the currently selected item in the completions list.
1074- *
1075- * @method getActiveItem
1076- * @public
1077- * @return {Node} The selected item node, or null if no item is active.
1078- */
1079- getActiveItem: function() {
1080- // It is ugly to have to check protected members of the menu
1081- // like this, but the 'currently selected item' should
1082- // really be public, don't you think?
1083- var menu = this.get(BOUNDING_BOX).menuNav;
1084- if (menu) {
1085- return menu._activeItem ? menu._activeItem : null;
1086- }
1087- return null;
1088- },
1089-
1090- /**
1091- * Select the Nth item in the completions list.
1092- *
1093- * @method _selectItem
1094- * @protected
1095- * @param index {NUM} The index of the item to select.
1096- * @param set_focus {Boolean} Set this to true if the selected item should
1097- * also recieve the keyboard focus.
1098- * @return {Node} The item that was selected, or null if it could not
1099- * be found.
1100- */
1101- _selectItem: function(index, set_focus) {
1102- var menu = this.get(BOUNDING_BOX).menuNav;
1103-
1104- // More ugliness, looking at protected object members that should
1105- // be made public.
1106- var firstItem = menu._rootMenu.all('.yui3-menuitem').item(0);
1107- var item = menu ? firstItem : null;
1108- if (!menu || !item) {
1109- return null;
1110- }
1111-
1112- var idx;
1113- for (idx = 0; idx < index; idx++) {
1114- item = item.next();
1115- if (!item) {
1116- return null;
1117- }
1118- }
1119-
1120- if (set_focus) {
1121- // We need an anchor to focus on, because some browsers (IE, ahem)
1122- // don't like focusing non-anchor things.
1123- var anchor = item.one('a');
1124-
1125- menu._focusManager.set("activeDescendant", anchor);
1126- menu._focusItem(item);
1127-
1128- if (anchor) {
1129- // Use a 5ms timer to give the browser rendering engine some
1130- // time to catch up to the JS call, and prevent a race
1131- // condition with the focus() method.
1132- Y.later(5, anchor, anchor.focus);
1133- }
1134- }
1135- menu._setActiveItem(item);
1136- return item;
1137- },
1138-
1139- /**
1140- * Set the autocomplete's <input> element, and align the autocomplete
1141- * widget's position to it.
1142- *
1143- * @method _setInput
1144- * @protected
1145- * @param node {Node|HTMLElement|Selector} The input node.
1146- * @return {Node} A Node instance, or null if the requested input node
1147- * could not be found.
1148- */
1149- _setInput: function(elem) {
1150- var node = Y.one(elem);
1151- if (node === null) {
1152- return null;
1153- }
1154-
1155- // We need to calculate the input area's caret position.
1156- Y.augment(node, Y.lp.ui.NodeCaretPos);
1157- return node;
1158- },
1159-
1160- /**
1161- * Handle new text inputs.
1162- *
1163- * @method _onInputKeyup
1164- * @protected
1165- * @param e {Event.Custom} The event object.
1166- */
1167- _onInputKeyup: function(e) {
1168- var input = this.get(INPUT);
1169- var caret_pos = null;
1170-
1171- if (input.getCaretPos !== undefined) {
1172- caret_pos = input.getCaretPos();
1173- }
1174-
1175- var query = this.parseQuery(input.get(VALUE), caret_pos);
1176- this.set(QUERY, query);
1177-
1178- if (e.keyCode === ESCAPE ||
1179- e.keyCode === RETURN ||
1180- e.keyCode === TAB ||
1181- e.keyCode === ARROW_DOWN) {
1182- // We don't want to re-display the matches list.
1183- return;
1184- }
1185-
1186- if (query === null) {
1187- // No valid user input yet
1188- this._last_input_was_completed = false;
1189- this.hide();
1190- return;
1191- }
1192-
1193- if (this.findMatches(query.text).length !== 0) {
1194- this._renderCompletions(query.text);
1195- this._last_input_was_completed = false;
1196- this.show();
1197- } else {
1198- this.hide();
1199- }
1200- },
1201-
1202- /**
1203- * Handle presses of keys like Tab and Enter
1204- *
1205- * @method _onInputKeydown
1206- * @protected
1207- * @param e {Event.Custom} The event object.
1208- */
1209- _onInputKeydown: function(e) {
1210- // Is this one of our completion keys; Tab, or Enter?
1211- if (e.keyCode === TAB || e.keyCode === RETURN) {
1212- /* Check that the last string was not completed and that there are
1213- matching queries (we don't want to try and complete the input if
1214- there are no matches). */
1215- if (this.get(QUERY) !== null
1216- && !this._last_input_was_completed
1217- && this.findMatches(this.get(QUERY).text).length !== 0) {
1218- // The user has an active query in the input box.
1219- this.completeInput();
1220- // Keep the tab key from switching focus away from the input
1221- // field.
1222- e.preventDefault();
1223- }
1224- } else if (e.keyCode === ESCAPE) {
1225- // Escape closes the currently displayed results
1226- this.hide();
1227- } else if (e.keyCode === ARROW_DOWN) {
1228- this._selectItem(1, true);
1229- // Prevent the browser from scrolling the window.
1230- e.preventDefault();
1231- }
1232- },
1233-
1234- /**
1235- * Handle clicks on the autocomplete widget list.
1236- *
1237- * @method _onListClick
1238- * @protected
1239- * @param e {Event.Custom} The event object.
1240- */
1241- _onListClick: function(e) {
1242- this.completeInput();
1243- e.preventDefault();
1244- }
1245-});
1246-
1247-
1248-Y.lp.ui.AutoComplete = AutoComplete;
1249-
1250-
1251-/**
1252- * A mixin class for calculating the caret position inside a Node
1253- * instance.
1254- *
1255- * @class NodeCaretPos
1256- */
1257-
1258-Y.lp.ui.NodeCaretPos = function() {};
1259-
1260-/**
1261- * Return the offset of the caret in a text field.
1262- *
1263- * @method getCaretPos
1264- * @public
1265- * @return {NUM} The distance from the start of the field to the caret, or
1266- * null if the position couldn't be calculated.
1267- */
1268-Y.lp.ui.NodeCaretPos.prototype.getCaretPos = function() {
1269- var elem = Y.Node.getDOMNode(this);
1270- if (elem.selectionEnd) {
1271- return elem.selectionEnd;
1272- } else if (document.selection) {
1273- var range = document.selection.createRange();
1274- if (range.parentElement() === elem) {
1275- var end_range = range.duplicate();
1276- end_range.moveStart("character", -elem.value.length);
1277- return end_range.text.length;
1278- }
1279- }
1280- return null;
1281-};
1282-
1283-
1284-}, "0.1", {"skinnable": true, "requires":["oop", "base", "event", "widget",
1285- "widget-stack", "node-menunav"]});
1286
1287=== removed directory 'lib/lp/app/javascript/autocomplete/tests'
1288=== removed file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html'
1289--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html 2012-10-26 09:46:28 +0000
1290+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html 1970-01-01 00:00:00 +0000
1291@@ -1,46 +0,0 @@
1292-<!DOCTYPE html>
1293-<!--
1294-Copyright 2012 Canonical Ltd. This software is licensed under the
1295-GNU Affero General Public License version 3 (see the file LICENSE).
1296--->
1297-
1298-<html>
1299- <head>
1300- <title>Autocomplete Tests</title>
1301-
1302- <!-- YUI and test setup -->
1303- <script type="text/javascript"
1304- src="../../../../../../build/js/yui/yui/yui.js">
1305- </script>
1306- <link rel="stylesheet"
1307- href="../../../../../../build/js/yui/console/assets/console-core.css" />
1308- <link rel="stylesheet"
1309- href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
1310- <link rel="stylesheet"
1311- href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
1312-
1313- <script type="text/javascript"
1314- src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
1315-
1316- <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
1317-
1318- <!-- Dependencies -->
1319- <!-- <script type="text/javascript" src="../../../../../../build/js/lp/..."></script> -->
1320-
1321- <!-- The module under test. -->
1322- <script type="text/javascript" src="../autocomplete.js"></script>
1323-
1324- <!-- Placeholder for any css asset for this module. -->
1325- <!-- <link rel="stylesheet" href="../assets/autocomplete-core.css" /> -->
1326-
1327- <!-- The test suite. -->
1328- <script type="text/javascript" src="test_autocomplete.js"></script>
1329-
1330- </head>
1331- <body class="yui3-skin-sam">
1332- <ul id="suites">
1333- <!-- <li>lp.large_indicator.test</li> -->
1334- <li>lp.autocomplete.test</li>
1335- </ul>
1336- </body>
1337-</html>
1338
1339=== removed file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js'
1340--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 2013-03-20 03:41:40 +0000
1341+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 1970-01-01 00:00:00 +0000
1342@@ -1,575 +0,0 @@
1343-/* Copyright 2012 Canonical Ltd. This software is licensed under the
1344- * GNU Affero General Public License version 3 (see the file LICENSE). */
1345-
1346-YUI.add('lp.autocomplete.test', function (Y) {
1347- var tests = Y.namespace('lp.autocomplete.test');
1348- tests.suite = new Y.Test.Suite('autocomplete Tests');
1349-
1350- /*****************************
1351- *
1352- * Helper methods and aliases
1353- *
1354- */
1355- var Assert = Y.Assert;
1356-
1357- /* Helper function to clean up a dynamically added widget instance. */
1358- function cleanup_widget(widget) {
1359- // Nuke the boundingBox, but only if we've touched the DOM.
1360- if (widget.get('rendered')) {
1361- var bb = widget.get('boundingBox');
1362- bb.get('parentNode').removeChild(bb);
1363- }
1364- // Kill the widget itself.
1365- widget.destroy();
1366- }
1367-
1368- /* A helper to create a simple text input box */
1369- function make_input(value) {
1370- var input = document.createElement('input');
1371- input.setAttribute('type', 'text');
1372- input.setAttribute('value', value || '');
1373- Y.one('body').appendChild(input);
1374- return input;
1375- }
1376-
1377- /* A helper to destroy a generic input: make_input()'s inverse */
1378- function kill_input(input) {
1379- Y.one('body').removeChild(input);
1380- }
1381-
1382- tests.suite.add(new Y.Test.Case({
1383- name:'test widget setup',
1384-
1385- setUp: function() {
1386- this.input = make_input();
1387- },
1388-
1389- tearDown: function() {
1390- kill_input(this.input);
1391- },
1392-
1393- test_library_exists: function () {
1394- Y.Assert.isObject(Y.lp.ui.AutoComplete,
1395- "Could not locate the lp.ui.autocomplete module");
1396- },
1397-
1398- test_widget_starts_hidden: function() {
1399- var autocomp = new Y.lp.ui.AutoComplete({ input: this.input });
1400- autocomp.render();
1401- Assert.isFalse(
1402- autocomp.get('visible'),
1403- "The widget should start out hidden.");
1404- }
1405- }));
1406-
1407- tests.suite.add(new Y.Test.Case({
1408-
1409- name:'test display of matching results',
1410-
1411- setUp: function() {
1412- this.input = make_input();
1413- this.autocomp = new Y.lp.ui.AutoComplete({
1414- input: this.input
1415- });
1416- },
1417-
1418- tearDown: function() {
1419- cleanup_widget(this.autocomp);
1420- kill_input(this.input);
1421- },
1422-
1423- /* A helper to option the completions list for a given input string. */
1424- complete_input: function(value) {
1425- this.input.value = value;
1426- var last_charcode = value.charCodeAt(value.length - 1);
1427- Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
1428- },
1429-
1430- /* Extract the matching text from the widget's autocompletion list. */
1431- get_completions: function() {
1432- if (!this.autocomp.get('rendered')) {
1433- Y.fail("Tried find matches for an unrendered widget.");
1434- return;
1435- }
1436-
1437- var matches = [];
1438- this.autocomp
1439- .get('boundingBox')
1440- .all('.item')
1441- .each(function(item) {
1442- matches.push(item.get('text'));
1443- });
1444- return matches;
1445- },
1446-
1447- test_autocomplete_is_visible_if_results_match: function() {
1448- this.autocomp.set('data', ['aaa']);
1449- this.autocomp.render();
1450-
1451- // We want to match the one and only data set element.
1452- this.complete_input('aa');
1453- Assert.isTrue(
1454- this.autocomp.get('visible'),
1455- "The widget should be visible if matching input was found.");
1456- },
1457-
1458- test_autocomplete_is_hidden_if_no_query_is_given: function() {
1459- this.autocomp.set('data', ['aaa']);
1460- this.autocomp.render();
1461-
1462- // We want to simulate an empty input field, but some action
1463- // triggers matching.
1464- this.complete_input('');
1465- Assert.isFalse(
1466- this.autocomp.get('visible'),
1467- "The widget should be hidden if the input field is empty.");
1468- },
1469-
1470- test_autocomplete_is_hidden_if_results_do_not_match: function() {
1471- this.autocomp.set('data', ['bbb']);
1472- this.autocomp.render();
1473-
1474- if (this.autocomp.get('visible')) {
1475- Y.fail("The autocomplete widget should start out hidden.");
1476- }
1477-
1478-
1479- // 'aa' shouldn't match any of the data.
1480- this.complete_input('aa');
1481- Assert.isFalse(
1482- this.autocomp.get('visible'),
1483- "The widget should be hidden if the query doesn't match any " +
1484- "possible completions.");
1485- },
1486-
1487- test_display_should_contain_all_matches: function() {
1488- var data = [
1489- 'aaa',
1490- 'baa'
1491- ];
1492-
1493- this.autocomp.set('data', data);
1494- this.autocomp.render();
1495-
1496- // Trigger autocompletion, should match all data items.
1497- this.complete_input('aa');
1498-
1499- // Grab the now-open menu
1500- var option_list = Y.one('.yui3-autocomplete-list');
1501- Assert.isObject(option_list,
1502- "The list of completion options should be open.");
1503-
1504- Y.ArrayAssert.itemsAreEqual(
1505- this.get_completions(),
1506- data,
1507- "Every autocomplete item should be present in the available " +
1508- "match keys.");
1509- },
1510-
1511- test_display_is_updated_with_new_completions: function() {
1512- // Create two pieces of data, each narrower than the other.
1513- this.autocomp.set('data', ['aaa', 'aab']);
1514- this.autocomp.render();
1515-
1516- // Trigger autocompletion for the loosest matches
1517- this.complete_input('aa');
1518- // Complete the narrower set
1519- this.complete_input('aaa');
1520-
1521- var completions = this.get_completions();
1522-
1523- Y.ArrayAssert.itemsAreEqual(
1524- ['aaa'],
1525- completions,
1526- "'aaa' should be the data item displayed after narrowing the " +
1527- "search with the query 'aaa'.");
1528- },
1529-
1530- test_matching_text_in_item_is_marked: function() {
1531- this.autocomp.set('data', ['aaa']);
1532- this.autocomp.render();
1533-
1534- // Display the matching input.
1535- var query = 'aa';
1536- this.complete_input(query);
1537-
1538- // Grab the matching item
1539- var matching_text = this.autocomp
1540- .get('boundingBox')
1541- .one('.item .matching-text');
1542-
1543- Assert.isNotNull(matching_text,
1544- "Some of the matching item's text should be marked matching.");
1545-
1546- Assert.areEqual(
1547- query,
1548- matching_text.get('text'),
1549- "The matching text should be the same as the query text.");
1550- },
1551-
1552- test_escape_key_should_close_completions_list: function() {
1553- this.autocomp.set('data', ['aaa']);
1554- this.autocomp.render();
1555-
1556- // Open the completions list
1557- this.complete_input('aa');
1558-
1559- // Hit the escape key to close the list
1560- Y.Event.simulate(this.input, 'keydown', { keyCode: 27 });
1561-
1562- Assert.isFalse(
1563- this.autocomp.get('visible'),
1564- "The list of completions should be closed after pressing the " +
1565- "escape key.");
1566- }
1567- }));
1568-
1569- tests.suite.add(new Y.Test.Case({
1570-
1571- name:'test result text marking method',
1572-
1573- test_match_at_beginning_should_be_marked: function() {
1574- var autocomp = new Y.lp.ui.AutoComplete();
1575- var marked_text = autocomp.markMatchingText('aabb', 'aa', 0);
1576-
1577- Assert.areEqual(
1578- '<span class="matching-text">aa</span>bb',
1579- marked_text,
1580- "The text at the beginning of the result should have been " +
1581- "marked.");
1582- },
1583-
1584- test_match_in_middle_should_be_marked: function() {
1585- var autocomp = new Y.lp.ui.AutoComplete();
1586- var marked_text = autocomp.markMatchingText('baab', 'aa', 1);
1587-
1588- Assert.areEqual(
1589- 'b<span class="matching-text">aa</span>b',
1590- marked_text,
1591- "The text in the middle of the result should have been " +
1592- "marked.");
1593- },
1594-
1595- test_match_at_end_should_be_marked: function() {
1596- var autocomp = new Y.lp.ui.AutoComplete();
1597- var marked_text = autocomp.markMatchingText('bbaa', 'aa', 2);
1598-
1599- Assert.areEqual(
1600- 'bb<span class="matching-text">aa</span>',
1601- marked_text,
1602- "The text at the end of the result should have been " +
1603- "marked.");
1604- }
1605- }));
1606-
1607-
1608- tests.suite.add(new Y.Test.Case({
1609-
1610- name:'test query parsing',
1611-
1612- setUp: function() {
1613- this.autocomplete = new Y.lp.ui.AutoComplete({
1614- delimiter: ' '
1615- });
1616- },
1617-
1618- test_space_for_delimiter: function() {
1619- Assert.areEqual(
1620- 'b',
1621- this.autocomplete.parseQuery('a b').text,
1622- "Input should be split around the 'space' character.");
1623- Assert.isNull(
1624- this.autocomplete.parseQuery(' '),
1625- "Space for input and delimiter should not parse.");
1626- },
1627-
1628- test_parsed_query_is_stripped_of_leading_whitespace: function() {
1629- this.autocomplete.set('delimiter', ',');
1630-
1631- Assert.areEqual(
1632- 'a',
1633- this.autocomplete.parseQuery(' a').text,
1634- "Leading whitespace at the start of the input string should " +
1635- "be stripped.");
1636-
1637- Assert.areEqual(
1638- 'b',
1639- this.autocomplete.parseQuery('a, b').text,
1640- "Leading whitespace between the last separator and the " +
1641- "current query should be stripped.");
1642- },
1643-
1644- test_query_is_taken_from_middle_of_input: function() {
1645- // Pick a caret position that is in the middle of the second result.
1646- var input = "aaa bbb ccc";
1647- var caret = 6;
1648-
1649- Assert.areEqual(
1650- 'bbb',
1651- this.autocomplete.parseQuery(input, caret).text,
1652- "The current query should be picked out of the middle of the " +
1653- "text input if the caret has been positioned there.");
1654- },
1655-
1656- test_query_is_taken_from_beginning_of_input: function() {
1657- // Pick a caret position that is in the first input's query
1658- var input = "aaa bbb";
1659- var caret = 2;
1660-
1661- Assert.areEqual(
1662- 'aaa',
1663- this.autocomplete.parseQuery(input, caret).text,
1664- "The first block of text should become the current query if " +
1665- "the caret is positioned within it.");
1666- }
1667- }));
1668-
1669- tests.suite.add(new Y.Test.Case({
1670-
1671- name:'test results matching algorithm',
1672-
1673- /* A helper function to determine if two match result items are equal */
1674- matches_are_equal: function(a, b) {
1675- if (Y.Lang.isUndefined(a)) {
1676- Assert.fail("Match set 'a' is of type 'undefined'!");
1677- }
1678- if (Y.Lang.isUndefined(b)) {
1679- Assert.fail("Match set 'b' is of type 'undefined'!");
1680- }
1681- return (a.text === b.text) && (a.offset === b.offset);
1682- },
1683-
1684- test_no_matches_returns_an_empty_array: function() {
1685- var autocomplete = new Y.lp.ui.AutoComplete({
1686- data: ['ccc']
1687- });
1688-
1689- var matches = autocomplete.findMatches('aa');
1690- Y.ArrayAssert.isEmpty(matches,
1691- "No data should have matched the query 'aa'");
1692- },
1693-
1694- test_match_last_item: function() {
1695- var autocomplete = new Y.lp.ui.AutoComplete({
1696- data: [
1697- 'ccc',
1698- 'bbb',
1699- 'aaa'
1700- ]
1701- });
1702-
1703- var matches = autocomplete.findMatches('aa');
1704-
1705- Y.ArrayAssert.itemsAreEquivalent(
1706- [{text: 'aaa', offset: 0}],
1707- matches,
1708- this.matches_are_equal,
1709- "One row should have matched the query 'aa'.");
1710- },
1711-
1712- test_match_ordering: function() {
1713- // Matches, in reverse order.
1714- var autocomplete = new Y.lp.ui.AutoComplete({
1715- data: [
1716- 'bbaa',
1717- 'baab',
1718- 'aabb'
1719- ]
1720- });
1721-
1722- var matches = autocomplete.findMatches('aa');
1723-
1724- Y.ArrayAssert.itemsAreEquivalent(
1725- [{text: 'aabb', offset: 0},
1726- {text: 'baab', offset: 1},
1727- {text: 'bbaa', offset: 2}],
1728- matches,
1729- this.matches_are_equal,
1730- "The match array should have all of it's keys in order.");
1731- },
1732-
1733- test_mixed_case_text_matches: function() {
1734- var autocomplete = new Y.lp.ui.AutoComplete({
1735- data: ['aBc']
1736- });
1737-
1738- var matches = autocomplete.findMatches('b');
1739-
1740- Y.ArrayAssert.itemsAreEquivalent(
1741- [{text:'aBc', offset: 1}],
1742- matches,
1743- this.matches_are_equal,
1744- "The match algorithm should be case insensitive.");
1745- },
1746-
1747- test_mixed_case_matches_come_in_stable_order: function() {
1748- // Data with the mixed-case coming first in order.
1749- var autocomplete = new Y.lp.ui.AutoComplete({
1750- data: ['aBc', 'aaa', 'abc']
1751- });
1752-
1753- var matches = autocomplete.findMatches('b');
1754-
1755- Y.ArrayAssert.itemsAreEquivalent(
1756- [{text: 'aBc', offset: 1},
1757- {text: 'abc', offset: 1}],
1758- matches,
1759- this.matches_are_equal,
1760- "Mixed-case matches should arrive in stable order.");
1761- }
1762- }));
1763-
1764-
1765- tests.suite.add(new Y.Test.Case({
1766-
1767- name:'test selecting results',
1768-
1769- setUp: function() {
1770- this.input = make_input();
1771- this.autocomp = new Y.lp.ui.AutoComplete({
1772- input: this.input
1773- });
1774- this.autocomp.render();
1775- },
1776-
1777- tearDown: function() {
1778- cleanup_widget(this.autocomp);
1779- kill_input(this.input);
1780- },
1781-
1782- // A helper to option the completions list for a given input string.
1783- complete_input: function(value) {
1784- this.input.value = value;
1785- var last_charcode = value.charCodeAt(value.length - 1);
1786- Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
1787- },
1788-
1789- // A helper to select the selected completion result with the Tab key.
1790- press_selection_key: function() {
1791- Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
1792- },
1793-
1794- test_pressing_matching_key_raises_menu: function() {
1795- this.autocomp.set('data', ['aaaa', 'aabb']);
1796- this.complete_input('aa');
1797- var box = this.autocomp.get('boundingBox');
1798- Assert.areEqual(
1799- '31000',
1800- box.getStyle('z-index'),
1801- "The menu z-index should be 31000; above it's overlay.");
1802- },
1803-
1804- test_pressing_enter_completes_current_input: function() {
1805- this.autocomp.set('data', ['aaaa', 'aabb']);
1806-
1807- // Open the completion options
1808- this.complete_input('aa');
1809-
1810- // Press 'Enter'
1811- Y.Event.simulate(this.input, "keydown", { keyCode: 13 });
1812-
1813- Assert.areEqual(
1814- 'aaaa ',
1815- this.input.value,
1816- "The first completion should have been appended to the " +
1817- "input's value after pressing the 'Enter' key.");
1818- },
1819-
1820- test_pressing_tab_completes_current_input: function() {
1821- this.autocomp.set('data', ['aaaa', 'aabb']);
1822-
1823- // Open the completion options
1824- this.complete_input('aa');
1825-
1826- // Press 'Tab'
1827- Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
1828-
1829- Assert.areEqual(
1830- 'aaaa ',
1831- this.input.value,
1832- "The first completion should have been appended to the " +
1833- "input's value after pressing the 'Enter' key.");
1834- },
1835-
1836- test_clicking_on_first_result_completes_input: function() {
1837- this.autocomp.set('data', ['aaaa', 'aabb']);
1838- this.complete_input('aa');
1839-
1840- // Click on the first displayed result
1841- var options = this.autocomp.get('contentBox').all('.item');
1842- var first_item = Y.Node.getDOMNode(options.item(0));
1843- Y.Event.simulate(first_item, 'click');
1844-
1845- Assert.areEqual(
1846- 'aaaa ',
1847- this.input.value,
1848- "The first completion should have been appended to the " +
1849- "input's value after clicking it's list node.");
1850- },
1851-
1852- test_selecting_results_hides_completion_list: function() {
1853- this.autocomp.set('data', 'aaa');
1854- this.complete_input('a');
1855- this.press_selection_key();
1856-
1857- Assert.isFalse(
1858- this.autocomp.get('visible'),
1859- "The completion list should be hidden after a result is " +
1860- "selected.");
1861- },
1862-
1863- test_completed_input_replaces_current_input: function() {
1864- this.autocomp.set('data', ['abba']);
1865-
1866- // Match the one and only result, but match the second character in
1867- // it. Throw in some pre-existing user input just to be sure things
1868- // work.
1869- this.complete_input('xxx b');
1870- this.press_selection_key();
1871-
1872- Assert.areEqual(
1873- 'xxx abba ',
1874- this.input.value,
1875- "The user's current query should have been replaced with the " +
1876- "selected value.");
1877- },
1878-
1879- test_completed_input_has_delimiter_appended_to_it: function() {
1880- var delimiter = ' ';
1881- this.autocomp.set('data', ['aaaa']);
1882- this.autocomp.set('delimiter', delimiter);
1883-
1884- this.complete_input('a');
1885- this.press_selection_key();
1886-
1887- Assert.areEqual(
1888- delimiter,
1889- this.input.value.charAt(this.input.value.length - 1),
1890- "The last character of the input should be the current " +
1891- "query delimiter.");
1892- },
1893-
1894- test_down_arrow_selects_second_result_in_list: function() {
1895- this.autocomp.set('data', ['first_item', 'second_item']);
1896-
1897- // Match the first result. It should be selected by default.
1898- this.complete_input('item');
1899-
1900- // Simulate pressing the down arrow key.
1901- Y.Event.simulate(this.input, 'keydown', { keyCode: 40 });
1902-
1903- // Now, select the second result.
1904- this.press_selection_key();
1905-
1906- Assert.areEqual(
1907- 'second_item ',
1908- this.input.value,
1909- "Pressing the down-arrow key should select the second option " +
1910- "in the completions list.");
1911- }
1912- }));
1913-
1914-}, '0.1', {
1915- 'requires': ['test', 'test-console', 'lp.autocomplete', 'node', 'event',
1916- 'event-simulate', 'lp.ui.autocomplete']
1917-});
1918
1919=== modified file 'lib/lp/bugs/javascript/bug_tags_entry.js'
1920--- lib/lp/bugs/javascript/bug_tags_entry.js 2013-01-30 00:58:45 +0000
1921+++ lib/lp/bugs/javascript/bug_tags_entry.js 2017-07-24 17:10:46 +0000
1922@@ -21,7 +21,6 @@
1923 var tags_edit_spinner;
1924 var tags_form;
1925 var available_tags;
1926-var autocomplete;
1927
1928 var A = 'a',
1929 VALUE = 'value',
1930@@ -127,7 +126,7 @@
1931 tag_list_span.removeClass(HIDDEN);
1932 tags_trigger.removeClass(HIDDEN);
1933 tags_form.addClass(HIDDEN);
1934- autocomplete.hide();
1935+ tag_input.blur();
1936 Y.lp.anim.green_flash({ node: tag_list_span }).run();
1937 namespace.update_ui();
1938 };
1939@@ -143,7 +142,6 @@
1940 tags_trigger.addClass(HIDDEN);
1941 tags_form.removeClass(HIDDEN);
1942 tag_input.focus();
1943- autocomplete.render();
1944 };
1945
1946
1947@@ -231,11 +229,6 @@
1948 ok_button.on('click', function(e) {
1949 e.halt();
1950 save_tags();
1951- /* Check to see if the autocomplete dialogue is still open
1952- and if so, close it. */
1953- if (!autocomplete._last_input_was_completed) {
1954- autocomplete.hide();
1955- }
1956 });
1957 cancel_button.on('click', function(e) {
1958 e.halt();
1959@@ -251,8 +244,45 @@
1960 });
1961 tags_trigger.addClass('js-action');
1962
1963- autocomplete = namespace.setup_tag_complete(
1964- '#tag-input', available_official_tags);
1965+ namespace.setup_tag_complete('#tag-input', available_official_tags);
1966+};
1967+
1968+
1969+/**
1970+ * Returns an array of results that contain the complete query as a phrase.
1971+ * Case-insensitive. Matches closer to the front of the user query are
1972+ * listed earlier.
1973+ *
1974+ * @method orderedPhraseMatch
1975+ * @param {String} query Query to match
1976+ * @param {Array} results Results to filter
1977+ * @return {Array} Filtered results
1978+ **/
1979+var orderedPhraseMatch = function(query, results) {
1980+ if (!query) { return results; }
1981+
1982+ query = query.toLowerCase();
1983+
1984+ var match_sets = [];
1985+ Y.Array.each(results, function(result) {
1986+ var i = result.text.toLowerCase().indexOf(query);
1987+ if (i > -1) {
1988+ if (!match_sets[i]) {
1989+ match_sets[i] = [];
1990+ }
1991+ match_sets[i].push(result);
1992+ }
1993+ });
1994+
1995+ var matches = [];
1996+ Y.Array.each(match_sets, function(match_set) {
1997+ if (match_set) {
1998+ Y.Array.each(match_set, function(match) {
1999+ matches.push(match);
2000+ });
2001+ }
2002+ });
2003+ return matches;
2004 };
2005
2006
2007@@ -262,22 +292,22 @@
2008 * @method setup_tag_complete
2009 */
2010 namespace.setup_tag_complete = function(input, official_tags) {
2011- var bounding_box = Y.Node.create(
2012- '<div class="bug-tag-complete"><div></div></div>');
2013- Y.one('body').appendChild(bounding_box);
2014- var autocomplete = new Y.lp.ui.AutoComplete({
2015- input: input,
2016- data: official_tags,
2017- boundingBox: bounding_box,
2018- contentBox: bounding_box.one('div')
2019- });
2020- autocomplete.get('input').on('focus', function(e) {
2021- autocomplete.render();
2022- });
2023- return autocomplete;
2024+ var input_node = Y.one(input);
2025+ input_node.plug(Y.Plugin.AutoComplete, {
2026+ queryDelimiter: ' ',
2027+ render: false,
2028+ resultFilters: orderedPhraseMatch,
2029+ resultHighlighter: 'phraseMatch',
2030+ source: official_tags
2031+ });
2032+ input_node.on('focus', function(e) {
2033+ input_node.ac.render();
2034+ });
2035+ return input_node.ac;
2036 };
2037 }, "0.1", {
2038 "requires": [
2039- "array-extras", "base", "io-base", "node", "substitute",
2040- "node-menunav", "lp.anim", "lp.ui.autocomplete", "lp.client"]
2041+ "array-extras", "autocomplete", "autocomplete-filters",
2042+ "autocomplete-highlighters", "base", "io-base", "node", "substitute",
2043+ "node-menunav", "lp.anim", "lp.client"]
2044 });
2045
2046=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.html'
2047--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.html 2012-10-26 09:54:28 +0000
2048+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.html 2017-07-24 17:10:46 +0000
2049@@ -35,8 +35,6 @@
2050 src="../../../../../build/js/lp/app/anim/anim.js"></script>
2051 <script type="text/javascript"
2052 src="../../../../../build/js/lp/app/extras/extras.js"></script>
2053- <script type="text/javascript"
2054- src="../../../../../build/js/lp/app/autocomplete/autocomplete.js"></script>
2055
2056 <!-- The module under test. -->
2057 <script type="text/javascript" src="../bug_tags_entry.js"></script>
2058
2059=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.js'
2060--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2013-01-30 01:25:57 +0000
2061+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2017-07-24 17:10:46 +0000
2062@@ -82,10 +82,11 @@
2063 // The form is created.
2064 var form_node = this.bug_tags_div.one('#tags-form');
2065 Y.Assert.isInstanceOf(Y.Node, form_node);
2066- Y.Assert.isInstanceOf(Y.Node, form_node.one('#tag-input'));
2067+ var input_node = form_node.one('#tag-input');
2068+ Y.Assert.isInstanceOf(Y.Node, input_node);
2069 Y.Assert.isInstanceOf(Y.Node, form_node.one('#tags-edit-spinner'));
2070 Y.Assert.isInstanceOf(Y.Node, form_node.one('#edit-tags-cancel'));
2071- Y.Assert.isInstanceOf(Y.Node, Y.one('.bug-tag-complete'));
2072+ Y.Assert.isInstanceOf(Y.AutoComplete, input_node.ac);
2073 },
2074
2075 test_show_activity: function() {
2076@@ -222,13 +223,10 @@
2077 // The Autocompleter nodes are provided.
2078 module.setup_tag_complete(
2079 'input[id="field.tag"]',['project-tag']);
2080- var completer_node = Y.one('.yui3-autocomplete');
2081- Y.Assert.isInstanceOf(Y.Node, completer_node);
2082- Y.Assert.isTrue(completer_node.hasClass('bug-tag-complete'));
2083- var completer_content = completer_node.one(
2084- '.yui3-autocomplete-content');
2085- Y.Assert.isInstanceOf(Y.Node, completer_content);
2086 var input = Y.one('input[id="field.tag"]');
2087+ input.simulate('focus');
2088+ Y.Assert.isTrue(input.hasClass('yui3-aclist-input'));
2089+ Y.Assert.isInstanceOf(Y.Node, Y.one('.yui3-aclist'));
2090 },
2091
2092 test_render_on_focus: function() {
2093
2094=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.html'
2095--- lib/lp/registry/javascript/tests/test_structural_subscription.html 2012-12-12 21:55:15 +0000
2096+++ lib/lp/registry/javascript/tests/test_structural_subscription.html 2017-07-24 17:10:46 +0000
2097@@ -48,8 +48,6 @@
2098 <script type="text/javascript"
2099 src="../../../../../build/js/lp/app/gallery-accordion/gallery-accordion.js"></script>
2100 <script type="text/javascript"
2101- src="../../../../../build/js/lp/app/autocomplete/autocomplete.js"></script>
2102- <script type="text/javascript"
2103 src="../../../../../build/js/lp/bugs/bug_tags_entry.js"></script>
2104
2105 <!-- The module under test. -->
2106
2107=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
2108--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2017-07-22 21:38:52 +0000
2109+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2017-07-24 17:10:46 +0000
2110@@ -419,8 +419,8 @@
2111 Assert.areEqual(
2112 'Add a mail subscription for Test bugs',
2113 header.get('text'));
2114- var bug_tags_node = Y.one(".bug-tag-complete");
2115- Assert.isInstanceOf(Y.Node, bug_tags_node);
2116+ var input_tags_node = Y.one('input[name="tags"]');
2117+ Assert.isInstanceOf(Y.AutoComplete, input_tags_node.ac);
2118 },
2119
2120 test_clean_up_overlay: function() {
2121
2122=== modified file 'lib/lp/scripts/utilities/js/combinecss.py'
2123--- lib/lp/scripts/utilities/js/combinecss.py 2017-01-18 00:55:27 +0000
2124+++ lib/lp/scripts/utilities/js/combinecss.py 2017-07-24 17:10:46 +0000
2125@@ -16,6 +16,7 @@
2126 'ubuntu-webfonts.css',
2127 'style.css',
2128 'yui/cssreset/cssreset.css',
2129+ 'yui/assets/skins/sam/autocomplete-list.css',
2130 'yui/assets/skins/sam/calendar-base.css',
2131 'yui/assets/skins/sam/calendar.css',
2132 'yui/assets/skins/sam/calendarnavigator.css',
2133@@ -27,7 +28,6 @@
2134 'build/ui/assets/skins/sam/lazr.css',
2135 'build/ui/assets/skins/sam/banner.css',
2136 'build/inlineedit/assets/skins/sam/editor.css',
2137- 'build/autocomplete/assets/skins/sam/autocomplete.css',
2138 'build/overlay/assets/skins/sam/pretty-overlay.css',
2139 'build/formoverlay/assets/formoverlay-core.css',
2140 'build/inlinehelp/assets/inlinehelp-core.css',