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;
793
Y.log('sendRequest: ' + request, 'info', 'autocomplete-base');
798
success: Y.bind(this._onResponse, this, query)
806
// -- Protected Lifecycle Methods ------------------------------------------
809
* Attaches event listeners and behaviors.
811
* @method _bindUIACBase
814
_bindUIACBase: function () {
815
var inputNode = this.get(INPUT_NODE),
816
tokenInput = inputNode && inputNode.tokenInput;
818
// If the inputNode has a node-tokeninput plugin attached, bind to the
819
// plugin's inputNode instead.
821
inputNode = tokenInput.get(INPUT_NODE);
822
this._set('tokenInput', tokenInput);
826
Y.error('No inputNode specified.');
830
this._inputNode = inputNode;
832
this._acBaseEvents = [
833
// This is the valueChange event on the inputNode, provided by the
834
// event-valuechange module, not our own valueChange.
835
inputNode.on(VALUE_CHANGE, this._onInputValueChange, this),
837
inputNode.on('blur', this._onInputBlur, this),
839
this.after(ALLOW_BROWSER_AC + 'Change', this._syncBrowserAutocomplete),
840
this.after(VALUE_CHANGE, this._afterValueChange)
845
* Detaches AutoCompleteBase event listeners.
847
* @method _destructorACBase
850
_destructorACBase: function () {
851
var events = this._acBaseEvents;
853
while (events && events.length) {
854
events.pop().detach();
859
* Synchronizes the UI state of the <code>inputNode</code>.
861
* @method _syncUIACBase
864
_syncUIACBase: function () {
865
this._syncBrowserAutocomplete();
866
this.set(VALUE, this.get(INPUT_NODE).get(VALUE));
869
// -- Protected Prototype Methods ------------------------------------------
872
* Creates a DataSource-like object that simply returns the specified array
873
* as a response. See the <code>source</code> attribute for more details.
875
* @method _createArraySource
876
* @param {Array} source
877
* @return {Object} DataSource-like object.
880
_createArraySource: function (source) {
883
return {sendRequest: function (request) {
884
that[_SOURCE_SUCCESS](source.concat(), request);
889
* Creates a DataSource-like object that passes the query to a
890
* custom-defined function, which is expected to return an array as a
891
* response. See the <code>source</code> attribute for more details.
893
* @method _createFunctionSource
894
* @param {Function} source Function that accepts a query parameter and
895
* returns an array of results.
896
* @return {Object} DataSource-like object.
899
_createFunctionSource: function (source) {
902
return {sendRequest: function (request) {
903
that[_SOURCE_SUCCESS](source(request.request) || [], request);
908
* Creates a DataSource-like object that looks up queries as properties on
909
* the specified object, and returns the found value (if any) as a response.
910
* See the <code>source</code> attribute for more details.
912
* @method _createObjectSource
913
* @param {Object} source
914
* @return {Object} DataSource-like object.
917
_createObjectSource: function (source) {
920
return {sendRequest: function (request) {
921
var query = request.request;
923
that[_SOURCE_SUCCESS](
924
YObject.owns(source, query) ? source[query] : [],
931
* Returns <code>true</code> if <i>value</i> is either a function or
934
* @method _functionValidator
935
* @param {Function|null} value Value to validate.
938
_functionValidator: function (value) {
939
return value === null || isFunction(value);
943
* Faster and safer alternative to Y.Object.getValue(). Doesn't bother
944
* casting the path to an array (since we already know it's an array) and
945
* doesn't throw an error if a value in the middle of the object hierarchy
946
* is neither <code>undefined</code> nor an object.
948
* @method _getObjectValue
949
* @param {Object} obj
950
* @param {Array} path
951
* @return {mixed} Located value, or <code>undefined</code> if the value was
952
* not found at the specified path.
955
_getObjectValue: function (obj, path) {
960
for (var i = 0, len = path.length; obj && i < len; i++) {
968
* Parses result responses, performs filtering and highlighting, and fires
969
* the <code>results</code> event.
971
* @method _parseResponse
972
* @param {String} query Query that generated these results.
973
* @param {Object} response Response containing results.
974
* @param {Object} data Raw response data.
977
_parseResponse: function (query, response, data) {
984
listLocator = this.get(RESULT_LIST_LOCATOR),
986
unfiltered = response && response.results,
1000
if (unfiltered && listLocator) {
1001
unfiltered = listLocator(unfiltered);
1004
if (unfiltered && unfiltered.length) {
1005
filters = this.get('resultFilters');
1006
textLocator = this.get('resultTextLocator');
1008
// Create a lightweight result object for each result to make them
1009
// easier to work with. The various properties on the object
1010
// represent different formats of the result, and will be populated
1012
for (i = 0, len = unfiltered.length; i < len; ++i) {
1013
result = unfiltered[i];
1014
text = textLocator ? textLocator(result) : result.toString();
1017
display: Escape.html(text),
1023
// Run the results through all configured result filters. Each
1024
// filter returns an array of (potentially fewer) result objects,
1025
// which is then passed to the next filter, and so on.
1026
for (i = 0, len = filters.length; i < len; ++i) {
1027
results = filters[i](query, results.concat());
1030
Y.log("Filter didn't return anything.", 'warn', 'autocomplete-base');
1034
if (!results.length) {
1039
if (results.length) {
1040
formatter = this.get('resultFormatter');
1041
highlighter = this.get('resultHighlighter');
1042
maxResults = this.get('maxResults');
1044
// If maxResults is set and greater than 0, limit the number of
1046
if (maxResults && maxResults > 0 &&
1047
results.length > maxResults) {
1048
results.length = maxResults;
1051
// Run the results through the configured highlighter (if any).
1052
// The highlighter returns an array of highlighted strings (not
1053
// an array of result objects), and these strings are then added
1054
// to each result object.
1056
highlighted = highlighter(query, results.concat());
1059
Y.log("Highlighter didn't return anything.", 'warn', 'autocomplete-base');
1063
for (i = 0, len = highlighted.length; i < len; ++i) {
1064
result = results[i];
1065
result.highlighted = highlighted[i];
1066
result.display = result.highlighted;
1070
// Run the results through the configured formatter (if any) to
1071
// produce the final formatted results. The formatter returns an
1072
// array of strings or Node instances (not an array of result
1073
// objects), and these strings/Nodes are then added to each
1076
formatted = formatter(query, results.concat());
1079
Y.log("Formatter didn't return anything.", 'warn', 'autocomplete-base');
1083
for (i = 0, len = formatted.length; i < len; ++i) {
1084
results[i].display = formatted[i];
1090
facade.results = results;
1091
this.fire(EVT_RESULTS, facade);
1096
* Returns the query portion of the specified input value, or
1097
* <code>null</code> if there is no suitable query within the input value.
1101
* If a query delimiter is defined, the query will be the last delimited
1102
* part of of the string.
1105
* @method _parseValue
1106
* @param {String} value Input value from which to extract the query.
1107
* @return {String|null} query
1110
_parseValue: function (value) {
1111
var delim = this.get(QUERY_DELIMITER);
1114
value = value.split(delim);
1115
value = value[value.length - 1];
1118
return Lang.trimLeft(value);
1122
* Setter for locator attributes.
1124
* @method _setLocator
1125
* @param {Function|String|null} locator
1126
* @return {Function|null}
1129
_setLocator: function (locator) {
1130
if (this[_FUNCTION_VALIDATOR](locator)) {
1136
locator = locator.toString().split('.');
1138
return function (result) {
1139
return result && that._getObjectValue(result, locator);
1144
* Setter for the <code>requestTemplate</code> attribute.
1146
* @method _setRequestTemplate
1147
* @param {Function|String|null} template
1148
* @return {Function|null}
1151
_setRequestTemplate: function (template) {
1152
if (this[_FUNCTION_VALIDATOR](template)) {
1156
template = template.toString();
1158
return function (query) {
1159
return Lang.sub(template, {query: encodeURIComponent(query)});
1164
* Setter for the <code>resultFilters</code> attribute.
1166
* @method _setResultFilters
1167
* @param {Array|Function|String|null} filters <code>null</code>, a filter
1168
* function, an array of filter functions, or a string or array of strings
1169
* representing the names of methods on
1170
* <code>Y.AutoCompleteFilters</code>.
1171
* @return {Array} Array of filter functions (empty if <i>filters</i> is
1172
* <code>null</code>).
1175
_setResultFilters: function (filters) {
1176
var acFilters, getFilterFunction;
1178
if (filters === null) {
1182
acFilters = Y.AutoCompleteFilters;
1184
getFilterFunction = function (filter) {
1185
if (isFunction(filter)) {
1189
if (isString(filter) && acFilters &&
1190
isFunction(acFilters[filter])) {
1191
return acFilters[filter];
1197
if (Lang.isArray(filters)) {
1198
filters = YArray.map(filters, getFilterFunction);
1199
return YArray.every(filters, function (f) { return !!f; }) ?
1200
filters : INVALID_VALUE;
1202
filters = getFilterFunction(filters);
1203
return filters ? [filters] : INVALID_VALUE;
1208
* Setter for the <code>resultHighlighter</code> attribute.
1210
* @method _setResultHighlighter
1211
* @param {Function|String|null} highlighter <code>null</code>, a
1212
* highlighter function, or a string representing the name of a method on
1213
* <code>Y.AutoCompleteHighlighters</code>.
1214
* @return {Function|null}
1217
_setResultHighlighter: function (highlighter) {
1220
if (this._functionValidator(highlighter)) {
1224
acHighlighters = Y.AutoCompleteHighlighters;
1226
if (isString(highlighter) && acHighlighters &&
1227
isFunction(acHighlighters[highlighter])) {
1228
return acHighlighters[highlighter];
1231
return INVALID_VALUE;
1235
* Setter for the <code>source</code> attribute. Returns a DataSource or
1236
* a DataSource-like object depending on the type of <i>source</i>.
1238
* @method _setSource
1239
* @param {Array|DataSource|Object|String} source AutoComplete source. See
1240
* the <code>source</code> attribute for details.
1241
* @return {DataSource|Object}
1244
_setSource: function (source) {
1245
var sourcesNotLoaded = 'autocomplete-sources module not loaded';
1247
if ((source && isFunction(source.sendRequest)) || source === null) {
1248
// Quacks like a DataSource instance (or null). Make it so!
1252
switch (Lang.type(source)) {
1254
if (this._createStringSource) {
1255
return this._createStringSource(source);
1258
Y.error(sourcesNotLoaded);
1259
return INVALID_VALUE;
1262
// Wrap the array in a teensy tiny fake DataSource that just returns
1263
// the array itself for each request. Filters will do the rest.
1264
return this._createArraySource(source);
1267
return this._createFunctionSource(source);
1270
// If the object is a JSONPRequest instance, use it as a JSONP
1272
if (Y.JSONPRequest && source instanceof Y.JSONPRequest) {
1273
if (this._createJSONPSource) {
1274
return this._createJSONPSource(source);
1277
Y.error(sourcesNotLoaded);
1278
return INVALID_VALUE;
1281
// Not a JSONPRequest instance. Wrap the object in a teensy tiny
1282
// fake DataSource that looks for the request as a property on the
1283
// object and returns it if it exists, or an empty array otherwise.
1284
return this._createObjectSource(source);
1287
return INVALID_VALUE;
1291
* Shared success callback for non-DataSource sources.
1293
* @method _sourceSuccess
1294
* @param {mixed} data Response data.
1295
* @param {Object} request Request object.
1298
_sourceSuccess: function (data, request) {
1299
request.callback.success({
1301
response: {results: data}
1306
* Synchronizes the UI state of the <code>allowBrowserAutocomplete</code>
1309
* @method _syncBrowserAutocomplete
1312
_syncBrowserAutocomplete: function () {
1313
var inputNode = this.get(INPUT_NODE);
1315
if (inputNode.get('nodeName').toLowerCase() === 'input') {
1316
inputNode.setAttribute('autocomplete',
1317
this.get(ALLOW_BROWSER_AC) ? 'on' : 'off');
1323
* Updates the query portion of the <code>value</code> attribute.
1327
* If a query delimiter is defined, the last delimited portion of the input
1328
* value will be replaced with the specified <i>value</i>.
1331
* @method _updateValue
1332
* @param {String} newVal New value.
1335
_updateValue: function (newVal) {
1336
var delim = this.get(QUERY_DELIMITER),
1341
newVal = Lang.trimLeft(newVal);
1344
insertDelim = trim(delim); // so we don't double up on spaces
1345
prevVal = YArray.map(trim(this.get(VALUE)).split(delim), trim);
1346
len = prevVal.length;
1349
prevVal[len - 1] = newVal;
1350
newVal = prevVal.join(insertDelim + ' ');
1353
newVal = newVal + insertDelim + ' ';
1356
this.set(VALUE, newVal);
1359
// -- Protected Event Handlers ---------------------------------------------
1362
* Handles change events for the <code>value</code> attribute.
1364
* @method _afterValueChange
1365
* @param {EventFacade} e
1368
_afterValueChange: function (e) {
1376
// Don't query on value changes that didn't come from the user.
1377
if (e.src !== AutoCompleteBase.UI_SRC) {
1378
this._inputNode.set(VALUE, newVal);
1382
Y.log('valueChange: new: "' + newVal + '"; old: "' + e.prevVal + '"', 'info', 'autocomplete-base');
1384
minQueryLength = this.get('minQueryLength');
1385
query = this._parseValue(newVal) || '';
1387
if (minQueryLength >= 0 && query.length >= minQueryLength) {
1388
delay = this.get('queryDelay');
1391
fire = function () {
1392
that.fire(EVT_QUERY, {
1399
clearTimeout(this._delay);
1400
this._delay = setTimeout(fire, delay);
1405
clearTimeout(this._delay);
1407
this.fire(EVT_CLEAR, {
1408
prevVal: e.prevVal ? this._parseValue(e.prevVal) : null
1414
* Handles <code>blur</code> events on the input node.
1416
* @method _onInputBlur
1417
* @param {EventFacade} e
1420
_onInputBlur: function (e) {
1421
var delim = this.get(QUERY_DELIMITER),
1426
// If a query delimiter is set and the input's value contains one or
1427
// more trailing delimiters, strip them.
1428
if (delim && !this.get('allowTrailingDelimiter')) {
1429
delim = Lang.trimRight(delim);
1430
value = newVal = this._inputNode.get(VALUE);
1433
while ((newVal = Lang.trimRight(newVal)) &&
1434
(delimPos = newVal.length - delim.length) &&
1435
newVal.lastIndexOf(delim) === delimPos) {
1437
newVal = newVal.substring(0, delimPos);
1440
// Delimiter is one or more space characters, so just trim the
1442
newVal = Lang.trimRight(newVal);
1445
if (newVal !== value) {
1446
this.set(VALUE, newVal);
1452
* Handles <code>valueChange</code> events on the input node and fires a
1453
* <code>query</code> event when the input value meets the configured
1456
* @method _onInputValueChange
1457
* @param {EventFacade} e
1460
_onInputValueChange: function (e) {
1461
var newVal = e.newVal;
1463
// Don't query if the internal value is the same as the new value
1464
// reported by valueChange.
1465
if (newVal === this.get(VALUE)) {
1469
this.set(VALUE, newVal, {src: AutoCompleteBase.UI_SRC});
1473
* Handles source responses and fires the <code>results</code> event.
1475
* @method _onResponse
1476
* @param {EventFacade} e
1479
_onResponse: function (query, e) {
1480
// Ignore stale responses that aren't for the current query.
1481
if (query === this.get(QUERY)) {
1482
this._parseResponse(query, e.response, e.data);
1486
// -- Protected Default Event Handlers -------------------------------------
1489
* Default <code>clear</code> event handler. Sets the <code>results</code>
1490
* property to an empty array and <code>query</code> to null.
1492
* @method _defClearFn
1495
_defClearFn: function () {
1496
this._set(QUERY, null);
1497
this._set(RESULTS, []);
1501
* Default <code>query</code> event handler. Sets the <code>query</code>
1502
* property and sends a request to the source if one is configured.
1504
* @method _defQueryFn
1505
* @param {EventFacade} e
1508
_defQueryFn: function (e) {
1509
var query = e.query;
1511
Y.log('query: "' + query + '"; inputValue: "' + e.inputValue + '"', 'info', 'autocomplete-base');
1512
this.sendRequest(query); // sendRequest will set the 'query' attribute
1516
* Default <code>results</code> event handler. Sets the <code>results</code>
1517
* property to the latest results.
1519
* @method _defResultsFn
1520
* @param {EventFacade} e
1523
_defResultsFn: function (e) {
1524
Y.log('results: ' + Y.dump(e.results), 'info', 'autocomplete-base');
1525
this._set(RESULTS, e[RESULTS]);
1529
Y.AutoCompleteBase = AutoCompleteBase;
1532
}, '3.3.0' ,{optional:['autocomplete-sources'], requires:['array-extras', 'base-build', 'escape', 'event-valuechange', 'node-base']});
1533
YUI.add('autocomplete-sources', function(Y) {
1536
* Mixes support for JSONP and YQL result sources into AutoCompleteBase.
1538
* @module autocomplete
1539
* @submodule autocomplete-sources
1544
_SOURCE_SUCCESS = '_sourceSuccess',
1546
MAX_RESULTS = 'maxResults',
1547
REQUEST_TEMPLATE = 'requestTemplate',
1548
RESULT_LIST_LOCATOR = 'resultListLocator';
1550
function ACSources() {}
1552
ACSources.prototype = {
1554
* Regular expression used to determine whether a String source is a YQL
1557
* @property _YQL_SOURCE_REGEX
1560
* @for AutoCompleteBase
1562
_YQL_SOURCE_REGEX: /^(?:select|set|use)\s+/i,
1565
* Creates a DataSource-like object that uses <code>Y.io</code> as a source.
1566
* See the <code>source</code> attribute for more details.
1568
* @method _createIOSource
1569
* @param {String} source URL.
1570
* @return {Object} DataSource-like object.
1572
* @for AutoCompleteBase
1574
_createIOSource: function (source) {
1578
ioRequest, lastRequest, loading;
1580
ioSource.sendRequest = function (request) {
1581
var _sendRequest = function (request) {
1582
var query = request.request,
1583
maxResults, requestTemplate, url;
1586
that[_SOURCE_SUCCESS](cache[query], request);
1588
maxResults = that.get(MAX_RESULTS);
1589
requestTemplate = that.get(REQUEST_TEMPLATE);
1592
if (requestTemplate) {
1593
url += requestTemplate(query);
1596
url = Lang.sub(url, {
1597
maxResults: maxResults > 0 ? maxResults : 1000,
1598
query : encodeURIComponent(query)
1601
// Cancel any outstanding requests.
1602
if (ioRequest && ioRequest.isInProgress()) {
1606
ioRequest = Y.io(url, {
1608
success: function (tid, response) {
1612
data = Y.JSON.parse(response.responseText);
1614
Y.error('JSON parse error', ex);
1618
cache[query] = data;
1619
that[_SOURCE_SUCCESS](data, request);
1627
// Keep track of the most recent request in case there are multiple
1628
// requests while we're waiting for the IO module to load. Only the
1629
// most recent request will be sent.
1630
lastRequest = request;
1635
// Lazy-load the io and json-parse modules if necessary, then
1636
// overwrite the sendRequest method to bypass this check in the
1638
Y.use('io-base', 'json-parse', function () {
1639
ioSource.sendRequest = _sendRequest;
1640
_sendRequest(lastRequest);
1649
* Creates a DataSource-like object that uses the specified JSONPRequest
1650
* instance as a source. See the <code>source</code> attribute for more
1653
* @method _createJSONPSource
1654
* @param {JSONPRequest|String} source URL string or JSONPRequest instance.
1655
* @return {Object} DataSource-like object.
1657
* @for AutoCompleteBase
1659
_createJSONPSource: function (source) {
1663
lastRequest, loading;
1665
jsonpSource.sendRequest = function (request) {
1666
var _sendRequest = function (request) {
1667
var query = request.request;
1670
that[_SOURCE_SUCCESS](cache[query], request);
1672
// Hack alert: JSONPRequest currently doesn't support
1673
// per-request callbacks, so we're reaching into the protected
1674
// _config object to make it happen.
1676
// This limitation is mentioned in the following JSONP
1677
// enhancement ticket:
1679
// http://yuilibrary.com/projects/yui3/ticket/2529371
1680
source._config.on.success = function (data) {
1681
cache[query] = data;
1682
that[_SOURCE_SUCCESS](data, request);
1689
// Keep track of the most recent request in case there are multiple
1690
// requests while we're waiting for the JSONP module to load. Only
1691
// the most recent request will be sent.
1692
lastRequest = request;
1697
// Lazy-load the JSONP module if necessary, then overwrite the
1698
// sendRequest method to bypass this check in the future.
1699
Y.use('jsonp', function () {
1700
// Turn the source into a JSONPRequest instance if it isn't
1702
if (!(source instanceof Y.JSONPRequest)) {
1703
source = new Y.JSONPRequest(source, {
1704
format: Y.bind(that._jsonpFormatter, that)
1708
jsonpSource.sendRequest = _sendRequest;
1709
_sendRequest(lastRequest);
1718
* Creates a DataSource-like object that calls the specified URL or
1719
* executes the specified YQL query for results. If the string starts
1720
* with "select ", "use ", or "set " (case-insensitive), it's assumed to be
1721
* a YQL query; otherwise, it's assumed to be a URL (which may be absolute
1722
* or relative). URLs containing a "{callback}" placeholder are assumed to
1723
* be JSONP URLs; all others will use XHR. See the <code>source</code>
1724
* attribute for more details.
1726
* @method _createStringSource
1727
* @param {String} source URL or YQL query.
1728
* @return {Object} DataSource-like object.
1730
* @for AutoCompleteBase
1732
_createStringSource: function (source) {
1733
if (this._YQL_SOURCE_REGEX.test(source)) {
1734
// Looks like a YQL query.
1735
return this._createYQLSource(source);
1736
} else if (source.indexOf('{callback}') !== -1) {
1737
// Contains a {callback} param and isn't a YQL query, so it must be
1739
return this._createJSONPSource(source);
1741
// Not a YQL query or JSONP, so we'll assume it's an XHR URL.
1742
return this._createIOSource(source);
1747
* Creates a DataSource-like object that uses the specified YQL query string
1748
* to create a YQL-based source. See the <code>source</code> attribute for
1749
* details. If no <code>resultListLocator</code> is defined, this method
1750
* will set a best-guess locator that might work for many typical YQL
1753
* @method _createYQLSource
1754
* @param {String} source YQL query.
1755
* @return {Object} DataSource-like object.
1757
* @for AutoCompleteBase
1759
_createYQLSource: function (source) {
1763
lastRequest, loading;
1765
if (!this.get(RESULT_LIST_LOCATOR)) {
1766
this.set(RESULT_LIST_LOCATOR, this._defaultYQLLocator);
1769
yqlSource.sendRequest = function (request) {
1772
_sendRequest = function (request) {
1773
var query = request.request,
1774
callback, env, maxResults, opts, yqlQuery;
1777
that[_SOURCE_SUCCESS](cache[query], request);
1779
callback = function (data) {
1780
cache[query] = data;
1781
that[_SOURCE_SUCCESS](data, request);
1784
env = that.get('yqlEnv');
1785
maxResults = that.get(MAX_RESULTS);
1787
opts = {proto: that.get('yqlProtocol')};
1789
yqlQuery = Lang.sub(source, {
1790
maxResults: maxResults > 0 ? maxResults : 1000,
1794
// Only create a new YQLRequest instance if this is the
1795
// first request. For subsequent requests, we'll reuse the
1796
// original instance.
1798
yqlRequest._callback = callback;
1799
yqlRequest._opts = opts;
1800
yqlRequest._params.q = yqlQuery;
1803
yqlRequest._params.env = env;
1806
yqlRequest = new Y.YQLRequest(yqlQuery, {
1807
on: {success: callback},
1808
allowCache: false // temp workaround until JSONP has per-URL callback proxies
1809
}, env ? {env: env} : null, opts);
1816
// Keep track of the most recent request in case there are multiple
1817
// requests while we're waiting for the YQL module to load. Only the
1818
// most recent request will be sent.
1819
lastRequest = request;
1822
// Lazy-load the YQL module if necessary, then overwrite the
1823
// sendRequest method to bypass this check in the future.
1826
Y.use('yql', function () {
1827
yqlSource.sendRequest = _sendRequest;
1828
_sendRequest(lastRequest);
1837
* Default resultListLocator used when a string-based YQL source is set and
1838
* the implementer hasn't already specified one.
1840
* @method _defaultYQLLocator
1841
* @param {Object} response YQL response object.
1844
* @for AutoCompleteBase
1846
_defaultYQLLocator: function (response) {
1847
var results = response && response.query && response.query.results,
1850
if (results && Lang.isObject(results)) {
1851
// If there's only a single value on YQL's results object, that
1852
// value almost certainly contains the array of results we want. If
1853
// there are 0 or 2+ values, then the values themselves are most
1854
// likely the results we want.
1855
values = Y.Object.values(results) || [];
1856
results = values.length === 1 ? values[0] : values;
1858
if (!Lang.isArray(results)) {
1859
results = [results];
1869
* URL formatter passed to <code>JSONPRequest</code> instances.
1871
* @method _jsonpFormatter
1872
* @param {String} url
1873
* @param {String} proxy
1874
* @param {String} query
1875
* @return {String} Formatted URL
1877
* @for AutoCompleteBase
1879
_jsonpFormatter: function (url, proxy, query) {
1880
var maxResults = this.get(MAX_RESULTS),
1881
requestTemplate = this.get(REQUEST_TEMPLATE);
1883
if (requestTemplate) {
1884
url += requestTemplate(query);
1887
return Lang.sub(url, {
1889
maxResults: maxResults > 0 ? maxResults : 1000,
1890
query : encodeURIComponent(query)
1897
* YQL environment file URL to load when the <code>source</code> is set to
1898
* a YQL query. Set this to <code>null</code> to use the default Open Data
1899
* Tables environment file (http://datatables.org/alltables.env).
1904
* @for AutoCompleteBase
1911
* URL protocol to use when the <code>source</code> is set to a YQL query.
1913
* @attribute yqlProtocol
1916
* @for AutoCompleteBase
1923
Y.Base.mix(Y.AutoCompleteBase, [ACSources]);
1926
}, '3.3.0' ,{optional:['io-base', 'json-parse', 'jsonp', 'yql'], requires:['autocomplete-base']});
1927
YUI.add('autocomplete-list', function(Y) {
1930
* Traditional autocomplete dropdown list widget, just like Mom used to make.
1932
* @module autocomplete
1933
* @submodule autocomplete-list
1934
* @class AutoCompleteList
1936
* @uses AutoCompleteBase
1937
* @uses WidgetPosition
1938
* @uses WidgetPositionAlign
1941
* @param {Object} config Configuration object.
1948
// keyCode constants.
1951
// String shorthand.
1952
_CLASS_ITEM = '_CLASS_ITEM',
1953
_CLASS_ITEM_ACTIVE = '_CLASS_ITEM_ACTIVE',
1954
_CLASS_ITEM_HOVER = '_CLASS_ITEM_HOVER',
1955
_SELECTOR_ITEM = '_SELECTOR_ITEM',
1957
ACTIVE_ITEM = 'activeItem',
1958
ALWAYS_SHOW_LIST = 'alwaysShowList',
1959
CIRCULAR = 'circular',
1960
HOVERED_ITEM = 'hoveredItem',
1965
RESULTS = 'results',
1966
VISIBLE = 'visible',
1970
EVT_SELECT = 'select',
1972
List = Y.Base.create('autocompleteList', Y.Widget, [
1975
Y.WidgetPositionAlign,
1978
// -- Prototype Properties -------------------------------------------------
1979
ARIA_TEMPLATE: '<div/>',
1980
ITEM_TEMPLATE: '<li/>',
1981
LIST_TEMPLATE: '<ul/>',
1983
// -- Lifecycle Prototype Methods ------------------------------------------
1984
initializer: function () {
1985
var inputNode = this.get('inputNode');
1988
Y.error('No inputNode specified.');
1992
this._inputNode = inputNode;
1993
this._listEvents = [];
1995
// This ensures that the list is rendered inside the same parent as the
1996
// input node by default, which is necessary for proper ARIA support.
1997
this.DEF_PARENT_NODE = inputNode.get('parentNode');
1999
// Cache commonly used classnames and selectors for performance.
2000
this[_CLASS_ITEM] = this.getClassName(ITEM);
2001
this[_CLASS_ITEM_ACTIVE] = this.getClassName(ITEM, 'active');
2002
this[_CLASS_ITEM_HOVER] = this.getClassName(ITEM, 'hover');
2003
this[_SELECTOR_ITEM] = '.' + this[_CLASS_ITEM];
2006
* Fires when an autocomplete suggestion is selected from the list,
2007
* typically via a keyboard action or mouse click.
2010
* @param {EventFacade} e Event facade with the following additional
2014
* <dt>itemNode (Node)</dt>
2016
* List item node that was selected.
2019
* <dt>result (Object)</dt>
2021
* AutoComplete result object.
2025
* @preventable _defSelectFn
2027
this.publish(EVT_SELECT, {
2028
defaultFn: this._defSelectFn
2032
destructor: function () {
2033
while (this._listEvents.length) {
2034
this._listEvents.pop().detach();
2038
bindUI: function () {
2043
renderUI: function () {
2044
var ariaNode = this._createAriaNode(),
2045
contentBox = this.get('contentBox'),
2046
inputNode = this._inputNode,
2048
parentNode = inputNode.get('parentNode');
2050
listNode = this._createListNode();
2051
this._set('listNode', listNode);
2052
contentBox.append(listNode);
2054
inputNode.addClass(this.getClassName('input')).setAttrs({
2055
'aria-autocomplete': LIST,
2056
'aria-expanded' : false,
2057
'aria-owns' : listNode.get('id'),
2061
// ARIA node must be outside the widget or announcements won't be made
2062
// when the widget is hidden.
2063
parentNode.append(ariaNode);
2065
this._ariaNode = ariaNode;
2066
this._boundingBox = this.get('boundingBox');
2067
this._contentBox = contentBox;
2068
this._listNode = listNode;
2069
this._parentNode = parentNode;
2072
syncUI: function () {
2073
this._syncResults();
2074
this._syncVisibility();
2077
// -- Public Prototype Methods ---------------------------------------------
2080
* Hides the list, unless the <code>alwaysShowList</code> attribute is
2081
* <code>true</code>.
2088
return this.get(ALWAYS_SHOW_LIST) ? this : this.set(VISIBLE, false);
2092
* Selects the specified <i>itemNode</i>, or the current
2093
* <code>activeItem</code> if <i>itemNode</i> is not specified.
2095
* @method selectItem
2096
* @param {Node} itemNode (optional) Item node to select.
2099
selectItem: function (itemNode) {
2101
if (!itemNode.hasClass(this[_CLASS_ITEM])) {
2105
itemNode = this.get(ACTIVE_ITEM);
2112
this.fire(EVT_SELECT, {
2114
result : itemNode.getData(RESULT)
2120
// -- Protected Prototype Methods ------------------------------------------
2123
* Activates the next item after the currently active item. If there is no
2124
* next item and the <code>circular</code> attribute is <code>true</code>,
2125
* focus will wrap back to the input node.
2127
* @method _activateNextItem
2131
_activateNextItem: function () {
2132
var item = this.get(ACTIVE_ITEM),
2136
nextItem = item.next(this[_SELECTOR_ITEM]) ||
2137
(this.get(CIRCULAR) ? null : item);
2139
nextItem = this._getFirstItemNode();
2142
this.set(ACTIVE_ITEM, nextItem);
2148
* Activates the item previous to the currently active item. If there is no
2149
* previous item and the <code>circular</code> attribute is
2150
* <code>true</code>, focus will wrap back to the input node.
2152
* @method _activatePrevItem
2156
_activatePrevItem: function () {
2157
var item = this.get(ACTIVE_ITEM),
2158
prevItem = item ? item.previous(this[_SELECTOR_ITEM]) :
2159
this.get(CIRCULAR) && this._getLastItemNode();
2161
this.set(ACTIVE_ITEM, prevItem || null);
2167
* Appends the specified result <i>items</i> to the list inside a new item
2171
* @param {Array|Node|HTMLElement|String} items Result item or array of
2173
* @return {NodeList} Added nodes.
2176
_add: function (items) {
2179
YArray.each(Lang.isArray(items) ? items : [items], function (item) {
2180
itemNodes.push(this._createItemNode(item).setData(RESULT, item));
2183
itemNodes = Y.all(itemNodes);
2184
this._listNode.append(itemNodes.toFrag());
2190
* Updates the ARIA live region with the specified message.
2193
* @param {String} stringId String id (from the <code>strings</code>
2194
* attribute) of the message to speak.
2195
* @param {Object} subs (optional) Substitutions for placeholders in the
2199
_ariaSay: function (stringId, subs) {
2200
var message = this.get('strings.' + stringId);
2201
this._ariaNode.setContent(subs ? Lang.sub(message, subs) : message);
2205
* Binds <code>inputNode</code> events and behavior.
2207
* @method _bindInput
2210
_bindInput: function () {
2211
var inputNode = this._inputNode,
2212
alignNode, alignWidth, tokenInput;
2214
// Null align means we can auto-align. Set align to false to prevent
2215
// auto-alignment, or a valid alignment config to customize the
2217
if (this.get('align') === null) {
2218
// If this is a tokenInput, align with its bounding box.
2219
// Otherwise, align with the inputNode. Bit of a cheat.
2220
tokenInput = this.get('tokenInput');
2221
alignNode = (tokenInput && tokenInput.get('boundingBox')) || inputNode;
2225
points: ['tl', 'bl']
2228
// If no width config is set, attempt to set the list's width to the
2229
// width of the alignment node. If the alignment node's width is
2230
// falsy, do nothing.
2231
if (!this.get(WIDTH) && (alignWidth = alignNode.get('offsetWidth'))) {
2232
this.set(WIDTH, alignWidth);
2236
// Attach inputNode events.
2237
this._listEvents.push(inputNode.on('blur', this._onListInputBlur, this));
2241
* Binds list events.
2246
_bindList: function () {
2247
this._listEvents.concat([
2249
mouseover: this._afterMouseOver,
2250
mouseout : this._afterMouseOut,
2252
activeItemChange : this._afterActiveItemChange,
2253
alwaysShowListChange: this._afterAlwaysShowListChange,
2254
hoveredItemChange : this._afterHoveredItemChange,
2255
resultsChange : this._afterResultsChange,
2256
visibleChange : this._afterVisibleChange
2259
this._listNode.delegate('click', this._onItemClick, this[_SELECTOR_ITEM], this)
2264
* Clears the contents of the tray.
2269
_clear: function () {
2270
this.set(ACTIVE_ITEM, null);
2271
this._set(HOVERED_ITEM, null);
2273
this._listNode.get('children').remove(true);
2277
* Creates and returns an ARIA live region node.
2279
* @method _createAriaNode
2280
* @return {Node} ARIA node.
2283
_createAriaNode: function () {
2284
var ariaNode = Node.create(this.ARIA_TEMPLATE);
2286
return ariaNode.addClass(this.getClassName('aria')).setAttrs({
2287
'aria-live': 'polite',
2293
* Creates and returns an item node with the specified <i>content</i>.
2295
* @method _createItemNode
2296
* @param {Object} result Result object.
2297
* @return {Node} Item node.
2300
_createItemNode: function (result) {
2301
var itemNode = Node.create(this.ITEM_TEMPLATE);
2303
return itemNode.addClass(this[_CLASS_ITEM]).setAttrs({
2304
id : Y.stamp(itemNode),
2306
}).setAttribute('data-text', result.text).append(result.display);
2310
* Creates and returns a list node.
2312
* @method _createListNode
2313
* @return {Node} List node.
2316
_createListNode: function () {
2317
var listNode = Node.create(this.LIST_TEMPLATE);
2319
return listNode.addClass(this.getClassName(LIST)).setAttrs({
2320
id : Y.stamp(listNode),
2326
* Gets the first item node in the list, or <code>null</code> if the list is
2329
* @method _getFirstItemNode
2330
* @return {Node|null}
2333
_getFirstItemNode: function () {
2334
return this._listNode.one(this[_SELECTOR_ITEM]);
2338
* Gets the last item node in the list, or <code>null</code> if the list is
2341
* @method _getLastItemNode
2342
* @return {Node|null}
2345
_getLastItemNode: function () {
2346
return this._listNode.one(this[_SELECTOR_ITEM] + ':last-child');
2350
* Synchronizes the results displayed in the list with those in the
2351
* <i>results</i> argument, or with the <code>results</code> attribute if an
2352
* argument is not provided.
2354
* @method _syncResults
2355
* @param {Array} results (optional) Results.
2358
_syncResults: function (results) {
2362
results = this.get(RESULTS);
2367
if (results.length) {
2368
items = this._add(results);
2369
this._ariaSay('items_available');
2372
if (this.get('activateFirstItem') && !this.get(ACTIVE_ITEM)) {
2373
this.set(ACTIVE_ITEM, this._getFirstItemNode());
2378
* Synchronizes the visibility of the tray with the <i>visible</i> argument,
2379
* or with the <code>visible</code> attribute if an argument is not
2382
* @method _syncVisibility
2383
* @param {Boolean} visible (optional) Visibility.
2386
_syncVisibility: function (visible) {
2387
if (this.get(ALWAYS_SHOW_LIST)) {
2389
this.set(VISIBLE, visible);
2392
if (typeof visible === 'undefined') {
2393
visible = this.get(VISIBLE);
2396
this._inputNode.set('aria-expanded', visible);
2397
this._boundingBox.set('aria-hidden', !visible);
2400
// Force WidgetPositionAlign to refresh its alignment.
2401
this._syncUIPosAlign();
2403
this.set(ACTIVE_ITEM, null);
2404
this._set(HOVERED_ITEM, null);
2406
// Force a reflow to work around a glitch in IE6 and 7 where some of
2407
// the contents of the list will sometimes remain visible after the
2408
// container is hidden.
2409
this._boundingBox.get('offsetWidth');
2413
// -- Protected Event Handlers ---------------------------------------------
2416
* Handles <code>activeItemChange</code> events.
2418
* @method _afterActiveItemChange
2419
* @param {EventTarget} e
2422
_afterActiveItemChange: function (e) {
2423
var inputNode = this._inputNode,
2425
prevVal = e.prevVal;
2427
// The previous item may have disappeared by the time this handler runs,
2428
// so we need to be careful.
2429
if (prevVal && prevVal._node) {
2430
prevVal.removeClass(this[_CLASS_ITEM_ACTIVE]);
2434
newVal.addClass(this[_CLASS_ITEM_ACTIVE]);
2435
inputNode.set('aria-activedescendant', newVal.get(ID));
2437
inputNode.removeAttribute('aria-activedescendant');
2440
if (this.get('scrollIntoView')) {
2441
(newVal || inputNode).scrollIntoView();
2446
* Handles <code>alwaysShowListChange</code> events.
2448
* @method _afterAlwaysShowListChange
2449
* @param {EventTarget} e
2452
_afterAlwaysShowListChange: function (e) {
2453
this.set(VISIBLE, e.newVal || this.get(RESULTS).length > 0);
2457
* Handles <code>hoveredItemChange</code> events.
2459
* @method _afterHoveredItemChange
2460
* @param {EventTarget} e
2463
_afterHoveredItemChange: function (e) {
2464
var newVal = e.newVal,
2465
prevVal = e.prevVal;
2468
prevVal.removeClass(this[_CLASS_ITEM_HOVER]);
2472
newVal.addClass(this[_CLASS_ITEM_HOVER]);
2477
* Handles <code>mouseover</code> events.
2479
* @method _afterMouseOver
2480
* @param {EventTarget} e
2483
_afterMouseOver: function (e) {
2484
var itemNode = e.domEvent.target.ancestor(this[_SELECTOR_ITEM], true);
2486
this._mouseOverList = true;
2489
this._set(HOVERED_ITEM, itemNode);
2494
* Handles <code>mouseout</code> events.
2496
* @method _afterMouseOut
2497
* @param {EventTarget} e
2500
_afterMouseOut: function () {
2501
this._mouseOverList = false;
2502
this._set(HOVERED_ITEM, null);
2506
* Handles <code>resultsChange</code> events.
2508
* @method _afterResultsChange
2509
* @param {EventFacade} e
2512
_afterResultsChange: function (e) {
2513
this._syncResults(e.newVal);
2515
if (!this.get(ALWAYS_SHOW_LIST)) {
2516
this.set(VISIBLE, !!e.newVal.length);
2521
* Handles <code>visibleChange</code> events.
2523
* @method _afterVisibleChange
2524
* @param {EventFacade} e
2527
_afterVisibleChange: function (e) {
2528
this._syncVisibility(!!e.newVal);
2532
* Handles <code>inputNode</code> <code>blur</code> events.
2534
* @method _onListInputBlur
2535
* @param {EventTarget} e
2538
_onListInputBlur: function (e) {
2539
// Hide the list on inputNode blur events, unless the mouse is currently
2540
// over the list (which indicates that the user is probably interacting
2541
// with it). The _lastInputKey property comes from the
2542
// autocomplete-list-keys module.
2543
if (!this._mouseOverList || this._lastInputKey === KEY_TAB) {
2549
* Delegated event handler for item <code>click</code> events.
2551
* @method _onItemClick
2552
* @param {EventTarget} e
2555
_onItemClick: function (e) {
2556
var itemNode = e.currentTarget;
2558
this.set(ACTIVE_ITEM, itemNode);
2559
this.selectItem(itemNode);
2562
// -- Protected Default Event Handlers -------------------------------------
2565
* Default <code>select</code> event handler.
2567
* @method _defSelectFn
2568
* @param {EventTarget} e
2571
_defSelectFn: function (e) {
2572
var text = e.result.text;
2574
// TODO: support typeahead completion, etc.
2575
this._inputNode.focus();
2576
this._updateValue(text);
2577
this._ariaSay('item_selected', {item: text});
2583
* If <code>true</code>, the first item in the list will be activated by
2584
* default when the list is initially displayed and when results change.
2586
* @attribute activateFirstItem
2590
activateFirstItem: {
2595
* Item that's currently active, if any. When the user presses enter,
2596
* this is the item that will be selected.
2598
* @attribute activeItem
2607
* If <code>true</code>, the list will remain visible even when there
2608
* are no results to display.
2610
* @attribute alwaysShowList
2619
* If <code>true</code>, keyboard navigation will wrap around to the
2620
* opposite end of the list when navigating past the first or last item.
2622
* @attribute circular
2631
* Item currently being hovered over by the mouse, if any.
2633
* @attribute hoveredItem
2643
* Node that will contain result items.
2645
* @attribute listNode
2655
* If <code>true</code>, the viewport will be scrolled to ensure that
2656
* the active list item is visible when necessary.
2658
* @attribute scrollIntoView
2667
* Translatable strings used by the AutoCompleteList widget.
2669
* @attribute strings
2673
valueFn: function () {
2674
return Y.Intl.get('autocomplete-list');
2679
* If <code>true</code>, pressing the tab key while the list is visible
2680
* will select the active item, if any.
2682
* @attribute tabSelect
2690
// The "visible" attribute is documented in Widget.
2696
CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
2699
Y.AutoCompleteList = List;
2702
* Alias for <a href="AutoCompleteList.html"><code>AutoCompleteList</code></a>.
2703
* See that class for API docs.
2705
* @class AutoComplete
2708
Y.AutoComplete = List;
2711
}, '3.3.0' ,{lang:['en'], requires:['autocomplete-base', 'selector-css3', 'widget', 'widget-position', 'widget-position-align', 'widget-stack'], after:['autocomplete-sources'], skinnable:true});
2712
YUI.add('autocomplete-plugin', function(Y) {
2715
* Binds an AutoCompleteList instance to a Node instance.
2717
* @module autocomplete
2718
* @submodule autocomplete-plugin
2723
* Binds an AutoCompleteList instance to a Node instance.
2731
* Y.one('#my-input').plug(Y.Plugin.AutoComplete, {
2732
* source: 'select * from search.suggest where query="{query}"'
2735
* // You can now access the AutoCompleteList instance at Y.one('#my-input').ac
2738
* @class Plugin.AutoComplete
2739
* @extends AutoCompleteList
2742
var Plugin = Y.Plugin;
2744
function ACListPlugin(config) {
2745
config.inputNode = config.host;
2747
// Render by default.
2748
if (!config.render && config.render !== false) {
2749
config.render = true;
2752
ACListPlugin.superclass.constructor.apply(this, arguments);
2755
Y.extend(ACListPlugin, Y.AutoCompleteList, {}, {
2756
NAME : 'autocompleteListPlugin',
2758
CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
2761
Plugin.AutoComplete = ACListPlugin;
2762
Plugin.AutoCompleteList = ACListPlugin;
2765
}, '3.3.0' ,{requires:['autocomplete-list', 'node-pluginhost']});
2768
YUI.add('autocomplete', function(Y){}, '3.3.0' ,{use:['autocomplete-base', 'autocomplete-sources', 'autocomplete-list', 'autocomplete-plugin']});