/**
 * @class YAHOO.ext.Resizable
 * @extends YAHOO.ext.util.Observable
 * <p>Applies drag handles to an element to make it resizable. The drag handles are inserted into the element 
 * and positioned absolute. Some elements, such as a textarea or image, don't support this. To overcome that, you can wrap
 * the textarea in a div and set "resizeChild" to true (or the id of the textarea).</p>
 * <p>
 * By default handles are displayed when the mouse is near the edge of the element. To make the handles displayed all the time,
 * add a class "yresizable-pinned" to the element. Note: depending on your element you may need to 
 * apply "adjustments" (see below) to get the handles to look right when pinned.
 * </p>
 * Here's a Resizable with every possible config option and it's default value:
<pre><code>
var resizer = new YAHOO.ext.Resizable('element-id', {
    resizeChild : false, // true to resize the firstChild or an id/element to resize
    adjustments : [0, 0],
    minWidth : 5,
    minHeight : 5,
    maxWidth : 10000,
    maxHeight : 10000,
    widthIncrement : 0, // snapping
    heightIncrement : 0, // snapping
    enabled : true,
    animate : false, // (ignored if resizeChild is true)
    duration : .35,
    dynamic : false, // true for real time sizing instead of proxy resizing
    multiDirectional : false, // true for 4 way adjustments
    disableTrackOver : false, // disable mouse over tracking for pinned resizers
    easing = YAHOO.util.Easing.easeOutStrong
});
resizer.on('resize', myHandler);
</code></pre>
* <p>
 * To hide a particular handle, set it's display to none in CSS, or through script:<br>
 * resizer.east.setDisplayed(false);
 * </p>
 * @constructor
 * Create a new resizable component
 * @param {String/HTMLElement/YAHOO.ext.Element} el The id or element to resize
 * @param {Object} config configuration options
  */
