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