app/jquery/jquery.growfield.js
changeset 2421 0979e7af115f
parent 2420 645f4de26f99
child 2422 44c500fc0eca
equal deleted inserted replaced
2420:645f4de26f99 2421:0979e7af115f
     1 /*
       
     2  * The MIT License
       
     3  *
       
     4  * Copyright (c) 2009 Johann Kuindji
       
     5  *
       
     6  * Permission is hereby granted, free of charge, to any person obtaining a copy
       
     7  * of this software and associated documentation files (the "Software"), to deal
       
     8  * in the Software without restriction, including without limitation the rights
       
     9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       
    10  * copies of the Software, and to permit persons to whom the Software is
       
    11  * furnished to do so, subject to the following conditions:
       
    12  *
       
    13  * The above copyright notice and this permission notice shall be included in
       
    14  * all copies or substantial portions of the Software.
       
    15  *
       
    16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
       
    17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
       
    18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
       
    19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
       
    20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
       
    21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
       
    22  * THE SOFTWARE.
       
    23  *
       
    24  * @author Johann Kuindji, Dmitriy Likhten
       
    25  * http://code.google.com/p/jquery-growfield/
       
    26  */
       
    27 (function($) {
       
    28 if ($.support === undefined) {
       
    29     $.support = { boxModel: $.boxModel };
       
    30 }
       
    31 var windowLoaded = false;
       
    32 $(window).one('load', function(){ windowLoaded=true; });
       
    33 
       
    34 // we need to adapt jquery animations for textareas.
       
    35 // by default, it changes display to 'block' if we're trying to
       
    36 // change width or height. We have to prevent this.
       
    37 // THIS WILL NOT ALTER JQUERY ORIGINAL BEHAVIORS, IT WILL HOWEVER ADD
       
    38 // SOME SO THAT GROWFIELD ANIMATIONS WORK CORRECTLY.
       
    39 $.fx.prototype.originalUpdate = $.fx.prototype.update;
       
    40 $.fx.prototype.update = false;
       
    41 $.fx.prototype.update = function () {
       
    42     if (!this.options.inline) {
       
    43         return this.originalUpdate.call(this);
       
    44     }
       
    45     if ( this.options.step ) {
       
    46         this.options.step.call( this.elem, this.now, this );
       
    47     }
       
    48     (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
       
    49 };
       
    50 
       
    51 $.growfield = function(dom,options){
       
    52     // Extend ptt(prototype) with our own private variables/
       
    53     // shared's functions are re-referenced and not cloned so
       
    54     // memory is kept at a minimum.
       
    55     var that = $.extend({
       
    56         dom: dom,
       
    57         o: $(dom),
       
    58         enabled: false,
       
    59         dummy: false,
       
    60         busy: false,
       
    61         initial: false,
       
    62         sizseRelated: false,
       
    63         prevH: false,
       
    64         firstH: false,
       
    65         restoreH: false,
       
    66         opt: $.extend({},$.fn.growfield.defaults,options)
       
    67     },$.growfield.ptt);
       
    68 
       
    69     return that;
       
    70 };
       
    71 
       
    72 //-----------------------------------------------------
       
    73 // This is the base class for all $.growfield objects
       
    74 // (their prototype)
       
    75 //-----------------------------------------------------
       
    76 $.growfield.ptt = (function(){
       
    77     //-----------------------------------------------------
       
    78     //EVENT HANDLERS for dealing with the growfield object
       
    79     //-----------------------------------------------------
       
    80     var manualKeyUp = function(e) {
       
    81         var obj = e.data;
       
    82         if (e.ctrlKey && (e.keyCode == 38 || e.keyCode == 40)){
       
    83             obj.update(
       
    84                 obj.o.outerHeight() + (obj.opt.step*( e.keyCode==38? -1: 1)),
       
    85                 obj.opt.animate
       
    86             );
       
    87         }
       
    88     };
       
    89 
       
    90     var keyUp = function(e) {
       
    91         var obj = e.data;
       
    92         if (!obj.busy){
       
    93             if ($.inArray(e.keyCode, [37,38,39,40]) === -1) {
       
    94                 obj.update(obj.getDummyHeight(), obj.opt.animate);
       
    95             }
       
    96         }
       
    97         return true;
       
    98     };
       
    99 
       
   100     var focus = function(e) {
       
   101         var obj = e.data;
       
   102         if (!obj.busy) {
       
   103             if (obj.opt.restore) {
       
   104                 obj.update(obj.dummy ? obj.getDummyHeight() : obj.restoreH, obj.opt.animate, 'growback');
       
   105             }
       
   106         }
       
   107     };
       
   108 
       
   109     var blur = function(e) {
       
   110         var obj = e.data;
       
   111         if (!obj.busy) {
       
   112             if (obj.opt.restore) {
       
   113                 obj.update(0, obj.opt.animate, 'restore');
       
   114             }
       
   115         }
       
   116     };
       
   117 
       
   118     var prepareSizeRelated = function(e) {
       
   119         var obj = e.data;
       
   120         var o = obj.o;
       
   121         var opt = obj.opt;
       
   122 
       
   123         if (!opt.min) {
       
   124             opt.min = parseInt(o.css('min-height'), 10) || obj.firstH || parseInt(o.height(), 10) || 20;
       
   125             if (opt.min <= 0) {
       
   126                 opt.min = 20; // opera fix
       
   127             }
       
   128             if (!obj.firstH) {
       
   129                 obj.firstH = opt.min;
       
   130             }
       
   131         }
       
   132         if (!opt.max) {
       
   133             opt.max = parseInt(o.css('max-height'), 10) || false;
       
   134             if (opt.max <= 0) {
       
   135                 opt.max = false; // opera fix
       
   136             }
       
   137         }
       
   138         if (!opt.step) {
       
   139             opt.step = parseInt(o.css('line-height'), 10) || parseInt(o.css('font-size'), 10) || 20;
       
   140         }
       
   141 
       
   142         var sr = {
       
   143             pt: parseInt(o.css('paddingTop'), 10)||0,
       
   144             pb: parseInt(o.css('paddingBottom'), 10)||0,
       
   145             bt: parseInt(o.css('borderTopWidth'), 10)||0,
       
   146             bb: parseInt(o.css('borderBottomWidth'), 10)||0,
       
   147             lh: parseInt(o.css('lineHeight'), 10) || false,
       
   148             fs: parseInt(o.css('fontSize'), 10) || false
       
   149         };
       
   150 
       
   151         obj.sizeRelated = sr;
       
   152     };
       
   153 
       
   154     /**
       
   155      * Create a dummy if one does not yet exist.
       
   156      */
       
   157     var createDummy = function(e) {
       
   158         var obj = e.data;
       
   159         if(!obj.dummy){
       
   160             var val = obj.o.val();
       
   161             // we need dummy to calculate scrollHeight
       
   162             // (there are some tricks that can't be applied to the textarea itself, otherwise user will see it)
       
   163             // Also, dummy must be a textarea too, and must be placed at the same position in DOM
       
   164             // in order to keep all the inherited styles
       
   165             var dummy = obj.o.clone();
       
   166             dummy.addClass('growfieldDummy');
       
   167             dummy.attr('tabindex', -9999);
       
   168             dummy.css({
       
   169                 position: 'absolute',
       
   170                 left: -9999,
       
   171                 top: 0,
       
   172                 height: '20px',
       
   173                 resize: 'none'});
       
   174             // The dummy must be inserted after otherwise google chrome will
       
   175             // focus on the dummy instead of on the actual text area, focus will always
       
   176             // be lost.
       
   177             dummy.insertAfter(obj.o);
       
   178             dummy.show();
       
   179 
       
   180             // if there is no initial value, we have to add some text, otherwise textarea will jitter
       
   181             // at the first keydown
       
   182             if (!val) {
       
   183                 dummy.val('dummy text');
       
   184             }
       
   185             obj.dummy = dummy;
       
   186             // lets set the initial height
       
   187             obj.update((!$.trim(val) || obj.opt.restore) ? 0 : obj.getDummyHeight(), false);
       
   188         }
       
   189     };
       
   190 
       
   191     /**
       
   192      * Remove the dummy if one exists
       
   193      */
       
   194     var removeDummy = function(e) {
       
   195         obj = e.data;
       
   196         if(obj.dummy){
       
   197             obj.dummy.remove();
       
   198             delete obj.dummy;
       
   199         }
       
   200     };
       
   201 
       
   202     //-----------------------------------------------------
       
   203     // END EVENT HANDLERS
       
   204     //-----------------------------------------------------
       
   205 
       
   206     // This will bind to $(document).ready if the height is loaded
       
   207     // or a window.load event already occurred.
       
   208     // OR it will just bind to the window.load event.
       
   209     var executeWhenReady = function(data,fn){
       
   210         if (data.o.height() !== 0 || windowLoaded) {
       
   211             $(document).ready(function(){
       
   212                 fn({data:data});
       
   213             });
       
   214         }
       
   215         else {
       
   216             $(window).one('load', data, fn);
       
   217         }
       
   218     };
       
   219 
       
   220     //-----------------------------------------------------
       
   221     // Public methods.
       
   222     //-----------------------------------------------------
       
   223     var that = {
       
   224         // Toggle the functionality.
       
   225         // enable or true will enable growfield
       
   226         // disable or false will disable growfield
       
   227         toggle: function(mode) {
       
   228             if ((mode=='disable' || mode===false)&&this.enabled) {
       
   229                 this.unbind();
       
   230             }
       
   231             else if ((mode=='enable' || mode===true)&&!this.enabled) {
       
   232                 this.bind();
       
   233             }
       
   234             return this;
       
   235         },
       
   236 
       
   237         // Bind all growfield events to the object.
       
   238         bind: function(){
       
   239             executeWhenReady(this,prepareSizeRelated);
       
   240             var opt = this.opt;
       
   241             var o = this.o;
       
   242 
       
   243             // auto mode, textarea grows as you type
       
   244             if (opt.auto) {
       
   245 
       
   246                 o.bind('keyup.growfield', this, keyUp);
       
   247                 this.initial = {
       
   248                     overflow: this.o.css('overflow'),
       
   249                     cssResize: this.o.css('resize')
       
   250                 };
       
   251                 // We want to ensure that safari and google chrome do not allow
       
   252                 // the user to drag-to-resize the field. This should only be enabled
       
   253                 // if auto mode is disabled.
       
   254                 if ($.browser.safari) {
       
   255                     o.css('resize', 'none');
       
   256                 }
       
   257                 o.css('overflow','hidden');
       
   258 
       
   259                 o.bind('focus.growfield', this, createDummy);
       
   260                 // all styles must be loaded before prepare elements
       
   261                 // we need to ensure the dummy exists at least for a short
       
   262                 // time so that we can calculate the initial state...
       
   263                 executeWhenReady(this, createDummy);
       
   264                 executeWhenReady(this, removeDummy);
       
   265             }
       
   266             // manual mode, textarea grows as you type ctrl + up|down
       
   267             else {
       
   268                 o.bind('keydown.growfield', this, manualKeyUp);
       
   269                 o.css('overflow-y', 'auto');
       
   270                 executeWhenReady(this,function(e){
       
   271                     e.data.update(e.data.o.height());
       
   272                 });
       
   273             }
       
   274             o.bind('focus.growfield', this, focus);
       
   275             o.bind('blur.growfield', this, blur);
       
   276             o.bind('blur.growfield', this, removeDummy);
       
   277 
       
   278             // Custom events provided in options
       
   279             if (opt.onHeightChange) {
       
   280                 o.bind('onHeightChange.growfield', opt.onHeightChange);
       
   281             }
       
   282             if (opt.onRestore) {
       
   283                 o.bind('onRestore.growfield', opt.onRestore);
       
   284             }
       
   285             if (opt.onGrowBack) {
       
   286                 o.bind('onGrowBack.growfield', opt.onGrowBack);
       
   287             }
       
   288 
       
   289             this.enabled = true;
       
   290 
       
   291             return this;
       
   292         },
       
   293 
       
   294         // Unbind all growfield events from the object (including custom events)
       
   295         unbind: function() {
       
   296             removeDummy({data:this});
       
   297             this.o.unbind('.growfield');
       
   298             this.o.css('overflow', this.initial.overflow);
       
   299             if ($.browser.safari) {
       
   300                 this.o.css('resize', this.initial.cssResize);
       
   301             }
       
   302             this.enabled = false;
       
   303 
       
   304             return this;
       
   305         },
       
   306 
       
   307         // Trigger custom events according to updateMode
       
   308         triggerEvents: function(updateMode) {
       
   309             var o = this.o;
       
   310             o.trigger('onHeightChange.growfield');
       
   311             if (updateMode == 'restore') {
       
   312                 o.trigger('onRestore.growfield');
       
   313             }
       
   314             if (updateMode == 'growback') {
       
   315                 o.trigger('onGrowBack.growfield');
       
   316             }
       
   317         },
       
   318 
       
   319         update: function(h, animate, updateMode) {
       
   320             var sr = this.sizeRelated;
       
   321             var val = this.o.val();
       
   322             var opt = this.opt;
       
   323             var dom = this.dom;
       
   324             var o = this.o;
       
   325             var th = this;
       
   326             var prev = this.prevH;
       
   327             var noHidden = !opt.auto;
       
   328             var noFocus = opt.auto;
       
   329 
       
   330             h = this.convertHeight(Math.round(h), 'inner');
       
   331             // get the right height according to min and max value
       
   332             h = opt.min > h ? opt.min :
       
   333                   opt.max && h > opt.max ? opt.max :
       
   334                   opt.auto && !val ? opt.min : h;
       
   335 
       
   336             if (opt.max && opt.auto) {
       
   337                 if (prev != opt.max && h == opt.max) { // now we reached maximum height
       
   338                     o.css('overflow-y', 'scroll');
       
   339                     if (!opt.animate) {
       
   340                         o.focus(); // browsers do loose cursor after changing overflow :(
       
   341                     }
       
   342                     noHidden = true;
       
   343                     noFocus = false;
       
   344                 }
       
   345                 if (prev == opt.max && h < opt.max) {
       
   346                     o.css('overflow-y', 'hidden');
       
   347                     if (!opt.animate) {
       
   348                         o.focus();
       
   349                     }
       
   350                     noFocus = false;
       
   351                 }
       
   352             }
       
   353 
       
   354             if (h == prev) {
       
   355                 return true;
       
   356             }
       
   357             // in case of restore in manual mode we have to store
       
   358             // previous height (we can't get it from dummy)
       
   359             if (!opt.auto && updateMode == 'restore') {
       
   360                 this.restoreH = this.convertHeight(this.prevH, 'outer');
       
   361             }
       
   362             this.prevH = h;
       
   363 
       
   364             if (animate) {
       
   365                 th.busy = true;
       
   366                 o.animate({height: h}, {
       
   367                     duration: opt.animate,
       
   368                     easing: ($.easing ? opt.easing : null),
       
   369                     overflow: null,
       
   370                     inline: true, // this option isn't jquery's. I added it by myself, see above
       
   371                     complete: function(){
       
   372                         // safari/chrome fix
       
   373                         // somehow textarea turns to overflow:scroll after animation
       
   374                         // i counldn't find it in jquery fx :(, so it looks like some bug
       
   375                         if (!noHidden) {
       
   376                             o.css('overflow', 'hidden');
       
   377                         }
       
   378                         // but if we still need to change overflow (due to opt.max option)
       
   379                         // we have to invoke focus() event, otherwise browser will loose cursor
       
   380                         if (!noFocus && updateMode != 'restore') {
       
   381                             o.focus();
       
   382                         }
       
   383                         if (updateMode == 'growback') {
       
   384                             dom.scrollTop = dom.scrollHeight;
       
   385                         }
       
   386                         th.busy = false;
       
   387                         th.triggerEvents(updateMode);
       
   388                     },
       
   389                     queue: false
       
   390                 });
       
   391             } else {
       
   392                 dom.style.height = h+'px';
       
   393                 this.triggerEvents(updateMode);
       
   394             }
       
   395         },
       
   396 
       
   397         getDummyHeight: function() {
       
   398             var val = this.o.val();
       
   399             var h = 0;
       
   400             var sr = this.sizeRelated;
       
   401             var add = "\n111\n111";
       
   402 
       
   403             // Safari has some defect with double new line symbol at the end
       
   404             // It inserts additional new line even if you have only one
       
   405             // But that't not the point :)
       
   406             // Another question is how much pixels to keep at the bottom of textarea.
       
   407             // We'll kill many rabbits at the same time by adding two new lines at the end
       
   408             // (but if we have font-size and line-height defined, we'll add two line-heights)
       
   409             if ($.browser.safari) {
       
   410                 val = val.substring(0, val.length-1); // safari has an additional new line ;(
       
   411             }
       
   412 
       
   413             if (!sr.lh || !sr.fs) {
       
   414                 val += add;
       
   415             }
       
   416 
       
   417             this.dummy.val(val);
       
   418 
       
   419             // IE requires to change height value in order to recalculate scrollHeight.
       
   420             // otherwise it stops recalculating scrollHeight after some magical number of pixels
       
   421             if ($.browser.msie) {
       
   422                 this.dummy[0].style.height = this.dummy[0].scrollHeight+'px';
       
   423             }
       
   424 
       
   425             h = this.dummy[0].scrollHeight;
       
   426 
       
   427             // if line-height is greater than font-size we'll add line-height + font-size
       
   428             // otherwise font-size * 2
       
   429             // there is no special logic in this behavior, it's been developed from visual testing
       
   430             if (sr.lh && sr.fs) {
       
   431                 h += sr.lh > sr.fs ? sr.lh+sr.fs :  sr.fs * 2;
       
   432             }
       
   433 
       
   434             // now we have to minimize dummy back, or we'll get wrong scrollHeight next time
       
   435             //if ($.browser.msie) {
       
   436             //    this.dummy[0].style.height = '20px'; // random number
       
   437             //}
       
   438 
       
   439             return h;
       
   440         },
       
   441 
       
   442         convertHeight: function(h, to) {
       
   443             var sr = this.sizeRelated, mod = (to=='inner' ? -1 : 1), bm = $.support.boxModel;
       
   444             // what we get here in 'h' is scrollHeight value.
       
   445             // so we need to subtract paddings not because of boxModel,
       
   446             // but only if browser includes them to the scroll height (which is not defined by box model)
       
   447             return h
       
   448                 + (bm ? sr.bt : 0) * mod
       
   449                 + (bm ? sr.bb : 0) * mod
       
   450                 + (bm ? sr.pt : 0) * mod
       
   451                 + (bm ? sr.pb : 0) * mod;
       
   452         }
       
   453 
       
   454     };
       
   455 
       
   456     return that;
       
   457 })();
       
   458 
       
   459 /**
       
   460  * The growfield function. This will make a textarea a growing text area.
       
   461  *
       
   462  * @param {Object} options - See API for details on possible paramaters.
       
   463  */
       
   464 $.fn.growfield = function(options) {
       
   465     // enable/disable is same thing as true/false
       
   466     switch(options){
       
   467         case 'enable':
       
   468             options = true;
       
   469             break;
       
   470         case 'disable':
       
   471             options = false;
       
   472             break;
       
   473     }
       
   474 
       
   475     // we need to know what was passed as the options
       
   476     var tp = typeof options;
       
   477 
       
   478     // These variables are used to reduce string comparisons
       
   479     // happening over and over.
       
   480     var conditions = {
       
   481         bool: tp == 'boolean',
       
   482         string: tp == 'string',
       
   483         object: tp == 'object',
       
   484         restart: options == 'restart',
       
   485         destroy: options == 'destroy'
       
   486     };
       
   487 
       
   488     // If the type of the options is a string
       
   489     // and is not one of the pre-defined ones, then
       
   490     // options is a preset.
       
   491     if(conditions.string && !conditions.destroy && !conditions.restart){
       
   492         options = $.fn.growfield.presets[options];
       
   493         // change to new conditions
       
   494         conditions.string = false;
       
   495         conditions.object = true;
       
   496     }
       
   497 
       
   498     // completely remove growfield from the dom elements
       
   499     if (conditions.destroy) {
       
   500         this.each(function() {
       
   501             var self = $(this);
       
   502             var gf = self.data('growfield');
       
   503             if (gf !== undefined) {
       
   504                 gf.unbind();
       
   505                 self.removeData('growfield');
       
   506             }
       
   507         });
       
   508     }
       
   509     // Apply growfield
       
   510     else {
       
   511         var textareaRegex = /textarea/i;
       
   512         this.each(function() {
       
   513             // only deal with textareas which are not dummy fields.
       
   514             if (textareaRegex.test(this.tagName) && !$(this).hasClass('growfieldDummy')) {
       
   515                 var o = $(this);
       
   516                 var gf = o.data('growfield');
       
   517                 // Create the new options
       
   518                 if (gf === undefined) {
       
   519                     gf = $.growfield(this,options);
       
   520                     o.data('growfield', gf);
       
   521 
       
   522                     // Bind only if the options is not a boolean
       
   523                     // or is not "false". Because options = a false boolean
       
   524                     // indicates intial bind should not happen.
       
   525                     if(!conditions.bool || options){
       
   526                         gf.bind();
       
   527                     }
       
   528                 }
       
   529                 // Otherwise apply actions based on the options provided
       
   530                 else {
       
   531                     // If new options provided, set them
       
   532                     if(conditions.object && options) {
       
   533                         $.extend(gf.opt,options);
       
   534                     }
       
   535                     // If toggling enable/disable then do it
       
   536                     else if (conditions.bool) {
       
   537                         gf.toggle(options);
       
   538                     }
       
   539                     // If restarting, restart
       
   540                     else if (conditions.restart) {
       
   541                         gf.unbind();
       
   542                         gf.bind();
       
   543                     }
       
   544                 }
       
   545             }
       
   546         });
       
   547     }
       
   548 
       
   549     return this;
       
   550 };
       
   551 
       
   552 /**
       
   553  * These are the default options to use, unless specified when invoking growfield.
       
   554  */
       
   555 $.fn.growfield.defaults ={
       
   556     // Should the growfield automatically expand?
       
   557     auto: true,
       
   558     // The animation speed for expanding (false = off)
       
   559     animate: 100,
       
   560     // The easiny function to use, if the jquery.easing plugin is not present during
       
   561     // execution, this will always be treated as null regardless of the set value
       
   562     easing: null,
       
   563     // The minimum height (defaults to CSS min-height, or the current height of the element)
       
   564     min: false,
       
   565     // The maximum height (defaults to CSS max-height, or unlimited)
       
   566     max: false,
       
   567     // Should the element restore to it's original size after focus is lost?
       
   568     restore: false,
       
   569     // How many pixels to expand when the user is about to have to scroll. Defaults to 1 line.
       
   570     step: false
       
   571 };
       
   572 
       
   573 /**
       
   574  * These are presets. The presets are indexed by name containing different preset
       
   575  * option objects. When growfield is invoked with the preset's name, that options object
       
   576  * is loaded without having to be specified each time.
       
   577  */
       
   578 $.fn.growfield.presets = {};
       
   579 
       
   580 })(jQuery);
       
   581