YAHOO.ext.Resizable = function(el, config){
    // in case global fcn not defined
    var getEl = YAHOO.ext.Element.get;
    
    this.el = getEl(el, true);
    this.el.autoBoxAdjust = true;
    // if the element isn't positioned, make it relative
    if(this.el.getStyle('position') != 'absolute'){
        this.el.setStyle('position', 'relative');
    }
    
    // create the handles and proxy
    var dh = YAHOO.ext.DomHelper;
    var tpl = dh.createTemplate({tag: 'div', cls: 'yresizable-handle yresizable-handle-{0}', html: '&nbsp;'});
    /** The east handle @type YAHOO.ext.Element */
    this.east = getEl(tpl.append(this.el.dom, ['east']), true);
    /** The south handle @type YAHOO.ext.Element */
    this.south = getEl(tpl.append(this.el.dom, ['south']), true);
    if(config && config.multiDirectional){
        /** The west handle @type YAHOO.ext.Element */
        this.west = getEl(tpl.append(this.el.dom, ['west']), true);
        /** The north handle @type YAHOO.ext.Element */
        this.north = getEl(tpl.append(this.el.dom, ['north']), true);
    }
    /** The corner handle @type YAHOO.ext.Element */
    this.corner = getEl(tpl.append(this.el.dom, ['southeast']), true);
    this.proxy = getEl(dh.insertBefore(document.body.firstChild, {tag: 'div', cls: 'yresizable-proxy', id: this.el.id + '-rzproxy'}), true);
    this.proxy.autoBoxAdjust = true;
    
    // wrapped event handlers to add and remove when sizing
    this.moveHandler = YAHOO.ext.EventManager.wrap(this.onMouseMove, this, true);
    this.upHandler = YAHOO.ext.EventManager.wrap(this.onMouseUp, this, true);
    this.selHandler = YAHOO.ext.EventManager.wrap(this.cancelSelection, this, true);
    
    // public events
    this.events = {
        /**
         * @event beforeresize
         * Fired before resize is allowed. Set enabled to false to cancel resize. 
         * @param {YAHOO.ext.Resizable} this
         * @param {YAHOO.ext.EventObject} e The mousedown event
         */
        'beforeresize' : new YAHOO.util.CustomEvent(),
        /**
         * @event resize
         * Fired after a resize. 
         * @param {YAHOO.ext.Resizable} this
         * @param {Number} width The new width
         * @param {Number} height The new height
         * @param {YAHOO.ext.EventObject} e The mouseup event
         */
        'resize' : new YAHOO.util.CustomEvent()
    };
    
    this.dir = null;
    
    // properties
    /** True to resizeSize the first child or id/element to resize @type YAHOO.ext.Element */
    this.resizeChild = false;
    /** 
     * Array [width, height] with values to be <b>added</b> to the resize operation's new size. 
     * @type Array */
    this.adjustments = [0, 0];
    /** The minimum width for the element @type Number */
    this.minWidth = 5;
    /** The minimum height for the element @type Number */
    this.minHeight = 5;
    /** The maximum width for the element @type Number */
    this.maxWidth = 10000;
    /** The maximum height for the element @type Number */
    this.maxHeight = 10000;
    /** false to disable resizing @type Boolean */
    this.enabled = true;
    /** True to animate the resize (not compatible with dynamic sizing) @type Boolean */
    this.animate = false;
    /** Animation duration @type Float */
    this.duration = .35;
    /** True to resize the element while dragging instead of using a proxy @type Boolean */
    this.dynamic = false;
    // these two are only available at config time
    this.multiDirectional = false;
    this.disableTrackOver = false;
    /** Animation easing @type YAHOO.util.Easing */
    this.easing = YAHOO.util.Easing ? YAHOO.util.Easing.easeOutStrong : null;
    /** The increment to snap the width resize in pixels (dynamic must be true) @type Number */
    this.widthIncrement = 0;
    /** The increment to snap the height resize in pixels (dynamic must be true) @type Number */
    this.heightIncrement = 0;
    
    YAHOO.ext.util.Config.apply(this, config);
    
    if(this.resizeChild){
        if(typeof this.resizeChild == 'boolean'){
            this.resizeChild = YAHOO.ext.Element.get(this.el.dom.firstChild, true);
        }else{
            this.resizeChild = YAHOO.ext.Element.get(this.resizeChild, true);
        }
    }
    
    // listen for mouse down on the handles
    var mdown = this.onMouseDown.createDelegate(this);
    this.east.mon('mousedown', mdown);
    this.south.mon('mousedown', mdown);
    if(this.multiDirectional){
        this.west.mon('mousedown', mdown);
        this.north.mon('mousedown', mdown);
    }
    this.corner.mon('mousedown', mdown);
    
    if(!this.disableTrackOver){
        // track mouse overs
        var mover = this.onMouseOver.createDelegate(this);
        // track mouse outs
        var mout = this.onMouseOut.createDelegate(this);
        
        this.east.mon('mouseover', mover);
        this.east.mon('mouseout', mout);
        this.south.mon('mouseover', mover);
        this.south.mon('mouseout', mout);
        if(this.multiDirectional){
            this.west.mon('mouseover', mover);
            this.west.mon('mouseout', mout);
            this.north.mon('mouseover', mover);
            this.north.mon('mouseout', mout);
        }
        this.corner.mon('mouseover', mover);
        this.corner.mon('mouseout', mout);
    }
    this.updateChildSize();
};

