Index: app/jquery/jquery-growfield.js |
=================================================================== |
new file mode 100644 |
--- /dev/null |
+++ b/app/jquery/jquery-growfield.js |
@@ -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); |
+ |