Merge branch 'master' into with-dvcs
[redakcja.git] / redakcja / static / js / lib / jquery / jquery.autocomplete.js
1 /*
2  * jQuery Autocomplete plugin 1.1
3  *
4  * Copyright (c) 2009 Jörn Zaefferer
5  *
6  * Dual licensed under the MIT and GPL licenses:
7  *   http://www.opensource.org/licenses/mit-license.php
8  *   http://www.gnu.org/licenses/gpl.html
9  *
10  * Revision: $Id: jquery.autocomplete.js 15 2009-08-22 10:30:27Z joern.zaefferer $
11  * 
12  * changed by Radek Czajka (FNP), 20100907: ignore alt keydown
13  */
14
15 ;(function($) {
16
17 $.fn.extend({
18         autocomplete: function(urlOrData, options) {
19                 var isUrl = typeof urlOrData == "string";
20                 options = $.extend({}, $.Autocompleter.defaults, {
21                         url: isUrl ? urlOrData : null,
22                         data: isUrl ? null : urlOrData,
23                         delay: isUrl ? $.Autocompleter.defaults.delay : 10,
24                         max: options && !options.scroll ? 10 : 150
25                 }, options);
26
27                 // if highlight is set to false, replace it with a do-nothing function
28                 options.highlight = options.highlight || function(value) { return value; };
29
30                 // if the formatMatch option is not specified, then use formatItem for backwards compatibility
31                 options.formatMatch = options.formatMatch || options.formatItem;
32
33                 return this.each(function() {
34                         new $.Autocompleter(this, options);
35                 });
36         },
37         result: function(handler) {
38                 return this.bind("result", handler);
39         },
40         search: function(handler) {
41                 return this.trigger("search", [handler]);
42         },
43         flushCache: function() {
44                 return this.trigger("flushCache");
45         },
46         setOptions: function(options){
47                 return this.trigger("setOptions", [options]);
48         },
49         unautocomplete: function() {
50                 return this.trigger("unautocomplete");
51         }
52 });
53
54 $.Autocompleter = function(input, options) {
55
56         var KEY = {
57                 UP: 38,
58                 DOWN: 40,
59                 DEL: 46,
60                 TAB: 9,
61                 RETURN: 13,
62                 ESC: 27,
63                 COMMA: 188,
64                 PAGEUP: 33,
65                 PAGEDOWN: 34,
66                 BACKSPACE: 8
67         };
68
69         // Create $ object for input element
70         var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
71
72         var timeout;
73         var previousValue = "";
74         var cache = $.Autocompleter.Cache(options);
75         var hasFocus = 0;
76         var lastKeyPressCode;
77         var config = {
78                 mouseDownOnSelect: false
79         };
80         var select = $.Autocompleter.Select(options, input, selectCurrent, config);
81
82         var blockSubmit;
83
84         // prevent form submit in opera when selecting with return key
85         $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
86                 if (blockSubmit) {
87                         blockSubmit = false;
88                         return false;
89                 }
90         });
91
92         // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
93         $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
94                 // a keypress means the input has focus
95                 // avoids issue where input had focus before the autocomplete was applied
96                 hasFocus = 1;
97                 // track last key pressed
98                 lastKeyPressCode = event.keyCode;
99                 switch(event.keyCode) {
100
101             // changed by fnp
102             case 0:
103                 break;
104             // changed by fnp
105
106                         case KEY.UP:
107                                 event.preventDefault();
108                                 if ( select.visible() ) {
109                                         select.prev();
110                                 } else {
111                                         onChange(0, true);
112                                 }
113                                 break;
114
115                         case KEY.DOWN:
116                                 event.preventDefault();
117                                 if ( select.visible() ) {
118                                         select.next();
119                                 } else {
120                                         onChange(0, true);
121                                 }
122                                 break;
123
124                         case KEY.PAGEUP:
125                                 event.preventDefault();
126                                 if ( select.visible() ) {
127                                         select.pageUp();
128                                 } else {
129                                         onChange(0, true);
130                                 }
131                                 break;
132
133                         case KEY.PAGEDOWN:
134                                 event.preventDefault();
135                                 if ( select.visible() ) {
136                                         select.pageDown();
137                                 } else {
138                                         onChange(0, true);
139                                 }
140                                 break;
141
142                         // matches also semicolon
143                         case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
144                         case KEY.TAB:
145                         case KEY.RETURN:
146                                 if( selectCurrent() ) {
147                                         // stop default to prevent a form submit, Opera needs special handling
148                                         event.preventDefault();
149                                         blockSubmit = true;
150                                         return false;
151                                 }
152                                 break;
153
154                         case KEY.ESC:
155                                 select.hide();
156                                 break;
157
158                         default:
159                                 clearTimeout(timeout);
160                                 timeout = setTimeout(onChange, options.delay);
161                                 break;
162                 }
163         }).focus(function(){
164                 // track whether the field has focus, we shouldn't process any
165                 // results if the field no longer has focus
166                 hasFocus++;
167         }).blur(function() {
168                 hasFocus = 0;
169                 if (!config.mouseDownOnSelect) {
170                         hideResults();
171                 }
172         }).click(function() {
173                 // show select when clicking in a focused field
174                 if ( hasFocus++ > 1 && !select.visible() ) {
175                         onChange(0, true);
176                 }
177         }).bind("search", function() {
178                 // TODO why not just specifying both arguments?
179                 var fn = (arguments.length > 1) ? arguments[1] : null;
180                 function findValueCallback(q, data) {
181                         var result;
182                         if( data && data.length ) {
183                                 for (var i=0; i < data.length; i++) {
184                                         if( data[i].result.toLowerCase() == q.toLowerCase() ) {
185                                                 result = data[i];
186                                                 break;
187                                         }
188                                 }
189                         }
190                         if( typeof fn == "function" ) fn(result);
191                         else $input.trigger("result", result && [result.data, result.value]);
192                 }
193                 $.each(trimWords($input.val()), function(i, value) {
194                         request(value, findValueCallback, findValueCallback);
195                 });
196         }).bind("flushCache", function() {
197                 cache.flush();
198         }).bind("setOptions", function() {
199                 $.extend(options, arguments[1]);
200                 // if we've updated the data, repopulate
201                 if ( "data" in arguments[1] )
202                         cache.populate();
203         }).bind("unautocomplete", function() {
204                 select.unbind();
205                 $input.unbind();
206                 $(input.form).unbind(".autocomplete");
207         });
208
209
210         function selectCurrent() {
211                 var selected = select.selected();
212                 if( !selected )
213                         return false;
214
215                 var v = selected.result;
216                 previousValue = v;
217
218                 if ( options.multiple ) {
219                         var words = trimWords($input.val());
220                         if ( words.length > 1 ) {
221                                 var seperator = options.multipleSeparator.length;
222                                 var cursorAt = $(input).selection().start;
223                                 var wordAt, progress = 0;
224                                 $.each(words, function(i, word) {
225                                         progress += word.length;
226                                         if (cursorAt <= progress) {
227                                                 wordAt = i;
228                                                 return false;
229                                         }
230                                         progress += seperator;
231                                 });
232                                 words[wordAt] = v;
233                                 // TODO this should set the cursor to the right position, but it gets overriden somewhere
234                                 //$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
235                                 v = words.join( options.multipleSeparator );
236                         }
237                         v += options.multipleSeparator;
238                 }
239
240                 $input.val(v);
241                 hideResultsNow();
242                 $input.trigger("result", [selected.data, selected.value]);
243                 return true;
244         }
245
246         function onChange(crap, skipPrevCheck) {
247                 if( lastKeyPressCode == KEY.DEL ) {
248                         select.hide();
249                         return;
250                 }
251
252                 var currentValue = $input.val();
253
254                 if ( !skipPrevCheck && currentValue == previousValue )
255                         return;
256
257                 previousValue = currentValue;
258
259                 currentValue = lastWord(currentValue);
260                 if ( currentValue.length >= options.minChars) {
261                         $input.addClass(options.loadingClass);
262                         if (!options.matchCase)
263                                 currentValue = currentValue.toLowerCase();
264                         request(currentValue, receiveData, hideResultsNow);
265                 } else {
266                         stopLoading();
267                         select.hide();
268                 }
269         };
270
271         function trimWords(value) {
272                 if (!value)
273                         return [""];
274                 if (!options.multiple)
275                         return [$.trim(value)];
276                 return $.map(value.split(options.multipleSeparator), function(word) {
277                         return $.trim(value).length ? $.trim(word) : null;
278                 });
279         }
280
281         function lastWord(value) {
282                 if ( !options.multiple )
283                         return value;
284                 var words = trimWords(value);
285                 if (words.length == 1)
286                         return words[0];
287                 var cursorAt = $(input).selection().start;
288                 if (cursorAt == value.length) {
289                         words = trimWords(value)
290                 } else {
291                         words = trimWords(value.replace(value.substring(cursorAt), ""));
292                 }
293                 return words[words.length - 1];
294         }
295
296         // fills in the input box w/the first match (assumed to be the best match)
297         // q: the term entered
298         // sValue: the first matching result
299         function autoFill(q, sValue){
300                 // autofill in the complete box w/the first match as long as the user hasn't entered in more data
301                 // if the last user key pressed was backspace, don't autofill
302                 if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
303                         // fill in the value (keep the case the user has typed)
304                         $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
305                         // select the portion of the value not typed by the user (so the next character will erase)
306                         $(input).selection(previousValue.length, previousValue.length + sValue.length);
307                 }
308         };
309
310         function hideResults() {
311                 clearTimeout(timeout);
312                 timeout = setTimeout(hideResultsNow, 200);
313         };
314
315         function hideResultsNow() {
316                 var wasVisible = select.visible();
317                 select.hide();
318                 clearTimeout(timeout);
319                 stopLoading();
320                 if (options.mustMatch) {
321                         // call search and run callback
322                         $input.search(
323                                 function (result){
324                                         // if no value found, clear the input box
325                                         if( !result ) {
326                                                 if (options.multiple) {
327                                                         var words = trimWords($input.val()).slice(0, -1);
328                                                         $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
329                                                 }
330                                                 else {
331                                                         $input.val( "" );
332                                                         $input.trigger("result", null);
333                                                 }
334                                         }
335                                 }
336                         );
337                 }
338         };
339
340         function receiveData(q, data) {
341                 if ( data && data.length && hasFocus ) {
342                         stopLoading();
343                         select.display(data, q);
344                         autoFill(q, data[0].value);
345                         select.show();
346                 } else {
347                         hideResultsNow();
348                 }
349         };
350
351         function request(term, success, failure) {
352                 if (!options.matchCase)
353                         term = term.toLowerCase();
354                 var data = cache.load(term);
355                 // recieve the cached data
356                 if (data && data.length) {
357                         success(term, data);
358                 // if an AJAX url has been supplied, try loading the data now
359                 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
360
361                         var extraParams = {
362                                 timestamp: +new Date()
363                         };
364                         $.each(options.extraParams, function(key, param) {
365                                 extraParams[key] = typeof param == "function" ? param() : param;
366                         });
367
368                         $.ajax({
369                                 // try to leverage ajaxQueue plugin to abort previous requests
370                                 mode: "abort",
371                                 // limit abortion to this input
372                                 port: "autocomplete" + input.name,
373                                 dataType: options.dataType,
374                                 url: options.url,
375                                 data: $.extend({
376                                         q: lastWord(term),
377                                         limit: options.max
378                                 }, extraParams),
379                                 success: function(data) {
380                                         var parsed = options.parse && options.parse(data) || parse(data);
381                                         cache.add(term, parsed);
382                                         success(term, parsed);
383                                 }
384                         });
385                 } else {
386                         // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
387                         select.emptyList();
388                         failure(term);
389                 }
390         };
391
392         function parse(data) {
393                 var parsed = [];
394                 var rows = data.split("\n");
395                 for (var i=0; i < rows.length; i++) {
396                         var row = $.trim(rows[i]);
397                         if (row) {
398                                 row = row.split("|");
399                                 parsed[parsed.length] = {
400                                         data: row,
401                                         value: row[0],
402                                         result: options.formatResult && options.formatResult(row, row[0]) || row[0]
403                                 };
404                         }
405                 }
406                 return parsed;
407         };
408
409         function stopLoading() {
410                 $input.removeClass(options.loadingClass);
411         };
412
413 };
414
415 $.Autocompleter.defaults = {
416         inputClass: "ac_input",
417         resultsClass: "ac_results",
418         loadingClass: "ac_loading",
419         minChars: 1,
420         delay: 400,
421         matchCase: false,
422         matchSubset: true,
423         matchContains: false,
424         cacheLength: 10,
425         max: 100,
426         mustMatch: false,
427         extraParams: {},
428         selectFirst: true,
429         formatItem: function(row) { return row[0]; },
430         formatMatch: null,
431         autoFill: false,
432         width: 0,
433         multiple: false,
434         multipleSeparator: ", ",
435         highlight: function(value, term) {
436                 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
437         },
438     scroll: true,
439     scrollHeight: 180
440 };
441
442 $.Autocompleter.Cache = function(options) {
443
444         var data = {};
445         var length = 0;
446
447         function matchSubset(s, sub) {
448                 if (!options.matchCase)
449                         s = s.toLowerCase();
450                 var i = s.indexOf(sub);
451                 if (options.matchContains == "word"){
452                         i = s.toLowerCase().search("\\b" + sub.toLowerCase());
453                 }
454                 if (i == -1) return false;
455                 return i == 0 || options.matchContains;
456         };
457
458         function add(q, value) {
459                 if (length > options.cacheLength){
460                         flush();
461                 }
462                 if (!data[q]){
463                         length++;
464                 }
465                 data[q] = value;
466         }
467
468         function populate(){
469                 if( !options.data ) return false;
470                 // track the matches
471                 var stMatchSets = {},
472                         nullData = 0;
473
474                 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
475                 if( !options.url ) options.cacheLength = 1;
476
477                 // track all options for minChars = 0
478                 stMatchSets[""] = [];
479
480                 // loop through the array and create a lookup structure
481                 for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
482                         var rawValue = options.data[i];
483                         // if rawValue is a string, make an array otherwise just reference the array
484                         rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
485
486                         var value = options.formatMatch(rawValue, i+1, options.data.length);
487                         if ( value === false )
488                                 continue;
489
490                         var firstChar = value.charAt(0).toLowerCase();
491                         // if no lookup array for this character exists, look it up now
492                         if( !stMatchSets[firstChar] )
493                                 stMatchSets[firstChar] = [];
494
495                         // if the match is a string
496                         var row = {
497                                 value: value,
498                                 data: rawValue,
499                                 result: options.formatResult && options.formatResult(rawValue) || value
500                         };
501
502                         // push the current match into the set list
503                         stMatchSets[firstChar].push(row);
504
505                         // keep track of minChars zero items
506                         if ( nullData++ < options.max ) {
507                                 stMatchSets[""].push(row);
508                         }
509                 };
510
511                 // add the data items to the cache
512                 $.each(stMatchSets, function(i, value) {
513                         // increase the cache size
514                         options.cacheLength++;
515                         // add to the cache
516                         add(i, value);
517                 });
518         }
519
520         // populate any existing data
521         setTimeout(populate, 25);
522
523         function flush(){
524                 data = {};
525                 length = 0;
526         }
527
528         return {
529                 flush: flush,
530                 add: add,
531                 populate: populate,
532                 load: function(q) {
533                         if (!options.cacheLength || !length)
534                                 return null;
535                         /*
536                          * if dealing w/local data and matchContains than we must make sure
537                          * to loop through all the data collections looking for matches
538                          */
539                         if( !options.url && options.matchContains ){
540                                 // track all matches
541                                 var csub = [];
542                                 // loop through all the data grids for matches
543                                 for( var k in data ){
544                                         // don't search through the stMatchSets[""] (minChars: 0) cache
545                                         // this prevents duplicates
546                                         if( k.length > 0 ){
547                                                 var c = data[k];
548                                                 $.each(c, function(i, x) {
549                                                         // if we've got a match, add it to the array
550                                                         if (matchSubset(x.value, q)) {
551                                                                 csub.push(x);
552                                                         }
553                                                 });
554                                         }
555                                 }
556                                 return csub;
557                         } else
558                         // if the exact item exists, use it
559                         if (data[q]){
560                                 return data[q];
561                         } else
562                         if (options.matchSubset) {
563                                 for (var i = q.length - 1; i >= options.minChars; i--) {
564                                         var c = data[q.substr(0, i)];
565                                         if (c) {
566                                                 var csub = [];
567                                                 $.each(c, function(i, x) {
568                                                         if (matchSubset(x.value, q)) {
569                                                                 csub[csub.length] = x;
570                                                         }
571                                                 });
572                                                 return csub;
573                                         }
574                                 }
575                         }
576                         return null;
577                 }
578         };
579 };
580
581 $.Autocompleter.Select = function (options, input, select, config) {
582         var CLASSES = {
583                 ACTIVE: "ac_over"
584         };
585
586         var listItems,
587                 active = -1,
588                 data,
589                 term = "",
590                 needsInit = true,
591                 element,
592                 list;
593
594         // Create results
595         function init() {
596                 if (!needsInit)
597                         return;
598                 element = $("<div/>")
599                 .hide()
600                 .addClass(options.resultsClass)
601                 .css("position", "absolute")
602                 .appendTo(document.body);
603
604                 list = $("<ul/>").appendTo(element).mouseover( function(event) {
605                         if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
606                     active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
607                             $(target(event)).addClass(CLASSES.ACTIVE);
608                 }
609                 }).click(function(event) {
610                         $(target(event)).addClass(CLASSES.ACTIVE);
611                         select();
612                         // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
613                         input.focus();
614                         return false;
615                 }).mousedown(function() {
616                         config.mouseDownOnSelect = true;
617                 }).mouseup(function() {
618                         config.mouseDownOnSelect = false;
619                 });
620
621                 if( options.width > 0 )
622                         element.css("width", options.width);
623
624                 needsInit = false;
625         }
626
627         function target(event) {
628                 var element = event.target;
629                 while(element && element.tagName != "LI")
630                         element = element.parentNode;
631                 // more fun with IE, sometimes event.target is empty, just ignore it then
632                 if(!element)
633                         return [];
634                 return element;
635         }
636
637         function moveSelect(step) {
638                 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
639                 movePosition(step);
640         var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
641         if(options.scroll) {
642             var offset = 0;
643             listItems.slice(0, active).each(function() {
644                                 offset += this.offsetHeight;
645                         });
646             if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
647                 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
648             } else if(offset < list.scrollTop()) {
649                 list.scrollTop(offset);
650             }
651         }
652         };
653
654         function movePosition(step) {
655                 active += step;
656                 if (active < 0) {
657                         active = listItems.size() - 1;
658                 } else if (active >= listItems.size()) {
659                         active = 0;
660                 }
661         }
662
663         function limitNumberOfItems(available) {
664                 return options.max && options.max < available
665                         ? options.max
666                         : available;
667         }
668
669         function fillList() {
670                 list.empty();
671                 var max = limitNumberOfItems(data.length);
672                 for (var i=0; i < max; i++) {
673                         if (!data[i])
674                                 continue;
675                         var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
676                         if ( formatted === false )
677                                 continue;
678                         var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
679                         $.data(li, "ac_data", data[i]);
680                 }
681                 listItems = list.find("li");
682                 if ( options.selectFirst ) {
683                         listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
684                         active = 0;
685                 }
686                 // apply bgiframe if available
687                 if ( $.fn.bgiframe )
688                         list.bgiframe();
689         }
690
691         return {
692                 display: function(d, q) {
693                         init();
694                         data = d;
695                         term = q;
696                         fillList();
697                 },
698                 next: function() {
699                         moveSelect(1);
700                 },
701                 prev: function() {
702                         moveSelect(-1);
703                 },
704                 pageUp: function() {
705                         if (active != 0 && active - 8 < 0) {
706                                 moveSelect( -active );
707                         } else {
708                                 moveSelect(-8);
709                         }
710                 },
711                 pageDown: function() {
712                         if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
713                                 moveSelect( listItems.size() - 1 - active );
714                         } else {
715                                 moveSelect(8);
716                         }
717                 },
718                 hide: function() {
719                         element && element.hide();
720                         listItems && listItems.removeClass(CLASSES.ACTIVE);
721                         active = -1;
722                 },
723                 visible : function() {
724                         return element && element.is(":visible");
725                 },
726                 current: function() {
727                         return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
728                 },
729                 show: function() {
730                         var offset = $(input).offset();
731                         element.css({
732                                 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
733                                 top: offset.top + input.offsetHeight,
734                                 left: offset.left
735                         }).show();
736             if(options.scroll) {
737                 list.scrollTop(0);
738                 list.css({
739                                         maxHeight: options.scrollHeight,
740                                         overflow: 'auto'
741                                 });
742
743                 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
744                                         var listHeight = 0;
745                                         listItems.each(function() {
746                                                 listHeight += this.offsetHeight;
747                                         });
748                                         var scrollbarsVisible = listHeight > options.scrollHeight;
749                     list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
750                                         if (!scrollbarsVisible) {
751                                                 // IE doesn't recalculate width when scrollbar disappears
752                                                 listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
753                                         }
754                 }
755
756             }
757                 },
758                 selected: function() {
759                         var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
760                         return selected && selected.length && $.data(selected[0], "ac_data");
761                 },
762                 emptyList: function (){
763                         list && list.empty();
764                 },
765                 unbind: function() {
766                         element && element.remove();
767                 }
768         };
769 };
770
771 $.fn.selection = function(start, end) {
772         if (start !== undefined) {
773                 return this.each(function() {
774                         if( this.createTextRange ){
775                                 var selRange = this.createTextRange();
776                                 if (end === undefined || start == end) {
777                                         selRange.move("character", start);
778                                         selRange.select();
779                                 } else {
780                                         selRange.collapse(true);
781                                         selRange.moveStart("character", start);
782                                         selRange.moveEnd("character", end);
783                                         selRange.select();
784                                 }
785                         } else if( this.setSelectionRange ){
786                                 this.setSelectionRange(start, end);
787                         } else if( this.selectionStart ){
788                                 this.selectionStart = start;
789                                 this.selectionEnd = end;
790                         }
791                 });
792         }
793         var field = this[0];
794         if ( field.createTextRange ) {
795                 var range = document.selection.createRange(),
796                         orig = field.value,
797                         teststring = "<->",
798                         textLength = range.text.length;
799                 range.text = teststring;
800                 var caretAt = field.value.indexOf(teststring);
801                 field.value = orig;
802                 this.selection(caretAt, caretAt + textLength);
803                 return {
804                         start: caretAt,
805                         end: caretAt + textLength
806                 }
807         } else if( field.selectionStart !== undefined ){
808                 return {
809                         start: field.selectionStart,
810                         end: field.selectionEnd
811                 }
812         }
813 };
814
815 })(jQuery);