More search fixes.
[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         highlight: function(value, term) {
434                 return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
435         },
436     scroll: true,
437     scrollHeight: 180
438 };
439
440 $.Autocompleter.Cache = function(options) {
441
442         var data = {};
443         var length = 0;
444         
445         function matchSubset(s, sub) {
446                 if (!options.matchCase) 
447                         s = s.toLowerCase();
448                 var i = s.indexOf(sub);
449                 if (options.matchContains == "word"){
450                         i = s.toLowerCase().search("\\b" + sub.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1").toLowerCase());
451                 }
452                 if (i == -1) return false;
453                 return i == 0 || options.matchContains;
454         };
455         
456         function add(q, value) {
457                 if (length > options.cacheLength){
458                         flush();
459                 }
460                 if (!data[q]){ 
461                         length++;
462                 }
463                 data[q] = value;
464         }
465         
466         function populate(){
467                 if( !options.data ) return false;
468                 // track the matches
469                 var stMatchSets = {},
470                         nullData = 0;
471
472                 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
473                 if( !options.url ) options.cacheLength = 1;
474                 
475                 // track all options for minChars = 0
476                 stMatchSets[""] = [];
477                 
478                 // loop through the array and create a lookup structure
479                 for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
480                         var rawValue = options.data[i];
481                         // if rawValue is a string, make an array otherwise just reference the array
482                         rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
483                         
484                         var value = options.formatMatch(rawValue, i+1, options.data.length);
485                         if ( value === false )
486                                 continue;
487                                 
488                         var firstChar = value.charAt(0).toLowerCase();
489                         // if no lookup array for this character exists, look it up now
490                         if( !stMatchSets[firstChar] ) 
491                                 stMatchSets[firstChar] = [];
492
493                         // if the match is a string
494                         var row = {
495                                 value: value,
496                                 data: rawValue,
497                                 result: options.formatResult && options.formatResult(rawValue) || value
498                         };
499                         
500                         // push the current match into the set list
501                         stMatchSets[firstChar].push(row);
502
503                         // keep track of minChars zero items
504                         if ( nullData++ < options.max ) {
505                                 stMatchSets[""].push(row);
506                         }
507                 };
508
509                 // add the data items to the cache
510                 $.each(stMatchSets, function(i, value) {
511                         // increase the cache size
512                         options.cacheLength++;
513                         // add to the cache
514                         add(i, value);
515                 });
516         }
517         
518         // populate any existing data
519         setTimeout(populate, 25);
520         
521         function flush(){
522                 data = {};
523                 length = 0;
524         }
525         
526         return {
527                 flush: flush,
528                 add: add,
529                 populate: populate,
530                 load: function(q) {
531                         if (!options.cacheLength || !length)
532                                 return null;
533                         /* 
534                          * if dealing w/local data and matchContains than we must make sure
535                          * to loop through all the data collections looking for matches
536                          */
537                         if( !options.url && options.matchContains ){
538                                 // track all matches
539                                 var csub = [];
540                                 // loop through all the data grids for matches
541                                 for( var k in data ){
542                                         // don't search through the stMatchSets[""] (minChars: 0) cache
543                                         // this prevents duplicates
544                                         if( k.length > 0 ){
545                                                 var c = data[k];
546                                                 $.each(c, function(i, x) {
547                                                         // if we've got a match, add it to the array
548                                                         if (matchSubset(x.value, q)) {
549                                                                 csub.push(x);
550                                                         }
551                                                 });
552                                         }
553                                 }                               
554                                 return csub;
555                         } else 
556                         // if the exact item exists, use it
557                         if (data[q]){
558                                 return data[q];
559                         } else
560                         if (options.matchSubset) {
561                                 for (var i = q.length - 1; i >= options.minChars; i--) {
562                                         var c = data[q.substr(0, i)];
563                                         if (c) {
564                                                 var csub = [];
565                                                 $.each(c, function(i, x) {
566                                                         if (matchSubset(x.value, q)) {
567                                                                 csub[csub.length] = x;
568                                                         }
569                                                 });
570                                                 return csub;
571                                         }
572                                 }
573                         }
574                         return null;
575                 }
576         };
577 };
578
579 $.Autocompleter.Select = function (options, input, select, config) {
580         var CLASSES = {
581                 ACTIVE: "ac_over"
582         };
583         
584         var listItems,
585                 active = -1,
586                 data,
587                 term = "",
588                 needsInit = true,
589                 element,
590                 list;
591         
592         // Create results
593         function init() {
594                 if (!needsInit)
595                         return;
596                 element = $("<div/>")
597                 .hide()
598                 .addClass(options.resultsClass)
599                 .css("position", "absolute")
600                 .appendTo(document.body);
601         
602                 list = $("<ul/>").appendTo(element).mouseover( function(event) {
603                         if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
604                     active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
605                             $(target(event)).addClass(CLASSES.ACTIVE);            
606                 }
607                 }).click(function(event) {
608                         $(target(event)).addClass(CLASSES.ACTIVE);
609                         select();
610                         // TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
611                         input.focus();
612                         return false;
613                 }).mousedown(function() {
614                         config.mouseDownOnSelect = true;
615                 }).mouseup(function() {
616                         config.mouseDownOnSelect = false;
617                 });
618                 
619                 if( options.width > 0 )
620                         element.css("width", options.width);
621                         
622                 needsInit = false;
623         } 
624         
625         function target(event) {
626                 var element = event.target;
627                 while(element && element.tagName != "LI")
628                         element = element.parentNode;
629                 // more fun with IE, sometimes event.target is empty, just ignore it then
630                 if(!element)
631                         return [];
632                 return element;
633         }
634
635         function moveSelect(step) {
636                 listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
637                 movePosition(step);
638         var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
639         if(options.scroll) {
640             var offset = 0;
641             listItems.slice(0, active).each(function() {
642                                 offset += this.offsetHeight;
643                         });
644             if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
645                 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
646             } else if(offset < list.scrollTop()) {
647                 list.scrollTop(offset);
648             }
649         }
650         };
651         
652         function movePosition(step) {
653                 active += step;
654                 if (active < 0) {
655                         active = listItems.size() - 1;
656                 } else if (active >= listItems.size()) {
657                         active = 0;
658                 }
659         }
660         
661         function limitNumberOfItems(available) {
662                 return options.max && options.max < available
663                         ? options.max
664                         : available;
665         }
666         
667         function fillList() {
668                 list.empty();
669                 var max = limitNumberOfItems(data.length);
670                 for (var i=0; i < max; i++) {
671                         if (!data[i])
672                                 continue;
673                         var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
674                         if ( formatted === false )
675                                 continue;
676                         var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
677                         $.data(li, "ac_data", data[i]);
678                 }
679                 listItems = list.find("li");
680                 if ( options.selectFirst ) {
681                         listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
682                         active = 0;
683                 }
684                 // apply bgiframe if available
685                 if ( $.fn.bgiframe )
686                         list.bgiframe();
687         }
688         
689         return {
690                 display: function(d, q) {
691                         init();
692                         data = d;
693                         term = q;
694                         fillList();
695                 },
696                 next: function() {
697                         moveSelect(1);
698                 },
699                 prev: function() {
700                         moveSelect(-1);
701                 },
702                 pageUp: function() {
703                         if (active != 0 && active - 8 < 0) {
704                                 moveSelect( -active );
705                         } else {
706                                 moveSelect(-8);
707                         }
708                 },
709                 pageDown: function() {
710                         if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
711                                 moveSelect( listItems.size() - 1 - active );
712                         } else {
713                                 moveSelect(8);
714                         }
715                 },
716                 hide: function() {
717                         element && element.hide();
718                         listItems && listItems.removeClass(CLASSES.ACTIVE);
719                         active = -1;
720                 },
721                 visible : function() {
722                         return element && element.is(":visible");
723                 },
724                 current: function() {
725                         return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
726                 },
727                 show: function() {
728                         var offset = $(input).offset();
729                         element.css({
730                                 width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
731                                 top: offset.top + input.offsetHeight,
732                                 left: offset.left
733                         }).show();
734             if(options.scroll) {
735                 list.scrollTop(0);
736                 list.css({
737                                         maxHeight: options.scrollHeight,
738                                         overflow: 'auto'
739                                 });
740                                 
741                 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
742                                         var listHeight = 0;
743                                         listItems.each(function() {
744                                                 listHeight += this.offsetHeight;
745                                         });
746                                         var scrollbarsVisible = listHeight > options.scrollHeight;
747                     list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
748                                         if (!scrollbarsVisible) {
749                                                 // IE doesn't recalculate width when scrollbar disappears
750                                                 listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
751                                         }
752                 }
753                 
754             }
755                 },
756                 selected: function() {
757                         var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
758                         return selected && selected.length && $.data(selected[0], "ac_data");
759                 },
760                 emptyList: function (){
761                         list && list.empty();
762                 },
763                 unbind: function() {
764                         element && element.remove();
765                 }
766         };
767 };
768
769 $.fn.selection = function(start, end) {
770         if (start !== undefined) {
771                 return this.each(function() {
772                         if( this.createTextRange ){
773                                 var selRange = this.createTextRange();
774                                 if (end === undefined || start == end) {
775                                         selRange.move("character", start);
776                                         selRange.select();
777                                 } else {
778                                         selRange.collapse(true);
779                                         selRange.moveStart("character", start);
780                                         selRange.moveEnd("character", end);
781                                         selRange.select();
782                                 }
783                         } else if( this.setSelectionRange ){
784                                 this.setSelectionRange(start, end);
785                         } else if( this.selectionStart ){
786                                 this.selectionStart = start;
787                                 this.selectionEnd = end;
788                         }
789                 });
790         }
791         var field = this[0];
792         if ( field.createTextRange ) {
793                 var range = document.selection.createRange(),
794                         orig = field.value,
795                         teststring = "<->",
796                         textLength = range.text.length;
797                 range.text = teststring;
798                 var caretAt = field.value.indexOf(teststring);
799                 field.value = orig;
800                 this.selection(caretAt, caretAt + textLength);
801                 return {
802                         start: caretAt,
803                         end: caretAt + textLength
804                 }
805         } else if( field.selectionStart !== undefined ){
806                 return {
807                         start: field.selectionStart,
808                         end: field.selectionEnd
809                 }
810         }
811 };
812
813 })(jQuery);