]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - jssource/src_files/include/javascript/yui3/build/autocomplete/autocomplete-base.js
Release 6.5.0
[Github/sugarcrm.git] / jssource / src_files / include / javascript / yui3 / build / autocomplete / autocomplete-base.js
1 /*
2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
5 version: 3.3.0
6 build: 3167
7 */
8 YUI.add('autocomplete-base', function(Y) {
9
10 /**
11  * Provides automatic input completion or suggestions for text input fields and
12  * textareas.
13  *
14  * @module autocomplete
15  * @since 3.3.0
16  */
17
18 /**
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.
22  *
23  * @module autocomplete
24  * @submodule autocomplete-base
25  */
26
27 /**
28  * <p>
29  * Extension that provides core autocomplete logic (but no UI implementation)
30  * for a text input field or textarea.
31  * </p>
32  *
33  * <p>
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>.
38  * </p>
39  *
40  * <p>
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.
44  * </p>
45  *
46  * <p>
47  * <code>Y.Widget</code>-based example:
48  * </p>
49  *
50  * <pre>
51  * YUI().use('autocomplete-base', 'widget', function (Y) {
52  * &nbsp;&nbsp;var MyAC = Y.Base.create('myAC', Y.Widget, [Y.AutoCompleteBase], {
53  * &nbsp;&nbsp;&nbsp;&nbsp;// Custom prototype methods and properties.
54  * &nbsp;&nbsp;}, {
55  * &nbsp;&nbsp;&nbsp;&nbsp;// Custom static methods and properties.
56  * &nbsp;&nbsp;});
57  * &nbsp;
58  * &nbsp;&nbsp;// Custom implementation code.
59  * });
60  * </pre>
61  *
62  * <p>
63  * <code>Y.Base</code>-based example:
64  * </p>
65  *
66  * <pre>
67  * YUI().use('autocomplete-base', function (Y) {
68  * &nbsp;&nbsp;var MyAC = Y.Base.create('myAC', Y.Base, [Y.AutoCompleteBase], {
69  * &nbsp;&nbsp;&nbsp;&nbsp;initializer: function () {
70  * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this._bindUIACBase();
71  * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this._syncUIACBase();
72  * &nbsp;&nbsp;&nbsp;&nbsp;},
73  * &nbsp;
74  * &nbsp;&nbsp;&nbsp;&nbsp;// Custom prototype methods and properties.
75  * &nbsp;&nbsp;}, {
76  * &nbsp;&nbsp;&nbsp;&nbsp;// Custom static methods and properties.
77  * &nbsp;&nbsp;});
78  * &nbsp;
79  * &nbsp;&nbsp;// Custom implementation code.
80  * });
81  * </pre>
82  *
83  * @class AutoCompleteBase
84  */
85
86 var Escape  = Y.Escape,
87     Lang    = Y.Lang,
88     YArray  = Y.Array,
89     YObject = Y.Object,
90
91     isFunction = Lang.isFunction,
92     isString   = Lang.isString,
93     trim       = Lang.trim,
94
95     INVALID_VALUE = Y.Attribute.INVALID_VALUE,
96
97     _FUNCTION_VALIDATOR = '_functionValidator',
98     _SOURCE_SUCCESS     = '_sourceSuccess',
99
100     ALLOW_BROWSER_AC    = 'allowBrowserAutocomplete',
101     INPUT_NODE          = 'inputNode',
102     QUERY               = 'query',
103     QUERY_DELIMITER     = 'queryDelimiter',
104     REQUEST_TEMPLATE    = 'requestTemplate',
105     RESULTS             = 'results',
106     RESULT_LIST_LOCATOR = 'resultListLocator',
107     VALUE               = 'value',
108     VALUE_CHANGE        = 'valueChange',
109
110     EVT_CLEAR   = 'clear',
111     EVT_QUERY   = QUERY,
112     EVT_RESULTS = RESULTS;
113
114 function AutoCompleteBase() {
115     // AOP bindings.
116     Y.before(this._bindUIACBase, this, 'bindUI');
117     Y.before(this._destructorACBase, this, 'destructor');
118     Y.before(this._syncUIACBase, this, 'syncUI');
119
120     // -- Public Events --------------------------------------------------------
121
122     /**
123      * Fires after the query has been completely cleared or no longer meets the
124      * minimum query length requirement.
125      *
126      * @event clear
127      * @param {EventFacade} e Event facade with the following additional
128      *   properties:
129      *
130      * <dl>
131      *   <dt>prevVal (String)</dt>
132      *   <dd>
133      *     Value of the query before it was cleared.
134      *   </dd>
135      * </dl>
136      *
137      * @preventable _defClearFn
138      */
139     this.publish(EVT_CLEAR, {
140         defaultFn: this._defClearFn
141     });
142
143     /**
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.
146      *
147      * @event query
148      * @param {EventFacade} e Event facade with the following additional
149      *   properties:
150      *
151      * <dl>
152      *   <dt>inputValue (String)</dt>
153      *   <dd>
154      *     Full contents of the text input field or textarea that generated
155      *     the query.
156      *   </dd>
157      *
158      *   <dt>query (String)</dt>
159      *   <dd>
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>.
163      *   </dd>
164      * </dl>
165      *
166      * @preventable _defQueryFn
167      */
168     this.publish(EVT_QUERY, {
169         defaultFn: this._defQueryFn
170     });
171
172     /**
173      * Fires after query results are received from the <code>source</code>. If
174      * no source has been set, this event will not fire.
175      *
176      * @event results
177      * @param {EventFacade} e Event facade with the following additional
178      *   properties:
179      *
180      * <dl>
181      *   <dt>data (Array|Object)</dt>
182      *   <dd>
183      *     Raw, unfiltered result data (if available).
184      *   </dd>
185      *
186      *   <dt>query (String)</dt>
187      *   <dd>
188      *     Query that generated these results.
189      *   </dd>
190      *
191      *   <dt>results (Array)</dt>
192      *   <dd>
193      *     Array of filtered, formatted, and highlighted results. Each item in
194      *     the array is an object with the following properties:
195      *
196      *     <dl>
197      *       <dt>display (Node|HTMLElement|String)</dt>
198      *       <dd>
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.
202      *       </dd>
203      *
204      *       <dt>highlighted (String)</dt>
205      *       <dd>
206      *         Highlighted (but not formatted) result text. This property will
207      *         only be set if a highlighter is in use.
208      *       </dd>
209      *
210      *       <dt>raw (mixed)</dt>
211      *       <dd>
212      *         Raw, unformatted result in whatever form it was provided by the
213      *         <code>source</code>.
214      *       </dd>
215      *
216      *       <dt>text (String)</dt>
217      *       <dd>
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.
222      *       </dd>
223      *     </dl>
224      *   </dd>
225      * </dl>
226      *
227      * @preventable _defResultsFn
228      */
229     this.publish(EVT_RESULTS, {
230         defaultFn: this._defResultsFn
231     });
232 }
233
234 // -- Public Static Properties -------------------------------------------------
235 AutoCompleteBase.ATTRS = {
236     /**
237      * Whether or not to enable the browser's built-in autocomplete
238      * functionality for input fields.
239      *
240      * @attribute allowBrowserAutocomplete
241      * @type Boolean
242      * @default false
243      */
244     allowBrowserAutocomplete: {
245         value: false
246     },
247
248     /**
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
252      * delimiters.
253      *
254      * @attribute allowTrailingDelimiter
255      * @type Boolean
256      * @default false
257      */
258     allowTrailingDelimiter: {
259         value: false
260     },
261
262     /**
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.
265      *
266      * @attribute inputNode
267      * @type Node|HTMLElement|String
268      * @writeonce
269      */
270     inputNode: {
271         setter: Y.one,
272         writeOnce: 'initOnly'
273     },
274
275     /**
276      * Maximum number of results to return. A value of <code>0</code> or less
277      * will allow an unlimited number of results.
278      *
279      * @attribute maxResults
280      * @type Number
281      * @default 0
282      */
283     maxResults: {
284         value: 0
285     },
286
287     /**
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.
292      *
293      * @attribute minQueryLength
294      * @type Number
295      * @default 1
296      */
297     minQueryLength: {
298         value: 1
299     },
300
301     /**
302      * <p>
303      * Current query, or <code>null</code> if there is no current query.
304      * </p>
305      *
306      * <p>
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.
312      * </p>
313      *
314      * @attribute query
315      * @type String|null
316      * @default null
317      * @readonly
318      */
319     query: {
320         readOnly: true,
321         value: null
322     },
323
324     /**
325      * <p>
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
329      * begin.
330      * </p>
331      *
332      * <p>
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.
336      * </p>
337      *
338      * @attribute queryDelay
339      * @type Number
340      * @default 100
341      */
342     queryDelay: {
343         value: 100
344     },
345
346     /**
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
350      * modified.
351      *
352      * @attribute queryDelimiter
353      * @type String|null
354      * @default null
355      */
356     queryDelimiter: {
357         value: null
358     },
359
360     /**
361      * <p>
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
368      * URLs).
369      * </p>
370      *
371      * <p>
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.
375      * </p>
376      *
377      * @attribute requestTemplate
378      * @type Function|String|null
379      * @default null
380      */
381     requestTemplate: {
382         setter: '_setRequestTemplate',
383         value: null
384     },
385
386     /**
387      * <p>
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
392      * result object.
393      * </p>
394      *
395      * <p>
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.
401      * </p>
402      *
403      * <p>
404      * If no <code>source</code> is set, result filters will not be called.
405      * </p>
406      *
407      * <p>
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).
412      * </p>
413      *
414      * @attribute resultFilters
415      * @type Array
416      * @default []
417      */
418     resultFilters: {
419         setter: '_setResultFilters',
420         value: []
421     },
422
423     /**
424      * <p>
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.
430      * </p>
431      *
432      * <p>
433      * See the documentation for the <code>results</code> event for a list of
434      * the properties available on each result object.
435      * </p>
436      *
437      * <p>
438      * If no <code>source</code> is set, the formatter will not be called.
439      * </p>
440      *
441      * @attribute resultFormatter
442      * @type Function|null
443      */
444     resultFormatter: {
445         validator: _FUNCTION_VALIDATOR
446     },
447
448     /**
449      * <p>
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.
455      * </p>
456      *
457      * <p>
458      * See the documentation for the <code>results</code> event for a list of
459      * the properties available on each result object.
460      * </p>
461      *
462      * <p>
463      * If no <code>source</code> is set, the highlighter will not be called.
464      * </p>
465      *
466      * @attribute resultHighlighter
467      * @type Function|null
468      */
469     resultHighlighter: {
470         setter: '_setResultHighlighter'
471     },
472
473     /**
474      * <p>
475      * Locator that should be used to extract an array of results from a
476      * non-array response.
477      * </p>
478      *
479      * <p>
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
482      * define a locator.
483      * </p>
484      *
485      * <p>
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).
490      * </p>
491      *
492      * <p>
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.
496      * </p>
497      *
498      * @attribute resultListLocator
499      * @type Function|String|null
500      */
501     resultListLocator: {
502         setter: '_setLocator'
503     },
504
505     /**
506      * Current results, or an empty array if there are no results.
507      *
508      * @attribute results
509      * @type Array
510      * @default []
511      * @readonly
512      */
513     results: {
514         readOnly: true,
515         value: []
516     },
517
518     /**
519      * <p>
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.
524      * </p>
525      *
526      * <p>
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.
530      * </p>
531      *
532      * <p>
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).
537      * </p>
538      *
539      * <p>
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.
543      * </p>
544      *
545      * @attribute resultTextLocator
546      * @type Function|String|null
547      */
548     resultTextLocator: {
549         setter: '_setLocator'
550     },
551
552     /**
553      * <p>
554      * Source for autocomplete results. The following source types are
555      * supported:
556      * </p>
557      *
558      * <dl>
559      *   <dt>Array</dt>
560      *   <dd>
561      *     <p>
562      *     <i>Example:</i> <code>['first result', 'second result', 'etc']</code>
563      *     </p>
564      *
565      *     <p>
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
568      *     implementation.
569      *     </p>
570      *   </dd>
571      *
572      *   <dt>DataSource</dt>
573      *   <dd>
574      *     <p>
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.
578      *     </p>
579      *   </dd>
580      *
581      *   <dt>Function</dt>
582      *   <dd>
583      *     <p>
584      *     <i>Example:</i> <code>function (query) { return ['foo', 'bar']; }</code>
585      *     </p>
586      *
587      *     <p>
588      *     A function source will be called with the current query as a
589      *     parameter, and should return an array of results.
590      *     </p>
591      *   </dd>
592      *
593      *   <dt>Object</dt>
594      *   <dd>
595      *     <p>
596      *     <i>Example:</i> <code>{foo: ['foo result 1', 'foo result 2'], bar: ['bar result']}</code>
597      *     </p>
598      *
599      *     <p>
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.
603      *     </p>
604      *
605      *     <p>
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.
609      *     </p>
610      *   </dd>
611      * </dl>
612      *
613      * <p>
614      * If the optional <code>autocomplete-sources</code> module is loaded, then
615      * the following additional source types will be supported as well:
616      * </p>
617      *
618      * <dl>
619      *   <dt>String (JSONP URL)</dt>
620      *   <dd>
621      *     <p>
622      *     <i>Example:</i> <code>'http://example.com/search?q={query}&callback={callback}'</code>
623      *     </p>
624      *
625      *     <p>
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).
635      *     </p>
636      *
637      *     <p>
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.
641      *     </p>
642      *
643      *     <p>
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.
647      *     </p>
648      *   </dd>
649      *
650      *   <dt>String (XHR URL)</dt>
651      *   <dd>
652      *     <p>
653      *     <i>Example:</i> <code>'http://example.com/search?q={query}'</code>
654      *     </p>
655      *
656      *     <p>
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).
663      *     </p>
664      *
665      *     <p>
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.
671      *     </p>
672      *
673      *     <p>
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
677      *     if possible.
678      *     </p>
679      *   </dd>
680      *
681      *   <dt>String (YQL query)</dt>
682      *   <dd>
683      *     <p>
684      *     <i>Example:</i> <code>'select * from search.suggest where query="{query}"'</code>
685      *     </p>
686      *
687      *     <p>
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).
695      *     </p>
696      *
697      *     <p>
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.
701      *     </p>
702      *   </dd>
703      * </dl>
704      *
705      * <p>
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.
709      * </p>
710      *
711      * @attribute source
712      * @type Array|DataSource|Function|Object|String|null
713      */
714     source: {
715         setter: '_setSource'
716     },
717
718     /**
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.
722      *
723      * @attribute tokenInput
724      * @type Plugin.TokenInput
725      * @readonly
726      */
727     tokenInput: {
728         readOnly: true
729     },
730
731     /**
732      * Current value of the input node.
733      *
734      * @attribute value
735      * @type String
736      * @default ''
737      */
738     value: {
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
742         // the value.
743         value: ''
744     }
745 };
746
747 AutoCompleteBase.CSS_PREFIX = 'ac';
748 AutoCompleteBase.UI_SRC = (Y.Widget && Y.Widget.UI_SRC) || 'ui';
749
750 AutoCompleteBase.prototype = {
751     // -- Public Prototype Methods ---------------------------------------------
752
753     /**
754      * <p>
755      * Sends a request to the configured source. If no source is configured,
756      * this method won't do anything.
757      * </p>
758      *
759      * <p>
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.
764      * </p>
765      *
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
770      *   be used.
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.
774      * @chainable
775      */
776     sendRequest: function (query, requestTemplate) {
777         var request,
778             source = this.get('source');
779
780         if (query || query === '') {
781             this._set(QUERY, query);
782         } else {
783             query = this.get(QUERY);
784         }
785
786         if (source) {
787             if (!requestTemplate) {
788                 requestTemplate = this.get(REQUEST_TEMPLATE);
789             }
790
791             request = requestTemplate ? requestTemplate(query) : query;
792
793
794             source.sendRequest({
795                 request: request,
796                 callback: {
797                     success: Y.bind(this._onResponse, this, query)
798                 }
799             });
800         }
801
802         return this;
803     },
804
805     // -- Protected Lifecycle Methods ------------------------------------------
806
807     /**
808      * Attaches event listeners and behaviors.
809      *
810      * @method _bindUIACBase
811      * @protected
812      */
813     _bindUIACBase: function () {
814         var inputNode  = this.get(INPUT_NODE),
815             tokenInput = inputNode && inputNode.tokenInput;
816
817         // If the inputNode has a node-tokeninput plugin attached, bind to the
818         // plugin's inputNode instead.
819         if (tokenInput) {
820             inputNode = tokenInput.get(INPUT_NODE);
821             this._set('tokenInput', tokenInput);
822         }
823
824         if (!inputNode) {
825             Y.error('No inputNode specified.');
826             return;
827         }
828
829         this._inputNode = inputNode;
830
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),
835
836             inputNode.on('blur', this._onInputBlur, this),
837
838             this.after(ALLOW_BROWSER_AC + 'Change', this._syncBrowserAutocomplete),
839             this.after(VALUE_CHANGE, this._afterValueChange)
840         ];
841     },
842
843     /**
844      * Detaches AutoCompleteBase event listeners.
845      *
846      * @method _destructorACBase
847      * @protected
848      */
849     _destructorACBase: function () {
850         var events = this._acBaseEvents;
851
852         while (events && events.length) {
853             events.pop().detach();
854         }
855     },
856
857     /**
858      * Synchronizes the UI state of the <code>inputNode</code>.
859      *
860      * @method _syncUIACBase
861      * @protected
862      */
863     _syncUIACBase: function () {
864         this._syncBrowserAutocomplete();
865         this.set(VALUE, this.get(INPUT_NODE).get(VALUE));
866     },
867
868     // -- Protected Prototype Methods ------------------------------------------
869
870     /**
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.
873      *
874      * @method _createArraySource
875      * @param {Array} source
876      * @return {Object} DataSource-like object.
877      * @protected
878      */
879     _createArraySource: function (source) {
880         var that = this;
881
882         return {sendRequest: function (request) {
883             that[_SOURCE_SUCCESS](source.concat(), request);
884         }};
885     },
886
887     /**
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.
891      *
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.
896      * @protected
897      */
898     _createFunctionSource: function (source) {
899         var that = this;
900
901         return {sendRequest: function (request) {
902             that[_SOURCE_SUCCESS](source(request.request) || [], request);
903         }};
904     },
905
906     /**
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.
910      *
911      * @method _createObjectSource
912      * @param {Object} source
913      * @return {Object} DataSource-like object.
914      * @protected
915      */
916     _createObjectSource: function (source) {
917         var that = this;
918
919         return {sendRequest: function (request) {
920             var query = request.request;
921
922             that[_SOURCE_SUCCESS](
923                 YObject.owns(source, query) ? source[query] : [],
924                 request
925             );
926         }};
927     },
928
929     /**
930      * Returns <code>true</code> if <i>value</i> is either a function or
931      * <code>null</code>.
932      *
933      * @method _functionValidator
934      * @param {Function|null} value Value to validate.
935      * @protected
936      */
937     _functionValidator: function (value) {
938         return value === null || isFunction(value);
939     },
940
941     /**
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.
946      *
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.
952      * @protected
953      */
954     _getObjectValue: function (obj, path) {
955         if (!obj) {
956             return;
957         }
958
959         for (var i = 0, len = path.length; obj && i < len; i++) {
960             obj = obj[path[i]];
961         }
962
963         return obj;
964     },
965
966     /**
967      * Parses result responses, performs filtering and highlighting, and fires
968      * the <code>results</code> event.
969      *
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.
974      * @protected
975      */
976     _parseResponse: function (query, response, data) {
977         var facade = {
978                 data   : data,
979                 query  : query,
980                 results: []
981             },
982
983             listLocator = this.get(RESULT_LIST_LOCATOR),
984             results     = [],
985             unfiltered  = response && response.results,
986
987             filters,
988             formatted,
989             formatter,
990             highlighted,
991             highlighter,
992             i,
993             len,
994             maxResults,
995             result,
996             text,
997             textLocator;
998
999         if (unfiltered && listLocator) {
1000             unfiltered = listLocator(unfiltered);
1001         }
1002
1003         if (unfiltered && unfiltered.length) {
1004             filters     = this.get('resultFilters');
1005             textLocator = this.get('resultTextLocator');
1006
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
1010             // as we go.
1011             for (i = 0, len = unfiltered.length; i < len; ++i) {
1012                 result = unfiltered[i];
1013                 text   = textLocator ? textLocator(result) : result.toString();
1014
1015                 results.push({
1016                     display: Escape.html(text),
1017                     raw    : result,
1018                     text   : text
1019                 });
1020             }
1021
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());
1027
1028                 if (!results) {
1029                     return;
1030                 }
1031
1032                 if (!results.length) {
1033                     break;
1034                 }
1035             }
1036
1037             if (results.length) {
1038                 formatter   = this.get('resultFormatter');
1039                 highlighter = this.get('resultHighlighter');
1040                 maxResults  = this.get('maxResults');
1041
1042                 // If maxResults is set and greater than 0, limit the number of
1043                 // results.
1044                 if (maxResults && maxResults > 0 &&
1045                         results.length > maxResults) {
1046                     results.length = maxResults;
1047                 }
1048
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.
1053                 if (highlighter) {
1054                     highlighted = highlighter(query, results.concat());
1055
1056                     if (!highlighted) {
1057                         return;
1058                     }
1059
1060                     for (i = 0, len = highlighted.length; i < len; ++i) {
1061                         result = results[i];
1062                         result.highlighted = highlighted[i];
1063                         result.display     = result.highlighted;
1064                     }
1065                 }
1066
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
1071                 // result object.
1072                 if (formatter) {
1073                     formatted = formatter(query, results.concat());
1074
1075                     if (!formatted) {
1076                         return;
1077                     }
1078
1079                     for (i = 0, len = formatted.length; i < len; ++i) {
1080                         results[i].display = formatted[i];
1081                     }
1082                 }
1083             }
1084         }
1085
1086         facade.results = results;
1087         this.fire(EVT_RESULTS, facade);
1088     },
1089
1090     /**
1091      * <p>
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.
1094      * </p>
1095      *
1096      * <p>
1097      * If a query delimiter is defined, the query will be the last delimited
1098      * part of of the string.
1099      * </p>
1100      *
1101      * @method _parseValue
1102      * @param {String} value Input value from which to extract the query.
1103      * @return {String|null} query
1104      * @protected
1105      */
1106     _parseValue: function (value) {
1107         var delim = this.get(QUERY_DELIMITER);
1108
1109         if (delim) {
1110             value = value.split(delim);
1111             value = value[value.length - 1];
1112         }
1113
1114         return Lang.trimLeft(value);
1115     },
1116
1117     /**
1118      * Setter for locator attributes.
1119      *
1120      * @method _setLocator
1121      * @param {Function|String|null} locator
1122      * @return {Function|null}
1123      * @protected
1124      */
1125     _setLocator: function (locator) {
1126         if (this[_FUNCTION_VALIDATOR](locator)) {
1127             return locator;
1128         }
1129
1130         var that = this;
1131
1132         locator = locator.toString().split('.');
1133
1134         return function (result) {
1135             return result && that._getObjectValue(result, locator);
1136         };
1137     },
1138
1139     /**
1140      * Setter for the <code>requestTemplate</code> attribute.
1141      *
1142      * @method _setRequestTemplate
1143      * @param {Function|String|null} template
1144      * @return {Function|null}
1145      * @protected
1146      */
1147     _setRequestTemplate: function (template) {
1148         if (this[_FUNCTION_VALIDATOR](template)) {
1149             return template;
1150         }
1151
1152         template = template.toString();
1153
1154         return function (query) {
1155             return Lang.sub(template, {query: encodeURIComponent(query)});
1156         };
1157     },
1158
1159     /**
1160      * Setter for the <code>resultFilters</code> attribute.
1161      *
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>).
1169      * @protected
1170      */
1171     _setResultFilters: function (filters) {
1172         var acFilters, getFilterFunction;
1173
1174         if (filters === null) {
1175             return [];
1176         }
1177
1178         acFilters = Y.AutoCompleteFilters;
1179
1180         getFilterFunction = function (filter) {
1181             if (isFunction(filter)) {
1182                 return filter;
1183             }
1184
1185             if (isString(filter) && acFilters &&
1186                     isFunction(acFilters[filter])) {
1187                 return acFilters[filter];
1188             }
1189
1190             return false;
1191         };
1192
1193         if (Lang.isArray(filters)) {
1194             filters = YArray.map(filters, getFilterFunction);
1195             return YArray.every(filters, function (f) { return !!f; }) ?
1196                     filters : INVALID_VALUE;
1197         } else {
1198             filters = getFilterFunction(filters);
1199             return filters ? [filters] : INVALID_VALUE;
1200         }
1201     },
1202
1203     /**
1204      * Setter for the <code>resultHighlighter</code> attribute.
1205      *
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}
1211      * @protected
1212      */
1213     _setResultHighlighter: function (highlighter) {
1214         var acHighlighters;
1215
1216         if (this._functionValidator(highlighter)) {
1217             return highlighter;
1218         }
1219
1220         acHighlighters = Y.AutoCompleteHighlighters;
1221
1222         if (isString(highlighter) && acHighlighters &&
1223                 isFunction(acHighlighters[highlighter])) {
1224             return acHighlighters[highlighter];
1225         }
1226
1227         return INVALID_VALUE;
1228     },
1229
1230     /**
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>.
1233      *
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}
1238      * @protected
1239      */
1240     _setSource: function (source) {
1241         var sourcesNotLoaded = 'autocomplete-sources module not loaded';
1242
1243         if ((source && isFunction(source.sendRequest)) || source === null) {
1244             // Quacks like a DataSource instance (or null). Make it so!
1245             return source;
1246         }
1247
1248         switch (Lang.type(source)) {
1249         case 'string':
1250             if (this._createStringSource) {
1251                 return this._createStringSource(source);
1252             }
1253
1254             Y.error(sourcesNotLoaded);
1255             return INVALID_VALUE;
1256
1257         case 'array':
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);
1261
1262         case 'function':
1263             return this._createFunctionSource(source);
1264
1265         case 'object':
1266             // If the object is a JSONPRequest instance, use it as a JSONP
1267             // source.
1268             if (Y.JSONPRequest && source instanceof Y.JSONPRequest) {
1269                 if (this._createJSONPSource) {
1270                     return this._createJSONPSource(source);
1271                 }
1272
1273                 Y.error(sourcesNotLoaded);
1274                 return INVALID_VALUE;
1275             }
1276
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);
1281         }
1282
1283         return INVALID_VALUE;
1284     },
1285
1286     /**
1287      * Shared success callback for non-DataSource sources.
1288      *
1289      * @method _sourceSuccess
1290      * @param {mixed} data Response data.
1291      * @param {Object} request Request object.
1292      * @protected
1293      */
1294     _sourceSuccess: function (data, request) {
1295         request.callback.success({
1296             data: data,
1297             response: {results: data}
1298         });
1299     },
1300
1301     /**
1302      * Synchronizes the UI state of the <code>allowBrowserAutocomplete</code>
1303      * attribute.
1304      *
1305      * @method _syncBrowserAutocomplete
1306      * @protected
1307      */
1308     _syncBrowserAutocomplete: function () {
1309         var inputNode = this.get(INPUT_NODE);
1310
1311         if (inputNode.get('nodeName').toLowerCase() === 'input') {
1312             inputNode.setAttribute('autocomplete',
1313                     this.get(ALLOW_BROWSER_AC) ? 'on' : 'off');
1314         }
1315     },
1316
1317     /**
1318      * <p>
1319      * Updates the query portion of the <code>value</code> attribute.
1320      * </p>
1321      *
1322      * <p>
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>.
1325      * </p>
1326      *
1327      * @method _updateValue
1328      * @param {String} newVal New value.
1329      * @protected
1330      */
1331     _updateValue: function (newVal) {
1332         var delim = this.get(QUERY_DELIMITER),
1333             insertDelim,
1334             len,
1335             prevVal;
1336
1337         newVal = Lang.trimLeft(newVal);
1338
1339         if (delim) {
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;
1343
1344             if (len > 1) {
1345                 prevVal[len - 1] = newVal;
1346                 newVal = prevVal.join(insertDelim + ' ');
1347             }
1348
1349             newVal = newVal + insertDelim + ' ';
1350         }
1351
1352         this.set(VALUE, newVal);
1353     },
1354
1355     // -- Protected Event Handlers ---------------------------------------------
1356
1357     /**
1358      * Handles change events for the <code>value</code> attribute.
1359      *
1360      * @method _afterValueChange
1361      * @param {EventFacade} e
1362      * @protected
1363      */
1364     _afterValueChange: function (e) {
1365         var delay,
1366             fire,
1367             minQueryLength,
1368             newVal = e.newVal,
1369             query,
1370             that;
1371
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);
1375             return;
1376         }
1377
1378
1379         minQueryLength = this.get('minQueryLength');
1380         query          = this._parseValue(newVal) || '';
1381
1382         if (minQueryLength >= 0 && query.length >= minQueryLength) {
1383             delay = this.get('queryDelay');
1384             that  = this;
1385
1386             fire = function () {
1387                 that.fire(EVT_QUERY, {
1388                     inputValue: newVal,
1389                     query     : query
1390                 });
1391             };
1392
1393             if (delay) {
1394                 clearTimeout(this._delay);
1395                 this._delay = setTimeout(fire, delay);
1396             } else {
1397                 fire();
1398             }
1399         } else {
1400             clearTimeout(this._delay);
1401
1402             this.fire(EVT_CLEAR, {
1403                 prevVal: e.prevVal ? this._parseValue(e.prevVal) : null
1404             });
1405         }
1406     },
1407
1408     /**
1409      * Handles <code>blur</code> events on the input node.
1410      *
1411      * @method _onInputBlur
1412      * @param {EventFacade} e
1413      * @protected
1414      */
1415     _onInputBlur: function (e) {
1416         var delim = this.get(QUERY_DELIMITER),
1417             delimPos,
1418             newVal,
1419             value;
1420
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);
1426
1427             if (delim) {
1428                 while ((newVal = Lang.trimRight(newVal)) &&
1429                         (delimPos = newVal.length - delim.length) &&
1430                         newVal.lastIndexOf(delim) === delimPos) {
1431
1432                     newVal = newVal.substring(0, delimPos);
1433                 }
1434             } else {
1435                 // Delimiter is one or more space characters, so just trim the
1436                 // value.
1437                 newVal = Lang.trimRight(newVal);
1438             }
1439
1440             if (newVal !== value) {
1441                 this.set(VALUE, newVal);
1442             }
1443         }
1444     },
1445
1446     /**
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
1449      * criteria.
1450      *
1451      * @method _onInputValueChange
1452      * @param {EventFacade} e
1453      * @protected
1454      */
1455     _onInputValueChange: function (e) {
1456         var newVal = e.newVal;
1457
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)) {
1461             return;
1462         }
1463
1464         this.set(VALUE, newVal, {src: AutoCompleteBase.UI_SRC});
1465     },
1466
1467     /**
1468      * Handles source responses and fires the <code>results</code> event.
1469      *
1470      * @method _onResponse
1471      * @param {EventFacade} e
1472      * @protected
1473      */
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);
1478         }
1479     },
1480
1481     // -- Protected Default Event Handlers -------------------------------------
1482
1483     /**
1484      * Default <code>clear</code> event handler. Sets the <code>results</code>
1485      * property to an empty array and <code>query</code> to null.
1486      *
1487      * @method _defClearFn
1488      * @protected
1489      */
1490     _defClearFn: function () {
1491         this._set(QUERY, null);
1492         this._set(RESULTS, []);
1493     },
1494
1495     /**
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.
1498      *
1499      * @method _defQueryFn
1500      * @param {EventFacade} e
1501      * @protected
1502      */
1503     _defQueryFn: function (e) {
1504         var query = e.query;
1505
1506         this.sendRequest(query); // sendRequest will set the 'query' attribute
1507     },
1508
1509     /**
1510      * Default <code>results</code> event handler. Sets the <code>results</code>
1511      * property to the latest results.
1512      *
1513      * @method _defResultsFn
1514      * @param {EventFacade} e
1515      * @protected
1516      */
1517     _defResultsFn: function (e) {
1518         this._set(RESULTS, e[RESULTS]);
1519     }
1520 };
1521
1522 Y.AutoCompleteBase = AutoCompleteBase;
1523
1524
1525 }, '3.3.0' ,{optional:['autocomplete-sources'], requires:['array-extras', 'base-build', 'escape', 'event-valuechange', 'node-base']});