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