Roo/Resizable.js
[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 {
82     this.el = Roo.get(el);
83
84     if(config && config.wrap){
85         config.resizeChild = this.el;
86         this.el = this.el.wrap(typeof config.wrap == "object" ? config.wrap : {cls:"xresizable-wrap"});
87         this.el.id = this.el.dom.id = config.resizeChild.id + "-rzwrap";
88         this.el.setStyle("overflow", "hidden");
89         this.el.setPositioning(config.resizeChild.getPositioning());
90         config.resizeChild.clearPositioning();
91         if(!config.width || !config.height){
92             var csize = config.resizeChild.getSize();
93             this.el.setSize(csize.width, csize.height);
94         }
95         if(config.pinned && !config.adjustments){
96             config.adjustments = "auto";
97         }
98     }
99
100     this.proxy = this.el.createProxy({tag: "div", cls: "x-resizable-proxy", id: this.el.id + "-rzproxy"});
101     this.proxy.unselectable();
102     this.proxy.enableDisplayMode('block');
103
104     Roo.apply(this, config);
105
106     if(this.pinned){
107         this.disableTrackOver = true;
108         this.el.addClass("x-resizable-pinned");
109     }
110     // if the element isn't positioned, make it relative
111     var position = this.el.getStyle("position");
112     if(position != "absolute" && position != "fixed"){
113         this.el.setStyle("position", "relative");
114     }
115     if(!this.handles){ // no handles passed, must be legacy style
116         this.handles = 's,e,se';
117         if(this.multiDirectional){
118             this.handles += ',n,w';
119         }
120     }
121     if(this.handles == "all"){
122         this.handles = "n s e w ne nw se sw";
123     }
124     var hs = this.handles.split(/\s*?[,;]\s*?| /);
125     var ps = Roo.Resizable.positions;
126     for(var i = 0, len = hs.length; i < len; i++){
127         if(hs[i] && ps[hs[i]]){
128             var pos = ps[hs[i]];
129             this[pos] = new Roo.Resizable.Handle(this, pos, this.disableTrackOver, this.transparent);
130         }
131     }
132     // legacy
133     this.corner = this.southeast;
134     
135     // updateBox = the box can move..
136     if(this.handles.indexOf("n") != -1 || this.handles.indexOf("w") != -1 || this.handles.indexOf("hd") != -1) {
137         this.updateBox = true;
138     }
139
140     this.activeHandle = null;
141
142     if(this.resizeChild){
143         if(typeof this.resizeChild == "boolean"){
144             this.resizeChild = Roo.get(this.el.dom.firstChild, true);
145         }else{
146             this.resizeChild = Roo.get(this.resizeChild, true);
147         }
148     }
149     
150     if(this.adjustments == "auto"){
151         var rc = this.resizeChild;
152         var hw = this.west, he = this.east, hn = this.north, hs = this.south;
153         if(rc && (hw || hn)){
154             rc.position("relative");
155             rc.setLeft(hw ? hw.el.getWidth() : 0);
156             rc.setTop(hn ? hn.el.getHeight() : 0);
157         }
158         this.adjustments = [
159             (he ? -he.el.getWidth() : 0) + (hw ? -hw.el.getWidth() : 0),
160             (hn ? -hn.el.getHeight() : 0) + (hs ? -hs.el.getHeight() : 0) -1
161         ];
162     }
163
164     if(this.draggable){
165         this.dd = this.dynamic ?
166             this.el.initDD(null) : this.el.initDDProxy(null, {dragElId: this.proxy.id});
167         this.dd.setHandleElId(this.resizeChild ? this.resizeChild.id : this.el.id);
168     }
169
170     // public events
171     this.addEvents({
172         /**
173          * @event beforeresize
174          * Fired before resize is allowed. Set enabled to false to cancel resize.
175          * @param {Roo.Resizable} this
176          * @param {Roo.EventObject} e The mousedown event
177          */
178         "beforeresize" : true,
179         /**
180          * @event resize
181          * Fired after a resize.
182          * @param {Roo.Resizable} this
183          * @param {Number} width The new width
184          * @param {Number} height The new height
185          * @param {Roo.EventObject} e The mouseup event
186          */
187         "resize" : true
188     });
189
190     if(this.width !== null && this.height !== null){
191         this.resizeTo(this.width, this.height);
192     }else{
193         this.updateChildSize();
194     }
195     if(Roo.isIE){
196         this.el.dom.style.zoom = 1;
197     }
198     Roo.Resizable.superclass.constructor.call(this);
199 };
200
201 Roo.extend(Roo.Resizable, Roo.util.Observable, {
202         resizeChild : false,
203         adjustments : [0, 0],
204         minWidth : 5,
205         minHeight : 5,
206         maxWidth : 10000,
207         maxHeight : 10000,
208         enabled : true,
209         animate : false,
210         duration : .35,
211         dynamic : false,
212         handles : false,
213         multiDirectional : false,
214         disableTrackOver : false,
215         easing : 'easeOutStrong',
216         widthIncrement : 0,
217         heightIncrement : 0,
218         pinned : false,
219         width : null,
220         height : null,
221         preserveRatio : false,
222         transparent: false,
223         minX: 0,
224         minY: 0,
225         draggable: false,
226
227         /**
228          * @cfg {String/HTMLElement/Element} constrainTo Constrain the resize to a particular element
229          */
230         constrainTo: undefined,
231         /**
232          * @cfg {Roo.lib.Region} resizeRegion Constrain the resize to a particular region
233          */
234         resizeRegion: undefined,
235
236
237     /**
238      * Perform a manual resize
239      * @param {Number} width
240      * @param {Number} height
241      */
242     resizeTo : function(width, height){
243         this.el.setSize(width, height);
244         this.updateChildSize();
245         this.fireEvent("resize", this, width, height, null);
246     },
247
248     // private
249     startSizing : function(e, handle){
250         this.fireEvent("beforeresize", this, e);
251         if(this.enabled){ // 2nd enabled check in case disabled before beforeresize handler
252
253             if(!this.overlay){
254                 this.overlay = this.el.createProxy({tag: "div", cls: "x-resizable-overlay", html: "&#160;"});
255                 this.overlay.unselectable();
256                 this.overlay.enableDisplayMode("block");
257                 this.overlay.on("mousemove", this.onMouseMove, this);
258                 this.overlay.on("mouseup", this.onMouseUp, this);
259             }
260             this.overlay.setStyle("cursor", handle.el.getStyle("cursor"));
261
262             this.resizing = true;
263             this.startBox = this.el.getBox();
264             this.startPoint = e.getXY();
265             this.offsets = [(this.startBox.x + this.startBox.width) - this.startPoint[0],
266                             (this.startBox.y + this.startBox.height) - this.startPoint[1]];
267
268             this.overlay.setSize(Roo.lib.Dom.getViewWidth(true), Roo.lib.Dom.getViewHeight(true));
269             this.overlay.show();
270
271             if(this.constrainTo) {
272                 var ct = Roo.get(this.constrainTo);
273                 this.resizeRegion = ct.getRegion().adjust(
274                     ct.getFrameWidth('t'),
275                     ct.getFrameWidth('l'),
276                     -ct.getFrameWidth('b'),
277                     -ct.getFrameWidth('r')
278                 );
279             }
280
281             this.proxy.setStyle('visibility', 'hidden'); // workaround display none
282             this.proxy.show();
283             this.proxy.setBox(this.startBox);
284             if(!this.dynamic){
285                 this.proxy.setStyle('visibility', 'visible');
286             }
287         }
288     },
289
290     // private
291     onMouseDown : function(handle, e){
292         if(this.enabled){
293             e.stopEvent();
294             this.activeHandle = handle;
295             this.startSizing(e, handle);
296         }
297     },
298
299     // private
300     onMouseUp : function(e){
301         var size = this.resizeElement();
302         this.resizing = false;
303         this.handleOut();
304         this.overlay.hide();
305         this.proxy.hide();
306         this.fireEvent("resize", this, size.width, size.height, e);
307     },
308
309     // private
310     updateChildSize : function(){
311         if(this.resizeChild){
312             var el = this.el;
313             var child = this.resizeChild;
314             var adj = this.adjustments;
315             if(el.dom.offsetWidth){
316                 var b = el.getSize(true);
317                 child.setSize(b.width+adj[0], b.height+adj[1]);
318             }
319             // Second call here for IE
320             // The first call enables instant resizing and
321             // the second call corrects scroll bars if they
322             // exist
323             if(Roo.isIE){
324                 setTimeout(function(){
325                     if(el.dom.offsetWidth){
326                         var b = el.getSize(true);
327                         child.setSize(b.width+adj[0], b.height+adj[1]);
328                     }
329                 }, 10);
330             }
331         }
332     },
333
334     // private
335     snap : function(value, inc, min){
336         if(!inc || !value) return value;
337         var newValue = value;
338         var m = value % inc;
339         if(m > 0){
340             if(m > (inc/2)){
341                 newValue = value + (inc-m);
342             }else{
343                 newValue = value - m;
344             }
345         }
346         return Math.max(min, newValue);
347     },
348
349     // private
350     resizeElement : function(){
351         var box = this.proxy.getBox();
352         if(this.updateBox){
353             this.el.setBox(box, false, this.animate, this.duration, null, this.easing);
354         }else{
355             this.el.setSize(box.width, box.height, this.animate, this.duration, null, this.easing);
356         }
357         this.updateChildSize();
358         if(!this.dynamic){
359             this.proxy.hide();
360         }
361         return box;
362     },
363
364     // private
365     constrain : function(v, diff, m, mx){
366         if(v - diff < m){
367             diff = v - m;
368         }else if(v - diff > mx){
369             diff = mx - v;
370         }
371         return diff;
372     },
373
374     // private
375     onMouseMove : function(e){
376         if(this.enabled){
377             try{// try catch so if something goes wrong the user doesn't get hung
378
379             if(this.resizeRegion && !this.resizeRegion.contains(e.getPoint())) {
380                 return;
381             }
382
383             //var curXY = this.startPoint;
384             var curSize = this.curSize || this.startBox;
385             var x = this.startBox.x, y = this.startBox.y;
386             var ox = x, oy = y;
387             var w = curSize.width, h = curSize.height;
388             var ow = w, oh = h;
389             var mw = this.minWidth, mh = this.minHeight;
390             var mxw = this.maxWidth, mxh = this.maxHeight;
391             var wi = this.widthIncrement;
392             var hi = this.heightIncrement;
393
394             var eventXY = e.getXY();
395             var diffX = -(this.startPoint[0] - Math.max(this.minX, eventXY[0]));
396             var diffY = -(this.startPoint[1] - Math.max(this.minY, eventXY[1]));
397
398             var pos = this.activeHandle.position;
399
400             switch(pos){
401                 case "east":
402                     w += diffX;
403                     w = Math.min(Math.max(mw, w), mxw);
404                     break;
405              
406                 case "south":
407                     h += diffY;
408                     h = Math.min(Math.max(mh, h), mxh);
409                     break;
410                 case "southeast":
411                     w += diffX;
412                     h += diffY;
413                     w = Math.min(Math.max(mw, w), mxw);
414                     h = Math.min(Math.max(mh, h), mxh);
415                     break;
416                 case "north":
417                     diffY = this.constrain(h, diffY, mh, mxh);
418                     y += diffY;
419                     h -= diffY;
420                     break;
421                 case "hdrag":
422                     diffX = this.constrain(w, diffX, mw+diffX, mxw);
423                     x += diffX;
424                     w -= diffX;
425                     break;
426                 case "west":
427                     diffX = this.constrain(w, diffX, mw, mxw);
428                     x += diffX;
429                     w -= diffX;
430                     break;
431                 case "northeast":
432                     w += diffX;
433                     w = Math.min(Math.max(mw, w), mxw);
434                     diffY = this.constrain(h, diffY, mh, mxh);
435                     y += diffY;
436                     h -= diffY;
437                     break;
438                 case "northwest":
439                     diffX = this.constrain(w, diffX, mw, mxw);
440                     diffY = this.constrain(h, diffY, mh, mxh);
441                     y += diffY;
442                     h -= diffY;
443                     x += diffX;
444                     w -= diffX;
445                     break;
446                case "southwest":
447                     diffX = this.constrain(w, diffX, mw, mxw);
448                     h += diffY;
449                     h = Math.min(Math.max(mh, h), mxh);
450                     x += diffX;
451                     w -= diffX;
452                     break;
453             }
454
455             var sw = this.snap(w, wi, mw);
456             var sh = this.snap(h, hi, mh);
457             if(sw != w || sh != h){
458                 switch(pos){
459                     case "northeast":
460                         y -= sh - h;
461                     break;
462                     case "north":
463                         y -= sh - h;
464                         break;
465                     case "southwest":
466                         x -= sw - w;
467                     break;
468                     case "hdrag":
469                     case "west":
470                         x -= sw - w;
471                         break;
472                     case "northwest":
473                         x -= sw - w;
474                         y -= sh - h;
475                     break;
476                 }
477                 w = sw;
478                 h = sh;
479             }
480
481             if(this.preserveRatio){
482                 switch(pos){
483                     case "southeast":
484                     case "east":
485                         h = oh * (w/ow);
486                         h = Math.min(Math.max(mh, h), mxh);
487                         w = ow * (h/oh);
488                        break;
489                     case "south":
490                         w = ow * (h/oh);
491                         w = Math.min(Math.max(mw, w), mxw);
492                         h = oh * (w/ow);
493                         break;
494                     case "northeast":
495                         w = ow * (h/oh);
496                         w = Math.min(Math.max(mw, w), mxw);
497                         h = oh * (w/ow);
498                     break;
499                     case "north":
500                         var tw = w;
501                         w = ow * (h/oh);
502                         w = Math.min(Math.max(mw, w), mxw);
503                         h = oh * (w/ow);
504                         x += (tw - w) / 2;
505                         break;
506                     case "southwest":
507                         h = oh * (w/ow);
508                         h = Math.min(Math.max(mh, h), mxh);
509                         var tw = w;
510                         w = ow * (h/oh);
511                         x += tw - w;
512                         break;
513                     case "west":
514                         var th = h;
515                         h = oh * (w/ow);
516                         h = Math.min(Math.max(mh, h), mxh);
517                         y += (th - h) / 2;
518                         var tw = w;
519                         w = ow * (h/oh);
520                         x += tw - w;
521                        break;
522                     case "northwest":
523                         var tw = w;
524                         var th = h;
525                         h = oh * (w/ow);
526                         h = Math.min(Math.max(mh, h), mxh);
527                         w = ow * (h/oh);
528                         y += th - h;
529                         x += tw - w;
530                        break;
531
532                 }
533             }
534             if (pos == 'hdrag') {
535                 w = ow;
536             }
537             this.proxy.setBounds(x, y, w, h);
538             if(this.dynamic){
539                 this.resizeElement();
540             }
541             }catch(e){}
542         }
543     },
544
545     // private
546     handleOver : function(){
547         if(this.enabled){
548             this.el.addClass("x-resizable-over");
549         }
550     },
551
552     // private
553     handleOut : function(){
554         if(!this.resizing){
555             this.el.removeClass("x-resizable-over");
556         }
557     },
558
559     /**
560      * Returns the element this component is bound to.
561      * @return {Roo.Element}
562      */
563     getEl : function(){
564         return this.el;
565     },
566
567     /**
568      * Returns the resizeChild element (or null).
569      * @return {Roo.Element}
570      */
571     getResizeChild : function(){
572         return this.resizeChild;
573     },
574
575     /**
576      * Destroys this resizable. If the element was wrapped and
577      * removeEl is not true then the element remains.
578      * @param {Boolean} removeEl (optional) true to remove the element from the DOM
579      */
580     destroy : function(removeEl){
581         this.proxy.remove();
582         if(this.overlay){
583             this.overlay.removeAllListeners();
584             this.overlay.remove();
585         }
586         var ps = Roo.Resizable.positions;
587         for(var k in ps){
588             if(typeof ps[k] != "function" && this[ps[k]]){
589                 var h = this[ps[k]];
590                 h.el.removeAllListeners();
591                 h.el.remove();
592             }
593         }
594         if(removeEl){
595             this.el.update("");
596             this.el.remove();
597         }
598     }
599 });
600
601 // private
602 // hash to map config positions to true positions
603 Roo.Resizable.positions = {
604     n: "north", s: "south", e: "east", w: "west", se: "southeast", sw: "southwest", nw: "northwest", ne: "northeast", 
605     hd: "hdrag"
606 };
607
608 // private
609 Roo.Resizable.Handle = function(rz, pos, disableTrackOver, transparent){
610     if(!this.tpl){
611         // only initialize the template if resizable is used
612         var tpl = Roo.DomHelper.createTemplate(
613             {tag: "div", cls: "x-resizable-handle x-resizable-handle-{0}"}
614         );
615         tpl.compile();
616         Roo.Resizable.Handle.prototype.tpl = tpl;
617     }
618     this.position = pos;
619     this.rz = rz;
620     // show north drag fro topdra
621     var handlepos = pos == 'hdrag' ? 'north' : pos;
622     
623     this.el = this.tpl.append(rz.el.dom, [handlepos], true);
624     if (pos == 'hdrag') {
625         this.el.setStyle('cursor', 'pointer');
626     }
627     this.el.unselectable();
628     if(transparent){
629         this.el.setOpacity(0);
630     }
631     this.el.on("mousedown", this.onMouseDown, this);
632     if(!disableTrackOver){
633         this.el.on("mouseover", this.onMouseOver, this);
634         this.el.on("mouseout", this.onMouseOut, this);
635     }
636 };
637
638 // private
639 Roo.Resizable.Handle.prototype = {
640     afterResize : function(rz){
641         // do nothing
642     },
643     // private
644     onMouseDown : function(e){
645         this.rz.onMouseDown(this, e);
646     },
647     // private
648     onMouseOver : function(e){
649         this.rz.handleOver(this, e);
650     },
651     // private
652     onMouseOut : function(e){
653         this.rz.handleOut(this, e);
654     }
655 };