YAHOO.extendX(YAHOO.ext.Resizable, YAHOO.ext.util.Observable, {
    /**
     * Perform a manual resize
     * @param {Number} width
     * @param {Number} height
     */
    resizeTo : function(width, height){
        this.el.setSize(width, height);
        this.fireEvent('resize', this, width, height, null);
    },
    
    cancelSelection : function(e){
        e.preventDefault();
    },
    
    startSizing : function(e){
        this.fireEvent('beforeresize', this, e);
        if(this.enabled){ // 2nd enabled check in case disabled before beforeresize handler
            e.preventDefault();
            this.startBox = this.el.getBox();
            this.startPoint = e.getXY();
            this.offsets = [(this.startBox.x + this.startBox.width) - this.startPoint[0],
                            (this.startBox.y + this.startBox.height) - this.startPoint[1]];
            this.proxy.setBox(this.startBox);
            if(!this.dynamic){
                this.proxy.show();
            }
            YAHOO.util.Event.on(document.body, 'selectstart', this.selHandler);
            YAHOO.util.Event.on(document.body, 'mousemove', this.moveHandler);
            YAHOO.util.Event.on(document.body, 'mouseup', this.upHandler);
        }
    },
    
    onMouseDown : function(e){
        if(this.enabled){
            var t = e.getTarget();
            if(t == this.corner.dom){
                this.dir = 'both';
                this.proxy.setStyle('cursor', this.corner.getStyle('cursor'));
                this.startSizing(e);
            }else if(t == this.east.dom){
                this.dir = 'east';
                this.proxy.setStyle('cursor', this.east.getStyle('cursor'));
                this.startSizing(e);
            }else if(t == this.south.dom){
                this.dir = 'south';
                this.proxy.setStyle('cursor', this.south.getStyle('cursor'));
                this.startSizing(e);
            }else if(t == this.west.dom){
                this.dir = 'west';
                this.proxy.setStyle('cursor', this.west.getStyle('cursor'));
                this.startSizing(e);
            }else if(t == this.north.dom){
                this.dir = 'north';
                this.proxy.setStyle('cursor', this.north.getStyle('cursor'));
                this.startSizing(e);
            }
        }          
    },
    
    onMouseUp : function(e){
        YAHOO.util.Event.removeListener(document.body, 'selectstart', this.selHandler);
        YAHOO.util.Event.removeListener(document.body, 'mousemove', this.moveHandler);
        YAHOO.util.Event.removeListener(document.body, 'mouseup', this.upHandler);
        var size = this.resizeElement();
        this.fireEvent('resize', this, size.width, size.height, e);
    },
    
    updateChildSize : function(){
        if(this.resizeChild && this.el.dom.offsetWidth){
            var el = this.el;
            var child = this.resizeChild;
            var adj = this.adjustments;
            setTimeout(function(){
                var b = el.getBox(true);
                child.setSize(b.width+adj[0], b.height+adj[1]);
            }, 1);
        }
    },
    
    snap : function(value, inc){
        if(!inc || !value) return value;
        var newValue = value;
        var m = value % inc;
        if(m > 0){
            if(m > (inc/2)){
                newValue = value + (inc-m);
            }else{
                newValue = value - m;
            }
        }
        return newValue;
    },
    
    resizeElement : function(){
        var box = this.proxy.getBox();
        box.width = this.snap(box.width, this.widthIncrement);
        box.height = this.snap(box.height, this.heightIncrement);
        if(this.multiDirectional){
            this.el.setBox(box, false, this.animate, this.duration, null, this.easing);
        }else{
            this.el.setSize(box.width, box.height, this.animate, this.duration, null, this.easing);
        }
        this.updateChildSize();
        this.proxy.hide();
        return box;
    },
    
    onMouseMove : function(e){
        if(this.enabled){
            var xy = e.getXY();
            if(this.dir == 'both' || this.dir == 'east' || this.dir == 'south'){
                var w = Math.min(Math.max(this.minWidth, xy[0]-this.startBox.x+this.offsets[0]),this.maxWidth);
                var h = Math.min(Math.max(this.minHeight, xy[1]-this.startBox.y+this.offsets[1]), this.maxHeight);
                if(this.dir == 'both'){
                    this.proxy.setSize(w, h);
                }else if(this.dir == 'east'){
                    this.proxy.setWidth(w);
                }else if(this.dir == 'south'){
                    this.proxy.setHeight(h);
                }
            }else{
                var x = this.startBox.x + (xy[0]-this.startPoint[0]);
                var y = this.startBox.y + (xy[1]-this.startPoint[1]);
                var w = this.startBox.width+(this.startBox.x-x);
                var h = this.startBox.height+(this.startBox.y-y);
                if(this.dir == 'west' && w <= this.maxWidth && w >= this.minWidth){
                    this.proxy.setX(x);
                    this.proxy.setWidth(w);
                }else if(this.dir == 'north' && h <= this.maxHeight && h >= this.minHeight){
                    this.proxy.setY(y);
                    this.proxy.setHeight(h);
                }
            }
            if(this.dynamic){
                this.resizeElement();
            }
        }
    },
    
    onMouseOver : function(){
        if(this.enabled) this.el.addClass('yresizable-over');
    },
    
    onMouseOut : function(){
        this.el.removeClass('yresizable-over');
    }
});

Copyright © 2006 Jack Slocum. All rights reserved.