/* * jQuery textarea suggest plugin * * Copyright (c) 2009-2010 Roman Imankulov * * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * * Requires: * - jQuery (tested with 1.3.x and 1.4.x) * - jquery.a-tools >= 1.4.1 (http://plugins.jquery.com/project/a-tools) */ /*globals jQuery,document */ (function ($) { // workaround for Opera browser if (navigator.userAgent.match(/opera/i)) { $(document).keypress(function (e) { if ($.asuggestFocused) { $.asuggestFocused.focus(); $.asuggestFocused = null; e.preventDefault(); e.stopPropagation(); } }); } $.asuggestKeys = { UNKNOWN: 0, SHIFT: 16, CTRL: 17, ALT: 18, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, DEL: 46, TAB: 9, RETURN: 13, ESC: 27, COMMA: 188, PAGEUP: 33, PAGEDOWN: 34, BACKSPACE: 8, SPACE: 32 }; $.asuggestFocused = null; $.fn.asuggest = function (suggests, options) { return this.each(function () { $.makeSuggest(this, suggests, options); }); }; $.fn.asuggest.defaults = { 'delimiters': '\n ', 'minChunkSize': 1, 'cycleOnTab': true, 'autoComplete': true, 'endingSymbols': ' ', 'stopSuggestionKeys': [$.asuggestKeys.RETURN, $.asuggestKeys.SPACE], 'ignoreCase': false }; /* Make suggest: * * create and return jQuery object on the top of DOM object * and store suggests as part of this object * * @param area: HTML DOM element to add suggests to * @param suggests: The array of suggest strings * @param options: The options object */ $.makeSuggest = function (area, suggests, options) { options = $.extend({}, $.fn.asuggest.defaults, options); var KEY = $.asuggestKeys, $area = $(area); $area.suggests = suggests; $area.options = options; /* Internal method: get the chunk of text before the cursor */ $area.getChunk = function () { var delimiters = this.options.delimiters.split(''), // array of chars textBeforeCursor = this.val().substr(0, this.getSelection().start), indexOfDelimiter = -1, i, d, idx; for (i = 0; i < delimiters.length; i++) { d = delimiters[i]; idx = textBeforeCursor.lastIndexOf(d); if (idx > indexOfDelimiter) { indexOfDelimiter = idx; } } if (indexOfDelimiter < 0) { return textBeforeCursor; } else { return textBeforeCursor.substr(indexOfDelimiter + 1); } }; /* Internal method: get completion. * If performCycle is true then analyze getChunk() and and getSelection() */ $area.getCompletion = function (performCycle) { var text = this.getChunk(), selectionText = this.getSelection().text, suggests = this.suggests, foundAlreadySelectedValue = false, firstMatchedValue = null, i, suggest; // search the variant for (i = 0; i < suggests.length; i++) { suggest = suggests[i]; if ($area.options.ignoreCase) { suggest = suggest.toLowerCase(); text = text.toLowerCase(); } // some variant is found if (suggest.indexOf(text) === 0) { if (performCycle) { if (text + selectionText === suggest) { foundAlreadySelectedValue = true; } else if (foundAlreadySelectedValue) { return suggest.substr(text.length); } else if (firstMatchedValue === null) { firstMatchedValue = suggest; } } else { return suggest.substr(text.length); } } } if (performCycle && firstMatchedValue) { return firstMatchedValue.substr(text.length); } else { return null; } }; $area.updateSelection = function (completion) { if (completion) { var _selectionStart = $area.getSelection().start, _selectionEnd = _selectionStart + completion.length; if ($area.getSelection().text === "") { if ($area.val().length === _selectionStart) { // Weird IE workaround, I really have no idea why it works $area.setCaretPos(_selectionStart + 10000); } $area.insertAtCaretPos(completion); } else { $area.replaceSelection(completion); } $area.setSelection(_selectionStart, _selectionEnd); } }; $area.unbind('keydown.asuggest').bind('keydown.asuggest', function (e) { if (e.keyCode === KEY.TAB) { if ($area.options.cycleOnTab) { var chunk = $area.getChunk(); if (chunk.length >= $area.options.minChunkSize) { $area.updateSelection($area.getCompletion(true)); } e.preventDefault(); e.stopPropagation(); $area.focus(); $.asuggestFocused = this; return false; } } // Check for conditions to stop suggestion if ($area.getSelection().length && $.inArray(e.keyCode, $area.options.stopSuggestionKeys) !== -1) { // apply suggestion. Clean up selection and insert a space var _selectionEnd = $area.getSelection().end + $area.options.endingSymbols.length; var _text = $area.getSelection().text + $area.options.endingSymbols; $area.replaceSelection(_text); $area.setSelection(_selectionEnd, _selectionEnd); e.preventDefault(); e.stopPropagation(); this.focus(); $.asuggestFocused = this; return false; } }); $area.unbind('keyup.asuggest').bind('keyup.asuggest', function (e) { var hasSpecialKeys = e.altKey || e.metaKey || e.ctrlKey, hasSpecialKeysOrShift = hasSpecialKeys || e.shiftKey; switch (e.keyCode) { case KEY.UNKNOWN: // Special key released case KEY.SHIFT: case KEY.CTRL: case KEY.ALT: case KEY.RETURN: // we don't want to suggest when RETURN key has pressed (another IE workaround) break; case KEY.TAB: if (!hasSpecialKeysOrShift && $area.options.cycleOnTab) { break; } case KEY.ESC: case KEY.BACKSPACE: case KEY.DEL: case KEY.UP: case KEY.DOWN: case KEY.LEFT: case KEY.RIGHT: if (!hasSpecialKeysOrShift && $area.options.autoComplete) { $area.replaceSelection(""); } break; default: if (!hasSpecialKeys && $area.options.autoComplete) { var chunk = $area.getChunk(); if (chunk.length >= $area.options.minChunkSize) { $area.updateSelection($area.getCompletion(false)); } } break; } }); return $area; }; }(jQuery));