2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('autocomplete-base', function(Y) {
11 * Provides automatic input completion or suggestions for text input fields and
14 * @module autocomplete
19 * <code>Y.Base</code> extension that provides core autocomplete logic (but no
20 * UI implementation) for a text input field or textarea. Must be mixed into a
21 * <code>Y.Base</code>-derived class to be useful.
23 * @module autocomplete
24 * @submodule autocomplete-base
29 * Extension that provides core autocomplete logic (but no UI implementation)
30 * for a text input field or textarea.
34 * The <code>AutoCompleteBase</code> class provides events and attributes that
35 * abstract away core autocomplete logic and configuration, but does not provide
36 * a widget implementation or suggestion UI. For a prepackaged autocomplete
37 * widget, see <code>AutoCompleteList</code>.
41 * This extension cannot be instantiated directly, since it doesn't provide an
42 * actual implementation. It's intended to be mixed into a
43 * <code>Y.Base</code>-based class or widget.
47 * <code>Y.Widget</code>-based example:
51 * YUI().use('autocomplete-base', 'widget', function (Y) {
52 * var MyAC = Y.Base.create('myAC', Y.Widget, [Y.AutoCompleteBase], {
53 * // Custom prototype methods and properties.
55 * // Custom static methods and properties.
58 * // Custom implementation code.
63 * <code>Y.Base</code>-based example:
67 * YUI().use('autocomplete-base', function (Y) {
68 * var MyAC = Y.Base.create('myAC', Y.Base, [Y.AutoCompleteBase], {
69 * initializer: function () {
70 * this._bindUIACBase();
71 * this._syncUIACBase();
72 * },
74 * // Custom prototype methods and properties.
76 * // Custom static methods and properties.
79 * // Custom implementation code.
83 * @class AutoCompleteBase
86 var Escape = Y.Escape,
91 isFunction = Lang.isFunction,
92 isString = Lang.isString,
95 INVALID_VALUE = Y.Attribute.INVALID_VALUE,
97 _FUNCTION_VALIDATOR = '_functionValidator',
98 _SOURCE_SUCCESS = '_sourceSuccess',
100 ALLOW_BROWSER_AC = 'allowBrowserAutocomplete',
101 INPUT_NODE = 'inputNode',
103 QUERY_DELIMITER = 'queryDelimiter',
104 REQUEST_TEMPLATE = 'requestTemplate',
106 RESULT_LIST_LOCATOR = 'resultListLocator',
108 VALUE_CHANGE = 'valueChange',
112 EVT_RESULTS = RESULTS;
114 function AutoCompleteBase() {
116 Y.before(this._bindUIACBase, this, 'bindUI');
117 Y.before(this._destructorACBase, this, 'destructor');
118 Y.before(this._syncUIACBase, this, 'syncUI');
120 // -- Public Events --------------------------------------------------------
123 * Fires after the query has been completely cleared or no longer meets the
124 * minimum query length requirement.
127 * @param {EventFacade} e Event facade with the following additional
131 * <dt>prevVal (String)</dt>
133 * Value of the query before it was cleared.
137 * @preventable _defClearFn
139 this.publish(EVT_CLEAR, {
140 defaultFn: this._defClearFn
144 * Fires when the contents of the input field have changed and the input
145 * value meets the criteria necessary to generate an autocomplete query.
148 * @param {EventFacade} e Event facade with the following additional
152 * <dt>inputValue (String)</dt>
154 * Full contents of the text input field or textarea that generated
158 * <dt>query (String)</dt>
160 * Autocomplete query. This is the string that will be used to
161 * request completion results. It may or may not be the same as
162 * <code>inputValue</code>.
166 * @preventable _defQueryFn
168 this.publish(EVT_QUERY, {
169 defaultFn: this._defQueryFn
173 * Fires after query results are received from the <code>source</code>. If
174 * no source has been set, this event will not fire.
177 * @param {EventFacade} e Event facade with the following additional
181 * <dt>data (Array|Object)</dt>
183 * Raw, unfiltered result data (if available).
186 * <dt>query (String)</dt>
188 * Query that generated these results.
191 * <dt>results (Array)</dt>
193 * Array of filtered, formatted, and highlighted results. Each item in
194 * the array is an object with the following properties:
197 * <dt>display (Node|HTMLElement|String)</dt>
199 * Formatted result HTML suitable for display to the user. If no
200 * custom formatter is set, this will be an HTML-escaped version of
201 * the string in the <code>text</code> property.
204 * <dt>highlighted (String)</dt>
206 * Highlighted (but not formatted) result text. This property will
207 * only be set if a highlighter is in use.
210 * <dt>raw (mixed)</dt>
212 * Raw, unformatted result in whatever form it was provided by the
213 * <code>source</code>.
216 * <dt>text (String)</dt>
218 * Plain text version of the result, suitable for being inserted
219 * into the value of a text input field or textarea when the result
220 * is selected by a user. This value is not HTML-escaped and should
221 * not be inserted into the page using innerHTML.
227 * @preventable _defResultsFn
229 this.publish(EVT_RESULTS, {
230 defaultFn: this._defResultsFn
234 // -- Public Static Properties -------------------------------------------------
235 AutoCompleteBase.ATTRS = {
237 * Whether or not to enable the browser's built-in autocomplete
238 * functionality for input fields.
240 * @attribute allowBrowserAutocomplete
244 allowBrowserAutocomplete: {
249 * When a <code>queryDelimiter</code> is set, trailing delimiters will
250 * automatically be stripped from the input value by default when the
251 * input node loses focus. Set this to <code>true</code> to allow trailing
254 * @attribute allowTrailingDelimiter
258 allowTrailingDelimiter: {
263 * Node to monitor for changes, which will generate <code>query</code>
264 * events when appropriate. May be either an input field or a textarea.
266 * @attribute inputNode
267 * @type Node|HTMLElement|String
272 writeOnce: 'initOnly'
276 * Maximum number of results to return. A value of <code>0</code> or less
277 * will allow an unlimited number of results.
279 * @attribute maxResults
288 * Minimum number of characters that must be entered before a
289 * <code>query</code> event will be fired. A value of <code>0</code>
290 * allows empty queries; a negative value will effectively disable all
291 * <code>query</code> events.
293 * @attribute minQueryLength
303 * Current query, or <code>null</code> if there is no current query.
307 * The query might not be the same as the current value of the input
308 * node, both for timing reasons (due to <code>queryDelay</code>) and
309 * because when one or more <code>queryDelimiter</code> separators are
310 * in use, only the last portion of the delimited input string will be
311 * used as the query value.
326 * Number of milliseconds to delay after input before triggering a
327 * <code>query</code> event. If new input occurs before this delay is
328 * over, the previous input event will be ignored and a new delay will
333 * This can be useful both to throttle queries to a remote data source
334 * and to avoid distracting the user by showing them less relevant
335 * results before they've paused their typing.
338 * @attribute queryDelay
347 * Query delimiter string. When a delimiter is configured, the input value
348 * will be split on the delimiter, and only the last portion will be used in
349 * autocomplete queries and updated when the <code>query</code> attribute is
352 * @attribute queryDelimiter
362 * Source request template. This can be a function that accepts a query as a
363 * parameter and returns a request string, or it can be a string containing
364 * the placeholder "{query}", which will be replaced with the actual
365 * URI-encoded query. In either case, the resulting string will be appended
366 * to the request URL when the <code>source</code> attribute is set to a
367 * remote DataSource, JSONP URL, or XHR URL (it will not be appended to YQL
372 * While <code>requestTemplate</code> may be set to either a function or
373 * a string, it will always be returned as a function that accepts a
374 * query argument and returns a string.
377 * @attribute requestTemplate
378 * @type Function|String|null
382 setter: '_setRequestTemplate',
388 * Array of local result filter functions. If provided, each filter
389 * will be called with two arguments when results are received: the query
390 * and an array of result objects. See the documentation for the
391 * <code>results</code> event for a list of the properties available on each
396 * Each filter is expected to return a filtered or modified version of the
397 * results array, which will then be passed on to subsequent filters, then
398 * the <code>resultHighlighter</code> function (if set), then the
399 * <code>resultFormatter</code> function (if set), and finally to
400 * subscribers to the <code>results</code> event.
404 * If no <code>source</code> is set, result filters will not be called.
408 * Prepackaged result filters provided by the autocomplete-filters and
409 * autocomplete-filters-accentfold modules can be used by specifying the
410 * filter name as a string, such as <code>'phraseMatch'</code> (assuming
411 * the necessary filters module is loaded).
414 * @attribute resultFilters
419 setter: '_setResultFilters',
425 * Function which will be used to format results. If provided, this function
426 * will be called with two arguments after results have been received and
427 * filtered: the query and an array of result objects. The formatter is
428 * expected to return an array of HTML strings or Node instances containing
429 * the desired HTML for each result.
433 * See the documentation for the <code>results</code> event for a list of
434 * the properties available on each result object.
438 * If no <code>source</code> is set, the formatter will not be called.
441 * @attribute resultFormatter
442 * @type Function|null
445 validator: _FUNCTION_VALIDATOR
450 * Function which will be used to highlight results. If provided, this
451 * function will be called with two arguments after results have been
452 * received and filtered: the query and an array of filtered result objects.
453 * The highlighter is expected to return an array of highlighted result
454 * text in the form of HTML strings.
458 * See the documentation for the <code>results</code> event for a list of
459 * the properties available on each result object.
463 * If no <code>source</code> is set, the highlighter will not be called.
466 * @attribute resultHighlighter
467 * @type Function|null
470 setter: '_setResultHighlighter'
475 * Locator that should be used to extract an array of results from a
476 * non-array response.
480 * By default, no locator is applied, and all responses are assumed to be
481 * arrays by default. If all responses are already arrays, you don't need to
486 * The locator may be either a function (which will receive the raw response
487 * as an argument and must return an array) or a string representing an
488 * object path, such as "foo.bar.baz" (which would return the value of
489 * <code>result.foo.bar.baz</code> if the response is an object).
493 * While <code>resultListLocator</code> may be set to either a function or a
494 * string, it will always be returned as a function that accepts a response
495 * argument and returns an array.
498 * @attribute resultListLocator
499 * @type Function|String|null
502 setter: '_setLocator'
506 * Current results, or an empty array if there are no results.
520 * Locator that should be used to extract a plain text string from a
521 * non-string result item. The resulting text value will typically be the
522 * value that ends up being inserted into an input field or textarea when
523 * the user of an autocomplete implementation selects a result.
527 * By default, no locator is applied, and all results are assumed to be
528 * plain text strings. If all results are already plain text strings, you
529 * don't need to define a locator.
533 * The locator may be either a function (which will receive the raw result
534 * as an argument and must return a string) or a string representing an
535 * object path, such as "foo.bar.baz" (which would return the value of
536 * <code>result.foo.bar.baz</code> if the result is an object).
540 * While <code>resultTextLocator</code> may be set to either a function or a
541 * string, it will always be returned as a function that accepts a result
542 * argument and returns a string.
545 * @attribute resultTextLocator
546 * @type Function|String|null
549 setter: '_setLocator'
554 * Source for autocomplete results. The following source types are
562 * <i>Example:</i> <code>['first result', 'second result', 'etc']</code>
566 * The full array will be provided to any configured filters for each
567 * query. This is an easy way to create a fully client-side autocomplete
572 * <dt>DataSource</dt>
575 * A <code>DataSource</code> instance or other object that provides a
576 * DataSource-like <code>sendRequest</code> method. See the
577 * <code>DataSource</code> documentation for details.
584 * <i>Example:</i> <code>function (query) { return ['foo', 'bar']; }</code>
588 * A function source will be called with the current query as a
589 * parameter, and should return an array of results.
596 * <i>Example:</i> <code>{foo: ['foo result 1', 'foo result 2'], bar: ['bar result']}</code>
600 * An object will be treated as a query hashmap. If a property on the
601 * object matches the current query, the value of that property will be
602 * used as the response.
606 * The response is assumed to be an array of results by default. If the
607 * response is not an array, provide a <code>resultListLocator</code> to
608 * process the response and return an array.
614 * If the optional <code>autocomplete-sources</code> module is loaded, then
615 * the following additional source types will be supported as well:
619 * <dt>String (JSONP URL)</dt>
622 * <i>Example:</i> <code>'http://example.com/search?q={query}&callback={callback}'</code>
626 * If a URL with a <code>{callback}</code> placeholder is provided, it
627 * will be used to make a JSONP request. The <code>{query}</code>
628 * placeholder will be replaced with the current query, and the
629 * <code>{callback}</code> placeholder will be replaced with an
630 * internally-generated JSONP callback name. Both placeholders must
631 * appear in the URL, or the request will fail. An optional
632 * <code>{maxResults}</code> placeholder may also be provided, and will
633 * be replaced with the value of the maxResults attribute (or 1000 if
634 * the maxResults attribute is 0 or less).
638 * The response is assumed to be an array of results by default. If the
639 * response is not an array, provide a <code>resultListLocator</code> to
640 * process the response and return an array.
644 * <strong>The <code>jsonp</code> module must be loaded in order for
645 * JSONP URL sources to work.</strong> If the <code>jsonp</code> module
646 * is not already loaded, it will be loaded on demand if possible.
650 * <dt>String (XHR URL)</dt>
653 * <i>Example:</i> <code>'http://example.com/search?q={query}'</code>
657 * If a URL without a <code>{callback}</code> placeholder is provided,
658 * it will be used to make a same-origin XHR request. The
659 * <code>{query}</code> placeholder will be replaced with the current
660 * query. An optional <code>{maxResults}</code> placeholder may also be
661 * provided, and will be replaced with the value of the maxResults
662 * attribute (or 1000 if the maxResults attribute is 0 or less).
666 * The response is assumed to be a JSON array of results by default. If
667 * the response is a JSON object and not an array, provide a
668 * <code>resultListLocator</code> to process the response and return an
669 * array. If the response is in some form other than JSON, you will
670 * need to use a custom DataSource instance as the source.
674 * <strong>The <code>io-base</code> and <code>json-parse</code> modules
675 * must be loaded in order for XHR URL sources to work.</strong> If
676 * these modules are not already loaded, they will be loaded on demand
681 * <dt>String (YQL query)</dt>
684 * <i>Example:</i> <code>'select * from search.suggest where query="{query}"'</code>
688 * If a YQL query is provided, it will be used to make a YQL request.
689 * The <code>{query}</code> placeholder will be replaced with the
690 * current autocomplete query. This placeholder must appear in the YQL
691 * query, or the request will fail. An optional
692 * <code>{maxResults}</code> placeholder may also be provided, and will
693 * be replaced with the value of the maxResults attribute (or 1000 if
694 * the maxResults attribute is 0 or less).
698 * <strong>The <code>yql</code> module must be loaded in order for YQL
699 * sources to work.</strong> If the <code>yql</code> module is not
700 * already loaded, it will be loaded on demand if possible.
706 * As an alternative to providing a source, you could simply listen for
707 * <code>query</code> events and handle them any way you see fit. Providing
708 * a source is optional, but will usually be simpler.
712 * @type Array|DataSource|Function|Object|String|null
719 * If the <code>inputNode</code> specified at instantiation time has a
720 * <code>node-tokeninput</code> plugin attached to it, this attribute will
721 * be a reference to the <code>Y.Plugin.TokenInput</code> instance.
723 * @attribute tokenInput
724 * @type Plugin.TokenInput
732 * Current value of the input node.
739 // Why duplicate this._inputNode.get('value')? Because we need a
740 // reliable way to track the source of value changes. We want to perform
741 // completion when the user changes the value, but not when we change
747 AutoCompleteBase.CSS_PREFIX = 'ac';
748 AutoCompleteBase.UI_SRC = (Y.Widget && Y.Widget.UI_SRC) || 'ui';
750 AutoCompleteBase.prototype = {
751 // -- Public Prototype Methods ---------------------------------------------
755 * Sends a request to the configured source. If no source is configured,
756 * this method won't do anything.
760 * Usually there's no reason to call this method manually; it will be
761 * called automatically when user input causes a <code>query</code> event to
762 * be fired. The only time you'll need to call this method manually is if
763 * you want to force a request to be sent when no user input has occurred.
766 * @method sendRequest
767 * @param {String} query (optional) Query to send. If specified, the
768 * <code>query</code> attribute will be set to this query. If not
769 * specified, the current value of the <code>query</code> attribute will
771 * @param {Function} requestTemplate (optional) Request template function.
772 * If not specified, the current value of the <code>requestTemplate</code>
773 * attribute will be used.
776 sendRequest: function (query, requestTemplate) {
778 source = this.get('source');
780 if (query || query === '') {
781 this._set(QUERY, query);
783 query = this.get(QUERY);
787 if (!requestTemplate) {
788 requestTemplate = this.get(REQUEST_TEMPLATE);
791 request = requestTemplate ? requestTemplate(query) : query;
797 success: Y.bind(this._onResponse, this, query)
805 // -- Protected Lifecycle Methods ------------------------------------------
808 * Attaches event listeners and behaviors.
810 * @method _bindUIACBase
813 _bindUIACBase: function () {
814 var inputNode = this.get(INPUT_NODE),
815 tokenInput = inputNode && inputNode.tokenInput;
817 // If the inputNode has a node-tokeninput plugin attached, bind to the
818 // plugin's inputNode instead.
820 inputNode = tokenInput.get(INPUT_NODE);
821 this._set('tokenInput', tokenInput);
825 Y.error('No inputNode specified.');
829 this._inputNode = inputNode;
831 this._acBaseEvents = [
832 // This is the valueChange event on the inputNode, provided by the
833 // event-valuechange module, not our own valueChange.
834 inputNode.on(VALUE_CHANGE, this._onInputValueChange, this),
836 inputNode.on('blur', this._onInputBlur, this),
838 this.after(ALLOW_BROWSER_AC + 'Change', this._syncBrowserAutocomplete),
839 this.after(VALUE_CHANGE, this._afterValueChange)
844 * Detaches AutoCompleteBase event listeners.
846 * @method _destructorACBase
849 _destructorACBase: function () {
850 var events = this._acBaseEvents;
852 while (events && events.length) {
853 events.pop().detach();
858 * Synchronizes the UI state of the <code>inputNode</code>.
860 * @method _syncUIACBase
863 _syncUIACBase: function () {
864 this._syncBrowserAutocomplete();
865 this.set(VALUE, this.get(INPUT_NODE).get(VALUE));
868 // -- Protected Prototype Methods ------------------------------------------
871 * Creates a DataSource-like object that simply returns the specified array
872 * as a response. See the <code>source</code> attribute for more details.
874 * @method _createArraySource
875 * @param {Array} source
876 * @return {Object} DataSource-like object.
879 _createArraySource: function (source) {
882 return {sendRequest: function (request) {
883 that[_SOURCE_SUCCESS](source.concat(), request);
888 * Creates a DataSource-like object that passes the query to a
889 * custom-defined function, which is expected to return an array as a
890 * response. See the <code>source</code> attribute for more details.
892 * @method _createFunctionSource
893 * @param {Function} source Function that accepts a query parameter and
894 * returns an array of results.
895 * @return {Object} DataSource-like object.
898 _createFunctionSource: function (source) {
901 return {sendRequest: function (request) {
902 that[_SOURCE_SUCCESS](source(request.request) || [], request);
907 * Creates a DataSource-like object that looks up queries as properties on
908 * the specified object, and returns the found value (if any) as a response.
909 * See the <code>source</code> attribute for more details.
911 * @method _createObjectSource
912 * @param {Object} source
913 * @return {Object} DataSource-like object.
916 _createObjectSource: function (source) {
919 return {sendRequest: function (request) {
920 var query = request.request;
922 that[_SOURCE_SUCCESS](
923 YObject.owns(source, query) ? source[query] : [],
930 * Returns <code>true</code> if <i>value</i> is either a function or
933 * @method _functionValidator
934 * @param {Function|null} value Value to validate.
937 _functionValidator: function (value) {
938 return value === null || isFunction(value);
942 * Faster and safer alternative to Y.Object.getValue(). Doesn't bother
943 * casting the path to an array (since we already know it's an array) and
944 * doesn't throw an error if a value in the middle of the object hierarchy
945 * is neither <code>undefined</code> nor an object.
947 * @method _getObjectValue
948 * @param {Object} obj
949 * @param {Array} path
950 * @return {mixed} Located value, or <code>undefined</code> if the value was
951 * not found at the specified path.
954 _getObjectValue: function (obj, path) {
959 for (var i = 0, len = path.length; obj && i < len; i++) {
967 * Parses result responses, performs filtering and highlighting, and fires
968 * the <code>results</code> event.
970 * @method _parseResponse
971 * @param {String} query Query that generated these results.
972 * @param {Object} response Response containing results.
973 * @param {Object} data Raw response data.
976 _parseResponse: function (query, response, data) {
983 listLocator = this.get(RESULT_LIST_LOCATOR),
985 unfiltered = response && response.results,
999 if (unfiltered && listLocator) {
1000 unfiltered = listLocator(unfiltered);
1003 if (unfiltered && unfiltered.length) {
1004 filters = this.get('resultFilters');
1005 textLocator = this.get('resultTextLocator');
1007 // Create a lightweight result object for each result to make them
1008 // easier to work with. The various properties on the object
1009 // represent different formats of the result, and will be populated
1011 for (i = 0, len = unfiltered.length; i < len; ++i) {
1012 result = unfiltered[i];
1013 text = textLocator ? textLocator(result) : result.toString();
1016 display: Escape.html(text),
1022 // Run the results through all configured result filters. Each
1023 // filter returns an array of (potentially fewer) result objects,
1024 // which is then passed to the next filter, and so on.
1025 for (i = 0, len = filters.length; i < len; ++i) {
1026 results = filters[i](query, results.concat());
1032 if (!results.length) {
1037 if (results.length) {
1038 formatter = this.get('resultFormatter');
1039 highlighter = this.get('resultHighlighter');
1040 maxResults = this.get('maxResults');
1042 // If maxResults is set and greater than 0, limit the number of
1044 if (maxResults && maxResults > 0 &&
1045 results.length > maxResults) {
1046 results.length = maxResults;
1049 // Run the results through the configured highlighter (if any).
1050 // The highlighter returns an array of highlighted strings (not
1051 // an array of result objects), and these strings are then added
1052 // to each result object.
1054 highlighted = highlighter(query, results.concat());
1060 for (i = 0, len = highlighted.length; i < len; ++i) {
1061 result = results[i];
1062 result.highlighted = highlighted[i];
1063 result.display = result.highlighted;
1067 // Run the results through the configured formatter (if any) to
1068 // produce the final formatted results. The formatter returns an
1069 // array of strings or Node instances (not an array of result
1070 // objects), and these strings/Nodes are then added to each
1073 formatted = formatter(query, results.concat());
1079 for (i = 0, len = formatted.length; i < len; ++i) {
1080 results[i].display = formatted[i];
1086 facade.results = results;
1087 this.fire(EVT_RESULTS, facade);
1092 * Returns the query portion of the specified input value, or
1093 * <code>null</code> if there is no suitable query within the input value.
1097 * If a query delimiter is defined, the query will be the last delimited
1098 * part of of the string.
1101 * @method _parseValue
1102 * @param {String} value Input value from which to extract the query.
1103 * @return {String|null} query
1106 _parseValue: function (value) {
1107 var delim = this.get(QUERY_DELIMITER);
1110 value = value.split(delim);
1111 value = value[value.length - 1];
1114 return Lang.trimLeft(value);
1118 * Setter for locator attributes.
1120 * @method _setLocator
1121 * @param {Function|String|null} locator
1122 * @return {Function|null}
1125 _setLocator: function (locator) {
1126 if (this[_FUNCTION_VALIDATOR](locator)) {
1132 locator = locator.toString().split('.');
1134 return function (result) {
1135 return result && that._getObjectValue(result, locator);
1140 * Setter for the <code>requestTemplate</code> attribute.
1142 * @method _setRequestTemplate
1143 * @param {Function|String|null} template
1144 * @return {Function|null}
1147 _setRequestTemplate: function (template) {
1148 if (this[_FUNCTION_VALIDATOR](template)) {
1152 template = template.toString();
1154 return function (query) {
1155 return Lang.sub(template, {query: encodeURIComponent(query)});
1160 * Setter for the <code>resultFilters</code> attribute.
1162 * @method _setResultFilters
1163 * @param {Array|Function|String|null} filters <code>null</code>, a filter
1164 * function, an array of filter functions, or a string or array of strings
1165 * representing the names of methods on
1166 * <code>Y.AutoCompleteFilters</code>.
1167 * @return {Array} Array of filter functions (empty if <i>filters</i> is
1168 * <code>null</code>).
1171 _setResultFilters: function (filters) {
1172 var acFilters, getFilterFunction;
1174 if (filters === null) {
1178 acFilters = Y.AutoCompleteFilters;
1180 getFilterFunction = function (filter) {
1181 if (isFunction(filter)) {
1185 if (isString(filter) && acFilters &&
1186 isFunction(acFilters[filter])) {
1187 return acFilters[filter];
1193 if (Lang.isArray(filters)) {
1194 filters = YArray.map(filters, getFilterFunction);
1195 return YArray.every(filters, function (f) { return !!f; }) ?
1196 filters : INVALID_VALUE;
1198 filters = getFilterFunction(filters);
1199 return filters ? [filters] : INVALID_VALUE;
1204 * Setter for the <code>resultHighlighter</code> attribute.
1206 * @method _setResultHighlighter
1207 * @param {Function|String|null} highlighter <code>null</code>, a
1208 * highlighter function, or a string representing the name of a method on
1209 * <code>Y.AutoCompleteHighlighters</code>.
1210 * @return {Function|null}
1213 _setResultHighlighter: function (highlighter) {
1216 if (this._functionValidator(highlighter)) {
1220 acHighlighters = Y.AutoCompleteHighlighters;
1222 if (isString(highlighter) && acHighlighters &&
1223 isFunction(acHighlighters[highlighter])) {
1224 return acHighlighters[highlighter];
1227 return INVALID_VALUE;
1231 * Setter for the <code>source</code> attribute. Returns a DataSource or
1232 * a DataSource-like object depending on the type of <i>source</i>.
1234 * @method _setSource
1235 * @param {Array|DataSource|Object|String} source AutoComplete source. See
1236 * the <code>source</code> attribute for details.
1237 * @return {DataSource|Object}
1240 _setSource: function (source) {
1241 var sourcesNotLoaded = 'autocomplete-sources module not loaded';
1243 if ((source && isFunction(source.sendRequest)) || source === null) {
1244 // Quacks like a DataSource instance (or null). Make it so!
1248 switch (Lang.type(source)) {
1250 if (this._createStringSource) {
1251 return this._createStringSource(source);
1254 Y.error(sourcesNotLoaded);
1255 return INVALID_VALUE;
1258 // Wrap the array in a teensy tiny fake DataSource that just returns
1259 // the array itself for each request. Filters will do the rest.
1260 return this._createArraySource(source);
1263 return this._createFunctionSource(source);
1266 // If the object is a JSONPRequest instance, use it as a JSONP
1268 if (Y.JSONPRequest && source instanceof Y.JSONPRequest) {
1269 if (this._createJSONPSource) {
1270 return this._createJSONPSource(source);
1273 Y.error(sourcesNotLoaded);
1274 return INVALID_VALUE;
1277 // Not a JSONPRequest instance. Wrap the object in a teensy tiny
1278 // fake DataSource that looks for the request as a property on the
1279 // object and returns it if it exists, or an empty array otherwise.
1280 return this._createObjectSource(source);
1283 return INVALID_VALUE;
1287 * Shared success callback for non-DataSource sources.
1289 * @method _sourceSuccess
1290 * @param {mixed} data Response data.
1291 * @param {Object} request Request object.
1294 _sourceSuccess: function (data, request) {
1295 request.callback.success({
1297 response: {results: data}
1302 * Synchronizes the UI state of the <code>allowBrowserAutocomplete</code>
1305 * @method _syncBrowserAutocomplete
1308 _syncBrowserAutocomplete: function () {
1309 var inputNode = this.get(INPUT_NODE);
1311 if (inputNode.get('nodeName').toLowerCase() === 'input') {
1312 inputNode.setAttribute('autocomplete',
1313 this.get(ALLOW_BROWSER_AC) ? 'on' : 'off');
1319 * Updates the query portion of the <code>value</code> attribute.
1323 * If a query delimiter is defined, the last delimited portion of the input
1324 * value will be replaced with the specified <i>value</i>.
1327 * @method _updateValue
1328 * @param {String} newVal New value.
1331 _updateValue: function (newVal) {
1332 var delim = this.get(QUERY_DELIMITER),
1337 newVal = Lang.trimLeft(newVal);
1340 insertDelim = trim(delim); // so we don't double up on spaces
1341 prevVal = YArray.map(trim(this.get(VALUE)).split(delim), trim);
1342 len = prevVal.length;
1345 prevVal[len - 1] = newVal;
1346 newVal = prevVal.join(insertDelim + ' ');
1349 newVal = newVal + insertDelim + ' ';
1352 this.set(VALUE, newVal);
1355 // -- Protected Event Handlers ---------------------------------------------
1358 * Handles change events for the <code>value</code> attribute.
1360 * @method _afterValueChange
1361 * @param {EventFacade} e
1364 _afterValueChange: function (e) {
1372 // Don't query on value changes that didn't come from the user.
1373 if (e.src !== AutoCompleteBase.UI_SRC) {
1374 this._inputNode.set(VALUE, newVal);
1379 minQueryLength = this.get('minQueryLength');
1380 query = this._parseValue(newVal) || '';
1382 if (minQueryLength >= 0 && query.length >= minQueryLength) {
1383 delay = this.get('queryDelay');
1386 fire = function () {
1387 that.fire(EVT_QUERY, {
1394 clearTimeout(this._delay);
1395 this._delay = setTimeout(fire, delay);
1400 clearTimeout(this._delay);
1402 this.fire(EVT_CLEAR, {
1403 prevVal: e.prevVal ? this._parseValue(e.prevVal) : null
1409 * Handles <code>blur</code> events on the input node.
1411 * @method _onInputBlur
1412 * @param {EventFacade} e
1415 _onInputBlur: function (e) {
1416 var delim = this.get(QUERY_DELIMITER),
1421 // If a query delimiter is set and the input's value contains one or
1422 // more trailing delimiters, strip them.
1423 if (delim && !this.get('allowTrailingDelimiter')) {
1424 delim = Lang.trimRight(delim);
1425 value = newVal = this._inputNode.get(VALUE);
1428 while ((newVal = Lang.trimRight(newVal)) &&
1429 (delimPos = newVal.length - delim.length) &&
1430 newVal.lastIndexOf(delim) === delimPos) {
1432 newVal = newVal.substring(0, delimPos);
1435 // Delimiter is one or more space characters, so just trim the
1437 newVal = Lang.trimRight(newVal);
1440 if (newVal !== value) {
1441 this.set(VALUE, newVal);
1447 * Handles <code>valueChange</code> events on the input node and fires a
1448 * <code>query</code> event when the input value meets the configured
1451 * @method _onInputValueChange
1452 * @param {EventFacade} e
1455 _onInputValueChange: function (e) {
1456 var newVal = e.newVal;
1458 // Don't query if the internal value is the same as the new value
1459 // reported by valueChange.
1460 if (newVal === this.get(VALUE)) {
1464 this.set(VALUE, newVal, {src: AutoCompleteBase.UI_SRC});
1468 * Handles source responses and fires the <code>results</code> event.
1470 * @method _onResponse
1471 * @param {EventFacade} e
1474 _onResponse: function (query, e) {
1475 // Ignore stale responses that aren't for the current query.
1476 if (query === this.get(QUERY)) {
1477 this._parseResponse(query, e.response, e.data);
1481 // -- Protected Default Event Handlers -------------------------------------
1484 * Default <code>clear</code> event handler. Sets the <code>results</code>
1485 * property to an empty array and <code>query</code> to null.
1487 * @method _defClearFn
1490 _defClearFn: function () {
1491 this._set(QUERY, null);
1492 this._set(RESULTS, []);
1496 * Default <code>query</code> event handler. Sets the <code>query</code>
1497 * property and sends a request to the source if one is configured.
1499 * @method _defQueryFn
1500 * @param {EventFacade} e
1503 _defQueryFn: function (e) {
1504 var query = e.query;
1506 this.sendRequest(query); // sendRequest will set the 'query' attribute
1510 * Default <code>results</code> event handler. Sets the <code>results</code>
1511 * property to the latest results.
1513 * @method _defResultsFn
1514 * @param {EventFacade} e
1517 _defResultsFn: function (e) {
1518 this._set(RESULTS, e[RESULTS]);
1522 Y.AutoCompleteBase = AutoCompleteBase;
1525 }, '3.3.0' ,{optional:['autocomplete-sources'], requires:['array-extras', 'base-build', 'escape', 'event-valuechange', 'node-base']});
1526 YUI.add('autocomplete-sources', function(Y) {
1529 * Mixes support for JSONP and YQL result sources into AutoCompleteBase.
1531 * @module autocomplete
1532 * @submodule autocomplete-sources
1537 _SOURCE_SUCCESS = '_sourceSuccess',
1539 MAX_RESULTS = 'maxResults',
1540 REQUEST_TEMPLATE = 'requestTemplate',
1541 RESULT_LIST_LOCATOR = 'resultListLocator';
1543 function ACSources() {}
1545 ACSources.prototype = {
1547 * Regular expression used to determine whether a String source is a YQL
1550 * @property _YQL_SOURCE_REGEX
1553 * @for AutoCompleteBase
1555 _YQL_SOURCE_REGEX: /^(?:select|set|use)\s+/i,
1558 * Creates a DataSource-like object that uses <code>Y.io</code> as a source.
1559 * See the <code>source</code> attribute for more details.
1561 * @method _createIOSource
1562 * @param {String} source URL.
1563 * @return {Object} DataSource-like object.
1565 * @for AutoCompleteBase
1567 _createIOSource: function (source) {
1571 ioRequest, lastRequest, loading;
1573 ioSource.sendRequest = function (request) {
1574 var _sendRequest = function (request) {
1575 var query = request.request,
1576 maxResults, requestTemplate, url;
1579 that[_SOURCE_SUCCESS](cache[query], request);
1581 maxResults = that.get(MAX_RESULTS);
1582 requestTemplate = that.get(REQUEST_TEMPLATE);
1585 if (requestTemplate) {
1586 url += requestTemplate(query);
1589 url = Lang.sub(url, {
1590 maxResults: maxResults > 0 ? maxResults : 1000,
1591 query : encodeURIComponent(query)
1594 // Cancel any outstanding requests.
1595 if (ioRequest && ioRequest.isInProgress()) {
1599 ioRequest = Y.io(url, {
1601 success: function (tid, response) {
1605 data = Y.JSON.parse(response.responseText);
1607 Y.error('JSON parse error', ex);
1611 cache[query] = data;
1612 that[_SOURCE_SUCCESS](data, request);
1620 // Keep track of the most recent request in case there are multiple
1621 // requests while we're waiting for the IO module to load. Only the
1622 // most recent request will be sent.
1623 lastRequest = request;
1628 // Lazy-load the io and json-parse modules if necessary, then
1629 // overwrite the sendRequest method to bypass this check in the
1631 Y.use('io-base', 'json-parse', function () {
1632 ioSource.sendRequest = _sendRequest;
1633 _sendRequest(lastRequest);
1642 * Creates a DataSource-like object that uses the specified JSONPRequest
1643 * instance as a source. See the <code>source</code> attribute for more
1646 * @method _createJSONPSource
1647 * @param {JSONPRequest|String} source URL string or JSONPRequest instance.
1648 * @return {Object} DataSource-like object.
1650 * @for AutoCompleteBase
1652 _createJSONPSource: function (source) {
1656 lastRequest, loading;
1658 jsonpSource.sendRequest = function (request) {
1659 var _sendRequest = function (request) {
1660 var query = request.request;
1663 that[_SOURCE_SUCCESS](cache[query], request);
1665 // Hack alert: JSONPRequest currently doesn't support
1666 // per-request callbacks, so we're reaching into the protected
1667 // _config object to make it happen.
1669 // This limitation is mentioned in the following JSONP
1670 // enhancement ticket:
1672 // http://yuilibrary.com/projects/yui3/ticket/2529371
1673 source._config.on.success = function (data) {
1674 cache[query] = data;
1675 that[_SOURCE_SUCCESS](data, request);
1682 // Keep track of the most recent request in case there are multiple
1683 // requests while we're waiting for the JSONP module to load. Only
1684 // the most recent request will be sent.
1685 lastRequest = request;
1690 // Lazy-load the JSONP module if necessary, then overwrite the
1691 // sendRequest method to bypass this check in the future.
1692 Y.use('jsonp', function () {
1693 // Turn the source into a JSONPRequest instance if it isn't
1695 if (!(source instanceof Y.JSONPRequest)) {
1696 source = new Y.JSONPRequest(source, {
1697 format: Y.bind(that._jsonpFormatter, that)
1701 jsonpSource.sendRequest = _sendRequest;
1702 _sendRequest(lastRequest);
1711 * Creates a DataSource-like object that calls the specified URL or
1712 * executes the specified YQL query for results. If the string starts
1713 * with "select ", "use ", or "set " (case-insensitive), it's assumed to be
1714 * a YQL query; otherwise, it's assumed to be a URL (which may be absolute
1715 * or relative). URLs containing a "{callback}" placeholder are assumed to
1716 * be JSONP URLs; all others will use XHR. See the <code>source</code>
1717 * attribute for more details.
1719 * @method _createStringSource
1720 * @param {String} source URL or YQL query.
1721 * @return {Object} DataSource-like object.
1723 * @for AutoCompleteBase
1725 _createStringSource: function (source) {
1726 if (this._YQL_SOURCE_REGEX.test(source)) {
1727 // Looks like a YQL query.
1728 return this._createYQLSource(source);
1729 } else if (source.indexOf('{callback}') !== -1) {
1730 // Contains a {callback} param and isn't a YQL query, so it must be
1732 return this._createJSONPSource(source);
1734 // Not a YQL query or JSONP, so we'll assume it's an XHR URL.
1735 return this._createIOSource(source);
1740 * Creates a DataSource-like object that uses the specified YQL query string
1741 * to create a YQL-based source. See the <code>source</code> attribute for
1742 * details. If no <code>resultListLocator</code> is defined, this method
1743 * will set a best-guess locator that might work for many typical YQL
1746 * @method _createYQLSource
1747 * @param {String} source YQL query.
1748 * @return {Object} DataSource-like object.
1750 * @for AutoCompleteBase
1752 _createYQLSource: function (source) {
1756 lastRequest, loading;
1758 if (!this.get(RESULT_LIST_LOCATOR)) {
1759 this.set(RESULT_LIST_LOCATOR, this._defaultYQLLocator);
1762 yqlSource.sendRequest = function (request) {
1765 _sendRequest = function (request) {
1766 var query = request.request,
1767 callback, env, maxResults, opts, yqlQuery;
1770 that[_SOURCE_SUCCESS](cache[query], request);
1772 callback = function (data) {
1773 cache[query] = data;
1774 that[_SOURCE_SUCCESS](data, request);
1777 env = that.get('yqlEnv');
1778 maxResults = that.get(MAX_RESULTS);
1780 opts = {proto: that.get('yqlProtocol')};
1782 yqlQuery = Lang.sub(source, {
1783 maxResults: maxResults > 0 ? maxResults : 1000,
1787 // Only create a new YQLRequest instance if this is the
1788 // first request. For subsequent requests, we'll reuse the
1789 // original instance.
1791 yqlRequest._callback = callback;
1792 yqlRequest._opts = opts;
1793 yqlRequest._params.q = yqlQuery;
1796 yqlRequest._params.env = env;
1799 yqlRequest = new Y.YQLRequest(yqlQuery, {
1800 on: {success: callback},
1801 allowCache: false // temp workaround until JSONP has per-URL callback proxies
1802 }, env ? {env: env} : null, opts);
1809 // Keep track of the most recent request in case there are multiple
1810 // requests while we're waiting for the YQL module to load. Only the
1811 // most recent request will be sent.
1812 lastRequest = request;
1815 // Lazy-load the YQL module if necessary, then overwrite the
1816 // sendRequest method to bypass this check in the future.
1819 Y.use('yql', function () {
1820 yqlSource.sendRequest = _sendRequest;
1821 _sendRequest(lastRequest);
1830 * Default resultListLocator used when a string-based YQL source is set and
1831 * the implementer hasn't already specified one.
1833 * @method _defaultYQLLocator
1834 * @param {Object} response YQL response object.
1837 * @for AutoCompleteBase
1839 _defaultYQLLocator: function (response) {
1840 var results = response && response.query && response.query.results,
1843 if (results && Lang.isObject(results)) {
1844 // If there's only a single value on YQL's results object, that
1845 // value almost certainly contains the array of results we want. If
1846 // there are 0 or 2+ values, then the values themselves are most
1847 // likely the results we want.
1848 values = Y.Object.values(results) || [];
1849 results = values.length === 1 ? values[0] : values;
1851 if (!Lang.isArray(results)) {
1852 results = [results];
1862 * URL formatter passed to <code>JSONPRequest</code> instances.
1864 * @method _jsonpFormatter
1865 * @param {String} url
1866 * @param {String} proxy
1867 * @param {String} query
1868 * @return {String} Formatted URL
1870 * @for AutoCompleteBase
1872 _jsonpFormatter: function (url, proxy, query) {
1873 var maxResults = this.get(MAX_RESULTS),
1874 requestTemplate = this.get(REQUEST_TEMPLATE);
1876 if (requestTemplate) {
1877 url += requestTemplate(query);
1880 return Lang.sub(url, {
1882 maxResults: maxResults > 0 ? maxResults : 1000,
1883 query : encodeURIComponent(query)
1890 * YQL environment file URL to load when the <code>source</code> is set to
1891 * a YQL query. Set this to <code>null</code> to use the default Open Data
1892 * Tables environment file (http://datatables.org/alltables.env).
1897 * @for AutoCompleteBase
1904 * URL protocol to use when the <code>source</code> is set to a YQL query.
1906 * @attribute yqlProtocol
1909 * @for AutoCompleteBase
1916 Y.Base.mix(Y.AutoCompleteBase, [ACSources]);
1919 }, '3.3.0' ,{optional:['io-base', 'json-parse', 'jsonp', 'yql'], requires:['autocomplete-base']});
1920 YUI.add('autocomplete-list', function(Y) {
1923 * Traditional autocomplete dropdown list widget, just like Mom used to make.
1925 * @module autocomplete
1926 * @submodule autocomplete-list
1927 * @class AutoCompleteList
1929 * @uses AutoCompleteBase
1930 * @uses WidgetPosition
1931 * @uses WidgetPositionAlign
1934 * @param {Object} config Configuration object.
1941 // keyCode constants.
1944 // String shorthand.
1945 _CLASS_ITEM = '_CLASS_ITEM',
1946 _CLASS_ITEM_ACTIVE = '_CLASS_ITEM_ACTIVE',
1947 _CLASS_ITEM_HOVER = '_CLASS_ITEM_HOVER',
1948 _SELECTOR_ITEM = '_SELECTOR_ITEM',
1950 ACTIVE_ITEM = 'activeItem',
1951 ALWAYS_SHOW_LIST = 'alwaysShowList',
1952 CIRCULAR = 'circular',
1953 HOVERED_ITEM = 'hoveredItem',
1958 RESULTS = 'results',
1959 VISIBLE = 'visible',
1963 EVT_SELECT = 'select',
1965 List = Y.Base.create('autocompleteList', Y.Widget, [
1968 Y.WidgetPositionAlign,
1971 // -- Prototype Properties -------------------------------------------------
1972 ARIA_TEMPLATE: '<div/>',
1973 ITEM_TEMPLATE: '<li/>',
1974 LIST_TEMPLATE: '<ul/>',
1976 // -- Lifecycle Prototype Methods ------------------------------------------
1977 initializer: function () {
1978 var inputNode = this.get('inputNode');
1981 Y.error('No inputNode specified.');
1985 this._inputNode = inputNode;
1986 this._listEvents = [];
1988 // This ensures that the list is rendered inside the same parent as the
1989 // input node by default, which is necessary for proper ARIA support.
1990 this.DEF_PARENT_NODE = inputNode.get('parentNode');
1992 // Cache commonly used classnames and selectors for performance.
1993 this[_CLASS_ITEM] = this.getClassName(ITEM);
1994 this[_CLASS_ITEM_ACTIVE] = this.getClassName(ITEM, 'active');
1995 this[_CLASS_ITEM_HOVER] = this.getClassName(ITEM, 'hover');
1996 this[_SELECTOR_ITEM] = '.' + this[_CLASS_ITEM];
1999 * Fires when an autocomplete suggestion is selected from the list,
2000 * typically via a keyboard action or mouse click.
2003 * @param {EventFacade} e Event facade with the following additional
2007 * <dt>itemNode (Node)</dt>
2009 * List item node that was selected.
2012 * <dt>result (Object)</dt>
2014 * AutoComplete result object.
2018 * @preventable _defSelectFn
2020 this.publish(EVT_SELECT, {
2021 defaultFn: this._defSelectFn
2025 destructor: function () {
2026 while (this._listEvents.length) {
2027 this._listEvents.pop().detach();
2031 bindUI: function () {
2036 renderUI: function () {
2037 var ariaNode = this._createAriaNode(),
2038 contentBox = this.get('contentBox'),
2039 inputNode = this._inputNode,
2041 parentNode = inputNode.get('parentNode');
2043 listNode = this._createListNode();
2044 this._set('listNode', listNode);
2045 contentBox.append(listNode);
2047 inputNode.addClass(this.getClassName('input')).setAttrs({
2048 'aria-autocomplete': LIST,
2049 'aria-expanded' : false,
2050 'aria-owns' : listNode.get('id'),
2054 // ARIA node must be outside the widget or announcements won't be made
2055 // when the widget is hidden.
2056 parentNode.append(ariaNode);
2058 this._ariaNode = ariaNode;
2059 this._boundingBox = this.get('boundingBox');
2060 this._contentBox = contentBox;
2061 this._listNode = listNode;
2062 this._parentNode = parentNode;
2065 syncUI: function () {
2066 this._syncResults();
2067 this._syncVisibility();
2070 // -- Public Prototype Methods ---------------------------------------------
2073 * Hides the list, unless the <code>alwaysShowList</code> attribute is
2074 * <code>true</code>.
2081 return this.get(ALWAYS_SHOW_LIST) ? this : this.set(VISIBLE, false);
2085 * Selects the specified <i>itemNode</i>, or the current
2086 * <code>activeItem</code> if <i>itemNode</i> is not specified.
2088 * @method selectItem
2089 * @param {Node} itemNode (optional) Item node to select.
2092 selectItem: function (itemNode) {
2094 if (!itemNode.hasClass(this[_CLASS_ITEM])) {
2098 itemNode = this.get(ACTIVE_ITEM);
2105 this.fire(EVT_SELECT, {
2107 result : itemNode.getData(RESULT)
2113 // -- Protected Prototype Methods ------------------------------------------
2116 * Activates the next item after the currently active item. If there is no
2117 * next item and the <code>circular</code> attribute is <code>true</code>,
2118 * focus will wrap back to the input node.
2120 * @method _activateNextItem
2124 _activateNextItem: function () {
2125 var item = this.get(ACTIVE_ITEM),
2129 nextItem = item.next(this[_SELECTOR_ITEM]) ||
2130 (this.get(CIRCULAR) ? null : item);
2132 nextItem = this._getFirstItemNode();
2135 this.set(ACTIVE_ITEM, nextItem);
2141 * Activates the item previous to the currently active item. If there is no
2142 * previous item and the <code>circular</code> attribute is
2143 * <code>true</code>, focus will wrap back to the input node.
2145 * @method _activatePrevItem
2149 _activatePrevItem: function () {
2150 var item = this.get(ACTIVE_ITEM),
2151 prevItem = item ? item.previous(this[_SELECTOR_ITEM]) :
2152 this.get(CIRCULAR) && this._getLastItemNode();
2154 this.set(ACTIVE_ITEM, prevItem || null);
2160 * Appends the specified result <i>items</i> to the list inside a new item
2164 * @param {Array|Node|HTMLElement|String} items Result item or array of
2166 * @return {NodeList} Added nodes.
2169 _add: function (items) {
2172 YArray.each(Lang.isArray(items) ? items : [items], function (item) {
2173 itemNodes.push(this._createItemNode(item).setData(RESULT, item));
2176 itemNodes = Y.all(itemNodes);
2177 this._listNode.append(itemNodes.toFrag());
2183 * Updates the ARIA live region with the specified message.
2186 * @param {String} stringId String id (from the <code>strings</code>
2187 * attribute) of the message to speak.
2188 * @param {Object} subs (optional) Substitutions for placeholders in the
2192 _ariaSay: function (stringId, subs) {
2193 var message = this.get('strings.' + stringId);
2194 this._ariaNode.setContent(subs ? Lang.sub(message, subs) : message);
2198 * Binds <code>inputNode</code> events and behavior.
2200 * @method _bindInput
2203 _bindInput: function () {
2204 var inputNode = this._inputNode,
2205 alignNode, alignWidth, tokenInput;
2207 // Null align means we can auto-align. Set align to false to prevent
2208 // auto-alignment, or a valid alignment config to customize the
2210 if (this.get('align') === null) {
2211 // If this is a tokenInput, align with its bounding box.
2212 // Otherwise, align with the inputNode. Bit of a cheat.
2213 tokenInput = this.get('tokenInput');
2214 alignNode = (tokenInput && tokenInput.get('boundingBox')) || inputNode;
2218 points: ['tl', 'bl']
2221 // If no width config is set, attempt to set the list's width to the
2222 // width of the alignment node. If the alignment node's width is
2223 // falsy, do nothing.
2224 if (!this.get(WIDTH) && (alignWidth = alignNode.get('offsetWidth'))) {
2225 this.set(WIDTH, alignWidth);
2229 // Attach inputNode events.
2230 this._listEvents.push(inputNode.on('blur', this._onListInputBlur, this));
2234 * Binds list events.
2239 _bindList: function () {
2240 this._listEvents.concat([
2242 mouseover: this._afterMouseOver,
2243 mouseout : this._afterMouseOut,
2245 activeItemChange : this._afterActiveItemChange,
2246 alwaysShowListChange: this._afterAlwaysShowListChange,
2247 hoveredItemChange : this._afterHoveredItemChange,
2248 resultsChange : this._afterResultsChange,
2249 visibleChange : this._afterVisibleChange
2252 this._listNode.delegate('click', this._onItemClick, this[_SELECTOR_ITEM], this)
2257 * Clears the contents of the tray.
2262 _clear: function () {
2263 this.set(ACTIVE_ITEM, null);
2264 this._set(HOVERED_ITEM, null);
2266 this._listNode.get('children').remove(true);
2270 * Creates and returns an ARIA live region node.
2272 * @method _createAriaNode
2273 * @return {Node} ARIA node.
2276 _createAriaNode: function () {
2277 var ariaNode = Node.create(this.ARIA_TEMPLATE);
2279 return ariaNode.addClass(this.getClassName('aria')).setAttrs({
2280 'aria-live': 'polite',
2286 * Creates and returns an item node with the specified <i>content</i>.
2288 * @method _createItemNode
2289 * @param {Object} result Result object.
2290 * @return {Node} Item node.
2293 _createItemNode: function (result) {
2294 var itemNode = Node.create(this.ITEM_TEMPLATE);
2296 return itemNode.addClass(this[_CLASS_ITEM]).setAttrs({
2297 id : Y.stamp(itemNode),
2299 }).setAttribute('data-text', result.text).append(result.display);
2303 * Creates and returns a list node.
2305 * @method _createListNode
2306 * @return {Node} List node.
2309 _createListNode: function () {
2310 var listNode = Node.create(this.LIST_TEMPLATE);
2312 return listNode.addClass(this.getClassName(LIST)).setAttrs({
2313 id : Y.stamp(listNode),
2319 * Gets the first item node in the list, or <code>null</code> if the list is
2322 * @method _getFirstItemNode
2323 * @return {Node|null}
2326 _getFirstItemNode: function () {
2327 return this._listNode.one(this[_SELECTOR_ITEM]);
2331 * Gets the last item node in the list, or <code>null</code> if the list is
2334 * @method _getLastItemNode
2335 * @return {Node|null}
2338 _getLastItemNode: function () {
2339 return this._listNode.one(this[_SELECTOR_ITEM] + ':last-child');
2343 * Synchronizes the results displayed in the list with those in the
2344 * <i>results</i> argument, or with the <code>results</code> attribute if an
2345 * argument is not provided.
2347 * @method _syncResults
2348 * @param {Array} results (optional) Results.
2351 _syncResults: function (results) {
2355 results = this.get(RESULTS);
2360 if (results.length) {
2361 items = this._add(results);
2362 this._ariaSay('items_available');
2365 if (this.get('activateFirstItem') && !this.get(ACTIVE_ITEM)) {
2366 this.set(ACTIVE_ITEM, this._getFirstItemNode());
2371 * Synchronizes the visibility of the tray with the <i>visible</i> argument,
2372 * or with the <code>visible</code> attribute if an argument is not
2375 * @method _syncVisibility
2376 * @param {Boolean} visible (optional) Visibility.
2379 _syncVisibility: function (visible) {
2380 if (this.get(ALWAYS_SHOW_LIST)) {
2382 this.set(VISIBLE, visible);
2385 if (typeof visible === 'undefined') {
2386 visible = this.get(VISIBLE);
2389 this._inputNode.set('aria-expanded', visible);
2390 this._boundingBox.set('aria-hidden', !visible);
2393 // Force WidgetPositionAlign to refresh its alignment.
2394 this._syncUIPosAlign();
2396 this.set(ACTIVE_ITEM, null);
2397 this._set(HOVERED_ITEM, null);
2399 // Force a reflow to work around a glitch in IE6 and 7 where some of
2400 // the contents of the list will sometimes remain visible after the
2401 // container is hidden.
2402 this._boundingBox.get('offsetWidth');
2406 // -- Protected Event Handlers ---------------------------------------------
2409 * Handles <code>activeItemChange</code> events.
2411 * @method _afterActiveItemChange
2412 * @param {EventTarget} e
2415 _afterActiveItemChange: function (e) {
2416 var inputNode = this._inputNode,
2418 prevVal = e.prevVal;
2420 // The previous item may have disappeared by the time this handler runs,
2421 // so we need to be careful.
2422 if (prevVal && prevVal._node) {
2423 prevVal.removeClass(this[_CLASS_ITEM_ACTIVE]);
2427 newVal.addClass(this[_CLASS_ITEM_ACTIVE]);
2428 inputNode.set('aria-activedescendant', newVal.get(ID));
2430 inputNode.removeAttribute('aria-activedescendant');
2433 if (this.get('scrollIntoView')) {
2434 (newVal || inputNode).scrollIntoView();
2439 * Handles <code>alwaysShowListChange</code> events.
2441 * @method _afterAlwaysShowListChange
2442 * @param {EventTarget} e
2445 _afterAlwaysShowListChange: function (e) {
2446 this.set(VISIBLE, e.newVal || this.get(RESULTS).length > 0);
2450 * Handles <code>hoveredItemChange</code> events.
2452 * @method _afterHoveredItemChange
2453 * @param {EventTarget} e
2456 _afterHoveredItemChange: function (e) {
2457 var newVal = e.newVal,
2458 prevVal = e.prevVal;
2461 prevVal.removeClass(this[_CLASS_ITEM_HOVER]);
2465 newVal.addClass(this[_CLASS_ITEM_HOVER]);
2470 * Handles <code>mouseover</code> events.
2472 * @method _afterMouseOver
2473 * @param {EventTarget} e
2476 _afterMouseOver: function (e) {
2477 var itemNode = e.domEvent.target.ancestor(this[_SELECTOR_ITEM], true);
2479 this._mouseOverList = true;
2482 this._set(HOVERED_ITEM, itemNode);
2487 * Handles <code>mouseout</code> events.
2489 * @method _afterMouseOut
2490 * @param {EventTarget} e
2493 _afterMouseOut: function () {
2494 this._mouseOverList = false;
2495 this._set(HOVERED_ITEM, null);
2499 * Handles <code>resultsChange</code> events.
2501 * @method _afterResultsChange
2502 * @param {EventFacade} e
2505 _afterResultsChange: function (e) {
2506 this._syncResults(e.newVal);
2508 if (!this.get(ALWAYS_SHOW_LIST)) {
2509 this.set(VISIBLE, !!e.newVal.length);
2514 * Handles <code>visibleChange</code> events.
2516 * @method _afterVisibleChange
2517 * @param {EventFacade} e
2520 _afterVisibleChange: function (e) {
2521 this._syncVisibility(!!e.newVal);
2525 * Handles <code>inputNode</code> <code>blur</code> events.
2527 * @method _onListInputBlur
2528 * @param {EventTarget} e
2531 _onListInputBlur: function (e) {
2532 // Hide the list on inputNode blur events, unless the mouse is currently
2533 // over the list (which indicates that the user is probably interacting
2534 // with it). The _lastInputKey property comes from the
2535 // autocomplete-list-keys module.
2536 if (!this._mouseOverList || this._lastInputKey === KEY_TAB) {
2542 * Delegated event handler for item <code>click</code> events.
2544 * @method _onItemClick
2545 * @param {EventTarget} e
2548 _onItemClick: function (e) {
2549 var itemNode = e.currentTarget;
2551 this.set(ACTIVE_ITEM, itemNode);
2552 this.selectItem(itemNode);
2555 // -- Protected Default Event Handlers -------------------------------------
2558 * Default <code>select</code> event handler.
2560 * @method _defSelectFn
2561 * @param {EventTarget} e
2564 _defSelectFn: function (e) {
2565 var text = e.result.text;
2567 // TODO: support typeahead completion, etc.
2568 this._inputNode.focus();
2569 this._updateValue(text);
2570 this._ariaSay('item_selected', {item: text});
2576 * If <code>true</code>, the first item in the list will be activated by
2577 * default when the list is initially displayed and when results change.
2579 * @attribute activateFirstItem
2583 activateFirstItem: {
2588 * Item that's currently active, if any. When the user presses enter,
2589 * this is the item that will be selected.
2591 * @attribute activeItem
2600 * If <code>true</code>, the list will remain visible even when there
2601 * are no results to display.
2603 * @attribute alwaysShowList
2612 * If <code>true</code>, keyboard navigation will wrap around to the
2613 * opposite end of the list when navigating past the first or last item.
2615 * @attribute circular
2624 * Item currently being hovered over by the mouse, if any.
2626 * @attribute hoveredItem
2636 * Node that will contain result items.
2638 * @attribute listNode
2648 * If <code>true</code>, the viewport will be scrolled to ensure that
2649 * the active list item is visible when necessary.
2651 * @attribute scrollIntoView
2660 * Translatable strings used by the AutoCompleteList widget.
2662 * @attribute strings
2666 valueFn: function () {
2667 return Y.Intl.get('autocomplete-list');
2672 * If <code>true</code>, pressing the tab key while the list is visible
2673 * will select the active item, if any.
2675 * @attribute tabSelect
2683 // The "visible" attribute is documented in Widget.
2689 CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
2692 Y.AutoCompleteList = List;
2695 * Alias for <a href="AutoCompleteList.html"><code>AutoCompleteList</code></a>.
2696 * See that class for API docs.
2698 * @class AutoComplete
2701 Y.AutoComplete = List;
2704 }, '3.3.0' ,{lang:['en'], requires:['autocomplete-base', 'selector-css3', 'widget', 'widget-position', 'widget-position-align', 'widget-stack'], after:['autocomplete-sources'], skinnable:true});
2705 YUI.add('autocomplete-plugin', function(Y) {
2708 * Binds an AutoCompleteList instance to a Node instance.
2710 * @module autocomplete
2711 * @submodule autocomplete-plugin
2716 * Binds an AutoCompleteList instance to a Node instance.
2724 * Y.one('#my-input').plug(Y.Plugin.AutoComplete, {
2725 * source: 'select * from search.suggest where query="{query}"'
2728 * // You can now access the AutoCompleteList instance at Y.one('#my-input').ac
2731 * @class Plugin.AutoComplete
2732 * @extends AutoCompleteList
2735 var Plugin = Y.Plugin;
2737 function ACListPlugin(config) {
2738 config.inputNode = config.host;
2740 // Render by default.
2741 if (!config.render && config.render !== false) {
2742 config.render = true;
2745 ACListPlugin.superclass.constructor.apply(this, arguments);
2748 Y.extend(ACListPlugin, Y.AutoCompleteList, {}, {
2749 NAME : 'autocompleteListPlugin',
2751 CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
2754 Plugin.AutoComplete = ACListPlugin;
2755 Plugin.AutoCompleteList = ACListPlugin;
2758 }, '3.3.0' ,{requires:['autocomplete-list', 'node-pluginhost']});
2761 YUI.add('autocomplete', function(Y){}, '3.3.0' ,{use:['autocomplete-base', 'autocomplete-sources', 'autocomplete-list', 'autocomplete-plugin']});