/**
* The AutoComplete Utility
*
* @module aui-autocomplete
*/
var Lang = A.Lang,
isArray = Lang.isArray,
isString = Lang.isString,
isNull = Lang.isNull,
isFunction = Lang.isFunction,
getClassName = A.getClassName,
KeyMap = A.Event.KeyMap,
ALERT = 'alert',
CONTENT = 'content',
HELPER = 'helper',
HIDDEN = 'hidden',
ICON = 'icon',
ITEM = 'item',
LIST = 'list',
LOADING = 'loading',
NAME = 'autocomplete',
RESET = 'reset',
RESULTS = 'results',
SELECTED = 'selected',
ICON_DEFAULT = 'icon-circle-arrow-down',
ICON_ERROR = ALERT,
ICON_LOADING = LOADING,
CSS_HIGLIGHT = getClassName(NAME, SELECTED),
CSS_HIDDEN = getClassName(HELPER, HIDDEN),
CSS_LIST_ITEM = getClassName(NAME, LIST, ITEM),
CSS_RESULTS_LIST = getClassName(HELPER, RESET),
CSS_RESULTS_OVERLAY = getClassName(NAME, RESULTS),
CSS_RESULTS_OVERLAY_CONTENT = getClassName(NAME, RESULTS, CONTENT),
BACKSPACE = 'BACKSPACE',
TAB = 'TAB',
ENTER = 'ENTER',
ALT = 'ALT',
ESC = 'ESC',
UP = 'UP',
DOWN = 'DOWN',
RIGHT = 'RIGHT',
WIN_IME = 'WIN_IME',
OVERLAY_ALIGN = {
node: null,
points: ['tl', 'bl']
},
BOUNDING_BOX = 'boundingBox',
CONTENT_BOX = 'contentBox';
/**
* <p><img src="assets/images/aui-autocomplete/main.png"/></p>
*
* A base class for AutoComplete, providing:
* <ul>
* <li>Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)</li>
* <li>Presenting users choices based on their input</li>
* <li>Separating selected items</li>
* <li>Keyboard interaction for selecting items</li>
* </ul>
*
* Quick Example:<br/>
*
* <pre><code>var instance = new A.AutoCompleteDeprecated({
* dataSource: [['AL', 'Alabama', 'The Heart of Dixie'],
* ['AK', 'Alaska', 'The Land of the Midnight Sun'],
* ['AZ', 'Arizona', 'The Grand Canyon State']],
* schema: {
* resultFields: ['key', 'name', 'description']
* },
* matchKey: 'name',
* delimChar: ',',
* typeAhead: true,
* contentBox: '#myAutoComplete'
* }).render();
* </code></pre>
*
* Check the list of <a href="AutoComplete.html#configattributes">Configuration Attributes</a> available for
* AutoComplete.
*
* @param config {Object} Object literal specifying widget configuration properties.
*
* @class AutoComplete
* @constructor
* @extends Component
*/
var AutoComplete = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property AutoComplete.NAME
* @type String
* @static
*/
NAME: NAME,
/**
* Static property used to define the default attribute
* configuration for the AutoComplete.
*
* @property AutoComplete.ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Always show the results container, instead of only showing when the
* user is requesting them.
*
* @attribute alwaysShowContainer
* @default false
* @type Boolean
*/
alwaysShowContainer: {
value: false
},
/**
* Automatically highlight the first item in the list when the results are
* made visible.
*
* @attribute autoHighlight
* @default true
* @type Boolean
*/
autoHighlight: {
value: true
},
/**
* If set to true, the <a href="AutoComplete.html#method_filterResults">filterResults</a>
* method will be run on the response from the data source.
*
* @attribute applyLocalFilter
* @default true
* @type Boolean
*/
applyLocalFilter: {
value: null
},
/**
* To use a button
*
* @attribute button
* @default true
* @type Boolean
* @deprecated
*/
button: {
value: true
},
/**
* The data source that results will be read from. This can either be
* an existing <a href="DataSource.html">DataSource</a> object, or it can be a
* value that would be passed to <a href="DataSource.html">DataSource</a>.
*
* @attribute dataSource
* @default null
* @type Object | String | Function | Array
*/
dataSource: {
value: null
},
/**
* The type of the data source passed into <a href="AutoComplete.html#config_dataSource">dataSource</a>.
* This can be used to explicitly declare what kind of <a href="DataSource.html">DataSource</a> object will
* be created.
*
* @attribute dataSourceType
* @default null
* @type String
*/
dataSourceType: {
value: null
},
/**
* The character used to indicate the beginning or ending of a new value. Most commonly used
* is a ",".
*
* @attribute delimChar
* @default null
* @type String
*/
delimChar: {
value: null,
setter: function(value) {
if (isString(value) && (value.length > 0)) {
value = [value];
}
else if (!isArray(value)) {
value = A.Attribute.INVALID_VALUE;
}
return value;
}
},
/**
* If <a href="AutoComplete.html#config_typeAhead">typeAhead</a> is true, this
* will clear a selection when the overlay closes unless a user explicitly selects an item.
*
* @attribute forceSelection
* @default false
* @type Boolean
*/
forceSelection: {
value: false
},
/**
* The input field which will recieve the users input.
*
* @attribute input
* @default null
* @type String | Node
*/
input: {
value: null
},
/**
* The key or numeric index in the schema to match the result against.
*
* @attribute matchKey
* @default 0
* @type String | Number
*/
matchKey: {
value: 0
},
/**
* The maximum number of results to display.
*
*
* @attribute maxResultsDisplayed
* @default 10
* @type Number
*/
maxResultsDisplayed: {
value: 10
},
/**
* The minimum number of characters required to query the data source.
*
* @attribute minQueryLength
* @default 1
* @type Number
*/
minQueryLength: {
value: 1
},
/**
* The amount of time in seconds to delay before submitting the query.
*
* @attribute queryDelay
* @default 0.2
* @type Number
*/
queryDelay: {
value: 0.2,
getter: function(value) {
return value * 1000;
}
},
/**
* When IME usage is detected or interval detection is explicitly enabled,
* AutoComplete will detect the input value at the given interval and send a
* query if the value has changed.
*
* @attribute queryInterval
* @default 0.5
* @type Number
*/
queryInterval: {
value: 0.5,
getter: function(value) {
return value * 1000;
}
},
/**
* When <a href="AutoComplete.html#config_applyLocalFilter">applyLocalFilter</a> is true,
* setting this to true will match only results with the same case.
*
* @attribute queryMatchCase
* @default false
* @type Boolean
*/
queryMatchCase: {
value: false
},
/**
* When <a href="AutoComplete.html#config_applyLocalFilter">applyLocalFilter</a> is true,
* setting this to true will match results which contain the query anywhere in the text,
* instead of just matching just items that start with the query.
*
* @attribute queryMatchContains
* @default false
* @type Boolean
*/
queryMatchContains: {
value: false
},
/**
* For IO DataSources, AutoComplete will automatically insert a "?" between the server URI and
* the encoded query string. To prevent this behavior, you can
* set this value to false. If you need to customize this even further, you
* can override the <a href="AutoComplete.html#method_generateRequest">generateRequest</a> method.
*
* @attribute queryQuestionMark
* @default true
* @type Boolean
*/
queryQuestionMark: {
value: true
},
/**
* A valid configuration object for any of <a href="module_datasource.html">DataSource</a> schema plugins.
*
* @attribute schema
* @default null
* @type Object
*/
schema: {
value: null
},
/**
* A valid type of <a href="module_datasource.html">DataSource</a> schema plugin, such as array, json, xml, etc.
*
* @attribute schemaType
* @default array
* @type String
*/
schemaType: {
value: '',
validator: isString
},
/**
* Whether or not the input field should be updated with selections.
*
* @attribute suppressInputUpdate
* @default false
* @type Boolean
*/
suppressInputUpdate: {
value: false
},
/**
* If <a href="AutoComplete.html#config_autoHighlight">autoHighlight</a> is enabled, whether or not the
* input field should be automatically updated with the first result as the user types,
* automatically selecting the portion of the text the user has not typed yet.
*
* @attribute typeAhead
* @default false
* @type Boolean
*/
typeAhead: {
value: false
},
/**
* If <a href="AutoComplete.html#config_typeAhead">typeAhead</a> is true, number of seconds
* to delay before updating the input. In order to prevent certain race conditions, this value must
* always be greater than the <a href="AutoComplete.html#config_queryDelay">queryDelay</a>.
*
* @attribute typeAheadDelay
* @default 0.2
* @type Number
*/
typeAheadDelay: {
value: 0.2,
getter: function(value) {
return value * 1000;
}
},
/**
* The unique ID of the input element.
*
* @attribute uniqueName
* @default null
* @type String
*/
uniqueName: {
value: null
}
},
prototype: {
/**
* Construction logic executed during AutoComplete instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function(config) {
var instance = this;
instance._overlayAlign = A.mix({}, OVERLAY_ALIGN);
instance._createDataSource();
},
/**
* Create the DOM structure for the AutoComplete. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
var instance = this;
instance._renderInput();
instance._renderOverlay();
},
/**
* Bind the events on the AutoComplete UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var instance = this;
var button = instance.button;
var inputNode = instance.inputNode;
instance.dataSource.on('request', A.bind(button.set, button, ICON, ICON_LOADING));
button.on('click', instance._onButtonMouseDown, instance);
inputNode.on('blur', instance._onTextboxBlur, instance);
inputNode.on('focus', instance._onTextboxFocus, instance);
inputNode.on('keydown', instance._onTextboxKeyDown, instance);
inputNode.on('keypress', instance._onTextboxKeyPress, instance);
inputNode.on('keyup', instance._onTextboxKeyUp, instance);
var overlayBoundingBox = instance.overlay.get(BOUNDING_BOX);
overlayBoundingBox.on('click', instance._onContainerClick, instance);
overlayBoundingBox.on('mouseout', instance._onContainerMouseout, instance);
overlayBoundingBox.on('mouseover', instance._onContainerMouseover, instance);
overlayBoundingBox.on('scroll', instance._onContainerScroll, instance);
/**
* Handles the containerCollapse event. Fired when the container is hidden.
*
* @event containerCollapse
* @param {Event.Facade} event The containerCollapse event.
*/
instance.publish('containerCollapse');
/**
* Handles the containerExpand event. Fired when the container is shown.
*
* @event containerExpand
* @param {Event.Facade} event The containerExpand event.
*/
instance.publish('containerExpand');
/**
* Handles the containerPopulate event. Fired when the container is populated.
*
* @event containerPopulate
* @param {Event.Facade} event The containerPopulate event.
*/
instance.publish('containerPopulate');
/**
* Handles the dataError event. Fired when there is an error accessing the data.
*
* @event dataError
* @param {Event.Facade} event The dataError event.
*/
instance.publish('dataError');
/**
* Handles the dataRequest event. Fired when ever a query is sent to the data source.
*
* @event dataRequest
* @param {Event.Facade} event The dataRequest event.
*/
instance.publish('dataRequest');
/**
* Handles the dataReturn event. Fired when data successfully comes back from the data request.
*
* @event dataReturn
* @param {Event.Facade} event The dataReturn event.
*/
instance.publish('dataReturn');
/**
* Handles the itemArrowFrom event. Fired when the user navigates via the keyboard away from
* a selected item.
*
* @event itemArrowFrom
* @param {Event.Facade} event The itemArrowFrom event.
*/
instance.publish('itemArrowFrom');
/**
* Handles the itemArrowTo event. Fired when the user navigates via the keyboard to a selected item.
*
* @event itemArrowTo
* @param {Event.Facade} event The itemArrowTo event.
*/
instance.publish('itemArrowTo');
/**
* Handles the itemMouseOut event. Fired when the user mouses away from an item.
*
* @event itemMouseOut
* @param {Event.Facade} event The itemMouseOut event.
*/
instance.publish('itemMouseOut');
/**
* Handles the itemMouseOver event. Fired when the user mouses over an item.
*
* @event itemMouseOver
* @param {Event.Facade} event The itemMouseOver event.
*/
instance.publish('itemMouseOver');
/**
* Handles the itemSelect event. Fired when an item in the list is selected.
*
* @event itemSelect
* @param {Event.Facade} event The itemSelect event.
*/
instance.publish('itemSelect');
/**
* Handles the selectionEnforce event. Fired if <a href="Autocomplete.html#config_forceSelection">forceSelection</a>
* is enabled and the users input element has been cleared because it did not match one of the results.
*
* @event selectionEnforce
* @param {Event.Facade} event The selectionEnforce event.
*/
instance.publish('selectionEnforce');
/**
* Handles the textboxBlur event. Fired when the user leaves the input element.
*
* @event textboxBlur
* @param {Event.Facade} event The textboxBlur event.
*/
instance.publish('textboxBlur');
/**
* Handles the textboxChange event. Fired when the value in the input element is changed.
*
* @event textboxChange
* @param {Event.Facade} event The textboxChange event.
*/
instance.publish('textboxChange');
/**
* Handles the textboxFocus event. Fired when user moves focus to the input element.
*
* @event textboxFocus
* @param {Event.Facade} event The textboxFocus event.
*/
instance.publish('textboxFocus');
/**
* Handles the textboxKey event. Fired when the input element receives key input.
*
* @event textboxKey
* @param {Event.Facade} event The textboxKey event.
*/
instance.publish('textboxKey');
/**
* Handles the typeAhead event. Fired when the input element has been pre-filled by the type-ahead feature.
*
* @event typeAhead
* @param {Event.Facade} event The typeAhead event.
*/
instance.publish('typeAhead');
/**
* Handles the unmatchedItemSelect event. Fired when a user selects something that does
* not match any of the displayed results.
*
* @event unmatchedItemSelect
* @param {Event.Facade} event The unmatchedItemSelect event.
*/
instance.publish('unmatchedItemSelect');
instance.overlay.after('visibleChange', instance._realignContainer, instance);
},
/**
* Sync the AutoComplete UI. Lifecycle.
*
* @method syncUI
* @protected
*/
syncUI: function() {
var instance = this;
instance.inputNode.setAttribute('autocomplete', 'off');
},
/**
* Destructor lifecycle implementation for the Autocomplete class.
* Purges events attached to the node (and all child nodes).
*
* @method destructor
* @protected
*/
destructor: function() {
var instance = this;
instance.overlay.destroy();
},
/**
* An overridable method that is executed before the result container is shown.
* The method can return false to prevent the container from being shown.
*
* @method doBeforeExpandContainer
* @param {String} query The query that was submitted to the data source
* @param {Object} allResults The parsed results
* @return {Boolean}
*/
doBeforeExpandContainer: function() {
return true;
},
/**
* An overridable method that is executed before the result overlay is loaded with results.
*
* @method doBeforeLoadData
* @param {EventFacade} event
* @return {Boolean}
*/
doBeforeLoadData: function(event) {
return true;
},
/**
* Executed by the data source as a mechanism to do simple client-side
* filtering of the results.
*
* @method filterResults
* @param {EventFacade} event
* @return {Object} Filtered response object
*/
filterResults: function(event) {
var instance = this;
var callback = event.callback;
var query = event.request;
var response = event.response;
if (callback && callback.argument && callback.argument.query) {
query = callback.argument.query;
}
if (query) {
var dataSource = instance.dataSource;
var allResults = response.results;
var filteredResults = [];
var matchFound = false;
var matchKey = instance.get('matchKey');
var matchCase = instance.get('queryMatchCase');
var matchContains = instance.get('queryMatchContains');
var showAll = (query == '*');
var fields = instance.get('schema.resultFields');
for (var i = allResults.length - 1; i >= 0; i--) {
var result = allResults[i];
var strResult = null;
if (isString(result)) {
strResult = result;
}
else if (isArray(result)) {
strResult = result[0];
}
else if (fields) {
strResult = result[matchKey || fields[0]];
}
if (isString(strResult)) {
var keyIndex = -1;
if (matchCase) {
keyIndex = strResult.indexOf(decodeURIComponent(query));
}
else {
keyIndex = strResult.toLowerCase().indexOf(decodeURIComponent(query).toLowerCase());
}
if (
(showAll) ||
(!matchContains && (keyIndex === 0)) ||
(matchContains && (keyIndex > -1))
) {
filteredResults.unshift(result);
}
}
}
response.results = filteredResults;
}
return response;
},
/**
* An overridable method for formatting the result of the query before it's displayed in the overlay.
*
* @method formatResult
* @param {Object} result The result data object
* @param {String} request The current query string
* @param {String} resultMatch The string from the results that matches the query
* @return {String}
*/
formatResult: function(result, request, resultMatch) {
return resultMatch || '';
},
/**
* An overridable method that creates an object to be passed to the sendRequest
* method of the data source object. Useful to overwrite if you wish to create
* a custom request object before it's sent.
*
* @method generateRequest
* @param {String} query The string currently being entered
* @return {Object}
*/
generateRequest: function(query) {
return {
request: query
};
},
/**
* Handles the response for the display of the results. This is a callback method
* that is fired by the sendRequest method so that results are ready to be accessed.
*
* @method handleResponse
* @param {EventFacade} event
*/
handleResponse: function(event) {
var instance = this;
instance._populateList(event);
var iconClass = ICON_DEFAULT;
if (event.error) {
iconClass = ICON_ERROR;
}
instance.button.set(ICON, iconClass);
},
/**
* Sends a query request to the data source object.
*
* @method sendQuery
* @param {String} query Query string
*/
sendQuery: function(query) {
var instance = this;
instance.set('focused', null);
var newQuery = query;
if (instance.get('delimChar')) {
query = instance.inputNode.get('value') + query;
}
instance._sendQuery(newQuery);
},
/**
* Clears the query interval
*
* @method _clearInterval
* @private
*/
_clearInterval: function() {
var instance = this;
if (instance._queryIntervalId) {
clearInterval(instance._queryIntervalId);
instance._queryIntervalId = null;
}
},
/**
* When <a href="Autocomplete.html#config_forceSelection">forceSelection</a> is true and
* the user tries to leave the input element without selecting an item from the results,
* the user selection is cleared.
*
* @method _clearSelection
* @protected
*/
_clearSelection: function() {
var instance = this;
var delimChar = instance.get('delimChar');
var extraction = {
previous: '',
query: instance.inputNode.get('value')
};
if (delimChar) {
extraction = instance._extractQuery(instance.inputNode.get('value'));
}
instance.fire('selectionEnforce', extraction.query);
},
/**
* Creates the data source object using the passed in <a href="Autocomplete.html#config_dataSource">dataSource</a>,
* and if it is a string, will use the <a href="Autocomplete.html#config_dataSourceType">dataSourceType</a> to
* create a new <a href="module_datasource.html">DataSource</a> object.
*
* @method _createDataSource
* @protected
* @return {String}
*/
_createDataSource: function() {
var instance = this;
instance._queryTask = A.debounce(instance.sendQuery, instance.get('queryDelay'), instance);
var dataSource = instance.get('dataSource');
var data = dataSource;
var dataSourceType = instance.get('dataSourceType');
if (!(dataSource instanceof A.DataSource.Local)) {
if (!dataSourceType) {
dataSourceType = 'Local';
if (isFunction(data)) {
dataSourceType = 'Function';
}
else if (isString(data)) {
dataSourceType = 'IO';
}
}
dataSource = new A.DataSource[dataSourceType]({
source: data
});
}
dataSource.on('error', instance.handleResponse, instance);
dataSource.after('response', instance.handleResponse, instance);
dataSourceType = dataSource.name;
if (dataSourceType == 'dataSourceLocal') {
instance.set('applyLocalFilter', true);
}
instance.set('dataSource', dataSource);
instance.set('dataSourceType', dataSourceType);
instance.dataSource = dataSource;
var schema = instance.get('schema');
if (schema) {
if (schema.fn) {
instance.dataSource.plug(schema);
}
else {
var schemaType = instance.get('schemaType');
var schemaTypes = {
array: A.Plugin.DataSourceArraySchema,
json: A.Plugin.DataSourceJSONSchema,
text: A.Plugin.DataSourceTextSchema,
xml: A.Plugin.DataSourceXMLSchema
};
schemaType = schemaType.toLowerCase() || 'array';
instance.dataSource.plug({
fn: schemaTypes[schemaType],
cfg: {
schema: schema
}
});
}
}
instance.set('schema', schema);
},
/**
* Enables query interval detection for IME support.
*
* @method _enableIntervalDetection
* @protected
*/
_enableIntervalDetection: function() {
var instance = this;
var queryInterval = instance.get('queryInterval');
if (!instance._queryIntervalId && queryInterval) {
instance._queryInterval = setInterval(A.bind(instance._onInterval, instance), queryInterval);
}
},
/**
* Extracts the right most query from the delimited string in the input.
*
* @method _extractQuery
* @param {String} query String to parse
* @protected
* @return {String}
*/
_extractQuery: function(query) {
var instance = this;
var delimChar = instance.get('delimChar');
var delimIndex = -1;
var i = delimChar.length - 1;
var newIndex, queryStart, previous;
for (; i >= 0; i--) {
newIndex = query.lastIndexOf(delimChar[i]);
if (newIndex > delimIndex) {
delimIndex = newIndex;
}
}
if (delimChar[i] == ' ') {
for (var j = delimChar.length - 1; j >= 0; j--) {
if (query[delimIndex - 1] == delimChar[j]) {
delimIndex--;
break;
}
}
}
if (delimIndex > -1) {
queryStart = delimIndex + 1;
while (query.charAt(queryStart) == ' ') {
queryStart += 1;
}
previous = query.substring(0, queryStart);
query = query.substring(queryStart);
}
else {
previous = '';
}
return {
previous: previous,
query: query
};
},
/**
* Focuses the input element.
*
* @method _focus
* @protected
*/
_focus: function() {
var instance = this;
setTimeout(
function() {
instance.inputNode.focus();
},
0
);
},
/**
* If there is a currently selected item, the right arrow key will select
* that item and jump to the end of the input element, otherwise the container is closed.
*
* @method _jumpSelection
* @protected
*/
_jumpSelection: function() {
var instance = this;
if (instance._elCurListItem) {
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
},
/**
* Triggered by the up and down arrow keys, changes the currently selected list element item, and scrolls the
* container if necessary.
*
* @method _moveSelection
* @param {Number} keyCode The numeric code of the key pressed
* @protected
*/
_moveSelection: function(keyCode) {
var instance = this;
if (instance.overlay.get('visible')) {
var elCurListItem = instance._elCurListItem;
var curItemIndex = -1;
if (elCurListItem) {
curItemIndex = Number(elCurListItem.getAttribute('data-listItemIndex'));
}
var newItemIndex = curItemIndex - 1;
if (KeyMap.isKey(keyCode, DOWN)) {
newItemIndex = curItemIndex + 1;
}
if (newItemIndex == -1) {
newItemIndex = instance._displayedItems - 1;
}
if (newItemIndex >= instance._displayedItems) {
newItemIndex = 0;
}
if (newItemIndex < -2) {
return;
}
if (elCurListItem) {
instance._toggleHighlight(elCurListItem, 'from');
instance.fire('itemArrowFrom', elCurListItem);
}
if (newItemIndex == -1) {
if (instance.get('delimChar')) {
instance.inputNode.set('value', instance._pastSelections + instance._currentQuery);
}
else {
instance.inputNode.set('value', instance._currentQuery);
}
return;
}
if (newItemIndex == -2) {
instance._toggleContainer(false);
return;
}
var elNewListItem = instance.resultList.get('childNodes').item(newItemIndex);
var elContent = instance.overlay.get(CONTENT_BOX);
var contentOverflow = elContent.getStyle('overflow');
var contentOverflowY = elContent.getStyle('overflowY');
var scrollOn = (contentOverflow == 'auto') || (contentOverflow == 'scroll') || (contentOverflowY ==
'auto') || (contentOverflowY == 'scroll');
if (scrollOn &&
(newItemIndex > -1) &&
(newItemIndex < instance._displayedItems)) {
var newScrollTop = -1;
var liTop = elNewListItem.get('offsetTop');
var liBottom = liTop + elNewListItem.get('offsetHeight');
var contentHeight = elContent.get('offsetHeight');
var contentScrollTop = elContent.get('scrollTop');
var contentBottom = contentHeight + contentScrollTop;
if (KeyMap.isKey(keyCode, DOWN)) {
if (liBottom > contentBottom) {
newScrollTop = (liBottom - contentHeight);
}
else if (liBottom < contentScrollTop) {
newScrollTop = liTop;
}
}
else {
if (liTop < contentHeight) {
newScrollTop = liTop;
}
else if (liTop > contentBottom) {
newScrollTop = (liBottom - contentHeight);
}
}
if (newScrollTop > -1) {
elContent.set('scrollTop', newScrollTop);
}
}
instance._toggleHighlight(elNewListItem, 'to');
instance.fire('itemArrowTo', elNewListItem);
if (instance.get('typeAhead')) {
instance._updateValue(elNewListItem);
}
}
},
/**
* Called when the user mouses down on the button element in the combobox.
*
* @method _onButtonMouseDown
* @param {EventFacade} event
* @protected
*/
_onButtonMouseDown: function(event) {
var instance = this;
event.halt();
instance._focus();
instance._sendQuery(instance.inputNode.get('value') + '*');
},
/**
* Handles when a user clicks on the container.
*
* @method _onContainerClick
* @param {EventFacade} event
* @protected
*/
_onContainerClick: function(event) {
var instance = this;
var target = event.target;
var tagName = target.get('nodeName').toLowerCase();
event.halt();
while (target && (tagName != 'table')) {
switch (tagName) {
case 'body':
return;
case 'li':
instance._toggleHighlight(target, 'to');
instance._selectItem(target);
return;
default:
break;
}
target = target.get('parentNode');
if (target) {
tagName = target.get('nodeName').toLowerCase();
}
}
},
/**
* Handles when a user mouses out of the container.
*
* @method _onContainerMouseout
* @param {EventFacade} event
* @protected
*/
_onContainerMouseout: function(event) {
var instance = this;
var target = event.target;
var tagName = target.get('nodeName').toLowerCase();
while (target && (tagName != 'table')) {
switch (tagName) {
case 'body':
return;
case 'li':
instance._toggleHighlight(target, 'from');
instance.fire('itemMouseOut', target);
break;
case 'ul':
instance._toggleHighlight(instance._elCurListItem, 'to');
break;
case 'div':
if (target.hasClass(CSS_RESULTS_OVERLAY)) {
instance._overContainer = false;
return;
}
break;
default:
break;
}
target = target.get('parentNode');
if (target) {
tagName = target.get('nodeName').toLowerCase();
}
}
},
/**
* Handles when a user mouses over the container.
*
* @method _onContainerMouseover
* @param {EventFacade} event
* @protected
*/
_onContainerMouseover: function(event) {
var instance = this;
var target = event.target;
var tagName = target.get('nodeName').toLowerCase();
while (target && (tagName != 'table')) {
switch (tagName) {
case 'body':
return;
case 'li':
instance._toggleHighlight(target, 'to');
instance.fire('itemMouseOut', target);
break;
case 'div':
if (target.hasClass(CSS_RESULTS_OVERLAY)) {
instance._overContainer = true;
return;
}
break;
default:
break;
}
target = target.get('parentNode');
if (target) {
tagName = target.get('nodeName').toLowerCase();
}
}
},
/**
* Handles the container scroll events.
*
* @method _onContainerScroll
* @param {EventFacade} event
* @protected
*/
_onContainerScroll: function(event) {
var instance = this;
instance._focus();
},
/**
* Enables the query to be triggered based on detecting text input via intervals instead of via
* key events.
*
* @method _onInterval
* @protected
*/
_onInterval: function() {
var instance = this;
var curValue = instance.inputNode.get('value');
var lastValue = instance._lastValue;
if (curValue != lastValue) {
instance._lastValue = curValue;
instance._sendQuery(curValue);
}
},
/**
* Handles the input element losing focus.
*
* @method _onTextboxBlur
* @param {EventFacade} event
* @protected
*/
_onTextboxBlur: function(event) {
var instance = this;
if (!instance._overContainer || KeyMap.isKey(instance._keyCode, TAB)) {
if (!instance._itemSelected) {
var elMatchListItem = instance._textMatchesOption();
var overlayVisible = instance.overlay.get('visible');
if (!overlayVisible || (overlayVisible && isNull(elMatchListItem))) {
if (instance.get('forceSelection')) {
instance._clearSelection();
}
else {
instance.fire('unmatchedItemSelect', instance._currentQuery);
}
}
else {
if (instance.get('forceSelection')) {
instance._selectItem(elMatchListItem);
}
}
}
instance._clearInterval();
instance.blur();
if (instance._initInputValue !== instance.inputNode.get('value')) {
instance.fire('textboxChange');
}
instance.fire('textboxBlur');
instance._toggleContainer(false);
}
else {
instance._focus();
}
},
/**
* Handles the input element gaining focus.
*
* @method _onTextboxFocus
* @param {EventFacade} event
* @protected
*/
_onTextboxFocus: function(event) {
var instance = this;
if (!instance.get('focused')) {
instance.inputNode.setAttribute('autocomplete', 'off');
instance.focus();
instance._initInputValue = instance.inputNode.get('value');
instance.fire('textboxFocus');
}
},
/**
* Handles the keydown events on the input element for functional keys.
*
* @method _onTextboxKeyDown
* @param {EventFacade} event
* @protected
*/
_onTextboxKeyDown: function(event) {
var instance = this;
var keyCode = event.keyCode;
if (instance._typeAheadDelayId != -1) {
clearTimeout(instance._typeAheadDelayId);
}
if (event.isKey(TAB)) {
if (instance._elCurListItem) {
if (instance.get('delimChar') && instance._keyCode != keyCode) {
if (instance.overlay.get('visible')) {
event.halt();
}
}
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
}
else if (event.isKey(ENTER)) {
if (instance._elCurListItem) {
if (instance._keyCode != keyCode) {
if (instance.overlay.get('visible')) {
event.halt();
}
}
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
}
else if (event.isKey(ESC)) {
instance._toggleContainer(false);
}
else if (event.isKey(UP)) {
if (instance.overlay.get('visible')) {
event.halt();
instance._moveSelection(keyCode);
}
}
else if (event.isKey(RIGHT)) {
instance._jumpSelection();
}
else if (event.isKey(DOWN)) {
if (instance.overlay.get('visible')) {
event.halt();
instance._moveSelection(keyCode);
}
}
else {
instance._itemSelected = false;
instance._toggleHighlight(instance._elCurListItem, 'from');
instance.fire('textboxKey', keyCode);
}
if (event.isKey(ALT)) {
instance._enableIntervalDetection();
}
instance._keyCode = keyCode;
},
/**
* Handles the key press events of the input element.
*
* @method _onTextboxKeyPress
* @param {EventFacade} event
* @protected
*/
_onTextboxKeyPress: function(event) {
var instance = this;
var keyCode = event.keyCode;
if (event.isKey(TAB)) {
if (instance.overlay.get('visible')) {
if (instance.get('delimChar')) {
event.halt();
}
if (instance._elCurListItem) {
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
}
}
else if (event.isKey(ENTER)) {
if (instance.overlay.get('visible')) {
event.halt();
if (instance._elCurListItem) {
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
}
}
if (event.isKey(WIN_IME)) {
instance._enableIntervalDetection();
}
},
/**
* Handles the keyup events of the input element.
*
* @method _onTextboxKeyUp
* @param {EventFacade} event
* @protected
*/
_onTextboxKeyUp: function(event) {
var instance = this;
var input = instance.inputNode;
var value = input.get('value');
if (event.isSpecialKey() && !event.isKey(BACKSPACE)) {
return;
}
instance._queryTask(value);
},
/**
* Populates the container with list items of the query results.
*
* @method _populateList
* @param {EventFacade} event
* @protected
*/
_populateList: function(event) {
var instance = this;
if (instance._typeAheadDelayId != -1) {
clearTimeout(instance._typeAheadDelayId);
}
var query = event.request;
var response = event.response;
var callback = event.callback;
var showAll = (query == '*');
if (callback && callback.argument && callback.argument.query) {
event.request = query = callback.argument.query;
}
var ok = instance.doBeforeLoadData(event);
if (ok && !event.error) {
instance.fire('dataReturn', event);
var focused = instance.get('focused');
if (showAll || focused || focused === null) {
var currentQuery = decodeURIComponent(query);
instance._currentQuery = currentQuery;
instance._itemSelected = false;
var allResults = event.response.results;
var itemsToShow = Math.min(allResults.length, instance.get('maxResultsDisplayed'));
var fields = instance.get('schema.resultFields');
var matchKey = instance.get('matchKey');
if (!matchKey && fields) {
matchKey = fields[0];
}
else {
matchKey = matchKey || 0;
}
if (itemsToShow > 0) {
var allListItemEls = instance.resultList.get('childNodes');
allListItemEls.each(
function(node, i, nodeList) {
if (i < itemsToShow) {
var result = allResults[i];
var resultMatch = '';
if (isString(result)) {
resultMatch = result;
}
else if (isArray(result)) {
resultMatch = result[0];
}
else {
resultMatch = result[matchKey];
}
node._resultMatch = resultMatch;
node._resultData = result;
node.html(instance.formatResult(result, currentQuery, resultMatch));
node.removeClass(CSS_HIDDEN);
}
else {
node.addClass(CSS_HIDDEN);
}
}
);
instance._displayedItems = itemsToShow;
instance.fire('containerPopulate', query, allResults);
if (query != '*' && instance.get('autoHighlight')) {
var elFirstListItem = instance.resultList.get('firstChild');
instance._toggleHighlight(elFirstListItem, 'to');
instance.fire('itemArrowTo', elFirstListItem);
instance._typeAhead(elFirstListItem, query);
}
else {
instance._toggleHighlight(instance._elCurListItem, 'from');
}
ok = instance.doBeforeExpandContainer(query, allResults);
instance._toggleContainer(ok);
}
else {
instance._toggleContainer(false);
}
return;
}
}
else {
instance.fire('dataError', query);
}
},
/**
* Realigns the container to the input element.
*
* @method _realignContainer
* @param {EventFacade} event
* @protected
*/
_realignContainer: function(event) {
var instance = this;
var overlayAlign = instance._overlayAlign;
if (event.newVal) {
instance.overlay._uiSetAlign(overlayAlign.node, overlayAlign.points);
}
},
/**
* Handles the rendering of the input element.
*
* @method _renderInput
* @protected
*/
_renderInput: function() {
var instance = this;
var contentBox = instance.get(CONTENT_BOX);
var input = instance.get('input');
var comboConfig = {
field: {
labelText: false
},
icons: [
{
icon: 'icon-circle-arrow-down',
id: 'trigger',
handler: {
fn: instance._onButtonMouseDown,
context: instance
}
}
]
};
var inputReference = null;
var inputParent = null;
if (input) {
input = A.one(input);
comboConfig.field.node = input;
inputReference = input.next();
inputParent = input.get('parentNode');
}
var comboBox = new A.Combobox(comboConfig).render(contentBox);
if (inputParent) {
var comboBoundingBox = comboBox.get('boundingBox');
inputParent.insertBefore(comboBoundingBox, inputReference);
}
instance.inputNode = comboBox.get('node');
instance.button = comboBox.icons.item(0);
instance.set('uniqueName', A.stamp(instance.inputNode));
},
/**
* Pre-populates the container with the
* <a href="Autocomplete.html#config_maxResultsDisplayed">maxResultsDisplayed</a>
* number of list items.
*
* @method _renderListElements
* @protected
*/
_renderListElements: function() {
var instance = this;
var maxResultsDisplayed = instance.get('maxResultsDisplayed');
var resultList = instance.resultList;
var listItems = [];
while (maxResultsDisplayed--) {
listItems[maxResultsDisplayed] = '<li class="' + CSS_HIDDEN + ' ' + CSS_LIST_ITEM +
'" data-listItemIndex="' + maxResultsDisplayed + '"></li>';
}
resultList.html(listItems.join(''));
},
/**
* Handles the creation of the overlay where the result list will be displayed.
*
* @method _renderOverlay
* @protected
*/
_renderOverlay: function() {
var instance = this;
var overlayAlign = instance._overlayAlign;
overlayAlign.node = instance.inputNode;
var overlay = new A.OverlayBase({
align: overlayAlign,
bodyContent: '<ul></ul>',
visible: false,
width: instance.inputNode.get('offsetWidth'),
zIndex: 1
});
var contentBox = overlay.get(CONTENT_BOX);
overlay.get(BOUNDING_BOX).addClass(CSS_RESULTS_OVERLAY);
contentBox.addClass(CSS_RESULTS_OVERLAY_CONTENT);
overlay.render();
overlay.addTarget(instance);
instance.overlay = overlay;
instance.resultList = contentBox.one('ul');
instance.resultList.addClass(CSS_RESULTS_LIST);
instance._renderListElements();
},
/**
* Selects a list item from the query results.
*
* @method _selectItem
* @param {Node} elListItem The list item to select
* @protected
*/
_selectItem: function(elListItem) {
var instance = this;
instance._itemSelected = true;
instance._updateValue(elListItem);
instance._pastSelections = instance.inputNode.get('value');
instance._clearInterval();
instance.fire('itemSelect', elListItem, elListItem._resultData);
instance._toggleContainer(false);
},
/**
* Makes a query request to the data source.
*
* @method _sendQuery
* @param {String} query The query string
* @protected
*/
_sendQuery: function(query) {
var instance = this;
if (instance.get('disabled')) {
instance._toggleContainer(false);
return;
}
var delimChar = instance.get('delimChar');
var minQueryLength = instance.get('minQueryLength');
if (delimChar) {
var extraction = instance._extractQuery(query);
query = extraction.query;
instance._pastSelections = extraction.previous;
}
if ((query && (query.length < minQueryLength)) || (!query && minQueryLength > 0)) {
instance._queryTask.cancel();
instance._toggleContainer(false);
return;
}
query = encodeURIComponent(query);
if (instance.get('applyLocalFilter')) {
instance.dataSource.on('response', instance.filterResults, instance);
}
var request = instance.generateRequest(query);
instance.fire('dataRequest', query, request);
instance.dataSource.sendRequest(request);
},
/**
* Checks to see if the value typed by the user matches any of the
* query results.
*
* @method _textMatchesOption
* @protected
*/
_textMatchesOption: function() {
var instance = this;
var elMatch = null;
var displayedItems = instance._displayedItems;
var listItems = instance.resultList.get('childNodes');
for (var i = 0; i < displayedItems.length; i++) {
var elListItem = listItems.item(i);
var match = ('' + elListItem._resultMatch).toLowerCase();
if (match == instance._currentQuery.toLowerCase()) {
elMatch = elListItem;
break;
}
}
return elMatch;
},
/**
* Toggles the display of the results container.
*
* @method _toggleContainer
* @param {Boolean} show Flag to force the showing or hiding of the container
* @protected
*/
_toggleContainer: function(show) {
var instance = this;
var overlay = instance.overlay;
if (instance.get('alwaysShowContainer') && overlay.get('visible')) {
return;
}
if (!show) {
instance._toggleHighlight(instance._elCurListItem, 'from');
instance._displayedItems = 0;
instance._currentQuery = null;
}
if (show) {
overlay.show();
instance.fire('containerExpand');
}
else {
overlay.hide();
instance.fire('containerCollapse');
}
},
/**
* Toggles the highlighting of a list item, and removes the highlighting from the previous item
*
* @method _toggleHighlight
* @param {Node} elNewListItem The item to be highlighted
* @param {String} action Whether we are moving to or from an item. Valid values are "to" or "from".
* @protected
*/
_toggleHighlight: function(elNewListItem, action) {
var instance = this;
if (elNewListItem) {
if (instance._elCurListItem) {
instance._elCurListItem.removeClass(CSS_HIGLIGHT);
instance._elCurListItem = null;
}
if (action == 'to') {
elNewListItem.addClass(CSS_HIGLIGHT);
instance._elCurListItem = elNewListItem;
}
}
},
/**
* Updates in the input element with the first result as the user types,
* selecting the text the user has not typed yet.
*
* @method _typeAhead
* @param {Node} elListItem The selected list item
* @param {String} query The query string
* @protected
*/
_typeAhead: function(elListItem, query) {
var instance = this;
if (!instance.get('typeAhead') || KeyMap.isKey(instance._keyCode, BACKSPACE)) {
return;
}
var inputEl = A.Node.getDOMNode(instance.inputNode);
if (inputEl.setSelectionRange || inputEl.createTextRange) {
instance._typeAheadDelayId = setTimeout(
function() {
var value = inputEl.value;
var start = value.length;
instance._updateValue(elListItem);
var end = inputEl.value.length;
instance.inputNode.selectText(start, end);
var prefill = inputEl.value.substr(start, end);
instance.fire('typeAhead', query, prefill);
},
instance.get('typeAheadDelay')
);
}
},
/**
* Updates the input element with the selected query result. If
* <a href="Autocomplete.html#config_delimChar">delimChar</a> has been set,
* then the value gets appended with the delimiter.
*
* @method _updateValue
* @param {Node} elListItem The selected list item
* @protected
*/
_updateValue: function(elListItem) {
var instance = this;
if (!instance.get('suppressInputUpdate')) {
var input = instance.inputNode;
var resultMatch = elListItem._resultMatch;
var delimChar = instance.get('delimChar');
delimChar = (delimChar && delimChar[0]) || delimChar;
var newValue = '';
if (delimChar) {
newValue = instance._pastSelections;
newValue += resultMatch + delimChar;
if (delimChar != ' ') {
newValue += ' ';
}
}
else {
newValue = resultMatch;
}
input.set('value', newValue);
if (input.get('type') == 'textarea') {
input.set('scrollTop', input.get('scrollHeight'));
}
var end = newValue.length;
input.selectText(end, end);
instance._elCurListItem = elListItem;
}
},
_currentQuery: null,
_displayedItems: 0,
_elCurListItem: null,
_initInputValue: null,
_itemSelected: false,
_keyCode: null,
_lastValue: null,
_overContainer: false,
_pastSelections: '',
_typeAheadDelayId: -1
}
});
A.AutoCompleteDeprecated = AutoComplete;