initial import
[roojs1] / Roo / Resizable.js
1 /*
2  * Based on:
3  * Ext JS Library 1.1.1
4  * Copyright(c) 2006-2007, Ext JS, LLC.
5  *
6  * Originally Released Under LGPL - original licence link has changed is not relivant.
7  *
8  * Fork - LGPL
9  * <script type="text/javascript">
10  */
11
12 /**
13  * @class Roo.Resizable
14  * @extends Roo.util.Observable
15  * <p>Applies drag handles to an element to make it resizable. The drag handles are inserted into the element
16  * and positioned absolute. Some elements, such as a textarea or image, don't support this. To overcome that, you can wrap
17  * the textarea in a div and set "resizeChild" to true (or to the id of the element), <b>or</b> set wrap:true in your config and
18  * the element will be wrapped for you automatically.</p>
19  * <p>Here is the list of valid resize handles:</p>
20  * <pre>
21 Value   Description
22 ------  -------------------
23  'n'     north
24  's'     south
25  'e'     east
26  'w'     west
27  'nw'    northwest
28  'sw'    southwest
29  'se'    southeast
30  'ne'    northeast
31  'all'   all
32 </pre>
33  * <p>Here's an example showing the creation of a typical Resizable:</p>
34  * <pre><code>
35 var resizer = new Roo.Resizable("element-id", {
36     handles: 'all',
37     minWidth: 200,
38     minHeight: 100,
39     maxWidth: 500,
40     maxHeight: 400,
41     pinned: true
42 });
43 resizer.on("resize", myHandler);
44 </code></pre>
45  * <p>To hide a particular handle, set its display to none in CSS, or through script:<br>
46  * resizer.east.setDisplayed(false);</p>
47  * @cfg {Boolean/String/Element} resizeChild True to resize the first child, or id/element to resize (defaults to false)
48  * @cfg {Array/String} adjustments String "auto" or an array [width, height] with values to be <b>added</b> to the
49  * resize operation's new size (defaults to [0, 0])
50  * @cfg {Number} minWidth The minimum width for the element (defaults to 5)
51  * @cfg {Number} minHeight The minimum height for the element (defaults to 5)
52  * @cfg {Number} maxWidth The maximum width for the element (defaults to 10000)
53  * @cfg {Number} maxHeight The maximum height for the element (defaults to 10000)
54  * @cfg {Boolean} enabled False to disable resizing (defaults to true)
55  * @cfg {Boolean} wrap True to wrap an element with a div if needed (required for textareas and images, defaults to false)
56  * @cfg {Number} width The width of the element in pixels (defaults to null)
57  * @cfg {Number} height The height of the element in pixels (defaults to null)
58  * @cfg {Boolean} animate True to animate the resize (not compatible with dynamic sizing, defaults to false)
59  * @cfg {Number} duration Animation duration if animate = true (defaults to .35)
60  * @cfg {Boolean} dynamic True to resize the element while dragging instead of using a proxy (defaults to false)
61  * @cfg {String} handles String consisting of the resize handles to display (defaults to undefined)
62  * @cfg {Boolean} multiDirectional <b>Deprecated</b>.  The old style of adding multi-direction resize handles, deprecated
63  * in favor of the handles config option (defaults to false)
64  * @cfg {Boolean} disableTrackOver True to disable mouse tracking. This is only applied at config time. (defaults to false)
65  * @cfg {String} easing Animation easing if animate = true (defaults to 'easingOutStrong')
66  * @cfg {Number} widthIncrement The increment to snap the width resize in pixels (dynamic must be true, defaults to 0)
67  * @cfg {Number} heightIncrement The increment to snap the height resize in pixels (dynamic must be true, defaults to 0)
68  * @cfg {Boolean} pinned True to ensure that the resize handles are always visible, false to display them only when the
69  * user mouses over the resizable borders. This is only applied at config time. (defaults to false)
70  * @cfg {Boolean} preserveRatio True to preserve the original ratio between height and width during resize (defaults to false)
71  * @cfg {Boolean} transparent True for transparent handles. This is only applied at config time. (defaults to false)
72  * @cfg {Number} minX The minimum allowed page X for the element (only used for west resizing, defaults to 0)
73  * @cfg {Number} minY The minimum allowed page Y for the element (only used for north resizing, defaults to 0)
74  * @cfg {Boolean} draggable Convenience to initialize drag drop (defaults to false)
75  * @constructor
76  * Create a new resizable component
77  * @param {String/HTMLElement/Roo.Element} el The id or element to resize
78  * @param {Object} config configuration options
79   */
80 Roo.Resizable = function(el, config){
81     this.el = Roo.get(el);
82
83     if(config && config.wrap){
84         config.resizeChild = this.el;
85         this.el = this.el.wrap(typeof config.wrap == "object" ? config.wrap : {cls:"xresizable-wrap"});
86         this.el.id = this.el.dom.id = config.resizeChild.id + "-rzwrap";
87         this.el.setStyle("overflow", "hidden");
88         this.el.setPositioning(config.resizeChild.getPositioning());
89         config.resizeChild.clearPositioning();
90         if(!config.width || !config.height){
91             var csize = config.resizeChild.getSize();
92             this.el.setSize(csize.width, csize.height);
93         }
94         if(config.pinned && !config.adjustments){
95             config.adjustments = "auto";
96         }
97     }
98
99     this.proxy = this.el.createProxy({tag: "div", cls: "x-resizable-proxy", id: this.el.id + "-rzproxy"});
100     this.proxy.unselectable();
101     this.proxy.enableDisplayMode('block');
102
103     Roo.apply(this, config);
104
105     if(this.pinned){
106         this.disableTrackOver = true;
107         this.el.addClass("x-resizable-pinned");
108     }
109     // if the element isn't positioned, make it relative
110     var position = this.el.getStyle("position");
111     if(position != "absolute" && position != "fixed"){
112         this.el.setStyle("position", "relative");
113     }
114     if(!this.handles){ // no handles passed, must be legacy style
115         this.handles = 's,e,se';
116         if(this.multiDirectional){
117             this.handles += ',n,w';
118         }
119     }
120     if(this.handles == "all"){
121         this.handles = "n s e w ne nw se sw";
122     }
123     var hs = this.handles.split(/\s*?[,;]\s*?| /);
124     var ps = Roo.Resizable.positions;
125     for(var i = 0, len = hs.length; i < len; i++){
126         if(hs[i] && ps[hs[i]]){
127             var pos = ps[hs[i]];
128             this[pos] = new Roo.Resizable.Handle(this, pos, this.disableTrackOver, this.transparent);
129         }
130     }
131     // legacy
132     this.corner = this.southeast;
133
134     if(this.handles.indexOf("n") != -1 || this.handles.indexOf("w") != -1){
135         this.updateBox = true;
136     }
137
138     this.activeHandle = null;
139
140     if(this.resizeChild){
141         if(typeof this.resizeChild == "boolean"){
142             this.resizeChild = Roo.get(this.el.dom.firstChild, true);
143         }else{
144             this.resizeChild = Roo.get(this.resizeChild, true);
145         }
146     }
147
148     if(this.adjustments == "auto"){
149         var rc = this.resizeChild;
150         var hw = this.west, he = this.east, hn = this.north, hs = this.south;
151         if(rc && (hw || hn)){
152             rc.position("relative");
153             rc.setLeft(hw ? hw.el.getWidth() : 0);
154             rc.setTop(hn ? hn.el.getHeight() : 0);
155         }
156         this.adjustments = [
157             (he ? -he.el.getWidth() : 0) + (hw ? -hw.el.getWidth() : 0),
158             (hn ? -hn.el.getHeight() : 0) + (hs ? -hs.el.getHeight() : 0) -1
159         ];
160     }
161
162     if(this.draggable){
163         this.dd = this.dynamic ?
164             this.el.initDD(null) : this.el.initDDProxy(null, {dragElId: this.proxy.id});
165         this.dd.setHandleElId(this.resizeChild ? this.resizeChild.id : this.el.id);
166     }
167
168     // public events
169     this.addEvents({
170         /**
171          * @event beforeresize
172          * Fired before resize is allowed. Set enabled to false to cancel resize.
173          * @param {Roo.Resizable} this
174          * @param {Roo.EventObject} e The mousedown event
175          */
176         "beforeresize" : true,
177         /**
178          * @event resize
179          * Fired after a resize.
180          * @param {Roo.Resizable} this
181          * @param {Number} width The new width
182          * @param {Number} height The new height
183          * @param {Roo.EventObject} e The mouseup event
184          */
185         "resize" : true
186     });
187
188     if(this.width !== null && this.height !== null){
189         this.resizeTo(this.width, this.height);
190     }else{
191         this.updateChildSize();
192     }
193     if(Roo.isIE){
194         this.el.dom.style.zoom = 1;
195     }
196     Roo.Resizable.superclass.constructor.call(this);
197 };
198
199 Roo.extend(Roo.Resizable, Roo.util.Observable, {
200         resizeChild : false,
201         adjustments : [0, 0],
202         minWidth : 5,
203         minHeight : 5,
204         maxWidth : 10000,
205         maxHeight : 10000,
206         enabled : true,
207         animate : false,
208         duration : .35,
209         dynamic : false,
210         handles : false,
211         multiDirectional : false,
212         disableTrackOver : false,
213         easing : 'easeOutStrong',
214         widthIncrement : 0,
215         heightIncrement : 0,
216         pinned : false,
217         width : null,
218         height : null,
219         preserveRatio : false,
220         transparent: false,
221         minX: 0,
222         minY: 0,
223         draggable: false,
224
225         /**
226          * @cfg {String/HTMLElement/Element} constrainTo Constrain the resize to a particular element
227          */
228         constrainTo: undefined,
229         /**
230          * @cfg {Roo.lib.Region} resizeRegion Constrain the resize to a particular region
231          */
232         resizeRegion: undefined,
233
234
235     /**
236      * Perform a manual resize
237      * @param {Number} width
238      * @param {Number} height
239      */
240     resizeTo : function(width, height){
241         this.el.setSize(width, height);
242         this.updateChildSize();
243         this.fireEvent("resize", this, width, height, null);
244     },
245
246     // private
247     startSizing : function(e, handle){
248         this.fireEvent("beforeresize", this, e);
249         if(this.enabled){ // 2nd enabled check in case disabled before beforeresize handler
250
251             if(!this.overlay){
252                 this.overlay = this.el.createProxy({tag: "div", cls: "x-resizable-overlay", html: "&#160;"});
253                 this.overlay.unselectable();
254                 this.overlay.enableDisplayMode("block");
255                 this.overlay.on("mousemove", this.onMouseMove, this);
256                 this.overlay.on("mouseup", this.onMouseUp, this);
257             }
258             this.overlay.setStyle("cursor", handle.el.getStyle("cursor"));
259
260             this.resizing = true;
261             this.startBox = this.el.getBox();
262             this.startPoint = e.getXY();
263             this.offsets = [(this.startBox.x + this.startBox.width) - this.startPoint[0],
264                             (this.startBox.y + this.startBox.height) - this.startPoint[1]];
265
266             this.overlay.setSize(Roo.lib.Dom.getViewWidth(true), Roo.lib.Dom.getViewHeight(true));
267             this.overlay.show();
268
269             if(this.constrainTo) {
270                 var ct = Roo.get(this.constrainTo);
271                 this.resizeRegion = ct.getRegion().adjust(
272                     ct.getFrameWidth('t'),
273                     ct.getFrameWidth('l'),
274                     -ct.getFrameWidth('b'),
275                     -ct.getFrameWidth('r')
276                 );
277             }
278
279             this.proxy.setStyle('visibility', 'hidden'); // workaround display none
280             this.proxy.show();
281             this.proxy.setBox(this.startBox);
282             if(!this.dynamic){
283                 this.proxy.setStyle('visibility', 'visible');
284             }
285         }
286     },
287
288     // private
289     onMouseDown : function(handle, e){
290         if(this.enabled){
291             e.stopEvent();
292             this.activeHandle = handle;
293             this.startSizing(e, handle);
294         }
295     },
296
297     // private
298     onMouseUp : function(e){
299         var size = this.resizeElement();
300         this.resizing = false;
301         this.handleOut();
302         this.overlay.hide();
303         this.proxy.hide();
304         this.fireEvent("resize", this, size.width, size.height, e);
305     },
306
307     // private
308     updateChildSize : function(){
309         if(this.resizeChild){
310             var el = this.el;
311             var child = this.resizeChild;
312             var adj = this.adjustments;
313             if(el.dom.offsetWidth){
314                 var b = el.getSize(true);
315                 child.setSize(b.width+adj[0], b.height+adj[1]);
316             }
317             // Second call here for IE
318             // The first call enables instant resizing and
319             // the second call corrects scroll bars if they
320             // exist
321             if(Roo.isIE){
322                 setTimeout(function(){
323                     if(el.dom.offsetWidth){
324                         var b = el.getSize(true);
325                         child.setSize(b.width+adj[0], b.height+adj[1]);
326                     }
327                 }, 10);
328             }
329         }
330     },
331
332     // private
333     snap : function(value, inc, min){
334         if(!inc || !value) return value;
335         var newValue = value;
336         var m = value % inc;
337         if(m > 0){
338             if(m > (inc/2)){
339                 newValue = value + (inc-m);
340             }else{
341                 newValue = value - m;
342             }
343         }
344         return Math.max(min, newValue);
345     },
346
347     // private
348     resizeElement : function(){
349         var box = this.proxy.getBox();
350         if(this.updateBox){
351             this.el.setBox(box, false, this.animate, this.duration, null, this.easing);
352         }else{
353             this.el.setSize(box.width, box.height, this.animate, this.duration, null, this.easing);
354         }
355         this.updateChildSize();
356         if(!this.dynamic){
357             this.proxy.hide();
358         }
359         return box;
360     },
361
362     // private
363     constrain : function(v, diff, m, mx){
364         if(v - diff < m){
365             diff = v - m;
366         }else if(v - diff > mx){
367             diff = mx - v;
368         }
369         return diff;
370     },
371
372     // private
373     onMouseMove : function(e){
374         if(this.enabled){
375             try{// try catch so if something goes wrong the user doesn't get hung
376
377             if(this.resizeRegion && !this.resizeRegion.contains(e.getPoint())) {
378                 return;
379             }
380
381             //var curXY = this.startPoint;
382             var curSize = this.curSize || this.startBox;
383             var x = this.startBox.x, y = this.startBox.y;
384             var ox = x, oy = y;
385             var w = curSize.width, h = curSize.height;
386             var ow = w, oh = h;
387             var mw = this.minWidth, mh = this.minHeight;
388             var mxw = this.maxWidth, mxh = this.maxHeight;
389             var wi = this.widthIncrement;
390             var hi = this.heightIncrement;
391
392             var eventXY = e.getXY();
393             var diffX = -(this.startPoint[0] - Math.max(this.minX, eventXY[0]));
394             var diffY = -(this.startPoint[1] - Math.max(this.minY, eventXY[1]));
395
396             var pos = this.activeHandle.position;
397
398             switch(pos){
399                 case "east":
400                     w += diffX;
401                     w = Math.min(Math.max(mw, w), mxw);
402                     break;
403                 case "south":
404                     h += diffY;
405                     h = Math.min(Math.max(mh, h), mxh);
406                     break;
407                 case "southeast":
408                     w += diffX;
409                     h += diffY;
410                     w = Math.min(Math.max(mw, w), mxw);
411                     h = Math.min(Math.max(mh, h), mxh);
412                     break;
413                 case "north":
414                     diffY = this.constrain(h, diffY, mh, mxh);
415                     y += diffY;
416                     h -= diffY;
417                     break;
418                 case "west":
419                     diffX = this.constrain(w, diffX, mw, mxw);
420                     x += diffX;
421                     w -= diffX;
422                     break;
423                 case "northeast":
424                     w += diffX;
425                     w = Math.min(Math.max(mw, w), mxw);
426                     diffY = this.constrain(h, diffY, mh, mxh);
427                     y += diffY;
428                     h -= diffY;
429                     break;
430                 case "northwest":
431                     diffX = this.constrain(w, diffX, mw, mxw);
432                     diffY = this.constrain(h, diffY, mh, mxh);
433                     y += diffY;
434                     h -= diffY;
435                     x += diffX;
436                     w -= diffX;
437                     break;
438                case "southwest":
439                     diffX = this.constrain(w, diffX, mw, mxw);
440                     h += diffY;
441                     h = Math.min(Math.max(mh, h), mxh);
442                     x += diffX;
443                     w -= diffX;
444                     break;
445             }
446
447             var sw = this.snap(w, wi, mw);
448             var sh = this.snap(h, hi, mh);
449             if(sw != w || sh != h){
450                 switch(pos){
451                     case "northeast":
452                         y -= sh - h;
453                     break;
454                     case "north":
455                         y -= sh - h;
456                         break;
457                     case "southwest":
458                         x -= sw - w;
459                     break;
460                     case "west":
461                         x -= sw - w;
462                         break;
463                     case "northwest":
464                         x -= sw - w;
465                         y -= sh - h;
466                     break;
467                 }
468                 w = sw;
469                 h = sh;
470             }
471
472             if(this.preserveRatio){
473                 switch(pos){
474                     case "southeast":
475                     case "east":
476                         h = oh * (w/ow);
477                         h = Math.min(Math.max(mh, h), mxh);
478                         w = ow * (h/oh);
479                        break;
480                     case "south":
481                         w = ow * (h/oh);
482                         w = Math.min(Math.max(mw, w), mxw);
483                         h = oh * (w/ow);
484                         break;
485                     case "northeast":
486                         w = ow * (h/oh);
487                         w = Math.min(Math.max(mw, w), mxw);
488                         h = oh * (w/ow);
489                     break;
490                     case "north":
491                         var tw = w;
492                         w = ow * (h/oh);
493                         w = Math.min(Math.max(mw, w), mxw);
494                         h = oh * (w/ow);
495                         x += (tw - w) / 2;
496                         break;
497                     case "southwest":
498                         h = oh * (w/ow);
499                         h = Math.min(Math.max(mh, h), mxh);
500                         var tw = w;
501                         w = ow * (h/oh);
502                         x += tw - w;
503                         break;
504                     case "west":
505                         var th = h;
506                         h = oh * (w/ow);
507                         h = Math.min(Math.max(mh, h), mxh);
508                         y += (th - h) / 2;
509                         var tw = w;
510                         w = ow * (h/oh);
511                         x += tw - w;
512                        break;
513                     case "northwest":
514                         var tw = w;
515                         var th = h;
516                         h = oh * (w/ow);
517                         h = Math.min(Math.max(mh, h), mxh);
518                         w = ow * (h/oh);
519                         y += th - h;
520                          x += tw - w;
521                        break;
522
523                 }
524             }
525             this.proxy.setBounds(x, y, w, h);
526             if(this.dynamic){
527                 this.resizeElement();
528             }
529             }catch(e){}
530         }
531     },
532
533     // private
534     handleOver : function(){
535         if(this.enabled){
536             this.el.addClass("x-resizable-over");
537         }
538     },
539
540     // private
541     handleOut : function(){
542         if(!this.resizing){
543             this.el.removeClass("x-resizable-over");
544         }
545     },
546
547     /**
548      * Returns the element this component is bound to.
549      * @return {Roo.Element}
550      */
551     getEl : function(){
552         return this.el;
553     },
554
555     /**
556      * Returns the resizeChild element (or null).
557      * @return {Roo.Element}
558      */
559     getResizeChild : function(){
560         return this.resizeChild;
561     },
562
563     /**
564      * Destroys this resizable. If the element was wrapped and
565      * removeEl is not true then the element remains.
566      * @param {Boolean} removeEl (optional) true to remove the element from the DOM
567      */
568     destroy : function(removeEl){
569         this.proxy.remove();
570         if(this.overlay){
571             this.overlay.removeAllListeners();
572             this.overlay.remove();
573         }
574         var ps = Roo.Resizable.positions;
575         for(var k in ps){
576             if(typeof ps[k] != "function" && this[ps[k]]){
577                 var h = this[ps[k]];
578                 h.el.removeAllListeners();
579                 h.el.remove();
580             }
581         }
582         if(removeEl){
583             this.el.update("");
584             this.el.remove();
585         }
586     }
587 });
588
589 // private
590 // hash to map config positions to true positions
591 Roo.Resizable.positions = {
592     n: "north", s: "south", e: "east", w: "west", se: "southeast", sw: "southwest", nw: "northwest", ne: "northeast"
593 };
594
595 // private
596 Roo.Resizable.Handle = function(rz, pos, disableTrackOver, transparent){
597     if(!this.tpl){
598         // only initialize the template if resizable is used
599         var tpl = Roo.DomHelper.createTemplate(
600             {tag: "div", cls: "x-resizable-handle x-resizable-handle-{0}"}
601         );
602         tpl.compile();
603         Roo.Resizable.Handle.prototype.tpl = tpl;
604     }
605     this.position = pos;
606     this.rz = rz;
607     this.el = this.tpl.append(rz.el.dom, [this.position], true);
608     this.el.unselectable();
609     if(transparent){
610         this.el.setOpacity(0);
611     }
612     this.el.on("mousedown", this.onMouseDown, this);
613     if(!disableTrackOver){
614         this.el.on("mouseover", this.onMouseOver, this);
615         this.el.on("mouseout", this.onMouseOut, this);
616     }
617 };
618
619 // private
620 Roo.Resizable.Handle.prototype = {
621     afterResize : function(rz){
622         // do nothing
623     },
624     // private
625     onMouseDown : function(e){
626         this.rz.onMouseDown(this, e);
627     },
628     // private
629     onMouseOver : function(e){
630         this.rz.handleOver(this, e);
631     },
632     // private
633     onMouseOut : function(e){
634         this.rz.handleOut(this, e);
635     }
636 };