|
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 |