Created
February 1, 2019 20:36
-
-
Save Kcko/daaa7ebe2e8cbe32760f8fad5040dd7a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* jQuery Autocomplete plugin 1.2.3 | |
* | |
* Copyright (c) 2009 Jörn Zaefferer | |
* | |
* Dual licensed under the MIT and GPL licenses: | |
* http://www.opensource.org/licenses/mit-license.php | |
* http://www.gnu.org/licenses/gpl.html | |
* | |
* With small modifications by Alfonso Gómez-Arzola. | |
* See changelog for details. | |
* | |
*/ | |
;(function($) { | |
$.fn.extend({ | |
autocomplete: function(urlOrData, options) { | |
var isUrl = typeof urlOrData == "string"; | |
options = $.extend({}, $.Autocompleter.defaults, { | |
url: isUrl ? urlOrData : null, | |
data: isUrl ? null : urlOrData, | |
delay: isUrl ? $.Autocompleter.defaults.delay : 10, | |
max: options && !options.scroll ? 10 : 150, | |
noRecord: "No Records." | |
}, options); | |
// if highlight is set to false, replace it with a do-nothing function | |
options.highlight = options.highlight || function(value) { return value; }; | |
// if the formatMatch option is not specified, then use formatItem for backwards compatibility | |
options.formatMatch = options.formatMatch || options.formatItem; | |
return this.each(function() { | |
new $.Autocompleter(this, options); | |
}); | |
}, | |
result: function(handler) { | |
return this.bind("result", handler); | |
}, | |
search: function(handler) { | |
return this.trigger("search", [handler]); | |
}, | |
flushCache: function() { | |
return this.trigger("flushCache"); | |
}, | |
setOptions: function(options){ | |
return this.trigger("setOptions", [options]); | |
}, | |
unautocomplete: function() { | |
return this.trigger("unautocomplete"); | |
} | |
}); | |
$.Autocompleter = function(input, options) { | |
var KEY = { | |
UP: 38, | |
DOWN: 40, | |
DEL: 46, | |
TAB: 9, | |
RETURN: 13, | |
ESC: 27, | |
COMMA: 188, | |
PAGEUP: 33, | |
PAGEDOWN: 34, | |
BACKSPACE: 8 | |
}; | |
var globalFailure = null; | |
if(options.failure != null && typeof options.failure == "function") { | |
globalFailure = options.failure; | |
} | |
// Create $ object for input element | |
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); | |
var timeout; | |
var previousValue = ""; | |
var cache = $.Autocompleter.Cache(options); | |
var hasFocus = 0; | |
var lastKeyPressCode; | |
var config = { | |
mouseDownOnSelect: false | |
}; | |
var select = $.Autocompleter.Select(options, input, selectCurrent, config); | |
var blockSubmit; | |
// prevent form submit in opera when selecting with return key | |
navigator.userAgent.indexOf("Opera") != -1 && $(input.form).bind("submit.autocomplete", function() { | |
if (blockSubmit) { | |
blockSubmit = false; | |
return false; | |
} | |
}); | |
// older versions of opera don't trigger keydown multiple times while pressed, others don't work with keypress at all | |
$input.bind((navigator.userAgent.indexOf("Opera") != -1 && !'KeyboardEvent' in window ? "keypress" : "keydown") + ".autocomplete", function(event) { | |
// a keypress means the input has focus | |
// avoids issue where input had focus before the autocomplete was applied | |
hasFocus = 1; | |
// track last key pressed | |
lastKeyPressCode = event.keyCode; | |
switch(event.keyCode) { | |
case KEY.UP: | |
if ( select.visible() ) { | |
event.preventDefault(); | |
select.prev(); | |
} else { | |
onChange(0, true); | |
} | |
break; | |
case KEY.DOWN: | |
if ( select.visible() ) { | |
event.preventDefault(); | |
select.next(); | |
} else { | |
onChange(0, true); | |
} | |
break; | |
case KEY.PAGEUP: | |
if ( select.visible() ) { | |
event.preventDefault(); | |
select.pageUp(); | |
} else { | |
onChange(0, true); | |
} | |
break; | |
case KEY.PAGEDOWN: | |
if ( select.visible() ) { | |
event.preventDefault(); | |
select.pageDown(); | |
} else { | |
onChange(0, true); | |
} | |
break; | |
// matches also semicolon | |
case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: | |
case KEY.TAB: | |
case KEY.RETURN: | |
if( selectCurrent() ) { | |
// stop default to prevent a form submit, Opera needs special handling | |
event.preventDefault(); | |
blockSubmit = true; | |
return false; | |
} | |
break; | |
case KEY.ESC: | |
select.hide(); | |
break; | |
default: | |
clearTimeout(timeout); | |
timeout = setTimeout(onChange, options.delay); | |
break; | |
} | |
}).focus(function(){ | |
// track whether the field has focus, we shouldn't process any | |
// results if the field no longer has focus | |
hasFocus++; | |
}).blur(function() { | |
hasFocus = 0; | |
if (!config.mouseDownOnSelect) { | |
hideResults(); | |
} | |
}).click(function() { | |
// show select when clicking in a focused field | |
// but if clickFire is true, don't require field | |
// to be focused to begin with; just show select | |
if( options.clickFire ) { | |
if ( !select.visible() ) { | |
onChange(0, true); | |
} | |
} else { | |
if ( hasFocus++ > 1 && !select.visible() ) { | |
onChange(0, true); | |
} | |
} | |
}).bind("search", function() { | |
// TODO why not just specifying both arguments? | |
var fn = (arguments.length > 1) ? arguments[1] : null; | |
function findValueCallback(q, data) { | |
var result; | |
if( data && data.length ) { | |
for (var i=0; i < data.length; i++) { | |
if( data[i].result.toLowerCase() == q.toLowerCase() ) { | |
result = data[i]; | |
break; | |
} | |
} | |
} | |
if( typeof fn == "function" ) fn(result); | |
else $input.trigger("result", result && [result.data, result.value]); | |
} | |
$.each(trimWords($input.val()), function(i, value) { | |
request(value, findValueCallback, findValueCallback); | |
}); | |
}).bind("flushCache", function() { | |
cache.flush(); | |
}).bind("setOptions", function() { | |
$.extend(true, options, arguments[1]); | |
// if we've updated the data, repopulate | |
if ( "data" in arguments[1] ) | |
cache.populate(); | |
}).bind("unautocomplete", function() { | |
select.unbind(); | |
$input.unbind(); | |
$(input.form).unbind(".autocomplete"); | |
}); | |
function selectCurrent() { | |
var selected = select.selected(); | |
if( !selected ) | |
return false; | |
var v = selected.result; | |
previousValue = v; | |
if ( options.multiple ) { | |
var words = trimWords($input.val()); | |
if ( words.length > 1 ) { | |
var seperator = options.multipleSeparator.length; | |
var cursorAt = $(input).selection().start; | |
var wordAt, progress = 0; | |
$.each(words, function(i, word) { | |
progress += word.length; | |
if (cursorAt <= progress) { | |
wordAt = i; | |
return false; | |
} | |
progress += seperator; | |
}); | |
words[wordAt] = v; | |
// TODO this should set the cursor to the right position, but it gets overriden somewhere | |
//$.Autocompleter.Selection(input, progress + seperator, progress + seperator); | |
v = words.join( options.multipleSeparator ); | |
} | |
v += options.multipleSeparator; | |
} | |
$input.val(v); | |
hideResultsNow(); | |
$input.trigger("result", [selected.data, selected.value]); | |
return true; | |
} | |
function onChange(crap, skipPrevCheck) { | |
if( lastKeyPressCode == KEY.DEL ) { | |
select.hide(); | |
return; | |
} | |
var currentValue = $input.val(); | |
if ( !skipPrevCheck && currentValue == previousValue ) | |
return; | |
previousValue = currentValue; | |
currentValue = lastWord(currentValue); | |
if ( currentValue.length >= options.minChars) { | |
$input.addClass(options.loadingClass); | |
if (!options.matchCase) | |
currentValue = currentValue.toLowerCase(); | |
request(currentValue, receiveData, hideResultsNow); | |
} else { | |
stopLoading(); | |
select.hide(); | |
} | |
}; | |
function trimWords(value) { | |
if (!value) | |
return [""]; | |
if (!options.multiple) | |
return [$.trim(value)]; | |
return $.map(value.split(options.multipleSeparator), function(word) { | |
return $.trim(value).length ? $.trim(word) : null; | |
}); | |
} | |
function lastWord(value) { | |
if ( !options.multiple ) | |
return value; | |
var words = trimWords(value); | |
if (words.length == 1) | |
return words[0]; | |
var cursorAt = $(input).selection().start; | |
if (cursorAt == value.length) { | |
words = trimWords(value) | |
} else { | |
words = trimWords(value.replace(value.substring(cursorAt), "")); | |
} | |
return words[words.length - 1]; | |
} | |
// fills in the input box w/the first match (assumed to be the best match) | |
// q: the term entered | |
// sValue: the first matching result | |
function autoFill(q, sValue){ | |
// autofill in the complete box w/the first match as long as the user hasn't entered in more data | |
// if the last user key pressed was backspace, don't autofill | |
if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { | |
// fill in the value (keep the case the user has typed) | |
$input.val($input.val() + sValue.substring(lastWord(previousValue).length)); | |
// select the portion of the value not typed by the user (so the next character will erase) | |
$(input).selection(previousValue.length, previousValue.length + sValue.length); | |
} | |
}; | |
function hideResults() { | |
clearTimeout(timeout); | |
timeout = setTimeout(hideResultsNow, 200); | |
}; | |
function hideResultsNow() { | |
var wasVisible = select.visible(); | |
select.hide(); | |
clearTimeout(timeout); | |
stopLoading(); | |
if (options.mustMatch) { | |
// call search and run callback | |
$input.search( | |
function (result){ | |
// if no value found, clear the input box | |
if( !result ) { | |
if (options.multiple) { | |
var words = trimWords($input.val()).slice(0, -1); | |
$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); | |
} | |
else { | |
$input.val( "" ); | |
$input.trigger("result", null); | |
} | |
} | |
} | |
); | |
} | |
}; | |
function receiveData(q, data) { | |
if ( data && data.length && hasFocus ) { | |
stopLoading(); | |
select.display(data, q); | |
autoFill(q, data[0].value); | |
select.show(); | |
} else { | |
hideResultsNow(); | |
} | |
}; | |
function request(term, success, failure) { | |
if (!options.matchCase) | |
term = term.toLowerCase(); | |
var data = cache.load(term); | |
// recieve the cached data | |
if (data) { | |
if(data.length) { | |
success(term, data); | |
} | |
else{ | |
var parsed = options.parse && options.parse(options.noRecord) || parse(options.noRecord); | |
success(term,parsed); | |
} | |
// if an AJAX url has been supplied, try loading the data now | |
} else if( (typeof options.url == "string") && (options.url.length > 0) ){ | |
var extraParams = { | |
timestamp: +new Date() | |
}; | |
$.each(options.extraParams, function(key, param) { | |
extraParams[key] = typeof param == "function" ? param() : param; | |
}); | |
$.ajax({ | |
// try to leverage ajaxQueue plugin to abort previous requests | |
mode: "abort", | |
// limit abortion to this input | |
port: "autocomplete" + input.name, | |
dataType: options.dataType, | |
url: options.url, | |
data: $.extend({ | |
q: lastWord(term), | |
limit: options.max | |
}, extraParams), | |
success: function(data) { | |
var parsed = options.parse && options.parse(data) || parse(data); | |
cache.add(term, parsed); | |
success(term, parsed); | |
} | |
}); | |
} else { | |
// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match | |
select.emptyList(); | |
if(globalFailure != null) { | |
globalFailure(); | |
} | |
else { | |
failure(term); | |
} | |
} | |
}; | |
function parse(data) { | |
var parsed = []; | |
var rows = data.split("\n"); | |
for (var i=0; i < rows.length; i++) { | |
var row = $.trim(rows[i]); | |
if (row) { | |
row = row.split("|"); | |
parsed[parsed.length] = { | |
data: row, | |
value: row[0], | |
result: options.formatResult && options.formatResult(row, row[0]) || row[0] | |
}; | |
} | |
} | |
return parsed; | |
}; | |
function stopLoading() { | |
$input.removeClass(options.loadingClass); | |
}; | |
}; | |
$.Autocompleter.defaults = { | |
inputClass: "ac_input", | |
resultsClass: "ac_results", | |
loadingClass: "ac_loading", | |
minChars: 1, | |
delay: 400, | |
matchCase: false, | |
matchSubset: true, | |
matchContains: false, | |
cacheLength: 100, | |
max: 1000, | |
mustMatch: false, | |
extraParams: {}, | |
selectFirst: true, | |
formatItem: function(row) { return row[0]; }, | |
formatMatch: null, | |
autoFill: false, | |
width: 0, | |
multiple: false, | |
multipleSeparator: " ", | |
inputFocus: true, | |
clickFire: false, | |
highlight: function(value, term) { | |
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"); | |
}, | |
scroll: true, | |
scrollHeight: 180, | |
scrollJumpPosition: true | |
}; | |
$.Autocompleter.Cache = function(options) { | |
var data = {}; | |
var length = 0; | |
function matchSubset(s, sub) { | |
if (!options.matchCase) | |
s = s.toLowerCase(); | |
var i = s.indexOf(sub); | |
if (options.matchContains == "word"){ | |
i = s.toLowerCase().search("\\b" + sub.toLowerCase()); | |
} | |
if (i == -1) return false; | |
return i == 0 || options.matchContains; | |
}; | |
function add(q, value) { | |
if (length > options.cacheLength){ | |
flush(); | |
} | |
if (!data[q]){ | |
length++; | |
} | |
data[q] = value; | |
} | |
function populate(){ | |
if( !options.data ) return false; | |
// track the matches | |
var stMatchSets = {}, | |
nullData = 0; | |
// no url was specified, we need to adjust the cache length to make sure it fits the local data store | |
if( !options.url ) options.cacheLength = 1; | |
// track all options for minChars = 0 | |
stMatchSets[""] = []; | |
// loop through the array and create a lookup structure | |
for ( var i = 0, ol = options.data.length; i < ol; i++ ) { | |
var rawValue = options.data[i]; | |
// if rawValue is a string, make an array otherwise just reference the array | |
rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; | |
var value = options.formatMatch(rawValue, i+1, options.data.length); | |
if ( typeof(value) === 'undefined' || value === false ) | |
continue; | |
var firstChar = value.charAt(0).toLowerCase(); | |
// if no lookup array for this character exists, look it up now | |
if( !stMatchSets[firstChar] ) | |
stMatchSets[firstChar] = []; | |
// if the match is a string | |
var row = { | |
value: value, | |
data: rawValue, | |
result: options.formatResult && options.formatResult(rawValue) || value | |
}; | |
// push the current match into the set list | |
stMatchSets[firstChar].push(row); | |
// keep track of minChars zero items | |
if ( nullData++ < options.max ) { | |
stMatchSets[""].push(row); | |
} | |
}; | |
// add the data items to the cache | |
$.each(stMatchSets, function(i, value) { | |
// increase the cache size | |
options.cacheLength++; | |
// add to the cache | |
add(i, value); | |
}); | |
} | |
// populate any existing data | |
setTimeout(populate, 25); | |
function flush(){ | |
data = {}; | |
length = 0; | |
} | |
return { | |
flush: flush, | |
add: add, | |
populate: populate, | |
load: function(q) { | |
if (!options.cacheLength || !length) | |
return null; | |
/* | |
* if dealing w/local data and matchContains than we must make sure | |
* to loop through all the data collections looking for matches | |
*/ | |
if( !options.url && options.matchContains ){ | |
// track all matches | |
var csub = []; | |
// loop through all the data grids for matches | |
for( var k in data ){ | |
// don't search through the stMatchSets[""] (minChars: 0) cache | |
// this prevents duplicates | |
if( k.length > 0 ){ | |
var c = data[k]; | |
$.each(c, function(i, x) { | |
// if we've got a match, add it to the array | |
if (matchSubset(x.value, q)) { | |
csub.push(x); | |
} | |
}); | |
} | |
} | |
return csub; | |
} else | |
// if the exact item exists, use it | |
if (data[q]){ | |
return data[q]; | |
} else | |
if (options.matchSubset) { | |
for (var i = q.length - 1; i >= options.minChars; i--) { | |
var c = data[q.substr(0, i)]; | |
if (c) { | |
var csub = []; | |
$.each(c, function(i, x) { | |
if (matchSubset(x.value, q)) { | |
csub[csub.length] = x; | |
} | |
}); | |
return csub; | |
} | |
} | |
} | |
return null; | |
} | |
}; | |
}; | |
$.Autocompleter.Select = function (options, input, select, config) { | |
var CLASSES = { | |
ACTIVE: "ac_over" | |
}; | |
var listItems, | |
active = -1, | |
data, | |
term = "", | |
needsInit = true, | |
element, | |
list; | |
// Create results | |
function init() { | |
if (!needsInit) | |
return; | |
element = $("<div/>") | |
.hide() | |
.addClass(options.resultsClass) | |
.css("position", "absolute") | |
.appendTo(document.body) | |
.hover(function(event) { | |
// Browsers except FF do not fire mouseup event on scrollbars, resulting in mouseDownOnSelect remaining true, and results list not always hiding. | |
if($(this).is(":visible")) { | |
input.focus(); | |
} | |
config.mouseDownOnSelect = false; | |
}); | |
list = $("<ul/>").appendTo(element).mouseover( function(event) { | |
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') { | |
active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event)); | |
$(target(event)).addClass(CLASSES.ACTIVE); | |
} | |
}).click(function(event) { | |
$(target(event)).addClass(CLASSES.ACTIVE); | |
select(); | |
if( options.inputFocus ) | |
input.focus(); | |
return false; | |
}).mousedown(function() { | |
config.mouseDownOnSelect = true; | |
}).mouseup(function() { | |
config.mouseDownOnSelect = false; | |
}); | |
if( options.width > 0 ) | |
element.css("width", options.width); | |
needsInit = false; | |
} | |
function target(event) { | |
var element = event.target; | |
while(element && element.tagName != "LI") | |
element = element.parentNode; | |
// more fun with IE, sometimes event.target is empty, just ignore it then | |
if(!element) | |
return []; | |
return element; | |
} | |
function moveSelect(step) { | |
listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE); | |
movePosition(step); | |
var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE); | |
if(options.scroll) { | |
var offset = 0; | |
listItems.slice(0, active).each(function() { | |
offset += this.offsetHeight; | |
}); | |
if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) { | |
list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight()); | |
} else if(offset < list.scrollTop()) { | |
list.scrollTop(offset); | |
} | |
} | |
}; | |
function movePosition(step) { | |
if (options.scrollJumpPosition || (!options.scrollJumpPosition && !((step < 0 && active == 0) || (step > 0 && active == listItems.size() - 1)) )) { | |
active += step; | |
if (active < 0) { | |
active = listItems.size() - 1; | |
} else if (active >= listItems.size()) { | |
active = 0; | |
} | |
} | |
} | |
function limitNumberOfItems(available) { | |
return options.max && options.max < available | |
? options.max | |
: available; | |
} | |
function fillList() { | |
list.empty(); | |
var max = limitNumberOfItems(data.length); | |
for (var i=0; i < max; i++) { | |
if (!data[i]) | |
continue; | |
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term); | |
if ( formatted === false ) | |
continue; | |
var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0]; | |
$.data(li, "ac_data", data[i]); | |
} | |
listItems = list.find("li"); | |
if ( options.selectFirst ) { | |
listItems.slice(0, 1).addClass(CLASSES.ACTIVE); | |
active = 0; | |
} | |
// apply bgiframe if available | |
if ( $.fn.bgiframe ) | |
list.bgiframe(); | |
} | |
return { | |
display: function(d, q) { | |
init(); | |
data = d; | |
term = q; | |
fillList(); | |
}, | |
next: function() { | |
moveSelect(1); | |
}, | |
prev: function() { | |
moveSelect(-1); | |
}, | |
pageUp: function() { | |
if (active != 0 && active - 8 < 0) { | |
moveSelect( -active ); | |
} else { | |
moveSelect(-8); | |
} | |
}, | |
pageDown: function() { | |
if (active != listItems.size() - 1 && active + 8 > listItems.size()) { | |
moveSelect( listItems.size() - 1 - active ); | |
} else { | |
moveSelect(8); | |
} | |
}, | |
hide: function() { | |
element && element.hide(); | |
listItems && listItems.removeClass(CLASSES.ACTIVE); | |
active = -1; | |
}, | |
visible : function() { | |
return element && element.is(":visible"); | |
}, | |
current: function() { | |
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]); | |
}, | |
show: function() { | |
var offset = $(input).offset(); | |
element.css({ | |
width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(), | |
top: offset.top + input.offsetHeight, | |
left: offset.left - 9 | |
}).show(); | |
if(options.scroll) { | |
list.scrollTop(0); | |
list.css({ | |
maxHeight: options.scrollHeight, | |
overflow: 'auto' | |
}); | |
if(navigator.userAgent.indexOf("MSIE") != -1 && typeof document.body.style.maxHeight === "undefined") { | |
var listHeight = 0; | |
listItems.each(function() { | |
listHeight += this.offsetHeight; | |
}); | |
var scrollbarsVisible = listHeight > options.scrollHeight; | |
list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight ); | |
if (!scrollbarsVisible) { | |
// IE doesn't recalculate width when scrollbar disappears | |
listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) ); | |
} | |
} | |
} | |
}, | |
selected: function() { | |
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE); | |
return selected && selected.length && $.data(selected[0], "ac_data"); | |
}, | |
emptyList: function (){ | |
list && list.empty(); | |
}, | |
unbind: function() { | |
element && element.remove(); | |
} | |
}; | |
}; | |
$.fn.selection = function(start, end) { | |
if (start !== undefined) { | |
return this.each(function() { | |
if( this.createTextRange ){ | |
var selRange = this.createTextRange(); | |
if (end === undefined || start == end) { | |
selRange.move("character", start); | |
selRange.select(); | |
} else { | |
selRange.collapse(true); | |
selRange.moveStart("character", start); | |
selRange.moveEnd("character", end); | |
selRange.select(); | |
} | |
} else if( this.setSelectionRange ){ | |
this.setSelectionRange(start, end); | |
} else if( this.selectionStart ){ | |
this.selectionStart = start; | |
this.selectionEnd = end; | |
} | |
}); | |
} | |
var field = this[0]; | |
if ( field.createTextRange ) { | |
var range = document.selection.createRange(), | |
orig = field.value, | |
teststring = "<->", | |
textLength = range.text.length; | |
range.text = teststring; | |
var caretAt = field.value.indexOf(teststring); | |
field.value = orig; | |
this.selection(caretAt, caretAt + textLength); | |
return { | |
start: caretAt, | |
end: caretAt + textLength | |
} | |
} else if( field.selectionStart !== undefined ){ | |
return { | |
start: field.selectionStart, | |
end: field.selectionEnd | |
} | |
} | |
}; | |
})(jQuery); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment