/**
* The ACE Editor Component
*
* @module aui-ace-editor
* @submodule aui-ace-autocomplete-base
*/
var Lang = A.Lang,
AArray = A.Array,
Do = A.Do,
ADOM = A.DOM,
FILL_MODE_INSERT = 1,
FILL_MODE_OVERWRITE = 0,
Base = function() {};
/**
* A base class for AutoCompleteBase.
*
* @class A.AceEditor.AutoCompleteBase
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
Base.prototype = {
/**
* Construction logic executed during AutoCompleteBase instantiation.
* Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this,
processor;
instance._editorCommands = [];
A.after(instance._bindUIACBase, instance, 'renderUI');
processor = instance.get('processor');
if (processor && !processor.get('host')) {
processor.set('host', instance);
}
instance._onResultsErrorFn = A.bind('_onResultsError', instance);
instance._onResultsSuccessFn = A.bind('_onResultsSuccess', instance);
},
/**
* Inserts the provided suggestion as a string to the editor. The added text
* can overwrite the match or to be inserted depending on the `fillMode`
* attribute.
*
* @method _addSuggestion
* @param {String} content
* @protected
* @return {Do.Halt} Instance of Do.Halt to stop function execution
*/
_addSuggestion: function(content) {
var instance = this,
cursorPosition,
data,
editor,
matchParams,
overwriteRange,
Range,
startColumn,
startRow;
instance._lockEditor = true;
editor = instance._getEditor();
data = instance.get('processor').getSuggestion(instance._matchParams.match, content);
if (instance.get('fillMode') === Base.FILL_MODE_OVERWRITE) {
matchParams = instance._matchParams;
startRow = matchParams.row;
startColumn = matchParams.column - matchParams.match.content.length;
cursorPosition = editor.getCursorPosition();
Range = ace.require('ace/range').Range;
overwriteRange = new Range(startRow, startColumn, cursorPosition.row, cursorPosition.column);
editor.getSession().replace(overwriteRange, data);
}
else {
editor.insert(data);
}
editor.focus();
instance._lockEditor = false;
instance.fire('addSuggestion', data);
return new Do.Halt(null);
},
/**
* Binds editor events.
*
* @method _bindUIACBase
* @protected
*/
_bindUIACBase: function() {
var instance = this,
editor;
instance.publish(
'cursorChange', {
defaultFn: instance._defaultCursorChangeFn
}
);
editor = instance._getEditor();
instance._onChangeFn = A.bind('_onEditorChange', instance);
editor.on('change', instance._onChangeFn);
editor.commands.addCommand({
name: 'showAutoComplete',
bindKey: A.merge(
instance.get('showListKey'), {
sender: 'editor|cli'
}
),
exec: function() {
var cursorPosition = editor.getCursorPosition();
instance._processAutoComplete(cursorPosition.row, cursorPosition.column);
}
});
instance._onEditorChangeCursorFn = A.bind('_onEditorChangeCursor', instance);
editor.getSelection().on('changeCursor', instance._onEditorChangeCursorFn);
instance.on('destroy', instance._destroyUIACBase, instance);
},
/**
* Checks if the cursor is out of the row/column on the latest match. If so,
* fires an `cursorOut` event.
*
* @method _defaultCursorChangeFn
* @param {CustomEvent} event The fired event
* @protected
*/
_defaultCursorChangeFn: function() {
var instance = this,
column,
cursorPosition,
editor,
matchParams,
row;
editor = instance._getEditor();
cursorPosition = editor.getCursorPosition();
row = cursorPosition.row;
column = cursorPosition.column;
matchParams = instance._matchParams;
if (row !== matchParams.row || column < matchParams.match.start) {
instance.fire('cursorOut');
}
},
/**
* Removes the listeners to editor commands.
*
* @method _destroyUIACBase
* @protected
*/
_destroyUIACBase: function() {
var instance = this,
editor;
editor = instance._getEditor();
editor.commands.removeCommand('showAutoComplete');
editor.removeListener('change', instance._onChangeFn);
editor.getSelection().removeListener('changeCursor', instance._onEditorChangeCursorFn);
instance._removeAutoCompleteCommands();
},
/**
* Returns the editor instance.
*
* @method _getEditor
* @protected
* @return {Object} Editor instance
*/
_getEditor: function() {
var instance = this;
return instance.get('host').getEditor();
},
/**
* Filters and sorts the found suggestions using the existing chain of
* `filters` and `sorters`.
*
* @method _filterResults
* @param {String} content
* @param {Array} results
* @protected
* @return {Array} The filtered results
*/
_filterResults: function(content, results) {
var instance = this,
filters,
i,
length,
sorters;
filters = instance.get('filters');
for (i = 0, length = filters.length; i < length; ++i) {
results = filters[i].call(instance, content, results.concat());
if (!results.length) {
break;
}
}
sorters = instance.get('sorters');
for (i = 0, length = sorters.length; i < length; ++i) {
results = sorters[i].call(instance, content, results.concat());
if (!results.length) {
break;
}
}
return results;
},
/**
* Checks for new line or tab character and adds a suggestion to the editor
* if so.
*
* @method _handleEnter
* @param {String} text
* @protected
* @return {Do.Halt} If text is new line or tab character, returns an
* instance of Do.Halt to stop function execution
*/
_handleEnter: function(text) {
var instance = this,
selectedEntry;
if (text === '\n' || text === '\t') {
selectedEntry = instance._getSelectedEntry();
return instance._addSuggestion(selectedEntry);
}
},
/**
* Handles editor change event. If editor is not locked and data action is
* insert or remove text, process auto complete.
*
* @method _onEditorChange
* @param {CustomEvent} event The fired event
* @protected
*/
_onEditorChange: function(event) {
var instance = this,
column,
data,
dataAction,
dataRange,
endRow,
startRow;
data = event.data;
dataAction = data.action;
if (!instance._lockEditor && (dataAction === 'insertText' || dataAction === 'removeText')) {
dataRange = data.range;
column = dataRange.start.column;
endRow = dataRange.end.row;
startRow = dataRange.start.row;
if (dataAction === 'insertText' && startRow === endRow) {
instance._processAutoComplete(startRow, column + 1);
}
instance.fire(
dataAction, {
column: column,
dataRange: dataRange,
endRow: endRow,
startRow: startRow
}
);
}
},
/**
* Fires cursor change event providing the current position as event
* payload.
*
* @method _onEditorChangeCursor
* @param {CustomEvent} event The fired event
* @protected
*/
_onEditorChangeCursor: function() {
var instance = this;
instance.fire('cursorChange', instance._getEditor().getCursorPosition());
},
/**
* Fires an `resultsError` event containing the error.
*
* @method _onResultsError
* @param error
* @protected
*/
_onResultsError: function(error) {
var instance = this;
instance.fire('resultsError', error);
},
/**
* Updates `results` attribute with the provided results.
*
* @method _onResultsSuccess
* @param {Array} results
* @protected
*/
_onResultsSuccess: function(results) {
var instance = this;
instance.set('results', results);
},
/**
* Overwrites the following editor commands:
* onTextInput,
* golinedown
* golineup
* gotoend
* gotolineend
* gotolinestart
* gotopagedown
* gotopageup
* gotostart
*
* @method _overwriteCommands
* @protected
*/
_overwriteCommands: function() {
var instance = this,
commands,
editor;
editor = instance._getEditor();
commands = editor.commands.commands;
instance._editorCommands.push(
Do.before(instance._handleEnter, editor, 'onTextInput', instance),
Do.before(instance._handleKey, commands.golinedown, 'exec', instance, 40),
Do.before(instance._handleKey, commands.golineup, 'exec', instance, 38),
Do.before(instance._handleKey, commands.gotoend, 'exec', instance, 35),
Do.before(instance._handleKey, commands.gotolineend, 'exec', instance, 35),
Do.before(instance._handleKey, commands.gotolinestart, 'exec', instance, 36),
Do.before(instance._handleKey, commands.gotopagedown, 'exec', instance, 34),
Do.before(instance._handleKey, commands.gotopageup, 'exec', instance, 33),
Do.before(instance._handleKey, commands.gotostart, 'exec', instance, 36)
);
},
/**
* Checks for phrase match.
*
* @method _phraseMatch
* @param {String} content The content to be checked for phrase match
* @param {Array} results The results to be filtered
* @param {Boolean} caseSensitive Should the check be case sensitive or not
* @protected
* @return {Array} The filtered results
*/
_phraseMatch: function(content, results, caseSensitive) {
if (!content) {
return results;
}
return AArray.filter(
results,
function(item) {
var result = true;
if (item === content) {
result = false;
}
else {
if (!caseSensitive) {
item = item.toLowerCase();
content = content.toLowerCase();
}
if (item.indexOf(content) === -1) {
result = false;
}
}
return result;
}
);
},
/**
* Invokes the loaded content processor and checks for match. If found,
* provides the match together with information about current row and column
* and invokes processor's `getResults` function in order to retrieve
* results. At the end, fires and `match` event with the following
* properties: column - the current column coords - the page coordinates of
* the match line - the current line match - the current match row - the
* current row
*
* @method _processAutoComplete
* @param {Number} row The row on which match happened
* @param {Number} column The column on which match happened
* @protected
*/
_processAutoComplete: function(row, column) {
var instance = this,
col,
coords,
editor,
line,
match,
processor;
col = column;
editor = instance._getEditor();
line = editor.getSession().getLine(row);
line = line.substring(0, column);
processor = instance.get('processor');
match = processor.getMatch(line);
if (Lang.isObject(match)) {
coords = editor.renderer.textToScreenCoordinates(row, column);
coords.pageX += ADOM.docScrollX();
coords.pageY += ADOM.docScrollY();
instance._matchParams = {
column: column,
match: match,
row: row
};
processor.getResults(match, instance._onResultsSuccessFn, instance._onResultsErrorFn);
}
instance.fire(
'match', {
column: column,
coords: coords,
line: line,
match: match,
row: row
}
);
},
/**
* Detaches the previously attached editor commands.
*
* @method _removeAutoCompleteCommands
* @protected
*/
_removeAutoCompleteCommands: function() {
var instance = this;
(new A.EventHandle(instance._editorCommands)).detach();
instance._editorCommands.length = 0;
},
/**
* Sorts the results in ascending order, taking in consideration the length
* of the content.
*
* @method _sortAscLength
* @param {String} content The text content
* @param {Array} results The results to be filtered
* @param {Boolean} caseSensitive Should we filter these results
* alphabetically
* @protected
* @return {Array} The sorted results
*/
_sortAscLength: function(content, results, caseSensitive) {
return results.sort(
function(item1, item2) {
var index1,
index2,
result;
result = 0;
if (!caseSensitive) {
item1 = item1.toLowerCase();
item2 = item2.toLowerCase();
}
index1 = item1.indexOf(content);
index2 = item2.indexOf(content);
if (index1 === 0 && index2 === 0) {
result = item1.localeCompare(item2);
}
else if (index1 === 0) {
result = -1;
}
else if (index2 === 0) {
result = 1;
}
else {
result = item1.localeCompare(item2);
}
return result;
}
);
},
/**
* Validates the value of `fillMode` attribute.
*
* @method _validateFillMode
* @param value
* @protected
* @return {Boolean} True if mode is 'overwrite' - value '0' or 'insert' -
* value '1'
*/
_validateFillMode: function(value) {
return (value === Base.FILL_MODE_OVERWRITE || value === Base.FILL_MODE_INSERT);
}
};
/**
* Exposes a constant for insert fill mode. See `fillMode` for more information.
*
* @property FILL_MODE_INSERT
* @static
*/
Base.FILL_MODE_INSERT = FILL_MODE_INSERT;
/**
* Exposes a constant for overwrite fill mode. See `fillMode` for more
* information.
*
* @property FILL_MODE_OVERWRITE
* @static
*/
Base.FILL_MODE_OVERWRITE = FILL_MODE_OVERWRITE;
/**
* Static property which provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
Base.NAME = 'ace-autocomplete-base';
/**
* Static property which provides a string to identify the namespace.
*
* @property NS
* @type String
* @static
*/
Base.NS = 'ace-autocomplete-base';
/**
* Static property used to define the default attribute
* configuration for AutoCompleteBase.
*
* @property ATTRS
* @type Object
* @static
*/
Base.ATTRS = {
/**
* The mode in which the AutoComplete should operate. Can be one of these:
* INSERT - value '0' or OVERWRITE - value '1'. In case of INSERT mode, when
* Editor adds a suggestion, it will be added next to the matched
* expression. In case of OVERWRITE mode, the suggestion will overwrite the
* matched expression.
*
* @attribute fillMode
* @default 1 - OVERWRITE mode
* @type Number
*/
fillMode: {
validator: '_validateFillMode',
value: Base.FILL_MODE_OVERWRITE
},
/**
* Provides an array of filter functions which will filter the results. By
* default there is one function which provides phrase match filtering.
*
* @attribute filters
* @default Array with one function which provides phrase match filtering
* @type Array
*/
filters: {
valueFn: function() {
var instance = this;
return [
instance._phraseMatch
];
}
},
/**
* The default processor which will be used to process matches.
*
* @attribute processor
* @type Object | Function
*/
processor: {
validator: function(value) {
return Lang.isObject(value) || Lang.isFunction(value);
}
},
/**
* The keyboard combination which should be used to show the list with found
* results.
*
* @attribute showListKey
* @default 'Alt-Space' for Mac, 'Ctrl-Space' for PC
* @type Object
*/
showListKey: {
validator: Lang.isObject,
value: {
mac: 'Alt-Space',
win: 'Ctrl-Space'
}
},
/**
* Provides an array of sorter functions which will sort the results. By
* default there is one function which sorts the results in ascending order.
*
* @attribute sorters
* @default Array with one function which sorts results in ascending order
* @type Array
*/
sorters: {
valueFn: function() {
var instance = this;
return [
instance._sortAscLength
];
}
}
};
A.AceEditor.AutoCompleteBase = Base;