/*
Copyright (c) 2010, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 3.3.0
build: 3167
*/
YUI.add('autocomplete-base', function(Y) {
/**
* Provides automatic input completion or suggestions for text input fields and
* textareas.
*
* @module autocomplete
* @since 3.3.0
*/
/**
* Y.Base
extension that provides core autocomplete logic (but no
* UI implementation) for a text input field or textarea. Must be mixed into a
* Y.Base
-derived class to be useful.
*
* @module autocomplete
* @submodule autocomplete-base
*/
/**
*
* Extension that provides core autocomplete logic (but no UI implementation) * for a text input field or textarea. *
* *
* The AutoCompleteBase
class provides events and attributes that
* abstract away core autocomplete logic and configuration, but does not provide
* a widget implementation or suggestion UI. For a prepackaged autocomplete
* widget, see AutoCompleteList
.
*
* This extension cannot be instantiated directly, since it doesn't provide an
* actual implementation. It's intended to be mixed into a
* Y.Base
-based class or widget.
*
* Y.Widget
-based example:
*
* YUI().use('autocomplete-base', 'widget', function (Y) { * var MyAC = Y.Base.create('myAC', Y.Widget, [Y.AutoCompleteBase], { * // Custom prototype methods and properties. * }, { * // Custom static methods and properties. * }); * * // Custom implementation code. * }); ** *
* Y.Base
-based example:
*
* YUI().use('autocomplete-base', function (Y) { * var MyAC = Y.Base.create('myAC', Y.Base, [Y.AutoCompleteBase], { * initializer: function () { * this._bindUIACBase(); * this._syncUIACBase(); * }, * * // Custom prototype methods and properties. * }, { * // Custom static methods and properties. * }); * * // Custom implementation code. * }); ** * @class AutoCompleteBase */ var Escape = Y.Escape, Lang = Y.Lang, YArray = Y.Array, YObject = Y.Object, isFunction = Lang.isFunction, isString = Lang.isString, trim = Lang.trim, INVALID_VALUE = Y.Attribute.INVALID_VALUE, _FUNCTION_VALIDATOR = '_functionValidator', _SOURCE_SUCCESS = '_sourceSuccess', ALLOW_BROWSER_AC = 'allowBrowserAutocomplete', INPUT_NODE = 'inputNode', QUERY = 'query', QUERY_DELIMITER = 'queryDelimiter', REQUEST_TEMPLATE = 'requestTemplate', RESULTS = 'results', RESULT_LIST_LOCATOR = 'resultListLocator', VALUE = 'value', VALUE_CHANGE = 'valueChange', EVT_CLEAR = 'clear', EVT_QUERY = QUERY, EVT_RESULTS = RESULTS; function AutoCompleteBase() { // AOP bindings. Y.before(this._bindUIACBase, this, 'bindUI'); Y.before(this._destructorACBase, this, 'destructor'); Y.before(this._syncUIACBase, this, 'syncUI'); // -- Public Events -------------------------------------------------------- /** * Fires after the query has been completely cleared or no longer meets the * minimum query length requirement. * * @event clear * @param {EventFacade} e Event facade with the following additional * properties: * *
inputValue
.
* source
. If
* no source has been set, this event will not fire.
*
* @event results
* @param {EventFacade} e Event facade with the following additional
* properties:
*
* text
property.
* source
.
* queryDelimiter
is set, trailing delimiters will
* automatically be stripped from the input value by default when the
* input node loses focus. Set this to true
to allow trailing
* delimiters.
*
* @attribute allowTrailingDelimiter
* @type Boolean
* @default false
*/
allowTrailingDelimiter: {
value: false
},
/**
* Node to monitor for changes, which will generate query
* events when appropriate. May be either an input field or a textarea.
*
* @attribute inputNode
* @type Node|HTMLElement|String
* @writeonce
*/
inputNode: {
setter: Y.one,
writeOnce: 'initOnly'
},
/**
* Maximum number of results to return. A value of 0
or less
* will allow an unlimited number of results.
*
* @attribute maxResults
* @type Number
* @default 0
*/
maxResults: {
value: 0
},
/**
* Minimum number of characters that must be entered before a
* query
event will be fired. A value of 0
* allows empty queries; a negative value will effectively disable all
* query
events.
*
* @attribute minQueryLength
* @type Number
* @default 1
*/
minQueryLength: {
value: 1
},
/**
*
* Current query, or null
if there is no current query.
*
* The query might not be the same as the current value of the input
* node, both for timing reasons (due to queryDelay
) and
* because when one or more queryDelimiter
separators are
* in use, only the last portion of the delimited input string will be
* used as the query value.
*
* Number of milliseconds to delay after input before triggering a
* query
event. If new input occurs before this delay is
* over, the previous input event will be ignored and a new delay will
* begin.
*
* This can be useful both to throttle queries to a remote data source * and to avoid distracting the user by showing them less relevant * results before they've paused their typing. *
* * @attribute queryDelay * @type Number * @default 100 */ queryDelay: { value: 100 }, /** * Query delimiter string. When a delimiter is configured, the input value * will be split on the delimiter, and only the last portion will be used in * autocomplete queries and updated when thequery
attribute is
* modified.
*
* @attribute queryDelimiter
* @type String|null
* @default null
*/
queryDelimiter: {
value: null
},
/**
*
* Source request template. This can be a function that accepts a query as a
* parameter and returns a request string, or it can be a string containing
* the placeholder "{query}", which will be replaced with the actual
* URI-encoded query. In either case, the resulting string will be appended
* to the request URL when the source
attribute is set to a
* remote DataSource, JSONP URL, or XHR URL (it will not be appended to YQL
* URLs).
*
* While requestTemplate
may be set to either a function or
* a string, it will always be returned as a function that accepts a
* query argument and returns a string.
*
* Array of local result filter functions. If provided, each filter
* will be called with two arguments when results are received: the query
* and an array of result objects. See the documentation for the
* results
event for a list of the properties available on each
* result object.
*
* Each filter is expected to return a filtered or modified version of the
* results array, which will then be passed on to subsequent filters, then
* the resultHighlighter
function (if set), then the
* resultFormatter
function (if set), and finally to
* subscribers to the results
event.
*
* If no source
is set, result filters will not be called.
*
* Prepackaged result filters provided by the autocomplete-filters and
* autocomplete-filters-accentfold modules can be used by specifying the
* filter name as a string, such as 'phraseMatch'
(assuming
* the necessary filters module is loaded).
*
* Function which will be used to format results. If provided, this function * will be called with two arguments after results have been received and * filtered: the query and an array of result objects. The formatter is * expected to return an array of HTML strings or Node instances containing * the desired HTML for each result. *
* *
* See the documentation for the results
event for a list of
* the properties available on each result object.
*
* If no source
is set, the formatter will not be called.
*
* Function which will be used to highlight results. If provided, this * function will be called with two arguments after results have been * received and filtered: the query and an array of filtered result objects. * The highlighter is expected to return an array of highlighted result * text in the form of HTML strings. *
* *
* See the documentation for the results
event for a list of
* the properties available on each result object.
*
* If no source
is set, the highlighter will not be called.
*
* Locator that should be used to extract an array of results from a * non-array response. *
* ** By default, no locator is applied, and all responses are assumed to be * arrays by default. If all responses are already arrays, you don't need to * define a locator. *
* *
* The locator may be either a function (which will receive the raw response
* as an argument and must return an array) or a string representing an
* object path, such as "foo.bar.baz" (which would return the value of
* result.foo.bar.baz
if the response is an object).
*
* While resultListLocator
may be set to either a function or a
* string, it will always be returned as a function that accepts a response
* argument and returns an array.
*
* Locator that should be used to extract a plain text string from a * non-string result item. The resulting text value will typically be the * value that ends up being inserted into an input field or textarea when * the user of an autocomplete implementation selects a result. *
* ** By default, no locator is applied, and all results are assumed to be * plain text strings. If all results are already plain text strings, you * don't need to define a locator. *
* *
* The locator may be either a function (which will receive the raw result
* as an argument and must return a string) or a string representing an
* object path, such as "foo.bar.baz" (which would return the value of
* result.foo.bar.baz
if the result is an object).
*
* While resultTextLocator
may be set to either a function or a
* string, it will always be returned as a function that accepts a result
* argument and returns a string.
*
* Source for autocomplete results. The following source types are * supported: *
* *
* Example: ['first result', 'second result', 'etc']
*
* The full array will be provided to any configured filters for each * query. This is an easy way to create a fully client-side autocomplete * implementation. *
*
* A DataSource
instance or other object that provides a
* DataSource-like sendRequest
method. See the
* DataSource
documentation for details.
*
* Example: function (query) { return ['foo', 'bar']; }
*
* A function source will be called with the current query as a * parameter, and should return an array of results. *
*
* Example: {foo: ['foo result 1', 'foo result 2'], bar: ['bar result']}
*
* An object will be treated as a query hashmap. If a property on the * object matches the current query, the value of that property will be * used as the response. *
* *
* The response is assumed to be an array of results by default. If the
* response is not an array, provide a resultListLocator
to
* process the response and return an array.
*
* If the optional autocomplete-sources
module is loaded, then
* the following additional source types will be supported as well:
*
* Example: 'http://example.com/search?q={query}&callback={callback}'
*
* If a URL with a {callback}
placeholder is provided, it
* will be used to make a JSONP request. The {query}
* placeholder will be replaced with the current query, and the
* {callback}
placeholder will be replaced with an
* internally-generated JSONP callback name. Both placeholders must
* appear in the URL, or the request will fail. An optional
* {maxResults}
placeholder may also be provided, and will
* be replaced with the value of the maxResults attribute (or 1000 if
* the maxResults attribute is 0 or less).
*
* The response is assumed to be an array of results by default. If the
* response is not an array, provide a resultListLocator
to
* process the response and return an array.
*
* The jsonp
module must be loaded in order for
* JSONP URL sources to work. If the jsonp
module
* is not already loaded, it will be loaded on demand if possible.
*
* Example: 'http://example.com/search?q={query}'
*
* If a URL without a {callback}
placeholder is provided,
* it will be used to make a same-origin XHR request. The
* {query}
placeholder will be replaced with the current
* query. An optional {maxResults}
placeholder may also be
* provided, and will be replaced with the value of the maxResults
* attribute (or 1000 if the maxResults attribute is 0 or less).
*
* The response is assumed to be a JSON array of results by default. If
* the response is a JSON object and not an array, provide a
* resultListLocator
to process the response and return an
* array. If the response is in some form other than JSON, you will
* need to use a custom DataSource instance as the source.
*
* The io-base
and json-parse
modules
* must be loaded in order for XHR URL sources to work. If
* these modules are not already loaded, they will be loaded on demand
* if possible.
*
* Example: 'select * from search.suggest where query="{query}"'
*
* If a YQL query is provided, it will be used to make a YQL request.
* The {query}
placeholder will be replaced with the
* current autocomplete query. This placeholder must appear in the YQL
* query, or the request will fail. An optional
* {maxResults}
placeholder may also be provided, and will
* be replaced with the value of the maxResults attribute (or 1000 if
* the maxResults attribute is 0 or less).
*
* The yql
module must be loaded in order for YQL
* sources to work. If the yql
module is not
* already loaded, it will be loaded on demand if possible.
*
* As an alternative to providing a source, you could simply listen for
* query
events and handle them any way you see fit. Providing
* a source is optional, but will usually be simpler.
*
inputNode
specified at instantiation time has a
* node-tokeninput
plugin attached to it, this attribute will
* be a reference to the Y.Plugin.TokenInput
instance.
*
* @attribute tokenInput
* @type Plugin.TokenInput
* @readonly
*/
tokenInput: {
readOnly: true
},
/**
* Current value of the input node.
*
* @attribute value
* @type String
* @default ''
*/
value: {
// Why duplicate this._inputNode.get('value')? Because we need a
// reliable way to track the source of value changes. We want to perform
// completion when the user changes the value, but not when we change
// the value.
value: ''
}
};
AutoCompleteBase.CSS_PREFIX = 'ac';
AutoCompleteBase.UI_SRC = (Y.Widget && Y.Widget.UI_SRC) || 'ui';
AutoCompleteBase.prototype = {
// -- Public Prototype Methods ---------------------------------------------
/**
* * Sends a request to the configured source. If no source is configured, * this method won't do anything. *
* *
* Usually there's no reason to call this method manually; it will be
* called automatically when user input causes a query
event to
* be fired. The only time you'll need to call this method manually is if
* you want to force a request to be sent when no user input has occurred.
*
query
attribute will be set to this query. If not
* specified, the current value of the query
attribute will
* be used.
* @param {Function} requestTemplate (optional) Request template function.
* If not specified, the current value of the requestTemplate
* attribute will be used.
* @chainable
*/
sendRequest: function (query, requestTemplate) {
var request,
source = this.get('source');
if (query || query === '') {
this._set(QUERY, query);
} else {
query = this.get(QUERY);
}
if (source) {
if (!requestTemplate) {
requestTemplate = this.get(REQUEST_TEMPLATE);
}
request = requestTemplate ? requestTemplate(query) : query;
source.sendRequest({
request: request,
callback: {
success: Y.bind(this._onResponse, this, query)
}
});
}
return this;
},
// -- Protected Lifecycle Methods ------------------------------------------
/**
* Attaches event listeners and behaviors.
*
* @method _bindUIACBase
* @protected
*/
_bindUIACBase: function () {
var inputNode = this.get(INPUT_NODE),
tokenInput = inputNode && inputNode.tokenInput;
// If the inputNode has a node-tokeninput plugin attached, bind to the
// plugin's inputNode instead.
if (tokenInput) {
inputNode = tokenInput.get(INPUT_NODE);
this._set('tokenInput', tokenInput);
}
if (!inputNode) {
Y.error('No inputNode specified.');
return;
}
this._inputNode = inputNode;
this._acBaseEvents = [
// This is the valueChange event on the inputNode, provided by the
// event-valuechange module, not our own valueChange.
inputNode.on(VALUE_CHANGE, this._onInputValueChange, this),
inputNode.on('blur', this._onInputBlur, this),
this.after(ALLOW_BROWSER_AC + 'Change', this._syncBrowserAutocomplete),
this.after(VALUE_CHANGE, this._afterValueChange)
];
},
/**
* Detaches AutoCompleteBase event listeners.
*
* @method _destructorACBase
* @protected
*/
_destructorACBase: function () {
var events = this._acBaseEvents;
while (events && events.length) {
events.pop().detach();
}
},
/**
* Synchronizes the UI state of the inputNode
.
*
* @method _syncUIACBase
* @protected
*/
_syncUIACBase: function () {
this._syncBrowserAutocomplete();
this.set(VALUE, this.get(INPUT_NODE).get(VALUE));
},
// -- Protected Prototype Methods ------------------------------------------
/**
* Creates a DataSource-like object that simply returns the specified array
* as a response. See the source
attribute for more details.
*
* @method _createArraySource
* @param {Array} source
* @return {Object} DataSource-like object.
* @protected
*/
_createArraySource: function (source) {
var that = this;
return {sendRequest: function (request) {
that[_SOURCE_SUCCESS](source.concat(), request);
}};
},
/**
* Creates a DataSource-like object that passes the query to a
* custom-defined function, which is expected to return an array as a
* response. See the source
attribute for more details.
*
* @method _createFunctionSource
* @param {Function} source Function that accepts a query parameter and
* returns an array of results.
* @return {Object} DataSource-like object.
* @protected
*/
_createFunctionSource: function (source) {
var that = this;
return {sendRequest: function (request) {
that[_SOURCE_SUCCESS](source(request.request) || [], request);
}};
},
/**
* Creates a DataSource-like object that looks up queries as properties on
* the specified object, and returns the found value (if any) as a response.
* See the source
attribute for more details.
*
* @method _createObjectSource
* @param {Object} source
* @return {Object} DataSource-like object.
* @protected
*/
_createObjectSource: function (source) {
var that = this;
return {sendRequest: function (request) {
var query = request.request;
that[_SOURCE_SUCCESS](
YObject.owns(source, query) ? source[query] : [],
request
);
}};
},
/**
* Returns true
if value is either a function or
* null
.
*
* @method _functionValidator
* @param {Function|null} value Value to validate.
* @protected
*/
_functionValidator: function (value) {
return value === null || isFunction(value);
},
/**
* Faster and safer alternative to Y.Object.getValue(). Doesn't bother
* casting the path to an array (since we already know it's an array) and
* doesn't throw an error if a value in the middle of the object hierarchy
* is neither undefined
nor an object.
*
* @method _getObjectValue
* @param {Object} obj
* @param {Array} path
* @return {mixed} Located value, or undefined
if the value was
* not found at the specified path.
* @protected
*/
_getObjectValue: function (obj, path) {
if (!obj) {
return;
}
for (var i = 0, len = path.length; obj && i < len; i++) {
obj = obj[path[i]];
}
return obj;
},
/**
* Parses result responses, performs filtering and highlighting, and fires
* the results
event.
*
* @method _parseResponse
* @param {String} query Query that generated these results.
* @param {Object} response Response containing results.
* @param {Object} data Raw response data.
* @protected
*/
_parseResponse: function (query, response, data) {
var facade = {
data : data,
query : query,
results: []
},
listLocator = this.get(RESULT_LIST_LOCATOR),
results = [],
unfiltered = response && response.results,
filters,
formatted,
formatter,
highlighted,
highlighter,
i,
len,
maxResults,
result,
text,
textLocator;
if (unfiltered && listLocator) {
unfiltered = listLocator(unfiltered);
}
if (unfiltered && unfiltered.length) {
filters = this.get('resultFilters');
textLocator = this.get('resultTextLocator');
// Create a lightweight result object for each result to make them
// easier to work with. The various properties on the object
// represent different formats of the result, and will be populated
// as we go.
for (i = 0, len = unfiltered.length; i < len; ++i) {
result = unfiltered[i];
text = textLocator ? textLocator(result) : result.toString();
results.push({
display: Escape.html(text),
raw : result,
text : text
});
}
// Run the results through all configured result filters. Each
// filter returns an array of (potentially fewer) result objects,
// which is then passed to the next filter, and so on.
for (i = 0, len = filters.length; i < len; ++i) {
results = filters[i](query, results.concat());
if (!results) {
return;
}
if (!results.length) {
break;
}
}
if (results.length) {
formatter = this.get('resultFormatter');
highlighter = this.get('resultHighlighter');
maxResults = this.get('maxResults');
// If maxResults is set and greater than 0, limit the number of
// results.
if (maxResults && maxResults > 0 &&
results.length > maxResults) {
results.length = maxResults;
}
// Run the results through the configured highlighter (if any).
// The highlighter returns an array of highlighted strings (not
// an array of result objects), and these strings are then added
// to each result object.
if (highlighter) {
highlighted = highlighter(query, results.concat());
if (!highlighted) {
return;
}
for (i = 0, len = highlighted.length; i < len; ++i) {
result = results[i];
result.highlighted = highlighted[i];
result.display = result.highlighted;
}
}
// Run the results through the configured formatter (if any) to
// produce the final formatted results. The formatter returns an
// array of strings or Node instances (not an array of result
// objects), and these strings/Nodes are then added to each
// result object.
if (formatter) {
formatted = formatter(query, results.concat());
if (!formatted) {
return;
}
for (i = 0, len = formatted.length; i < len; ++i) {
results[i].display = formatted[i];
}
}
}
}
facade.results = results;
this.fire(EVT_RESULTS, facade);
},
/**
*
* Returns the query portion of the specified input value, or
* null
if there is no suitable query within the input value.
*
* If a query delimiter is defined, the query will be the last delimited * part of of the string. *
* * @method _parseValue * @param {String} value Input value from which to extract the query. * @return {String|null} query * @protected */ _parseValue: function (value) { var delim = this.get(QUERY_DELIMITER); if (delim) { value = value.split(delim); value = value[value.length - 1]; } return Lang.trimLeft(value); }, /** * Setter for locator attributes. * * @method _setLocator * @param {Function|String|null} locator * @return {Function|null} * @protected */ _setLocator: function (locator) { if (this[_FUNCTION_VALIDATOR](locator)) { return locator; } var that = this; locator = locator.toString().split('.'); return function (result) { return result && that._getObjectValue(result, locator); }; }, /** * Setter for therequestTemplate
attribute.
*
* @method _setRequestTemplate
* @param {Function|String|null} template
* @return {Function|null}
* @protected
*/
_setRequestTemplate: function (template) {
if (this[_FUNCTION_VALIDATOR](template)) {
return template;
}
template = template.toString();
return function (query) {
return Lang.sub(template, {query: encodeURIComponent(query)});
};
},
/**
* Setter for the resultFilters
attribute.
*
* @method _setResultFilters
* @param {Array|Function|String|null} filters null
, a filter
* function, an array of filter functions, or a string or array of strings
* representing the names of methods on
* Y.AutoCompleteFilters
.
* @return {Array} Array of filter functions (empty if filters is
* null
).
* @protected
*/
_setResultFilters: function (filters) {
var acFilters, getFilterFunction;
if (filters === null) {
return [];
}
acFilters = Y.AutoCompleteFilters;
getFilterFunction = function (filter) {
if (isFunction(filter)) {
return filter;
}
if (isString(filter) && acFilters &&
isFunction(acFilters[filter])) {
return acFilters[filter];
}
return false;
};
if (Lang.isArray(filters)) {
filters = YArray.map(filters, getFilterFunction);
return YArray.every(filters, function (f) { return !!f; }) ?
filters : INVALID_VALUE;
} else {
filters = getFilterFunction(filters);
return filters ? [filters] : INVALID_VALUE;
}
},
/**
* Setter for the resultHighlighter
attribute.
*
* @method _setResultHighlighter
* @param {Function|String|null} highlighter null
, a
* highlighter function, or a string representing the name of a method on
* Y.AutoCompleteHighlighters
.
* @return {Function|null}
* @protected
*/
_setResultHighlighter: function (highlighter) {
var acHighlighters;
if (this._functionValidator(highlighter)) {
return highlighter;
}
acHighlighters = Y.AutoCompleteHighlighters;
if (isString(highlighter) && acHighlighters &&
isFunction(acHighlighters[highlighter])) {
return acHighlighters[highlighter];
}
return INVALID_VALUE;
},
/**
* Setter for the source
attribute. Returns a DataSource or
* a DataSource-like object depending on the type of source.
*
* @method _setSource
* @param {Array|DataSource|Object|String} source AutoComplete source. See
* the source
attribute for details.
* @return {DataSource|Object}
* @protected
*/
_setSource: function (source) {
var sourcesNotLoaded = 'autocomplete-sources module not loaded';
if ((source && isFunction(source.sendRequest)) || source === null) {
// Quacks like a DataSource instance (or null). Make it so!
return source;
}
switch (Lang.type(source)) {
case 'string':
if (this._createStringSource) {
return this._createStringSource(source);
}
Y.error(sourcesNotLoaded);
return INVALID_VALUE;
case 'array':
// Wrap the array in a teensy tiny fake DataSource that just returns
// the array itself for each request. Filters will do the rest.
return this._createArraySource(source);
case 'function':
return this._createFunctionSource(source);
case 'object':
// If the object is a JSONPRequest instance, use it as a JSONP
// source.
if (Y.JSONPRequest && source instanceof Y.JSONPRequest) {
if (this._createJSONPSource) {
return this._createJSONPSource(source);
}
Y.error(sourcesNotLoaded);
return INVALID_VALUE;
}
// Not a JSONPRequest instance. Wrap the object in a teensy tiny
// fake DataSource that looks for the request as a property on the
// object and returns it if it exists, or an empty array otherwise.
return this._createObjectSource(source);
}
return INVALID_VALUE;
},
/**
* Shared success callback for non-DataSource sources.
*
* @method _sourceSuccess
* @param {mixed} data Response data.
* @param {Object} request Request object.
* @protected
*/
_sourceSuccess: function (data, request) {
request.callback.success({
data: data,
response: {results: data}
});
},
/**
* Synchronizes the UI state of the allowBrowserAutocomplete
* attribute.
*
* @method _syncBrowserAutocomplete
* @protected
*/
_syncBrowserAutocomplete: function () {
var inputNode = this.get(INPUT_NODE);
if (inputNode.get('nodeName').toLowerCase() === 'input') {
inputNode.setAttribute('autocomplete',
this.get(ALLOW_BROWSER_AC) ? 'on' : 'off');
}
},
/**
*
* Updates the query portion of the value
attribute.
*
* If a query delimiter is defined, the last delimited portion of the input * value will be replaced with the specified value. *
* * @method _updateValue * @param {String} newVal New value. * @protected */ _updateValue: function (newVal) { var delim = this.get(QUERY_DELIMITER), insertDelim, len, prevVal; newVal = Lang.trimLeft(newVal); if (delim) { insertDelim = trim(delim); // so we don't double up on spaces prevVal = YArray.map(trim(this.get(VALUE)).split(delim), trim); len = prevVal.length; if (len > 1) { prevVal[len - 1] = newVal; newVal = prevVal.join(insertDelim + ' '); } newVal = newVal + insertDelim + ' '; } this.set(VALUE, newVal); }, // -- Protected Event Handlers --------------------------------------------- /** * Handles change events for thevalue
attribute.
*
* @method _afterValueChange
* @param {EventFacade} e
* @protected
*/
_afterValueChange: function (e) {
var delay,
fire,
minQueryLength,
newVal = e.newVal,
query,
that;
// Don't query on value changes that didn't come from the user.
if (e.src !== AutoCompleteBase.UI_SRC) {
this._inputNode.set(VALUE, newVal);
return;
}
minQueryLength = this.get('minQueryLength');
query = this._parseValue(newVal) || '';
if (minQueryLength >= 0 && query.length >= minQueryLength) {
delay = this.get('queryDelay');
that = this;
fire = function () {
that.fire(EVT_QUERY, {
inputValue: newVal,
query : query
});
};
if (delay) {
clearTimeout(this._delay);
this._delay = setTimeout(fire, delay);
} else {
fire();
}
} else {
clearTimeout(this._delay);
this.fire(EVT_CLEAR, {
prevVal: e.prevVal ? this._parseValue(e.prevVal) : null
});
}
},
/**
* Handles blur
events on the input node.
*
* @method _onInputBlur
* @param {EventFacade} e
* @protected
*/
_onInputBlur: function (e) {
var delim = this.get(QUERY_DELIMITER),
delimPos,
newVal,
value;
// If a query delimiter is set and the input's value contains one or
// more trailing delimiters, strip them.
if (delim && !this.get('allowTrailingDelimiter')) {
delim = Lang.trimRight(delim);
value = newVal = this._inputNode.get(VALUE);
if (delim) {
while ((newVal = Lang.trimRight(newVal)) &&
(delimPos = newVal.length - delim.length) &&
newVal.lastIndexOf(delim) === delimPos) {
newVal = newVal.substring(0, delimPos);
}
} else {
// Delimiter is one or more space characters, so just trim the
// value.
newVal = Lang.trimRight(newVal);
}
if (newVal !== value) {
this.set(VALUE, newVal);
}
}
},
/**
* Handles valueChange
events on the input node and fires a
* query
event when the input value meets the configured
* criteria.
*
* @method _onInputValueChange
* @param {EventFacade} e
* @protected
*/
_onInputValueChange: function (e) {
var newVal = e.newVal;
// Don't query if the internal value is the same as the new value
// reported by valueChange.
if (newVal === this.get(VALUE)) {
return;
}
this.set(VALUE, newVal, {src: AutoCompleteBase.UI_SRC});
},
/**
* Handles source responses and fires the results
event.
*
* @method _onResponse
* @param {EventFacade} e
* @protected
*/
_onResponse: function (query, e) {
// Ignore stale responses that aren't for the current query.
if (query === this.get(QUERY)) {
this._parseResponse(query, e.response, e.data);
}
},
// -- Protected Default Event Handlers -------------------------------------
/**
* Default clear
event handler. Sets the results
* property to an empty array and query
to null.
*
* @method _defClearFn
* @protected
*/
_defClearFn: function () {
this._set(QUERY, null);
this._set(RESULTS, []);
},
/**
* Default query
event handler. Sets the query
* property and sends a request to the source if one is configured.
*
* @method _defQueryFn
* @param {EventFacade} e
* @protected
*/
_defQueryFn: function (e) {
var query = e.query;
this.sendRequest(query); // sendRequest will set the 'query' attribute
},
/**
* Default results
event handler. Sets the results
* property to the latest results.
*
* @method _defResultsFn
* @param {EventFacade} e
* @protected
*/
_defResultsFn: function (e) {
this._set(RESULTS, e[RESULTS]);
}
};
Y.AutoCompleteBase = AutoCompleteBase;
}, '3.3.0' ,{optional:['autocomplete-sources'], requires:['array-extras', 'base-build', 'escape', 'event-valuechange', 'node-base']});