launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #21696
[Merge] lp:~cjwatson/launchpad/yui-autocomplete into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/yui-autocomplete into lp:launchpad.
Commit message:
Switch to the YUI autocomplete widget.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/yui-autocomplete/+merge/327804
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.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/yui-autocomplete into lp:launchpad.
=== removed directory 'lib/lp/app/javascript/autocomplete'
=== removed directory 'lib/lp/app/javascript/autocomplete/assets'
=== removed file 'lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css'
--- lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css 2011-06-29 14:56:15 +0000
+++ lib/lp/app/javascript/autocomplete/assets/autocomplete-core.css 1970-01-01 00:00:00 +0000
@@ -1,193 +0,0 @@
-/*
-Copyright (c) 2009, Canonical Ltd. All rights reserved.
-Licensed under the GNU Affero General Public License:
-http://www.gnu.org/licenses/agpl.txt
-*/
-
-/*
- * Make the z-index a bit higher than the lazr.Overlay, which is at
- * z-index 999, so we can use autocomplete widgets inside overlays without
- * hassle.
- */
-.yui3-autocomplete { position: absolute; z-index: 1050; }
-.yui3-autocomplete-hidden { display: none; }
-
-
-/*
-* Bring in the NodeMenuNav core CSS from YUI 3.0.0pr2.
-*
-* This saves us from jumping through hoops to pull the individual files
-* from YUI itself.
-*
-* This will have to change once the YUI Loader starts bringing in the CSS
-* file dependencies.
-*/
-.yui3-menu .yui3-menu {
-
- position: absolute;
- z-index: 1;
-
-}
-
-
-.yui3-menu .yui3-shim {
-
- /*
- Styles for the <iframe> shim used to prevent <select> elements from poking through
- submenus in IE < 7. Note: For peformance, creation of the <iframe> shim for each submenu
- is deferred until it is initially made visible by the user.
- */
-
- position: absolute;
- top: 0;
- left: 0;
- z-index: -1;
- opacity: 0;
- filter: alpha(opacity=0); /* For IE since it doesn't implement the CSS3 "opacity" property. */
- border: none;
- margin: 0;
- padding: 0;
- height: 100%;
- width: 100%;
-
-}
-
-.yui3-menu-hidden {
-
- /*
- Position hidden menus outside the viewport boundaries to prevent them from
- triggering scrollbars on the viewport.
- */
-
- top: -10000px;
- left: -10000px;
-
- /*
- Using "visibility:hidden" over "display" none because:
-
- 1) As the "position" property for submenus is set to "absolute", they are out of
- the document flow and take up no space. Therefore, from that perspective use of
- "display:none" is redundant.
-
- 2) According to MSDN use of "display:none" is more expensive:
- "Display is the more expensive of the two CSS properties, so if you are
- making elements appear and disappear often, visibility will be faster."
- (See http://msdn.microsoft.com/en-us/library/bb264005(VS.85).aspx)
- */
-
- visibility: hidden;
-
-}
-
-.yui3-menu li {
-
- list-style-type: none;
-
-}
-
-.yui3-menu ul,
-.yui3-menu li {
-
- margin: 0;
- padding: 0;
-
-}
-
-.yui3-menu-label,
-.yui3-menuitem-content {
-
- text-align: left;
- white-space: nowrap;
- display: block;
-
-}
-
-.yui3-menu-horizontal li {
-
- float: left;
- width: auto;
-
-}
-
-.yui3-menu-horizontal li li {
-
- float: none;
-
-}
-
-.yui3-menu-horizontal ul {
-
- /*
- Use of "zoom" sets the "hasLayout" property to "true" in IE (< 8). When "hasLayout" is
- set to "true", an element can clear its floated descendents. For more:
- http://msdn.microsoft.com/en-gb/library/ms533776(VS.85).aspx
- */
-
- *zoom: 1;
-
-}
-
-.yui3-menu-horizontal ul ul {
-
- /*
- No need to clear <ul>s of submenus of horizontal menus since <li>s of submenus
- aren't floated.
- */
-
- *zoom: normal;
-
-}
-
-.yui3-menu-horizontal>.yui3-menu-content>ul:after {
-
- /* Self-clearing solution for Opera, Webkit, Gecko and IE > 7 */
-
- content: "";
- display: block;
- clear: both;
- line-height: 0;
- font-size: 0;
- visibility: hidden;
-
-}
-
-
-/*
- The following two rules are for IE 7. Triggering "hasLayout" (via use of "zoom") prevents
- first-tier submenus from hiding when the mouse is moving from an menu label in a root menu to
- its corresponding submenu.
-*/
-
-.yui3-menu-content {
-
- *zoom: 1;
-
-}
-
-
-.yui3-menu-hidden .yui3-menu-content {
-
- *zoom: normal;
-
-}
-
-
-/*
- The following two rules are for IE 6 (Standards Mode and Quirks Mode) and IE 7 (Quirks Mode
- only). Triggering "hasLayout" (via use of "zoom") fixes a bug in IE where mousing mousing off
- the text node of menuitem or menu label will incorrectly trigger the mouseout event.
-*/
-
-.yui3-menuitem-content,
-.yui3-menu-label {
-
- _zoom: 1;
-
-}
-
-.yui3-menu-hiden .yui3-menuitem-content,
-.yui3-menu-hiden .yui3-menu-label {
-
- _zoom: normal;
-
-}
=== removed directory 'lib/lp/app/javascript/autocomplete/assets/skins'
=== removed directory 'lib/lp/app/javascript/autocomplete/assets/skins/sam'
=== removed file 'lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css'
--- lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css 2012-01-06 11:08:30 +0000
+++ lib/lp/app/javascript/autocomplete/assets/skins/sam/autocomplete-skin.css 1970-01-01 00:00:00 +0000
@@ -1,286 +0,0 @@
-/*
-Copyright (c) 2009, Canonical Ltd. All rights reserved.
-Licensed under the GNU Affero General Public License:
-http://www.gnu.org/licenses/agpl.txt
-*/
-
-.yui3-skin-sam .yui3-autocomplete-content { background-color: #fff; }
-.yui3-skin-sam .yui3-autocomplete-list {
- margin: 0;
- padding: 0 0.3em 0 0.3em;
- border: 1px solid black;
-}
-.yui3-skin-sam .yui3-autocomplete-list .item { list-style-type: none; }
-.yui3-skin-sam .yui3-autocomplete-list .item .matching-text { font-weight: bold; }
-.yui3-skin-sam .yui3-autocomplete-list .yui3-menuitem-content { padding: 0 0.3em; }
-
-
-/*
-* Bring in the NodeMenuNav skin CSS from YUI 3.0.0pr2.
-*
-* This saves us from jumping through hoops to pull the individual files
-* from YUI itself.
-*
-* This will have to change once the YUI Loader starts bringing in the CSS
-* file dependencies.
-*/
-.yui3-skin-sam .yui3-menu-content,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content {
-
- font-size: 93%; /* 12px */
- line-height: 1.5; /* 18px */
- *line-height: 1.45; /* For IE */
- border: solid 1px #808080;
- background: #fff;
- padding: 3px 0;
-
-}
-
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-content {
-
- font-size: 100%;
-
-}
-
-/* Horizontal menus */
-
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-content {
-
- line-height: 2; /* ~24px */
- *line-height: 1.9; /* For IE */
- background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
- padding: 0;
-
-}
-
-
-.yui3-skin-sam .yui3-menu ul,
-.yui3-skin-sam .yui3-menu ul ul {
-
- margin-top: 3px;
- padding-top: 3px;
- border-top: solid 1px #ccc;
-
-}
-
-.yui3-skin-sam .yui3-menu ul.first-of-type {
-
- border: 0;
- margin: 0;
- padding: 0;
-
-}
-
-.yui3-skin-sam .yui3-menu-horizontal ul {
-
- padding: 0;
- margin: 0;
- border: 0;
-
-}
-
-
-.yui3-skin-sam .yui3-menu li,
-.yui3-skin-sam .yui3-menu .yui3-menu li {
-
- /*
- For and IE 6 (Strict Mode and Quirks Mode) and IE 7 (Quirks Mode only): Used to collapse
- superfluous white space between <li> elements that is triggered by the "display" property
- of the <a> elements being set to "block" by node-menunav-core.css file.
- */
-
- _border-bottom: solid 1px #fff;
-
-}
-
-.yui3-skin-sam .yui3-menu-horizontal li {
-
- _border-bottom: 0;
-
-}
-
-.yui3-skin-sam .yui3-menubuttonnav li {
-
- border-right: solid 1px #ccc;
-
-}
-
-.yui3-skin-sam .yui3-splitbuttonnav li {
-
- border-right: solid 1px #808080;
-
-}
-
-.yui3-skin-sam .yui3-menubuttonnav li li,
-.yui3-skin-sam .yui3-splitbuttonnav li li {
-
- border-right: 0;
-
-}
-
-
-/* Menuitems and menu labels */
-
-
-.yui3-skin-sam .yui3-menu-label,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label,
-.yui3-skin-sam .yui3-menuitem-content,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-content {
-
- padding: 0 20px;
- color: #000;
- text-decoration: none;
- cursor: default;
-
- /*
- Necessary specify values for border, position and margin to override values specified in
- the selectors that follow.
- */
-
- float: none;
- border: 0;
- margin: 0;
-
-}
-
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label,
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-content {
-
- padding: 0 10px;
- border-style: solid;
- border-color: #808080;
- border-width: 1px 0;
- margin: -1px 0;
-
- float: left; /* Ensures that menu labels clear floated descendents. Also gets negative
- margins working in IE 7 (Strict Mode). */
- width: auto;
-
-}
-
-.yui3-skin-sam .yui3-menu-label,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label {
-
- background: url(vertical-menu-submenu-indicator.png) right center no-repeat;
-
-}
-
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label {
-
- background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 0;
-
-}
-
-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label,
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label {
-
- background-image: none;
-
-}
-
-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label {
-
- padding-right: 0;
-
-}
-
-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label em {
-
- font-style: normal;
- padding-right: 20px;
- display: block;
- background: url(horizontal-menu-submenu-indicator.png) right center no-repeat;
-
-}
-
-
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label {
-
- padding: 0;
-
-}
-
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label a {
-
- float: left;
- width: auto;
- color: #000;
- text-decoration: none;
- cursor: default;
- padding: 0 5px 0 10px;
-
-}
-
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label .yui3-menu-toggle {
-
- padding: 0; /* Override padding applied by the preceding rule. */
- border-left: solid 1px #ccc;
- width: 15px;
- overflow: hidden;
- text-indent: -1000px;
- background: url(horizontal-menu-submenu-indicator.png) 3px center no-repeat;
-
-}
-
-
-/* Selected menuitem */
-
-.yui3-skin-sam .yui3-menu-label-active,
-.yui3-skin-sam .yui3-menu-label-menuvisible,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-active,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menu-label-menuvisible {
-
- background-color: #B3D4FF;
-
-}
-
-.yui3-skin-sam .yui3-menuitem-active .yui3-menuitem-content,
-.yui3-skin-sam .yui3-menu .yui3-menu .yui3-menuitem-active .yui3-menuitem-content {
-
- background-image: none;
- background-color: #B3D4FF;
-
- /*
- Undo values set for "border-left-width" and "margin-left" when the root menu has a class of
- "yui3-menubuttonnav" or "yui3-splitbuttonnav" applied.
- */
-
- border-left-width: 0;
- margin-left: 0;
-
-}
-
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-active,
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menuitem-active .yui3-menuitem-content,
-.yui3-skin-sam .yui3-menu-horizontal .yui3-menu-label-menuvisible {
-
- border-color: #7D98B8;
- background: url(../../../../assets/skins/sam/sprite.png) repeat-x 0 -1700px;
-
-}
-
-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-active,
-.yui3-skin-sam .yui3-menubuttonnav .yui3-menuitem-active .yui3-menuitem-content,
-.yui3-skin-sam .yui3-menubuttonnav .yui3-menu-label-menuvisible,
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-active,
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menuitem-active .yui3-menuitem-content,
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
-
- border-left-width: 1px;
- margin-left: -1px;
-
-}
-
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible {
-
- border-color: #808080;
- background: transparent;
-
-}
-
-.yui3-skin-sam .yui3-splitbuttonnav .yui3-menu-label-menuvisible .yui3-menu-toggle {
-
- border-color: #7D98B8;
- background: url(horizontal-menu-submenu-toggle.png) left center no-repeat;
-
-}
=== removed file 'lib/lp/app/javascript/autocomplete/autocomplete.js'
--- lib/lp/app/javascript/autocomplete/autocomplete.js 2013-03-20 03:41:40 +0000
+++ lib/lp/app/javascript/autocomplete/autocomplete.js 1970-01-01 00:00:00 +0000
@@ -1,789 +0,0 @@
-/* Copyright 2009 Canonical Ltd. This software is licensed under the
- * GNU Affero General Public License version 3 (see the file LICENSE). */
-
-YUI.add('lp.ui.autocomplete', function(Y) {
-
-/**
- * A simple autocomplete widget.
- *
- * @module lp.ui.autocomplete
- * @namespace lp.ui.autocomplete
- */
-
-Y.namespace('lp.ui.autocomplete');
-
-
-var AUTOCOMP = 'autocomplete',
- BOUNDING_BOX = 'boundingBox',
- CONTENT_BOX = 'contentBox',
-
- INPUT = 'input',
- VALUE = 'value',
- QUERY = 'query',
- DATA = 'data',
- MATCHES = 'matches',
- RENDERED = 'rendered',
- DELIMITER = 'delimiter',
-
- TAB = 9,
- RETURN = 13,
- ESCAPE = 27,
- ARROW_DOWN = 40,
-
- getCN = Y.ClassNameManager.getClassName,
-
- C_LIST = getCN(AUTOCOMP, 'list');
-
-
-// We need a base class on which to build our autocomplete widget, so we will
-// make that class capable of positioning itself, too.
-var AutoCompleteBase = Y.Base.build(
- "AutoCompleteBase", Y.Widget, [Y.WidgetStack]);
-
-
-/**
- * A simple autocomplete widget.
- *
- * @class AutoComplete
- */
-
-function AutoComplete() {
- AutoComplete.superclass.constructor.apply(this, arguments);
-}
-
-AutoComplete.NAME = 'autocomplete';
-
-AutoComplete.LIST_TEMPLATE = '<ul></ul>';
-AutoComplete.ITEM_TEMPLATE = '<li class="item yui3-menuitem"></li>';
-AutoComplete.ITEM_CONTENT_TEMPLATE = (
- '<a href="#" class="yui3-menuitem-content"></a>');
-
-AutoComplete.ATTRS = {
- /**
- * The autocomplete data that we will be filtering to find matching
- * results.
- *
- * @attribute data
- * @type Hash
- * @default Obj
- */
- data: {
- valueFn: function() { return {}; }
- },
-
- /**
- * The delimiter to use when splitting the user's current input into
- * matchable query strings.
- *
- * @attribute delimiter
- * @type String
- * @default ' '
- */
- delimiter: {
- value: ' '
- },
-
- /**
- * The current subset of data matching the user's query, ordered by
- * accuracy. Contains an Array of hash objects; see
- * <code>filterResults</code>' return type for the details.
- *
- * @attribute matches
- * @type Array
- * @default []
- */
- matches: {
- valueFn: function() { return []; }
- },
-
- /**
- * The DOM element we watch for new input. May be set with a Node,
- * HTMLElement, or CSS selector. Setting this aligns the widget's
- * position.
- *
- * @attribute input
- * @type Node
- * @default null
- */
- input: {
- value: null,
- setter: function(val) {
- return this._setInput(val);
- }
- },
-
- /**
- * The user's current query. Contains a hash of values containing the
- * current query text, and the query offset. <code>null</code> if the
- * widget doesn't contain a valid query.
- *
- * See the <code>parseQuery</code> method for the hash details.
- *
- * @attribute query
- * @type Object
- * @default null
- */
- query: {
- value: null
- }
-};
-
-Y.extend(AutoComplete, AutoCompleteBase, {
-
- /**
- * The <ul> containing the current list of completions. May be null.
- *
- * @property _completions
- * @private
- */
- _completions: null,
-
- /**
- * Flag to indicate that the user just completed a string
- *
- * @property _last_input_was_completed
- * @private
- */
- _last_input_was_completed: false,
-
- /**
- * Initialize the widget.
- *
- * @method initializer
- * @protected
- */
- initializer: function() {
- // The widget starts out hidden.
- this.hide();
- },
-
- /**
- * Destroy the widget.
- *
- * @method destructor
- * @protected
- */
- destructor: function() {
- // Detach our keyboard input listener
- var input = this.get('INPUT');
- if (input && this.get(RENDERED)) {
- input.detach('keydown', this._onInputKeydown);
- input.detach('keyup', this._onInputKeyup);
- }
- },
-
- /**
- * Render the DOM and position the widget.
- *
- * @method renderUI
- * @protected
- */
- renderUI: function() {
- var input = this.get(INPUT);
- var bounding_box = this.get(BOUNDING_BOX);
- // Needed by the NodeMenuNav plugin
- bounding_box.addClass("yui3-menu");
- // Move ourself into position below the document body. This is
- // necessary so that the absolute widget positioning code sets
- // the correct coordinates.
- Y.one('body').appendChild(bounding_box);
- this.get(CONTENT_BOX)
- .setStyle('minWidth', input.get('offsetWidth') + "px")
- .addClass('yui3-menu-content');
-
- // Set the correct absolute coordinates on-screen. Bypass the
- // Widget.move() function, since it incorrectly positions the element
- // relative to the viewportal scroll.
- var iregion = input.get('region');
- bounding_box.setStyles({
- 'left': iregion.left + 'px',
- 'top': iregion.bottom + 'px'
- });
- // Disable the browser autocomplete so that it does not conflict.
- input.setAttribute('autocomplete', 'off');
- },
-
- /**
- * Render the completions list. Swaps out the existing list if one is
- * already present.
- *
- * @method _renderCompletions
- * @param query {String} The user's current query, used for formatting.
- * @protected
- */
- _renderCompletions: function(query) {
- var matches = this.get(MATCHES);
- if (!this.get(RENDERED) || !matches) {
- // Skip lots of rendering work, because if there are no matches,
- // then the autocomplete list will be hidden.
- return;
- }
-
- var list = Y.Node.create(AutoComplete.LIST_TEMPLATE);
- list.addClass(C_LIST);
-
- var result;
- var item;
- var match;
- var idx;
- for (idx = 0; idx < matches.length; idx++) {
- match = matches[idx];
- result = this.formatResult(match.text, query, match.offset);
- item = this._renderCompletion(result, idx);
- list.appendChild(item);
- }
-
- var cbox = this.get(CONTENT_BOX);
-
- var box = this.get(BOUNDING_BOX);
- box.unplug(Y.Plugin.NodeMenuNav);
-
- if (this._completions) {
- cbox.replaceChild(list, this._completions);
- } else {
- cbox.appendChild(list);
- }
-
- // Re-plug the MenuNav, so it updates the menu options.
- box.plug(Y.Plugin.NodeMenuNav);
- box.setStyle('z-index', '31000');
-
- // Highlight the first item.
- this._selectItem(0, false);
-
- this._completions = list;
- },
-
- /**
- * Render a completion list item.
- *
- * @method _renderCompletion
- * @protected
- * @param html_content {String} The completion's HTML text content.
- * @param item_index {NUM} The index of this completion item in the list.
- * @return {Node} The new list item.
- */
- _renderCompletion: function(html_content, item_index) {
- var item = Y.Node.create(AutoComplete.ITEM_TEMPLATE);
- item.setAttribute('id', this._makeItemID(item_index));
-
- var link = Y.Node.create(AutoComplete.ITEM_CONTENT_TEMPLATE);
- link.set('innerHTML', html_content);
- item.appendChild(link);
-
- return item;
- },
-
- /**
- * Generate a new item identifier string for a given item index.
- *
- * @method _makeItemID
- * @protected
- * @param index {NUM} The index of the item in the matches list.
- * @return {String} The generated ID.
- */
- _makeItemID: function(index) {
- return 'item' + index;
- },
-
- /**
- * Retrieve the given item node's index in the match list.
- *
- * @method _indexForItem
- * @protected
- * @param item {Node} The item node to retrieve the index from.
- * @return {NUM} The index as an integer, null if the index couldn't
- * be retrieved.
- */
- _indexForItem: function(item) {
- var id = parseInt(item.getAttribute('id').replace('item', ''), 10);
- return Y.Lang.isNumber(id) ? id : null;
- },
-
- /**
- * Bind the widget to the DOM.
- *
- * @method bindUI
- * @protected
- */
- bindUI: function() {
- // Save the handle so we can detach it later.
- var input = this.get(INPUT);
- input.on('keydown', this._onInputKeydown, this);
- input.on('keyup', this._onInputKeyup, this);
- this.get('contentBox').on('click', this._onListClick, this);
- },
-
- /**
- * Parse the user's input, returning the specific query string to be
- * matched. Returns null if the query is empty (with no characters typed
- * yet).
- *
- * @method parseQuery
- * @public
- * @param input {String} The textbox input to be parsed.
- * @param caret_pos {NUM} Optional: the position of the caret. Defaults
- * to the end of the string.
- * @return {Object} A hash containing:
- * <dl>
- * <dt>text</dt><dd>The query text</dd>
- * <dt>offset</dt><dd>The starting index of the query in the input</dd>
- * </dl>.
- * Returns <code>null</code> if the query couldn't be parsed.
- */
- parseQuery: function(input, caret_pos) {
- if (caret_pos <= 0) {
- // The caret is as the start of the input field, so no query
- // is possible.
- return null;
- }
-
- if (!Y.Lang.isNumber(caret_pos) || (caret_pos > input.length)) {
- caret_pos = input.length;
- }
-
- var delimiter = this.get(DELIMITER);
-
- // Start searches at the character before the cursor in the string.
- var start = input.lastIndexOf(delimiter, caret_pos - 1);
- var end = input.indexOf(delimiter, caret_pos - 1);
-
- if ((start === end) && (start !== -1)) {
- // The caret was on the delimiter itself.
- return null;
- }
-
- if (start === -1) {
- // There wasn't a delimiter between the caret and the start of the
- // string.
- start = 0;
- } else {
- // Move one character past the delimiter
- start++;
- }
-
- if (end === -1) {
- // There wasn't a delimiter between the caret and the end of the
- // string.
- end = input.length;
- }
-
- // Strip any leading whitespace.
- while ((input[start] === ' ' || input[start] === '\t')
- && (start <= end)) {
- start++;
- }
-
- if (start === end) {
- // The whitespace stripping took us to the end of the input.
- return null;
- }
-
- var query = {
- text: input.substring(start, end),
- offset: start
- };
- return query;
- },
-
- /**
- * Find inputs matching the user query and update the <em>matches<em>
- * attribute with the result.
- *
- * @method findMatches
- * @public
- * @param query {String} The user query we want to find matches for.
- * @return The array of matches, or an empty array if no results were
- * found.
- */
- findMatches: function(query) {
- var matches = this.filterResults(this.get(DATA), query);
- this.set(MATCHES, matches);
- return matches;
- },
-
- /**
- * Filter the widget's data set down to the matching results.
- *
- * The returned list of matches is in order of priority.
- *
- * The default implementation puts the matches closest to the front of the
- * user query first. Matches are case-insensitive.
- *
- * @method filterResults
- * @public
- * @param results {Array} The data to filter
- * @param query {String} The user's current query
- * @return Array of filtered and ordered match objects. Each match object
- * has the following keys:
- * <dl>
- * <dt>text</dt>
- * <dd>The query text</dd>
- * <dt>offset</dt>
- * <dd>The starting index of the query in the input</dd>
- * </dl>
- */
- filterResults: function(data, query) {
- // Find matches and push them into an array of arrays. The array
- // is indexed by the start of the match.
-
- var midx;
- var match_key;
- var match_string;
- var start_indicies = [];
-
- var lowercase_query = query.toLowerCase();
-
- if (data) {
- Y.Array.each(data, function(match_key) {
-
- match_string = match_key.toString();
- midx = match_string.toLowerCase().indexOf(lowercase_query);
-
- if (midx > -1) {
- if (!start_indicies[midx]) {
- start_indicies[midx] = [];
- }
- start_indicies[midx].push(match_string);
- }
- });
- }
-
- // Flatten the array of match indicies. Matches close to the front
- // of the user query have a higher priority, and come first in the
- // list of matches. Matches farther toward the end coming later.
- var matches = [];
- Y.Array.each(start_indicies, function(match_set, index) {
- if (match_set) {
- Y.Array.each(match_set, function(match) {
- matches.push({text: match, offset: index});
- });
- }
- });
-
- return matches;
- },
-
- /**
- * Format a possible completion for display.
- *
- * The returned string will appear as a list item's contents.
- *
- * @method formatResult
- * @public
- * @param result {String} The result data to format.
- * @param query {String} The user's current query.
- * @param offset {NUM} The offset of the matching text in the result.
- * @return {String} The HTML to be displayed.
- */
- formatResult: function(result, query, offset) {
- return this.markMatchingText(result, query, offset);
- },
-
- /**
- * Mark the portion of a result that matches the user query.
- *
- * @method markMatchingText
- * @public
- * @param text {String} The completion result text to be marked.
- * @param query {String} The user query string.
- * @param offset {NUM} The offset of the query in the text.
- * @return {String} The modified text.
- */
- markMatchingText: function(text, query, offset) {
- var start = offset;
- if (start < 0 || !query) {
- return text;
- }
-
- var end = start + query.length;
-
- var before = text.substring(0, start);
- var match = text.substring(start, end);
- var after = text.substring(end);
-
- // This is ugly, but I can't see a better way to do it at the moment.
- match = '<span class="matching-text">' + match + '</span>';
-
- return before + match + after;
- },
-
- /**
- * Complete the user's input using the item currently selected in the
- * completions list, or the first item if no list item was picked.
- *
- * @method completeInput
- * @public
- */
- completeInput: function() {
- var active_item = this.getActiveItem();
- if (active_item) {
- var item_index = this._indexForItem(active_item);
- if (item_index !== null) {
- this.completeInputUsingItem(item_index);
- }
- } else {
- // Select the first item in the list
- this.completeInputUsingItem(0);
- }
- this.get(INPUT).focus();
- this._last_input_was_completed = true;
- },
-
- /**
- * Completes the user's input using the specified match number.
- *
- * @method completeInputUsingItem
- * @public
- * @param match_num {NUM} The number of the match to select.
- */
- completeInputUsingItem: function(match_num) {
- var matches = this.get(MATCHES);
- if (matches.length === 0) {
- return;
- }
-
- if (match_num >= matches.length) {
- Y.fail("Failed to complete item number " + match_num +
- " because there are only " + matches.length + " matches " +
- "available.");
- return;
- }
-
- var completion_txt = matches[match_num].text;
- var query = this.get(QUERY);
- var delimiter = this.get(DELIMITER);
- var input = this.get(INPUT);
- var input_txt = input.get('value');
-
- // Drop the current query from the input string.
- var query_end = query.offset + query.text.length;
- var input_head = input_txt.substring(0, query.offset);
- var input_tail = input_txt.substring(query_end, input_txt.length);
- var tail_delimiter = delimiter;
- // Add the delimiter only if it's needed.
- if (input_tail.charAt(input_tail.length - 1) === delimiter) {
- tail_delimiter = '';
- }
-
- var new_input = [
- input_head, completion_txt, input_tail, tail_delimiter].join('');
-
- input.set(VALUE, new_input);
- this.hide();
- },
-
- /**
- * Return the currently selected item in the completions list.
- *
- * @method getActiveItem
- * @public
- * @return {Node} The selected item node, or null if no item is active.
- */
- getActiveItem: function() {
- // It is ugly to have to check protected members of the menu
- // like this, but the 'currently selected item' should
- // really be public, don't you think?
- var menu = this.get(BOUNDING_BOX).menuNav;
- if (menu) {
- return menu._activeItem ? menu._activeItem : null;
- }
- return null;
- },
-
- /**
- * Select the Nth item in the completions list.
- *
- * @method _selectItem
- * @protected
- * @param index {NUM} The index of the item to select.
- * @param set_focus {Boolean} Set this to true if the selected item should
- * also recieve the keyboard focus.
- * @return {Node} The item that was selected, or null if it could not
- * be found.
- */
- _selectItem: function(index, set_focus) {
- var menu = this.get(BOUNDING_BOX).menuNav;
-
- // More ugliness, looking at protected object members that should
- // be made public.
- var firstItem = menu._rootMenu.all('.yui3-menuitem').item(0);
- var item = menu ? firstItem : null;
- if (!menu || !item) {
- return null;
- }
-
- var idx;
- for (idx = 0; idx < index; idx++) {
- item = item.next();
- if (!item) {
- return null;
- }
- }
-
- if (set_focus) {
- // We need an anchor to focus on, because some browsers (IE, ahem)
- // don't like focusing non-anchor things.
- var anchor = item.one('a');
-
- menu._focusManager.set("activeDescendant", anchor);
- menu._focusItem(item);
-
- if (anchor) {
- // Use a 5ms timer to give the browser rendering engine some
- // time to catch up to the JS call, and prevent a race
- // condition with the focus() method.
- Y.later(5, anchor, anchor.focus);
- }
- }
- menu._setActiveItem(item);
- return item;
- },
-
- /**
- * Set the autocomplete's <input> element, and align the autocomplete
- * widget's position to it.
- *
- * @method _setInput
- * @protected
- * @param node {Node|HTMLElement|Selector} The input node.
- * @return {Node} A Node instance, or null if the requested input node
- * could not be found.
- */
- _setInput: function(elem) {
- var node = Y.one(elem);
- if (node === null) {
- return null;
- }
-
- // We need to calculate the input area's caret position.
- Y.augment(node, Y.lp.ui.NodeCaretPos);
- return node;
- },
-
- /**
- * Handle new text inputs.
- *
- * @method _onInputKeyup
- * @protected
- * @param e {Event.Custom} The event object.
- */
- _onInputKeyup: function(e) {
- var input = this.get(INPUT);
- var caret_pos = null;
-
- if (input.getCaretPos !== undefined) {
- caret_pos = input.getCaretPos();
- }
-
- var query = this.parseQuery(input.get(VALUE), caret_pos);
- this.set(QUERY, query);
-
- if (e.keyCode === ESCAPE ||
- e.keyCode === RETURN ||
- e.keyCode === TAB ||
- e.keyCode === ARROW_DOWN) {
- // We don't want to re-display the matches list.
- return;
- }
-
- if (query === null) {
- // No valid user input yet
- this._last_input_was_completed = false;
- this.hide();
- return;
- }
-
- if (this.findMatches(query.text).length !== 0) {
- this._renderCompletions(query.text);
- this._last_input_was_completed = false;
- this.show();
- } else {
- this.hide();
- }
- },
-
- /**
- * Handle presses of keys like Tab and Enter
- *
- * @method _onInputKeydown
- * @protected
- * @param e {Event.Custom} The event object.
- */
- _onInputKeydown: function(e) {
- // Is this one of our completion keys; Tab, or Enter?
- if (e.keyCode === TAB || e.keyCode === RETURN) {
- /* Check that the last string was not completed and that there are
- matching queries (we don't want to try and complete the input if
- there are no matches). */
- if (this.get(QUERY) !== null
- && !this._last_input_was_completed
- && this.findMatches(this.get(QUERY).text).length !== 0) {
- // The user has an active query in the input box.
- this.completeInput();
- // Keep the tab key from switching focus away from the input
- // field.
- e.preventDefault();
- }
- } else if (e.keyCode === ESCAPE) {
- // Escape closes the currently displayed results
- this.hide();
- } else if (e.keyCode === ARROW_DOWN) {
- this._selectItem(1, true);
- // Prevent the browser from scrolling the window.
- e.preventDefault();
- }
- },
-
- /**
- * Handle clicks on the autocomplete widget list.
- *
- * @method _onListClick
- * @protected
- * @param e {Event.Custom} The event object.
- */
- _onListClick: function(e) {
- this.completeInput();
- e.preventDefault();
- }
-});
-
-
-Y.lp.ui.AutoComplete = AutoComplete;
-
-
-/**
- * A mixin class for calculating the caret position inside a Node
- * instance.
- *
- * @class NodeCaretPos
- */
-
-Y.lp.ui.NodeCaretPos = function() {};
-
-/**
- * Return the offset of the caret in a text field.
- *
- * @method getCaretPos
- * @public
- * @return {NUM} The distance from the start of the field to the caret, or
- * null if the position couldn't be calculated.
- */
-Y.lp.ui.NodeCaretPos.prototype.getCaretPos = function() {
- var elem = Y.Node.getDOMNode(this);
- if (elem.selectionEnd) {
- return elem.selectionEnd;
- } else if (document.selection) {
- var range = document.selection.createRange();
- if (range.parentElement() === elem) {
- var end_range = range.duplicate();
- end_range.moveStart("character", -elem.value.length);
- return end_range.text.length;
- }
- }
- return null;
-};
-
-
-}, "0.1", {"skinnable": true, "requires":["oop", "base", "event", "widget",
- "widget-stack", "node-menunav"]});
=== removed directory 'lib/lp/app/javascript/autocomplete/tests'
=== removed file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html'
--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html 2012-10-26 09:46:28 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.html 1970-01-01 00:00:00 +0000
@@ -1,46 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright 2012 Canonical Ltd. This software is licensed under the
-GNU Affero General Public License version 3 (see the file LICENSE).
--->
-
-<html>
- <head>
- <title>Autocomplete Tests</title>
-
- <!-- YUI and test setup -->
- <script type="text/javascript"
- src="../../../../../../build/js/yui/yui/yui.js">
- </script>
- <link rel="stylesheet"
- href="../../../../../../build/js/yui/console/assets/console-core.css" />
- <link rel="stylesheet"
- href="../../../../../../build/js/yui/test-console/assets/skins/sam/test-console.css" />
- <link rel="stylesheet"
- href="../../../../../../build/js/yui/test/assets/skins/sam/test.css" />
-
- <script type="text/javascript"
- src="../../../../../../build/js/lp/app/testing/testrunner.js"></script>
-
- <link rel="stylesheet" href="../../../../app/javascript/testing/test.css" />
-
- <!-- Dependencies -->
- <!-- <script type="text/javascript" src="../../../../../../build/js/lp/..."></script> -->
-
- <!-- The module under test. -->
- <script type="text/javascript" src="../autocomplete.js"></script>
-
- <!-- Placeholder for any css asset for this module. -->
- <!-- <link rel="stylesheet" href="../assets/autocomplete-core.css" /> -->
-
- <!-- The test suite. -->
- <script type="text/javascript" src="test_autocomplete.js"></script>
-
- </head>
- <body class="yui3-skin-sam">
- <ul id="suites">
- <!-- <li>lp.large_indicator.test</li> -->
- <li>lp.autocomplete.test</li>
- </ul>
- </body>
-</html>
=== removed file 'lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js'
--- lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 2013-03-20 03:41:40 +0000
+++ lib/lp/app/javascript/autocomplete/tests/test_autocomplete.js 1970-01-01 00:00:00 +0000
@@ -1,575 +0,0 @@
-/* Copyright 2012 Canonical Ltd. This software is licensed under the
- * GNU Affero General Public License version 3 (see the file LICENSE). */
-
-YUI.add('lp.autocomplete.test', function (Y) {
- var tests = Y.namespace('lp.autocomplete.test');
- tests.suite = new Y.Test.Suite('autocomplete Tests');
-
- /*****************************
- *
- * Helper methods and aliases
- *
- */
- var Assert = Y.Assert;
-
- /* Helper function to clean up a dynamically added widget instance. */
- function cleanup_widget(widget) {
- // Nuke the boundingBox, but only if we've touched the DOM.
- if (widget.get('rendered')) {
- var bb = widget.get('boundingBox');
- bb.get('parentNode').removeChild(bb);
- }
- // Kill the widget itself.
- widget.destroy();
- }
-
- /* A helper to create a simple text input box */
- function make_input(value) {
- var input = document.createElement('input');
- input.setAttribute('type', 'text');
- input.setAttribute('value', value || '');
- Y.one('body').appendChild(input);
- return input;
- }
-
- /* A helper to destroy a generic input: make_input()'s inverse */
- function kill_input(input) {
- Y.one('body').removeChild(input);
- }
-
- tests.suite.add(new Y.Test.Case({
- name:'test widget setup',
-
- setUp: function() {
- this.input = make_input();
- },
-
- tearDown: function() {
- kill_input(this.input);
- },
-
- test_library_exists: function () {
- Y.Assert.isObject(Y.lp.ui.AutoComplete,
- "Could not locate the lp.ui.autocomplete module");
- },
-
- test_widget_starts_hidden: function() {
- var autocomp = new Y.lp.ui.AutoComplete({ input: this.input });
- autocomp.render();
- Assert.isFalse(
- autocomp.get('visible'),
- "The widget should start out hidden.");
- }
- }));
-
- tests.suite.add(new Y.Test.Case({
-
- name:'test display of matching results',
-
- setUp: function() {
- this.input = make_input();
- this.autocomp = new Y.lp.ui.AutoComplete({
- input: this.input
- });
- },
-
- tearDown: function() {
- cleanup_widget(this.autocomp);
- kill_input(this.input);
- },
-
- /* A helper to option the completions list for a given input string. */
- complete_input: function(value) {
- this.input.value = value;
- var last_charcode = value.charCodeAt(value.length - 1);
- Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
- },
-
- /* Extract the matching text from the widget's autocompletion list. */
- get_completions: function() {
- if (!this.autocomp.get('rendered')) {
- Y.fail("Tried find matches for an unrendered widget.");
- return;
- }
-
- var matches = [];
- this.autocomp
- .get('boundingBox')
- .all('.item')
- .each(function(item) {
- matches.push(item.get('text'));
- });
- return matches;
- },
-
- test_autocomplete_is_visible_if_results_match: function() {
- this.autocomp.set('data', ['aaa']);
- this.autocomp.render();
-
- // We want to match the one and only data set element.
- this.complete_input('aa');
- Assert.isTrue(
- this.autocomp.get('visible'),
- "The widget should be visible if matching input was found.");
- },
-
- test_autocomplete_is_hidden_if_no_query_is_given: function() {
- this.autocomp.set('data', ['aaa']);
- this.autocomp.render();
-
- // We want to simulate an empty input field, but some action
- // triggers matching.
- this.complete_input('');
- Assert.isFalse(
- this.autocomp.get('visible'),
- "The widget should be hidden if the input field is empty.");
- },
-
- test_autocomplete_is_hidden_if_results_do_not_match: function() {
- this.autocomp.set('data', ['bbb']);
- this.autocomp.render();
-
- if (this.autocomp.get('visible')) {
- Y.fail("The autocomplete widget should start out hidden.");
- }
-
-
- // 'aa' shouldn't match any of the data.
- this.complete_input('aa');
- Assert.isFalse(
- this.autocomp.get('visible'),
- "The widget should be hidden if the query doesn't match any " +
- "possible completions.");
- },
-
- test_display_should_contain_all_matches: function() {
- var data = [
- 'aaa',
- 'baa'
- ];
-
- this.autocomp.set('data', data);
- this.autocomp.render();
-
- // Trigger autocompletion, should match all data items.
- this.complete_input('aa');
-
- // Grab the now-open menu
- var option_list = Y.one('.yui3-autocomplete-list');
- Assert.isObject(option_list,
- "The list of completion options should be open.");
-
- Y.ArrayAssert.itemsAreEqual(
- this.get_completions(),
- data,
- "Every autocomplete item should be present in the available " +
- "match keys.");
- },
-
- test_display_is_updated_with_new_completions: function() {
- // Create two pieces of data, each narrower than the other.
- this.autocomp.set('data', ['aaa', 'aab']);
- this.autocomp.render();
-
- // Trigger autocompletion for the loosest matches
- this.complete_input('aa');
- // Complete the narrower set
- this.complete_input('aaa');
-
- var completions = this.get_completions();
-
- Y.ArrayAssert.itemsAreEqual(
- ['aaa'],
- completions,
- "'aaa' should be the data item displayed after narrowing the " +
- "search with the query 'aaa'.");
- },
-
- test_matching_text_in_item_is_marked: function() {
- this.autocomp.set('data', ['aaa']);
- this.autocomp.render();
-
- // Display the matching input.
- var query = 'aa';
- this.complete_input(query);
-
- // Grab the matching item
- var matching_text = this.autocomp
- .get('boundingBox')
- .one('.item .matching-text');
-
- Assert.isNotNull(matching_text,
- "Some of the matching item's text should be marked matching.");
-
- Assert.areEqual(
- query,
- matching_text.get('text'),
- "The matching text should be the same as the query text.");
- },
-
- test_escape_key_should_close_completions_list: function() {
- this.autocomp.set('data', ['aaa']);
- this.autocomp.render();
-
- // Open the completions list
- this.complete_input('aa');
-
- // Hit the escape key to close the list
- Y.Event.simulate(this.input, 'keydown', { keyCode: 27 });
-
- Assert.isFalse(
- this.autocomp.get('visible'),
- "The list of completions should be closed after pressing the " +
- "escape key.");
- }
- }));
-
- tests.suite.add(new Y.Test.Case({
-
- name:'test result text marking method',
-
- test_match_at_beginning_should_be_marked: function() {
- var autocomp = new Y.lp.ui.AutoComplete();
- var marked_text = autocomp.markMatchingText('aabb', 'aa', 0);
-
- Assert.areEqual(
- '<span class="matching-text">aa</span>bb',
- marked_text,
- "The text at the beginning of the result should have been " +
- "marked.");
- },
-
- test_match_in_middle_should_be_marked: function() {
- var autocomp = new Y.lp.ui.AutoComplete();
- var marked_text = autocomp.markMatchingText('baab', 'aa', 1);
-
- Assert.areEqual(
- 'b<span class="matching-text">aa</span>b',
- marked_text,
- "The text in the middle of the result should have been " +
- "marked.");
- },
-
- test_match_at_end_should_be_marked: function() {
- var autocomp = new Y.lp.ui.AutoComplete();
- var marked_text = autocomp.markMatchingText('bbaa', 'aa', 2);
-
- Assert.areEqual(
- 'bb<span class="matching-text">aa</span>',
- marked_text,
- "The text at the end of the result should have been " +
- "marked.");
- }
- }));
-
-
- tests.suite.add(new Y.Test.Case({
-
- name:'test query parsing',
-
- setUp: function() {
- this.autocomplete = new Y.lp.ui.AutoComplete({
- delimiter: ' '
- });
- },
-
- test_space_for_delimiter: function() {
- Assert.areEqual(
- 'b',
- this.autocomplete.parseQuery('a b').text,
- "Input should be split around the 'space' character.");
- Assert.isNull(
- this.autocomplete.parseQuery(' '),
- "Space for input and delimiter should not parse.");
- },
-
- test_parsed_query_is_stripped_of_leading_whitespace: function() {
- this.autocomplete.set('delimiter', ',');
-
- Assert.areEqual(
- 'a',
- this.autocomplete.parseQuery(' a').text,
- "Leading whitespace at the start of the input string should " +
- "be stripped.");
-
- Assert.areEqual(
- 'b',
- this.autocomplete.parseQuery('a, b').text,
- "Leading whitespace between the last separator and the " +
- "current query should be stripped.");
- },
-
- test_query_is_taken_from_middle_of_input: function() {
- // Pick a caret position that is in the middle of the second result.
- var input = "aaa bbb ccc";
- var caret = 6;
-
- Assert.areEqual(
- 'bbb',
- this.autocomplete.parseQuery(input, caret).text,
- "The current query should be picked out of the middle of the " +
- "text input if the caret has been positioned there.");
- },
-
- test_query_is_taken_from_beginning_of_input: function() {
- // Pick a caret position that is in the first input's query
- var input = "aaa bbb";
- var caret = 2;
-
- Assert.areEqual(
- 'aaa',
- this.autocomplete.parseQuery(input, caret).text,
- "The first block of text should become the current query if " +
- "the caret is positioned within it.");
- }
- }));
-
- tests.suite.add(new Y.Test.Case({
-
- name:'test results matching algorithm',
-
- /* A helper function to determine if two match result items are equal */
- matches_are_equal: function(a, b) {
- if (Y.Lang.isUndefined(a)) {
- Assert.fail("Match set 'a' is of type 'undefined'!");
- }
- if (Y.Lang.isUndefined(b)) {
- Assert.fail("Match set 'b' is of type 'undefined'!");
- }
- return (a.text === b.text) && (a.offset === b.offset);
- },
-
- test_no_matches_returns_an_empty_array: function() {
- var autocomplete = new Y.lp.ui.AutoComplete({
- data: ['ccc']
- });
-
- var matches = autocomplete.findMatches('aa');
- Y.ArrayAssert.isEmpty(matches,
- "No data should have matched the query 'aa'");
- },
-
- test_match_last_item: function() {
- var autocomplete = new Y.lp.ui.AutoComplete({
- data: [
- 'ccc',
- 'bbb',
- 'aaa'
- ]
- });
-
- var matches = autocomplete.findMatches('aa');
-
- Y.ArrayAssert.itemsAreEquivalent(
- [{text: 'aaa', offset: 0}],
- matches,
- this.matches_are_equal,
- "One row should have matched the query 'aa'.");
- },
-
- test_match_ordering: function() {
- // Matches, in reverse order.
- var autocomplete = new Y.lp.ui.AutoComplete({
- data: [
- 'bbaa',
- 'baab',
- 'aabb'
- ]
- });
-
- var matches = autocomplete.findMatches('aa');
-
- Y.ArrayAssert.itemsAreEquivalent(
- [{text: 'aabb', offset: 0},
- {text: 'baab', offset: 1},
- {text: 'bbaa', offset: 2}],
- matches,
- this.matches_are_equal,
- "The match array should have all of it's keys in order.");
- },
-
- test_mixed_case_text_matches: function() {
- var autocomplete = new Y.lp.ui.AutoComplete({
- data: ['aBc']
- });
-
- var matches = autocomplete.findMatches('b');
-
- Y.ArrayAssert.itemsAreEquivalent(
- [{text:'aBc', offset: 1}],
- matches,
- this.matches_are_equal,
- "The match algorithm should be case insensitive.");
- },
-
- test_mixed_case_matches_come_in_stable_order: function() {
- // Data with the mixed-case coming first in order.
- var autocomplete = new Y.lp.ui.AutoComplete({
- data: ['aBc', 'aaa', 'abc']
- });
-
- var matches = autocomplete.findMatches('b');
-
- Y.ArrayAssert.itemsAreEquivalent(
- [{text: 'aBc', offset: 1},
- {text: 'abc', offset: 1}],
- matches,
- this.matches_are_equal,
- "Mixed-case matches should arrive in stable order.");
- }
- }));
-
-
- tests.suite.add(new Y.Test.Case({
-
- name:'test selecting results',
-
- setUp: function() {
- this.input = make_input();
- this.autocomp = new Y.lp.ui.AutoComplete({
- input: this.input
- });
- this.autocomp.render();
- },
-
- tearDown: function() {
- cleanup_widget(this.autocomp);
- kill_input(this.input);
- },
-
- // A helper to option the completions list for a given input string.
- complete_input: function(value) {
- this.input.value = value;
- var last_charcode = value.charCodeAt(value.length - 1);
- Y.Event.simulate(this.input, 'keyup', { keyCode: last_charcode });
- },
-
- // A helper to select the selected completion result with the Tab key.
- press_selection_key: function() {
- Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
- },
-
- test_pressing_matching_key_raises_menu: function() {
- this.autocomp.set('data', ['aaaa', 'aabb']);
- this.complete_input('aa');
- var box = this.autocomp.get('boundingBox');
- Assert.areEqual(
- '31000',
- box.getStyle('z-index'),
- "The menu z-index should be 31000; above it's overlay.");
- },
-
- test_pressing_enter_completes_current_input: function() {
- this.autocomp.set('data', ['aaaa', 'aabb']);
-
- // Open the completion options
- this.complete_input('aa');
-
- // Press 'Enter'
- Y.Event.simulate(this.input, "keydown", { keyCode: 13 });
-
- Assert.areEqual(
- 'aaaa ',
- this.input.value,
- "The first completion should have been appended to the " +
- "input's value after pressing the 'Enter' key.");
- },
-
- test_pressing_tab_completes_current_input: function() {
- this.autocomp.set('data', ['aaaa', 'aabb']);
-
- // Open the completion options
- this.complete_input('aa');
-
- // Press 'Tab'
- Y.Event.simulate(this.input, "keydown", { keyCode: 9 });
-
- Assert.areEqual(
- 'aaaa ',
- this.input.value,
- "The first completion should have been appended to the " +
- "input's value after pressing the 'Enter' key.");
- },
-
- test_clicking_on_first_result_completes_input: function() {
- this.autocomp.set('data', ['aaaa', 'aabb']);
- this.complete_input('aa');
-
- // Click on the first displayed result
- var options = this.autocomp.get('contentBox').all('.item');
- var first_item = Y.Node.getDOMNode(options.item(0));
- Y.Event.simulate(first_item, 'click');
-
- Assert.areEqual(
- 'aaaa ',
- this.input.value,
- "The first completion should have been appended to the " +
- "input's value after clicking it's list node.");
- },
-
- test_selecting_results_hides_completion_list: function() {
- this.autocomp.set('data', 'aaa');
- this.complete_input('a');
- this.press_selection_key();
-
- Assert.isFalse(
- this.autocomp.get('visible'),
- "The completion list should be hidden after a result is " +
- "selected.");
- },
-
- test_completed_input_replaces_current_input: function() {
- this.autocomp.set('data', ['abba']);
-
- // Match the one and only result, but match the second character in
- // it. Throw in some pre-existing user input just to be sure things
- // work.
- this.complete_input('xxx b');
- this.press_selection_key();
-
- Assert.areEqual(
- 'xxx abba ',
- this.input.value,
- "The user's current query should have been replaced with the " +
- "selected value.");
- },
-
- test_completed_input_has_delimiter_appended_to_it: function() {
- var delimiter = ' ';
- this.autocomp.set('data', ['aaaa']);
- this.autocomp.set('delimiter', delimiter);
-
- this.complete_input('a');
- this.press_selection_key();
-
- Assert.areEqual(
- delimiter,
- this.input.value.charAt(this.input.value.length - 1),
- "The last character of the input should be the current " +
- "query delimiter.");
- },
-
- test_down_arrow_selects_second_result_in_list: function() {
- this.autocomp.set('data', ['first_item', 'second_item']);
-
- // Match the first result. It should be selected by default.
- this.complete_input('item');
-
- // Simulate pressing the down arrow key.
- Y.Event.simulate(this.input, 'keydown', { keyCode: 40 });
-
- // Now, select the second result.
- this.press_selection_key();
-
- Assert.areEqual(
- 'second_item ',
- this.input.value,
- "Pressing the down-arrow key should select the second option " +
- "in the completions list.");
- }
- }));
-
-}, '0.1', {
- 'requires': ['test', 'test-console', 'lp.autocomplete', 'node', 'event',
- 'event-simulate', 'lp.ui.autocomplete']
-});
=== modified file 'lib/lp/bugs/javascript/bug_tags_entry.js'
--- lib/lp/bugs/javascript/bug_tags_entry.js 2013-01-30 00:58:45 +0000
+++ lib/lp/bugs/javascript/bug_tags_entry.js 2017-07-20 15:57:38 +0000
@@ -21,7 +21,6 @@
var tags_edit_spinner;
var tags_form;
var available_tags;
-var autocomplete;
var A = 'a',
VALUE = 'value',
@@ -127,7 +126,7 @@
tag_list_span.removeClass(HIDDEN);
tags_trigger.removeClass(HIDDEN);
tags_form.addClass(HIDDEN);
- autocomplete.hide();
+ tag_input.blur();
Y.lp.anim.green_flash({ node: tag_list_span }).run();
namespace.update_ui();
};
@@ -143,7 +142,6 @@
tags_trigger.addClass(HIDDEN);
tags_form.removeClass(HIDDEN);
tag_input.focus();
- autocomplete.render();
};
@@ -231,11 +229,6 @@
ok_button.on('click', function(e) {
e.halt();
save_tags();
- /* Check to see if the autocomplete dialogue is still open
- and if so, close it. */
- if (!autocomplete._last_input_was_completed) {
- autocomplete.hide();
- }
});
cancel_button.on('click', function(e) {
e.halt();
@@ -251,8 +244,45 @@
});
tags_trigger.addClass('js-action');
- autocomplete = namespace.setup_tag_complete(
- '#tag-input', available_official_tags);
+ namespace.setup_tag_complete('#tag-input', available_official_tags);
+};
+
+
+/**
+ * Returns an array of results that contain the complete query as a phrase.
+ * Case-insensitive. Matches closer to the front of the user query are
+ * listed earlier.
+ *
+ * @method orderedPhraseMatch
+ * @param {String} query Query to match
+ * @param {Array} results Results to filter
+ * @return {Array} Filtered results
+ **/
+var orderedPhraseMatch = function(query, results) {
+ if (!query) { return results; }
+
+ query = query.toLowerCase();
+
+ var match_sets = [];
+ Y.Array.each(results, function(result) {
+ var i = result.text.toLowerCase().indexOf(query);
+ if (i > -1) {
+ if (!match_sets[i]) {
+ match_sets[i] = [];
+ }
+ match_sets[i].push(result);
+ }
+ });
+
+ var matches = [];
+ Y.Array.each(match_sets, function(match_set) {
+ if (match_set) {
+ Y.Array.each(match_set, function(match) {
+ matches.push(match);
+ });
+ }
+ });
+ return matches;
};
@@ -262,22 +292,22 @@
* @method setup_tag_complete
*/
namespace.setup_tag_complete = function(input, official_tags) {
- var bounding_box = Y.Node.create(
- '<div class="bug-tag-complete"><div></div></div>');
- Y.one('body').appendChild(bounding_box);
- var autocomplete = new Y.lp.ui.AutoComplete({
- input: input,
- data: official_tags,
- boundingBox: bounding_box,
- contentBox: bounding_box.one('div')
- });
- autocomplete.get('input').on('focus', function(e) {
- autocomplete.render();
- });
- return autocomplete;
+ var input_node = Y.one(input);
+ input_node.plug(Y.Plugin.AutoComplete, {
+ queryDelimiter: ' ',
+ render: false,
+ resultFilters: orderedPhraseMatch,
+ resultHighlighter: 'phraseMatch',
+ source: official_tags
+ });
+ input_node.on('focus', function(e) {
+ input_node.ac.render();
+ });
+ return input_node.ac;
};
}, "0.1", {
"requires": [
- "array-extras", "base", "io-base", "node", "substitute",
- "node-menunav", "lp.anim", "lp.ui.autocomplete", "lp.client"]
+ "array-extras", "autocomplete", "autocomplete-filters",
+ "autocomplete-highlighters", "base", "io-base", "node", "substitute",
+ "node-menunav", "lp.anim", "lp.client"]
});
=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.html'
--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.html 2012-10-26 09:54:28 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.html 2017-07-20 15:57:38 +0000
@@ -35,8 +35,6 @@
src="../../../../../build/js/lp/app/anim/anim.js"></script>
<script type="text/javascript"
src="../../../../../build/js/lp/app/extras/extras.js"></script>
- <script type="text/javascript"
- src="../../../../../build/js/lp/app/autocomplete/autocomplete.js"></script>
<!-- The module under test. -->
<script type="text/javascript" src="../bug_tags_entry.js"></script>
=== modified file 'lib/lp/bugs/javascript/tests/test_bug_tags_entry.js'
--- lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2013-01-30 01:25:57 +0000
+++ lib/lp/bugs/javascript/tests/test_bug_tags_entry.js 2017-07-20 15:57:38 +0000
@@ -82,10 +82,11 @@
// The form is created.
var form_node = this.bug_tags_div.one('#tags-form');
Y.Assert.isInstanceOf(Y.Node, form_node);
- Y.Assert.isInstanceOf(Y.Node, form_node.one('#tag-input'));
+ var input_node = form_node.one('#tag-input');
+ Y.Assert.isInstanceOf(Y.Node, input_node);
Y.Assert.isInstanceOf(Y.Node, form_node.one('#tags-edit-spinner'));
Y.Assert.isInstanceOf(Y.Node, form_node.one('#edit-tags-cancel'));
- Y.Assert.isInstanceOf(Y.Node, Y.one('.bug-tag-complete'));
+ Y.Assert.isInstanceOf(Y.AutoComplete, input_node.ac);
},
test_show_activity: function() {
@@ -222,13 +223,10 @@
// The Autocompleter nodes are provided.
module.setup_tag_complete(
'input[id="field.tag"]',['project-tag']);
- var completer_node = Y.one('.yui3-autocomplete');
- Y.Assert.isInstanceOf(Y.Node, completer_node);
- Y.Assert.isTrue(completer_node.hasClass('bug-tag-complete'));
- var completer_content = completer_node.one(
- '.yui3-autocomplete-content');
- Y.Assert.isInstanceOf(Y.Node, completer_content);
var input = Y.one('input[id="field.tag"]');
+ input.simulate('focus');
+ Y.Assert.isTrue(input.hasClass('yui3-aclist-input'));
+ Y.Assert.isInstanceOf(Y.Node, Y.one('.yui3-aclist'));
},
test_render_on_focus: function() {
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.html'
--- lib/lp/registry/javascript/tests/test_structural_subscription.html 2012-12-12 21:55:15 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.html 2017-07-20 15:57:38 +0000
@@ -48,8 +48,6 @@
<script type="text/javascript"
src="../../../../../build/js/lp/app/gallery-accordion/gallery-accordion.js"></script>
<script type="text/javascript"
- src="../../../../../build/js/lp/app/autocomplete/autocomplete.js"></script>
- <script type="text/javascript"
src="../../../../../build/js/lp/bugs/bug_tags_entry.js"></script>
<!-- The module under test. -->
=== modified file 'lib/lp/registry/javascript/tests/test_structural_subscription.js'
--- lib/lp/registry/javascript/tests/test_structural_subscription.js 2013-03-20 03:41:40 +0000
+++ lib/lp/registry/javascript/tests/test_structural_subscription.js 2017-07-20 15:57:38 +0000
@@ -459,8 +459,8 @@
Assert.areEqual(
'Add a mail subscription for Test bugs',
header.get('text'));
- var bug_tags_node = Y.one(".bug-tag-complete");
- Assert.isInstanceOf(Y.Node, bug_tags_node);
+ var input_tags_node = Y.one('input[name="tags"]');
+ Assert.isInstanceOf(Y.AutoComplete, input_tags_node.ac);
},
test_clean_up_overlay: function() {
=== modified file 'lib/lp/scripts/utilities/js/combinecss.py'
--- lib/lp/scripts/utilities/js/combinecss.py 2017-01-18 00:55:27 +0000
+++ lib/lp/scripts/utilities/js/combinecss.py 2017-07-20 15:57:38 +0000
@@ -16,6 +16,7 @@
'ubuntu-webfonts.css',
'style.css',
'yui/cssreset/cssreset.css',
+ 'yui/assets/skins/sam/autocomplete-list.css',
'yui/assets/skins/sam/calendar-base.css',
'yui/assets/skins/sam/calendar.css',
'yui/assets/skins/sam/calendarnavigator.css',
@@ -27,7 +28,6 @@
'build/ui/assets/skins/sam/lazr.css',
'build/ui/assets/skins/sam/banner.css',
'build/inlineedit/assets/skins/sam/editor.css',
- 'build/autocomplete/assets/skins/sam/autocomplete.css',
'build/overlay/assets/skins/sam/pretty-overlay.css',
'build/formoverlay/assets/formoverlay-core.css',
'build/inlinehelp/assets/inlinehelp-core.css',
Follow ups