aui-autocomplete [ 三 ] 源码解析

懒了, 只翻译几个目前用到的属性,其实英文写的很明白的,需要花时间看而已。
注意, 标记overridable的方法都是可以重写的,如何重写见 “aui-autocomplete [ 一 ]”

AUI.add('aui-autocomplete', function(A) {
/**
* The AutoComplete Utility
*
* @module aui-autocomplete
*/

var Lang = A.Lang,
isArray = Lang.isArray,
isString = Lang.isString,
isNull = Lang.isNull,
isFunction = Lang.isFunction,

getClassName = A.ClassNameManager.getClassName,

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 = 'circle-triangle-b',
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),

KEY_BACKSPACE = 8,
KEY_TAB = 9,
KEY_ENTER = 13,
KEY_SHIFT = 16,
KEY_CTRL = 17,
KEY_ALT = 18,
KEY_CAPS_LOCK = 20,
KEY_ESC = 27,
KEY_PAGEUP = 33,
KEY_END = 35,
KEY_HOME = 36,
KEY_UP = 38,
KEY_DOWN = 40,
KEY_RIGHT = 39,
KEY_LEFT = 37,
KEY_PRINT_SCREEN = 44,
KEY_INSERT = 44,
KEY_KOREAN_IME = 229,

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.AutoComplete({
* 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
},

/**
* 结果集显示时高亮第一个item

* 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 是否需要右侧的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
},

/**
* 不设置,单选; 设置此项可多选,input框中多个选中值用delimChar分隔,如aa,bb,cc

* 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;
}
},

/**
* The input field which will recieve the users input.
*
* @attribute input
* @default null
* @type String | Node
*/
input: {
value: null
},

/**
* 用schema中的哪一项匹配输入值, input默认显示选中项的matchKey字段
* 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
},

/**
* false,从第一个字符开始匹配; true, 结果集中的item, 任意位置的字符与输入字符相同即匹配。

* 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
},

/**
* autoHighlight = true时有效
* true, 自动选中第一个匹配结果,即使用户未输入完整, 同时更新值到input框中。
* 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));

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');
},

/**
* 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 = new A.DelayedTask(instance.sendQuery, 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('dataSource', 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
);
},

/**
* Whether or not the pressed key triggers some functionality or if it should
* be ignored.
*
* @method _isIgnoreKey
* @param {keyCode} Number The numeric code of the key pressed
* @protected
* @return {String}
*/
_isIgnoreKey: function(keyCode) {
var instance = this;

if (
(keyCode == KEY_TAB) ||
(keyCode == KEY_ENTER) ||
(keyCode == KEY_SHIFT) ||
(keyCode == KEY_CTRL) ||
(keyCode >= KEY_ALT && keyCode <= KEY_CAPS_LOCK) ||
(keyCode == KEY_ESC) ||
(keyCode >= KEY_PAGEUP && keyCode <= KEY_END) ||
(keyCode >= KEY_HOME && keyCode <= KEY_DOWN) ||
(keyCode >= KEY_PRINT_SCREEN && keyCode <= KEY_INSERT) ||
(keyCode == KEY_KOREAN_IME)
) {
return true;
}

return false;
},

/**
* 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 (keyCode == KEY_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 (keyCode == KEY_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.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 || (instance._keyCode == KEY_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);
}

switch (keyCode) {
case KEY_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);
}
break;

case KEY_ENTER:
if (instance._elCurListItem) {
if (instance._keyCode != keyCode) {
if (instance.overlay.get('visible')) {
event.halt();
}
}

instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
break;

case KEY_ESC:
instance._toggleContainer(false);
return;

case KEY_UP:
if (instance.overlay.get('visible')) {
event.halt();

instance._moveSelection(keyCode);
}
break;

case KEY_RIGHT:
instance._jumpSelection();
break;

case KEY_DOWN:
if (instance.overlay.get('visible')) {
event.halt();

instance._moveSelection(keyCode);
}
break;

default:
instance._itemSelected = false;
instance._toggleHighlight(instance._elCurListItem, 'from');

instance.fire('textboxKey', keyCode);
break;
}

if (keyCode == KEY_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;

switch (keyCode) {
case KEY_TAB:
if (instance.overlay.get('visible')) {
if (instance.get('delimChar')) {
event.halt();
}

if (instance._elCurListItem) {
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
}
break;

case 13:
if (instance.overlay.get('visible')) {
event.halt();

if (instance._elCurListItem) {
instance._selectItem(instance._elCurListItem);
}
else {
instance._toggleContainer(false);
}
}
break;

default:
break;
}

if (keyCode == KEY_KOREAN_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');
var keyCode = event.keyCode;

if (instance._isIgnoreKey(keyCode)) {
return;
}

instance._queryTask.delay(instance.get('queryDelay'), null, null, [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: 'circle-triangle-b',
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('trigger');

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')
}
);

var contentBox = overlay.get(CONTENT_BOX);

overlay.get(BOUNDING_BOX).addClass(CSS_RESULTS_OVERLAY);

contentBox.addClass(CSS_RESULTS_OVERLAY_CONTENT);

overlay.render(document.body);

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') || instance._keyCode == KEY_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.AutoComplete = AutoComplete;

}, '1.0.1' ,{skinnable:true, requires:['aui-base','aui-overlay-base','datasource','dataschema','aui-form-combobox']});

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值