# HG changeset patch # User Daniel Diniz # Date 1246046086 -7200 # Node ID 0979e7af115fb51c4df4602f35406f27041216b3 # Parent 645f4de26f994c28863af8fcabed5ab5458b337e Renamed jquery.growfield.js to jquery-growfield.js. Reviewed by: Lennard de Rijk diff -r 645f4de26f99 -r 0979e7af115f app/jquery/jquery-growfield.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/jquery/jquery-growfield.js Fri Jun 26 21:54:46 2009 +0200 @@ -0,0 +1,581 @@ +/* + * The MIT License + * + * Copyright (c) 2009 Johann Kuindji + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Johann Kuindji, Dmitriy Likhten + * http://code.google.com/p/jquery-growfield/ + */ +(function($) { +if ($.support === undefined) { + $.support = { boxModel: $.boxModel }; +} +var windowLoaded = false; +$(window).one('load', function(){ windowLoaded=true; }); + +// we need to adapt jquery animations for textareas. +// by default, it changes display to 'block' if we're trying to +// change width or height. We have to prevent this. +// THIS WILL NOT ALTER JQUERY ORIGINAL BEHAVIORS, IT WILL HOWEVER ADD +// SOME SO THAT GROWFIELD ANIMATIONS WORK CORRECTLY. +$.fx.prototype.originalUpdate = $.fx.prototype.update; +$.fx.prototype.update = false; +$.fx.prototype.update = function () { + if (!this.options.inline) { + return this.originalUpdate.call(this); + } + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); +}; + +$.growfield = function(dom,options){ + // Extend ptt(prototype) with our own private variables/ + // shared's functions are re-referenced and not cloned so + // memory is kept at a minimum. + var that = $.extend({ + dom: dom, + o: $(dom), + enabled: false, + dummy: false, + busy: false, + initial: false, + sizseRelated: false, + prevH: false, + firstH: false, + restoreH: false, + opt: $.extend({},$.fn.growfield.defaults,options) + },$.growfield.ptt); + + return that; +}; + +//----------------------------------------------------- +// This is the base class for all $.growfield objects +// (their prototype) +//----------------------------------------------------- +$.growfield.ptt = (function(){ + //----------------------------------------------------- + //EVENT HANDLERS for dealing with the growfield object + //----------------------------------------------------- + var manualKeyUp = function(e) { + var obj = e.data; + if (e.ctrlKey && (e.keyCode == 38 || e.keyCode == 40)){ + obj.update( + obj.o.outerHeight() + (obj.opt.step*( e.keyCode==38? -1: 1)), + obj.opt.animate + ); + } + }; + + var keyUp = function(e) { + var obj = e.data; + if (!obj.busy){ + if ($.inArray(e.keyCode, [37,38,39,40]) === -1) { + obj.update(obj.getDummyHeight(), obj.opt.animate); + } + } + return true; + }; + + var focus = function(e) { + var obj = e.data; + if (!obj.busy) { + if (obj.opt.restore) { + obj.update(obj.dummy ? obj.getDummyHeight() : obj.restoreH, obj.opt.animate, 'growback'); + } + } + }; + + var blur = function(e) { + var obj = e.data; + if (!obj.busy) { + if (obj.opt.restore) { + obj.update(0, obj.opt.animate, 'restore'); + } + } + }; + + var prepareSizeRelated = function(e) { + var obj = e.data; + var o = obj.o; + var opt = obj.opt; + + if (!opt.min) { + opt.min = parseInt(o.css('min-height'), 10) || obj.firstH || parseInt(o.height(), 10) || 20; + if (opt.min <= 0) { + opt.min = 20; // opera fix + } + if (!obj.firstH) { + obj.firstH = opt.min; + } + } + if (!opt.max) { + opt.max = parseInt(o.css('max-height'), 10) || false; + if (opt.max <= 0) { + opt.max = false; // opera fix + } + } + if (!opt.step) { + opt.step = parseInt(o.css('line-height'), 10) || parseInt(o.css('font-size'), 10) || 20; + } + + var sr = { + pt: parseInt(o.css('paddingTop'), 10)||0, + pb: parseInt(o.css('paddingBottom'), 10)||0, + bt: parseInt(o.css('borderTopWidth'), 10)||0, + bb: parseInt(o.css('borderBottomWidth'), 10)||0, + lh: parseInt(o.css('lineHeight'), 10) || false, + fs: parseInt(o.css('fontSize'), 10) || false + }; + + obj.sizeRelated = sr; + }; + + /** + * Create a dummy if one does not yet exist. + */ + var createDummy = function(e) { + var obj = e.data; + if(!obj.dummy){ + var val = obj.o.val(); + // we need dummy to calculate scrollHeight + // (there are some tricks that can't be applied to the textarea itself, otherwise user will see it) + // Also, dummy must be a textarea too, and must be placed at the same position in DOM + // in order to keep all the inherited styles + var dummy = obj.o.clone(); + dummy.addClass('growfieldDummy'); + dummy.attr('tabindex', -9999); + dummy.css({ + position: 'absolute', + left: -9999, + top: 0, + height: '20px', + resize: 'none'}); + // The dummy must be inserted after otherwise google chrome will + // focus on the dummy instead of on the actual text area, focus will always + // be lost. + dummy.insertAfter(obj.o); + dummy.show(); + + // if there is no initial value, we have to add some text, otherwise textarea will jitter + // at the first keydown + if (!val) { + dummy.val('dummy text'); + } + obj.dummy = dummy; + // lets set the initial height + obj.update((!$.trim(val) || obj.opt.restore) ? 0 : obj.getDummyHeight(), false); + } + }; + + /** + * Remove the dummy if one exists + */ + var removeDummy = function(e) { + obj = e.data; + if(obj.dummy){ + obj.dummy.remove(); + delete obj.dummy; + } + }; + + //----------------------------------------------------- + // END EVENT HANDLERS + //----------------------------------------------------- + + // This will bind to $(document).ready if the height is loaded + // or a window.load event already occurred. + // OR it will just bind to the window.load event. + var executeWhenReady = function(data,fn){ + if (data.o.height() !== 0 || windowLoaded) { + $(document).ready(function(){ + fn({data:data}); + }); + } + else { + $(window).one('load', data, fn); + } + }; + + //----------------------------------------------------- + // Public methods. + //----------------------------------------------------- + var that = { + // Toggle the functionality. + // enable or true will enable growfield + // disable or false will disable growfield + toggle: function(mode) { + if ((mode=='disable' || mode===false)&&this.enabled) { + this.unbind(); + } + else if ((mode=='enable' || mode===true)&&!this.enabled) { + this.bind(); + } + return this; + }, + + // Bind all growfield events to the object. + bind: function(){ + executeWhenReady(this,prepareSizeRelated); + var opt = this.opt; + var o = this.o; + + // auto mode, textarea grows as you type + if (opt.auto) { + + o.bind('keyup.growfield', this, keyUp); + this.initial = { + overflow: this.o.css('overflow'), + cssResize: this.o.css('resize') + }; + // We want to ensure that safari and google chrome do not allow + // the user to drag-to-resize the field. This should only be enabled + // if auto mode is disabled. + if ($.browser.safari) { + o.css('resize', 'none'); + } + o.css('overflow','hidden'); + + o.bind('focus.growfield', this, createDummy); + // all styles must be loaded before prepare elements + // we need to ensure the dummy exists at least for a short + // time so that we can calculate the initial state... + executeWhenReady(this, createDummy); + executeWhenReady(this, removeDummy); + } + // manual mode, textarea grows as you type ctrl + up|down + else { + o.bind('keydown.growfield', this, manualKeyUp); + o.css('overflow-y', 'auto'); + executeWhenReady(this,function(e){ + e.data.update(e.data.o.height()); + }); + } + o.bind('focus.growfield', this, focus); + o.bind('blur.growfield', this, blur); + o.bind('blur.growfield', this, removeDummy); + + // Custom events provided in options + if (opt.onHeightChange) { + o.bind('onHeightChange.growfield', opt.onHeightChange); + } + if (opt.onRestore) { + o.bind('onRestore.growfield', opt.onRestore); + } + if (opt.onGrowBack) { + o.bind('onGrowBack.growfield', opt.onGrowBack); + } + + this.enabled = true; + + return this; + }, + + // Unbind all growfield events from the object (including custom events) + unbind: function() { + removeDummy({data:this}); + this.o.unbind('.growfield'); + this.o.css('overflow', this.initial.overflow); + if ($.browser.safari) { + this.o.css('resize', this.initial.cssResize); + } + this.enabled = false; + + return this; + }, + + // Trigger custom events according to updateMode + triggerEvents: function(updateMode) { + var o = this.o; + o.trigger('onHeightChange.growfield'); + if (updateMode == 'restore') { + o.trigger('onRestore.growfield'); + } + if (updateMode == 'growback') { + o.trigger('onGrowBack.growfield'); + } + }, + + update: function(h, animate, updateMode) { + var sr = this.sizeRelated; + var val = this.o.val(); + var opt = this.opt; + var dom = this.dom; + var o = this.o; + var th = this; + var prev = this.prevH; + var noHidden = !opt.auto; + var noFocus = opt.auto; + + h = this.convertHeight(Math.round(h), 'inner'); + // get the right height according to min and max value + h = opt.min > h ? opt.min : + opt.max && h > opt.max ? opt.max : + opt.auto && !val ? opt.min : h; + + if (opt.max && opt.auto) { + if (prev != opt.max && h == opt.max) { // now we reached maximum height + o.css('overflow-y', 'scroll'); + if (!opt.animate) { + o.focus(); // browsers do loose cursor after changing overflow :( + } + noHidden = true; + noFocus = false; + } + if (prev == opt.max && h < opt.max) { + o.css('overflow-y', 'hidden'); + if (!opt.animate) { + o.focus(); + } + noFocus = false; + } + } + + if (h == prev) { + return true; + } + // in case of restore in manual mode we have to store + // previous height (we can't get it from dummy) + if (!opt.auto && updateMode == 'restore') { + this.restoreH = this.convertHeight(this.prevH, 'outer'); + } + this.prevH = h; + + if (animate) { + th.busy = true; + o.animate({height: h}, { + duration: opt.animate, + easing: ($.easing ? opt.easing : null), + overflow: null, + inline: true, // this option isn't jquery's. I added it by myself, see above + complete: function(){ + // safari/chrome fix + // somehow textarea turns to overflow:scroll after animation + // i counldn't find it in jquery fx :(, so it looks like some bug + if (!noHidden) { + o.css('overflow', 'hidden'); + } + // but if we still need to change overflow (due to opt.max option) + // we have to invoke focus() event, otherwise browser will loose cursor + if (!noFocus && updateMode != 'restore') { + o.focus(); + } + if (updateMode == 'growback') { + dom.scrollTop = dom.scrollHeight; + } + th.busy = false; + th.triggerEvents(updateMode); + }, + queue: false + }); + } else { + dom.style.height = h+'px'; + this.triggerEvents(updateMode); + } + }, + + getDummyHeight: function() { + var val = this.o.val(); + var h = 0; + var sr = this.sizeRelated; + var add = "\n111\n111"; + + // Safari has some defect with double new line symbol at the end + // It inserts additional new line even if you have only one + // But that't not the point :) + // Another question is how much pixels to keep at the bottom of textarea. + // We'll kill many rabbits at the same time by adding two new lines at the end + // (but if we have font-size and line-height defined, we'll add two line-heights) + if ($.browser.safari) { + val = val.substring(0, val.length-1); // safari has an additional new line ;( + } + + if (!sr.lh || !sr.fs) { + val += add; + } + + this.dummy.val(val); + + // IE requires to change height value in order to recalculate scrollHeight. + // otherwise it stops recalculating scrollHeight after some magical number of pixels + if ($.browser.msie) { + this.dummy[0].style.height = this.dummy[0].scrollHeight+'px'; + } + + h = this.dummy[0].scrollHeight; + + // if line-height is greater than font-size we'll add line-height + font-size + // otherwise font-size * 2 + // there is no special logic in this behavior, it's been developed from visual testing + if (sr.lh && sr.fs) { + h += sr.lh > sr.fs ? sr.lh+sr.fs : sr.fs * 2; + } + + // now we have to minimize dummy back, or we'll get wrong scrollHeight next time + //if ($.browser.msie) { + // this.dummy[0].style.height = '20px'; // random number + //} + + return h; + }, + + convertHeight: function(h, to) { + var sr = this.sizeRelated, mod = (to=='inner' ? -1 : 1), bm = $.support.boxModel; + // what we get here in 'h' is scrollHeight value. + // so we need to subtract paddings not because of boxModel, + // but only if browser includes them to the scroll height (which is not defined by box model) + return h + + (bm ? sr.bt : 0) * mod + + (bm ? sr.bb : 0) * mod + + (bm ? sr.pt : 0) * mod + + (bm ? sr.pb : 0) * mod; + } + + }; + + return that; +})(); + +/** + * The growfield function. This will make a textarea a growing text area. + * + * @param {Object} options - See API for details on possible paramaters. + */ +$.fn.growfield = function(options) { + // enable/disable is same thing as true/false + switch(options){ + case 'enable': + options = true; + break; + case 'disable': + options = false; + break; + } + + // we need to know what was passed as the options + var tp = typeof options; + + // These variables are used to reduce string comparisons + // happening over and over. + var conditions = { + bool: tp == 'boolean', + string: tp == 'string', + object: tp == 'object', + restart: options == 'restart', + destroy: options == 'destroy' + }; + + // If the type of the options is a string + // and is not one of the pre-defined ones, then + // options is a preset. + if(conditions.string && !conditions.destroy && !conditions.restart){ + options = $.fn.growfield.presets[options]; + // change to new conditions + conditions.string = false; + conditions.object = true; + } + + // completely remove growfield from the dom elements + if (conditions.destroy) { + this.each(function() { + var self = $(this); + var gf = self.data('growfield'); + if (gf !== undefined) { + gf.unbind(); + self.removeData('growfield'); + } + }); + } + // Apply growfield + else { + var textareaRegex = /textarea/i; + this.each(function() { + // only deal with textareas which are not dummy fields. + if (textareaRegex.test(this.tagName) && !$(this).hasClass('growfieldDummy')) { + var o = $(this); + var gf = o.data('growfield'); + // Create the new options + if (gf === undefined) { + gf = $.growfield(this,options); + o.data('growfield', gf); + + // Bind only if the options is not a boolean + // or is not "false". Because options = a false boolean + // indicates intial bind should not happen. + if(!conditions.bool || options){ + gf.bind(); + } + } + // Otherwise apply actions based on the options provided + else { + // If new options provided, set them + if(conditions.object && options) { + $.extend(gf.opt,options); + } + // If toggling enable/disable then do it + else if (conditions.bool) { + gf.toggle(options); + } + // If restarting, restart + else if (conditions.restart) { + gf.unbind(); + gf.bind(); + } + } + } + }); + } + + return this; +}; + +/** + * These are the default options to use, unless specified when invoking growfield. + */ +$.fn.growfield.defaults ={ + // Should the growfield automatically expand? + auto: true, + // The animation speed for expanding (false = off) + animate: 100, + // The easiny function to use, if the jquery.easing plugin is not present during + // execution, this will always be treated as null regardless of the set value + easing: null, + // The minimum height (defaults to CSS min-height, or the current height of the element) + min: false, + // The maximum height (defaults to CSS max-height, or unlimited) + max: false, + // Should the element restore to it's original size after focus is lost? + restore: false, + // How many pixels to expand when the user is about to have to scroll. Defaults to 1 line. + step: false +}; + +/** + * These are presets. The presets are indexed by name containing different preset + * option objects. When growfield is invoked with the preset's name, that options object + * is loaded without having to be specified each time. + */ +$.fn.growfield.presets = {}; + +})(jQuery); + diff -r 645f4de26f99 -r 0979e7af115f app/jquery/jquery.growfield.js --- a/app/jquery/jquery.growfield.js Tue Jun 23 20:54:03 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,581 +0,0 @@ -/* - * The MIT License - * - * Copyright (c) 2009 Johann Kuindji - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author Johann Kuindji, Dmitriy Likhten - * http://code.google.com/p/jquery-growfield/ - */ -(function($) { -if ($.support === undefined) { - $.support = { boxModel: $.boxModel }; -} -var windowLoaded = false; -$(window).one('load', function(){ windowLoaded=true; }); - -// we need to adapt jquery animations for textareas. -// by default, it changes display to 'block' if we're trying to -// change width or height. We have to prevent this. -// THIS WILL NOT ALTER JQUERY ORIGINAL BEHAVIORS, IT WILL HOWEVER ADD -// SOME SO THAT GROWFIELD ANIMATIONS WORK CORRECTLY. -$.fx.prototype.originalUpdate = $.fx.prototype.update; -$.fx.prototype.update = false; -$.fx.prototype.update = function () { - if (!this.options.inline) { - return this.originalUpdate.call(this); - } - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this ); -}; - -$.growfield = function(dom,options){ - // Extend ptt(prototype) with our own private variables/ - // shared's functions are re-referenced and not cloned so - // memory is kept at a minimum. - var that = $.extend({ - dom: dom, - o: $(dom), - enabled: false, - dummy: false, - busy: false, - initial: false, - sizseRelated: false, - prevH: false, - firstH: false, - restoreH: false, - opt: $.extend({},$.fn.growfield.defaults,options) - },$.growfield.ptt); - - return that; -}; - -//----------------------------------------------------- -// This is the base class for all $.growfield objects -// (their prototype) -//----------------------------------------------------- -$.growfield.ptt = (function(){ - //----------------------------------------------------- - //EVENT HANDLERS for dealing with the growfield object - //----------------------------------------------------- - var manualKeyUp = function(e) { - var obj = e.data; - if (e.ctrlKey && (e.keyCode == 38 || e.keyCode == 40)){ - obj.update( - obj.o.outerHeight() + (obj.opt.step*( e.keyCode==38? -1: 1)), - obj.opt.animate - ); - } - }; - - var keyUp = function(e) { - var obj = e.data; - if (!obj.busy){ - if ($.inArray(e.keyCode, [37,38,39,40]) === -1) { - obj.update(obj.getDummyHeight(), obj.opt.animate); - } - } - return true; - }; - - var focus = function(e) { - var obj = e.data; - if (!obj.busy) { - if (obj.opt.restore) { - obj.update(obj.dummy ? obj.getDummyHeight() : obj.restoreH, obj.opt.animate, 'growback'); - } - } - }; - - var blur = function(e) { - var obj = e.data; - if (!obj.busy) { - if (obj.opt.restore) { - obj.update(0, obj.opt.animate, 'restore'); - } - } - }; - - var prepareSizeRelated = function(e) { - var obj = e.data; - var o = obj.o; - var opt = obj.opt; - - if (!opt.min) { - opt.min = parseInt(o.css('min-height'), 10) || obj.firstH || parseInt(o.height(), 10) || 20; - if (opt.min <= 0) { - opt.min = 20; // opera fix - } - if (!obj.firstH) { - obj.firstH = opt.min; - } - } - if (!opt.max) { - opt.max = parseInt(o.css('max-height'), 10) || false; - if (opt.max <= 0) { - opt.max = false; // opera fix - } - } - if (!opt.step) { - opt.step = parseInt(o.css('line-height'), 10) || parseInt(o.css('font-size'), 10) || 20; - } - - var sr = { - pt: parseInt(o.css('paddingTop'), 10)||0, - pb: parseInt(o.css('paddingBottom'), 10)||0, - bt: parseInt(o.css('borderTopWidth'), 10)||0, - bb: parseInt(o.css('borderBottomWidth'), 10)||0, - lh: parseInt(o.css('lineHeight'), 10) || false, - fs: parseInt(o.css('fontSize'), 10) || false - }; - - obj.sizeRelated = sr; - }; - - /** - * Create a dummy if one does not yet exist. - */ - var createDummy = function(e) { - var obj = e.data; - if(!obj.dummy){ - var val = obj.o.val(); - // we need dummy to calculate scrollHeight - // (there are some tricks that can't be applied to the textarea itself, otherwise user will see it) - // Also, dummy must be a textarea too, and must be placed at the same position in DOM - // in order to keep all the inherited styles - var dummy = obj.o.clone(); - dummy.addClass('growfieldDummy'); - dummy.attr('tabindex', -9999); - dummy.css({ - position: 'absolute', - left: -9999, - top: 0, - height: '20px', - resize: 'none'}); - // The dummy must be inserted after otherwise google chrome will - // focus on the dummy instead of on the actual text area, focus will always - // be lost. - dummy.insertAfter(obj.o); - dummy.show(); - - // if there is no initial value, we have to add some text, otherwise textarea will jitter - // at the first keydown - if (!val) { - dummy.val('dummy text'); - } - obj.dummy = dummy; - // lets set the initial height - obj.update((!$.trim(val) || obj.opt.restore) ? 0 : obj.getDummyHeight(), false); - } - }; - - /** - * Remove the dummy if one exists - */ - var removeDummy = function(e) { - obj = e.data; - if(obj.dummy){ - obj.dummy.remove(); - delete obj.dummy; - } - }; - - //----------------------------------------------------- - // END EVENT HANDLERS - //----------------------------------------------------- - - // This will bind to $(document).ready if the height is loaded - // or a window.load event already occurred. - // OR it will just bind to the window.load event. - var executeWhenReady = function(data,fn){ - if (data.o.height() !== 0 || windowLoaded) { - $(document).ready(function(){ - fn({data:data}); - }); - } - else { - $(window).one('load', data, fn); - } - }; - - //----------------------------------------------------- - // Public methods. - //----------------------------------------------------- - var that = { - // Toggle the functionality. - // enable or true will enable growfield - // disable or false will disable growfield - toggle: function(mode) { - if ((mode=='disable' || mode===false)&&this.enabled) { - this.unbind(); - } - else if ((mode=='enable' || mode===true)&&!this.enabled) { - this.bind(); - } - return this; - }, - - // Bind all growfield events to the object. - bind: function(){ - executeWhenReady(this,prepareSizeRelated); - var opt = this.opt; - var o = this.o; - - // auto mode, textarea grows as you type - if (opt.auto) { - - o.bind('keyup.growfield', this, keyUp); - this.initial = { - overflow: this.o.css('overflow'), - cssResize: this.o.css('resize') - }; - // We want to ensure that safari and google chrome do not allow - // the user to drag-to-resize the field. This should only be enabled - // if auto mode is disabled. - if ($.browser.safari) { - o.css('resize', 'none'); - } - o.css('overflow','hidden'); - - o.bind('focus.growfield', this, createDummy); - // all styles must be loaded before prepare elements - // we need to ensure the dummy exists at least for a short - // time so that we can calculate the initial state... - executeWhenReady(this, createDummy); - executeWhenReady(this, removeDummy); - } - // manual mode, textarea grows as you type ctrl + up|down - else { - o.bind('keydown.growfield', this, manualKeyUp); - o.css('overflow-y', 'auto'); - executeWhenReady(this,function(e){ - e.data.update(e.data.o.height()); - }); - } - o.bind('focus.growfield', this, focus); - o.bind('blur.growfield', this, blur); - o.bind('blur.growfield', this, removeDummy); - - // Custom events provided in options - if (opt.onHeightChange) { - o.bind('onHeightChange.growfield', opt.onHeightChange); - } - if (opt.onRestore) { - o.bind('onRestore.growfield', opt.onRestore); - } - if (opt.onGrowBack) { - o.bind('onGrowBack.growfield', opt.onGrowBack); - } - - this.enabled = true; - - return this; - }, - - // Unbind all growfield events from the object (including custom events) - unbind: function() { - removeDummy({data:this}); - this.o.unbind('.growfield'); - this.o.css('overflow', this.initial.overflow); - if ($.browser.safari) { - this.o.css('resize', this.initial.cssResize); - } - this.enabled = false; - - return this; - }, - - // Trigger custom events according to updateMode - triggerEvents: function(updateMode) { - var o = this.o; - o.trigger('onHeightChange.growfield'); - if (updateMode == 'restore') { - o.trigger('onRestore.growfield'); - } - if (updateMode == 'growback') { - o.trigger('onGrowBack.growfield'); - } - }, - - update: function(h, animate, updateMode) { - var sr = this.sizeRelated; - var val = this.o.val(); - var opt = this.opt; - var dom = this.dom; - var o = this.o; - var th = this; - var prev = this.prevH; - var noHidden = !opt.auto; - var noFocus = opt.auto; - - h = this.convertHeight(Math.round(h), 'inner'); - // get the right height according to min and max value - h = opt.min > h ? opt.min : - opt.max && h > opt.max ? opt.max : - opt.auto && !val ? opt.min : h; - - if (opt.max && opt.auto) { - if (prev != opt.max && h == opt.max) { // now we reached maximum height - o.css('overflow-y', 'scroll'); - if (!opt.animate) { - o.focus(); // browsers do loose cursor after changing overflow :( - } - noHidden = true; - noFocus = false; - } - if (prev == opt.max && h < opt.max) { - o.css('overflow-y', 'hidden'); - if (!opt.animate) { - o.focus(); - } - noFocus = false; - } - } - - if (h == prev) { - return true; - } - // in case of restore in manual mode we have to store - // previous height (we can't get it from dummy) - if (!opt.auto && updateMode == 'restore') { - this.restoreH = this.convertHeight(this.prevH, 'outer'); - } - this.prevH = h; - - if (animate) { - th.busy = true; - o.animate({height: h}, { - duration: opt.animate, - easing: ($.easing ? opt.easing : null), - overflow: null, - inline: true, // this option isn't jquery's. I added it by myself, see above - complete: function(){ - // safari/chrome fix - // somehow textarea turns to overflow:scroll after animation - // i counldn't find it in jquery fx :(, so it looks like some bug - if (!noHidden) { - o.css('overflow', 'hidden'); - } - // but if we still need to change overflow (due to opt.max option) - // we have to invoke focus() event, otherwise browser will loose cursor - if (!noFocus && updateMode != 'restore') { - o.focus(); - } - if (updateMode == 'growback') { - dom.scrollTop = dom.scrollHeight; - } - th.busy = false; - th.triggerEvents(updateMode); - }, - queue: false - }); - } else { - dom.style.height = h+'px'; - this.triggerEvents(updateMode); - } - }, - - getDummyHeight: function() { - var val = this.o.val(); - var h = 0; - var sr = this.sizeRelated; - var add = "\n111\n111"; - - // Safari has some defect with double new line symbol at the end - // It inserts additional new line even if you have only one - // But that't not the point :) - // Another question is how much pixels to keep at the bottom of textarea. - // We'll kill many rabbits at the same time by adding two new lines at the end - // (but if we have font-size and line-height defined, we'll add two line-heights) - if ($.browser.safari) { - val = val.substring(0, val.length-1); // safari has an additional new line ;( - } - - if (!sr.lh || !sr.fs) { - val += add; - } - - this.dummy.val(val); - - // IE requires to change height value in order to recalculate scrollHeight. - // otherwise it stops recalculating scrollHeight after some magical number of pixels - if ($.browser.msie) { - this.dummy[0].style.height = this.dummy[0].scrollHeight+'px'; - } - - h = this.dummy[0].scrollHeight; - - // if line-height is greater than font-size we'll add line-height + font-size - // otherwise font-size * 2 - // there is no special logic in this behavior, it's been developed from visual testing - if (sr.lh && sr.fs) { - h += sr.lh > sr.fs ? sr.lh+sr.fs : sr.fs * 2; - } - - // now we have to minimize dummy back, or we'll get wrong scrollHeight next time - //if ($.browser.msie) { - // this.dummy[0].style.height = '20px'; // random number - //} - - return h; - }, - - convertHeight: function(h, to) { - var sr = this.sizeRelated, mod = (to=='inner' ? -1 : 1), bm = $.support.boxModel; - // what we get here in 'h' is scrollHeight value. - // so we need to subtract paddings not because of boxModel, - // but only if browser includes them to the scroll height (which is not defined by box model) - return h - + (bm ? sr.bt : 0) * mod - + (bm ? sr.bb : 0) * mod - + (bm ? sr.pt : 0) * mod - + (bm ? sr.pb : 0) * mod; - } - - }; - - return that; -})(); - -/** - * The growfield function. This will make a textarea a growing text area. - * - * @param {Object} options - See API for details on possible paramaters. - */ -$.fn.growfield = function(options) { - // enable/disable is same thing as true/false - switch(options){ - case 'enable': - options = true; - break; - case 'disable': - options = false; - break; - } - - // we need to know what was passed as the options - var tp = typeof options; - - // These variables are used to reduce string comparisons - // happening over and over. - var conditions = { - bool: tp == 'boolean', - string: tp == 'string', - object: tp == 'object', - restart: options == 'restart', - destroy: options == 'destroy' - }; - - // If the type of the options is a string - // and is not one of the pre-defined ones, then - // options is a preset. - if(conditions.string && !conditions.destroy && !conditions.restart){ - options = $.fn.growfield.presets[options]; - // change to new conditions - conditions.string = false; - conditions.object = true; - } - - // completely remove growfield from the dom elements - if (conditions.destroy) { - this.each(function() { - var self = $(this); - var gf = self.data('growfield'); - if (gf !== undefined) { - gf.unbind(); - self.removeData('growfield'); - } - }); - } - // Apply growfield - else { - var textareaRegex = /textarea/i; - this.each(function() { - // only deal with textareas which are not dummy fields. - if (textareaRegex.test(this.tagName) && !$(this).hasClass('growfieldDummy')) { - var o = $(this); - var gf = o.data('growfield'); - // Create the new options - if (gf === undefined) { - gf = $.growfield(this,options); - o.data('growfield', gf); - - // Bind only if the options is not a boolean - // or is not "false". Because options = a false boolean - // indicates intial bind should not happen. - if(!conditions.bool || options){ - gf.bind(); - } - } - // Otherwise apply actions based on the options provided - else { - // If new options provided, set them - if(conditions.object && options) { - $.extend(gf.opt,options); - } - // If toggling enable/disable then do it - else if (conditions.bool) { - gf.toggle(options); - } - // If restarting, restart - else if (conditions.restart) { - gf.unbind(); - gf.bind(); - } - } - } - }); - } - - return this; -}; - -/** - * These are the default options to use, unless specified when invoking growfield. - */ -$.fn.growfield.defaults ={ - // Should the growfield automatically expand? - auto: true, - // The animation speed for expanding (false = off) - animate: 100, - // The easiny function to use, if the jquery.easing plugin is not present during - // execution, this will always be treated as null regardless of the set value - easing: null, - // The minimum height (defaults to CSS min-height, or the current height of the element) - min: false, - // The maximum height (defaults to CSS max-height, or unlimited) - max: false, - // Should the element restore to it's original size after focus is lost? - restore: false, - // How many pixels to expand when the user is about to have to scroll. Defaults to 1 line. - step: false -}; - -/** - * These are presets. The presets are indexed by name containing different preset - * option objects. When growfield is invoked with the preset's name, that options object - * is loaded without having to be specified each time. - */ -$.fn.growfield.presets = {}; - -})(jQuery); -