PDA

View Full Version : Event zone. Receive mouse events from areas of elements.



Animal
22 Aug 2009, 2:22 AM
Instead of having to add specially positioned elements inside an element in order to receive events from a particular part of an element, this class allows you to define, using CSS-like notation, an area of an existing element from which you wish to receive mouse events.

Applications include resizing where mousedown should only be fired in the outer edges of an element. Or a trigger field where the trigger is just a background image positioned in an input field, not a element within a wrap.

The UX is shown first, and then an example page (drop it into examples/<anywhere>) which uses that to re-implement Ext.Resizable to use EventZones instead of handle elements.



/**
*
* Ext.ux.EventZone Extension Class for Ext 3.x Library
*
* @author Nigel White
*
* @license Ext.ux.EventZone is licensed under the terms of
* the Open Source LGPL 3.0 license. Commercial use is permitted to the extent
* that the code/component(s) do NOT become part of another Open Source or Commercially
* licensed development library or toolkit without explicit permission.
*
* License details: http://www.gnu.org/licenses/lgpl.html
*
* @class Ext.ux.EventZone
* <p>This class implements a "virtual element" at a relative size and position
* <i>within</i> an existing element. It provides mouse events from a zone of an element of
* defined dimensions.</p>
* <p>The zone is defined using <code>top</code>, <code>right</code>, <code>bottom</code>,
* <code>left</code>, <code>width</code> and <code>height</code> options which specify
* the bounds of the zone in a similar manner to the CSS style properties of those names.</p>
* @cfg {String|HtmlElement} el The element in which to create the zone.
* @cfg {Array} points An Array of points within the element defining the event zone.
* @cfg {Number} top The top of the zone. If negative means an offset from the bottom.
* @cfg {Number} right The right of the zone. If negative means an offset from the right.
* @cfg {Number} left The left of the zone. If negative means an offset from the right.
* @cfg {Number} bottom The bottom of the zone. If negative means an offset from the bottom.
* @cfg {Number} width The width of the zone.
* @cfg {Number} height The height of the zone.
* @constructor
* Create a new EventZone
* @param {Object} config The config object.
*/
Ext.ux.EventZone = Ext.extend(Ext.util.Observable, {

constructor: function(config) {
this.initialConfig = config;
this.addEvents(
/**
* @event mouseenter
* This event fires when the mouse enters the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mouseenter',
/**
* @event mousedown
* This event fires when the mouse button is depressed within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mousedown',
/**
* @event mousemove
* This event fires when the mouse moves within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mousemove',
/**
* @event mouseup
* This event fires when the mouse button is released within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mouseup',
/**
* @event mouseenter
* This event fires when the mouse is clicked within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'click',
/**
* @event mouseleave
* This event fires when the mouse leaves the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mouseleave'
);
Ext.apply(this, config);
this.el = Ext.get(this.el);

// If a polygon within the element is specified...
if (this.points) {
this.polygon = new Ext.lib.Polygon(this.points);
this.points = this.polygon.points;
}

Ext.ux.EventZone.superclass.constructor.call(this);
this.el.on({
mouseenter: this.handleMouseEvent,
mousedown: this.handleMouseEvent,
mousemove: this.handleMouseEvent,
mouseup: this.handleMouseEvent,
click: this.handleMouseEvent,
mouseleave: this.handleMouseEvent,
scope: this
});
},

handleMouseEvent: function(e) {
var r = this.polygon ? this.getPolygon() : this.getRegion();
var inBounds = r.contains(e.getPoint());

switch (e.type) {
// mouseenter fires this
case 'mouseover':
if (inBounds) {
this.mouseIn = true;
this.fireEvent('mouseenter', e, this);
}
break;
// mouseleave fires this
case 'mouseout':
this.mouseIn = false;
this.fireEvent('mouseleave', e, this);
break;
case 'mousemove':
if (inBounds) {
if (this.mouseIn) {
this.fireEvent('mousemove', e, this);
} else {
this.mouseIn = true;
this.fireEvent('mouseenter', e, this);
}
} else {
if (this.mouseIn) {
this.mouseIn = false;
this.fireEvent('mouseleave', e, this);
}
}
break;
default:
if (inBounds) {
this.fireEvent(e.type, e, this);
}
}
},

getPolygon: function() {
var xy = this.el.getXY();
return this.polygon.translate(xy[0], xy[1]);
},

getRegion: function() {
var r = this.el.getRegion();

// Adjust left boundary of region
if (Ext.isNumber(this.left)) {
if (this.left < 0) {
r.left = r.right + this.left;
} else {
r.left += this.left;
}
}

// Adjust right boundary of region
if (Ext.isNumber(this.width)) {
r.right = r.left + this.width;
} else if (Ext.isNumber(this.right)) {
r.right = (this.right < 0) ? r.right + this.right : r.left + this.right;
}

// Adjust top boundary of region
if (Ext.isNumber(this.top)) {
if (this.top < 0) {
r.top = r.bottom + this.top;
} else {
r.top += this.top;
}
}

// Adjust bottom boundary of region
if (Ext.isNumber(this.height)) {
r.bottom = r.top + this.height;
} else if (Ext.isNumber(this.bottom)) {
r.bottom = (this.bottom < 0) ? r.bottom + this.bottom : r.top + this.bottom;
}

return r;
}
});

/**
* @class Ext.lib.Polygon
* <p>This class encapsulates an absolute area of the document bounded by a list of points.</p>
* @constructor
* Create a new Polygon
* @param {Object} points An Array of <code>[n,n]</code> point specification Arrays, or
* an Array of Ext.lib.Points, or an HtmlElement, or an Ext.lib.Region.
*/
Ext.lib.Polygon = Ext.extend(Ext.lib.Region, {
constructor: function(points) {
var i, l, el;
if (l = points.length) {
if (points[0].x) {
for (i = 0; i < l; i++) {
points[i] = [ points[i].x, points[i].y ];
}
}
this.points = points;
} else {
if (el = Ext.get(points)) {
points = Ext.lib.Region.getRegion(el.dom);
}
if (points instanceof Ext.lib.Region) {
this.points = [
[points.left, points.top],
[points.right, points.top],
[points.right, points.bottom],
[points.left, points.bottom]
];
}
}
},

/**
* Returns a new Polygon translated by the specified <code>X</code> and <code>Y</code> increments.
* @param xDelta {Number} The <code>X</code> translation increment.
* @param xDelta {Number} The <code>Y</code> translation increment.
* @return {Polygon} The resulting Polygon.
*/
translate: function(xDelta, yDelta) {
var r = [], p = this.points, l = p.length, i;
for (i = 0; i < l; i++) {
r[i] = [ p[i][0] + xDelta, p[i][1] + yDelta ];
}
return new Ext.lib.Polygon(r);
},

/**
* Returns the area of this Polygon.
*/
getArea: function() {
var p = this.points, l = p.length, area = 0, i, j = 0;
for (i = 0; i < l; i++) {
j++;
if (j == l) {
j = 0;
}
area += (p[i][0] + p[j][0]) * (p[i][1] - p[j][1]);
}
return area * 0.5;
},

/**
* Returns <code>true</code> if this Polygon contains the specified point. Thanks
* to http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html for the algorithm.
* @param pt {Point|Number} Either an Ext.lib.Point object, or the <code>X</code> coordinate to test.
* @param py {Number} <b>Optional.</b> If the first parameter was an <code>X</code> coordinate, this is the <code>Y</code> coordinate.
*/
contains: function(px, py) {
var f = (arguments.length == 1),
p = this.points,
nvert = p.length,
j = nvert - 1,
i, j, c = false;
py = f ? px.y : py;
px = f ? px.x : px;
for (i = 0; i < nvert; j = i++) {
if ( ((p[i][1] > py) != (p[j][1] > py)) &&
(px < (p[j][0]-p[i][0]) * (py-p[i][1]) / (p[j][1]-p[i][1]) + p[i][0])) {
c = !c;
}
}
return c;
}
});


Test page:


<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<link rel="stylesheet" type="text/css" href="../../resources/css/ext-all.css" />
<link rel="stylesheet" type="text/css" href="../shared/examples.css" />
<script type="text/javascript" src="../../adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="../../ext-all.js"></script>
<script type="text/javascript">
/**
* @class Ext.ux.EventZone
* <p>This class implements a "virtual element" at a relative size and position
* <i>within</i> an existing element. It provides mouse events from a zone of an element of
* defined dimensions.</p>
* <p>The zone is defined using <code>top</code>, <code>right</code>, <code>bottom</code>,
* <code>left</code>, <code>width</code> and <code>height</code> options which specify
* the bounds of the zone in a similar manner to the CSS style properties of those names.</p>
* @cfg {String|HtmlElement} el The element in which to create the zone.
* @cfg {Array} points An Array of points within the element defining the event zone.
* @cfg {Number} top The top of the zone. If negative means an offset from the bottom.
* @cfg {Number} right The right of the zone. If negative means an offset from the right.
* @cfg {Number} left The left of the zone. If negative means an offset from the right.
* @cfg {Number} bottom The bottom of the zone. If negative means an offset from the bottom.
* @cfg {Number} width The width of the zone.
* @cfg {Number} height The height of the zone.
* @constructor
* Create a new EventZone
* @param {Object} config The config object.
*/
Ext.ux.EventZone = Ext.extend(Ext.util.Observable, {

constructor: function(config) {
this.initialConfig = config;
this.addEvents(
/**
* @event mouseenter
* This event fires when the mouse enters the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mouseenter',
/**
* @event mousedown
* This event fires when the mouse button is depressed within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mousedown',
/**
* @event mousemove
* This event fires when the mouse moves within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mousemove',
/**
* @event mouseup
* This event fires when the mouse button is released within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mouseup',
/**
* @event mouseenter
* This event fires when the mouse is clicked within the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'click',
/**
* @event mouseleave
* This event fires when the mouse leaves the zone.
* @param {EventObject} e the underlying mouse event.
* @param {EventZone} this
*/
'mouseleave'
);
Ext.apply(this, config);
this.el = Ext.get(this.el);

// If a polygon within the element is specified...
if (this.points) {
this.polygon = new Ext.lib.Polygon(this.points);
this.points = this.polygon.points;
}

Ext.ux.EventZone.superclass.constructor.call(this);
this.el.on({
mouseenter: this.handleMouseEvent,
mousedown: this.handleMouseEvent,
mousemove: this.handleMouseEvent,
mouseup: this.handleMouseEvent,
click: this.handleMouseEvent,
mouseleave: this.handleMouseEvent,
scope: this
});
},

handleMouseEvent: function(e) {
var r = this.polygon ? this.getPolygon() : this.getRegion();
var inBounds = r.contains(e.getPoint());

switch (e.type) {
// mouseenter fires this
case 'mouseover':
if (inBounds) {
this.mouseIn = true;
this.fireEvent('mouseenter', e, this);
}
break;
// mouseleave fires this
case 'mouseout':
this.mouseIn = false;
this.fireEvent('mouseleave', e, this);
break;
case 'mousemove':
if (inBounds) {
if (this.mouseIn) {
this.fireEvent('mousemove', e, this);
} else {
this.mouseIn = true;
this.fireEvent('mouseenter', e, this);
}
} else {
if (this.mouseIn) {
this.mouseIn = false;
this.fireEvent('mouseleave', e, this);
}
}
break;
default:
if (inBounds) {
this.fireEvent(e.type, e, this);
}
}
},

getPolygon: function() {
var xy = this.el.getXY();
return this.polygon.translate(xy[0], xy[1]);
},

getRegion: function() {
var r = this.el.getRegion();

// Adjust left boundary of region
if (Ext.isNumber(this.left)) {
if (this.left < 0) {
r.left = r.right + this.left;
} else {
r.left += this.left;
}
}

// Adjust right boundary of region
if (Ext.isNumber(this.width)) {
r.right = r.left + this.width;
} else if (Ext.isNumber(this.right)) {
r.right = (this.right < 0) ? r.right + this.right : r.left + this.right;
}

// Adjust top boundary of region
if (Ext.isNumber(this.top)) {
if (this.top < 0) {
r.top = r.bottom + this.top;
} else {
r.top += this.top;
}
}

// Adjust bottom boundary of region
if (Ext.isNumber(this.height)) {
r.bottom = r.top + this.height;
} else if (Ext.isNumber(this.bottom)) {
r.bottom = (this.bottom < 0) ? r.bottom + this.bottom : r.top + this.bottom;
}

return r;
}
});

/**
* @class Ext.lib.Polygon
* <p>This class encapsulates an absolute area of the document bounded by a list of points.</p>
* @constructor
* Create a new Polygon
* @param {Object} points An Array of <code>[n,n]</code> point specification Arrays, or
* an Array of Ext.lib.Points, or an HtmlElement, or an Ext.lib.Region.
*/
Ext.lib.Polygon = Ext.extend(Ext.lib.Region, {
constructor: function(points) {
var i, l, el;
if (l = points.length) {
if (points[0].x) {
for (i = 0; i < l; i++) {
points[i] = [ points[i].x, points[i].y ];
}
}
this.points = points;
} else {
if (el = Ext.get(points)) {
points = Ext.lib.Region.getRegion(el.dom);
}
if (points instanceof Ext.lib.Region) {
this.points = [
[points.left, points.top],
[points.right, points.top],
[points.right, points.bottom],
[points.left, points.bottom]
];
}
}
},

/**
* Returns a new Polygon translated by the specified <code>X</code> and <code>Y</code> increments.
* @param xDelta {Number} The <code>X</code> translation increment.
* @param xDelta {Number} The <code>Y</code> translation increment.
* @return {Polygon} The resulting Polygon.
*/
translate: function(xDelta, yDelta) {
var r = [], p = this.points, l = p.length, i;
for (i = 0; i < l; i++) {
r[i] = [ p[i][0] + xDelta, p[i][1] + yDelta ];
}
return new Ext.lib.Polygon(r);
},

/**
* Returns the area of this Polygon.
*/
getArea: function() {
var p = this.points, l = p.length, area = 0, i, j = 0;
for (i = 0; i < l; i++) {
j++;
if (j == l) {
j = 0;
}
area += (p[i][0] + p[j][0]) * (p[i][1] - p[j][1]);
}
return area * 0.5;
},

/**
* Returns <code>true</code> if this Polygon contains the specified point. Thanks
* to http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html for the algorithm.
* @param pt {Point|Number} Either an Ext.lib.Point object, or the <code>X</code> coordinate to test.
* @param py {Number} <b>Optional.</b> If the first parameter was an <code>X</code> coordinate, this is the <code>Y</code> coordinate.
*/
contains: function(px, py) {
var f = (arguments.length == 1),
p = this.points,
nvert = p.length,
j = nvert - 1,
i, j, c = false;
py = f ? px.y : py;
px = f ? px.x : px;
for (i = 0; i < nvert; j = i++) {
if ( ((p[i][1] > py) != (p[j][1] > py)) &&
(px < (p[j][0]-p[i][0]) * (py-p[i][1]) / (p[j][1]-p[i][1]) + p[i][0])) {
c = !c;
}
}
return c;
}
});

/**
* @class Ext.Resizable
* @extends Ext.util.Observable
* <p>Applies virtual drag handles to an element to make it resizable.</p>
* <p>Here is the list of valid resize handles:</p>
* <pre>
Value Description
------ -------------------
'n' north
's' south
'e' east
'w' west
'nw' northwest
'sw' southwest
'se' southeast
'ne' northeast
'all' all
</pre>
* <p>Here's an example showing the creation of a typical Resizable:</p>
* <pre><code>
var resizer = new Ext.Resizable('element-id', {
handles: 'all',
minWidth: 200,
minHeight: 100,
maxWidth: 500,
maxHeight: 400,
pinned: true
});
resizer.on('resize', myHandler);
</code></pre>
* <p>To hide a particular handle, set its display to none in CSS, or through script:<br>
* resizer.east.setDisplayed(false);</p>
* @constructor
* Create a new resizable component
* @param {Mixed} el The id or element to resize
* @param {Object} config configuration options
*/
Ext.Resizable = function(el, config){
this.el = Ext.get(el);

/**
* The proxy Element that is resized in place of the real Element during the resize operation.
* This may be queried using {@link Ext.Element#getBox} to provide the new area to resize to.
* Read only.
* @type Ext.Element.
* @property proxy
*/
this.proxy = this.el.createProxy({tag: 'div', cls: 'x-resizable-proxy', id: this.el.id + '-rzproxy'}, Ext.getBody());
this.proxy.unselectable();
this.proxy.enableDisplayMode('block');

Ext.apply(this, config);

if(this.pinned){
this.disableTrackOver = true;
this.el.addClass('x-resizable-pinned');
}
// if the element isn't positioned, make it relative
var position = this.el.getStyle('position');
if(position != 'absolute' && position != 'fixed'){
this.el.setStyle('position', 'relative');
}
if(!this.handles){ // no handles passed, must be legacy style
this.handles = 's,e,se';
if(this.multiDirectional){
this.handles += ',n,w';
}
}
if(this.handles == 'all'){
this.handles = 'n s e w ne nw se sw';
}
var hs = this.handles.split(/\s*?[,;]\s*?| /);
var ps = Ext.Resizable.positions;
for(var i = 0, len = hs.length; i < len; i++){
if(hs[i] && ps[hs[i]]){
var pos = ps[hs[i]];
this[pos] = new Ext.Resizable.Handle(this, pos);
}
}
// legacy
this.corner = this.southeast;

if(this.handles.indexOf('n') != -1 || this.handles.indexOf('w') != -1){
this.updateBox = true;
}
this.activeHandle = null;

if(this.adjustments == 'auto'){
var hw = this.west, he = this.east, hn = this.north, hs = this.south;
this.adjustments = [
(he ? -he.el.getWidth() : 0) + (hw ? -hw.el.getWidth() : 0),
(hn ? -hn.el.getHeight() : 0) + (hs ? -hs.el.getHeight() : 0) -1
];
}

if(this.draggable){
this.dd = this.dynamic ?
this.el.initDD(null) : this.el.initDDProxy(null, {dragElId: this.proxy.id});
this.dd.setHandleElId(this.el.id);
}

this.addEvents(
/**
* @event beforeresize
* Fired before resize is allowed. Set {@link #enabled} to false to cancel resize.
* @param {Ext.Resizable} this
* @param {Ext.EventObject} e The mousedown event
*/
'beforeresize',
/**
* @event resize
* Fired after a resize.
* @param {Ext.Resizable} this
* @param {Number} width The new width
* @param {Number} height The new height
* @param {Ext.EventObject} e The mouseup event
*/
'resize'
);

if(this.width !== null && this.height !== null){
this.resizeTo(this.width, this.height);
}
if(Ext.isIE){
this.el.dom.style.zoom = 1;
}
Ext.Resizable.superclass.constructor.call(this);
};

Ext.extend(Ext.Resizable, Ext.util.Observable, {

/**
* @cfg {Array/String} adjustments String 'auto' or an array [width, height] with values to be <b>added</b> to the
* resize operation's new size (defaults to <tt>[0, 0]</tt>)
*/
adjustments : [0, 0],
/**
* @cfg {Boolean} animate True to animate the resize (not compatible with dynamic sizing, defaults to false)
*/
animate : false,
/**
* @cfg {Mixed} constrainTo Constrain the resize to a particular element
*/
/**
* @cfg {Boolean} draggable Convenience to initialize drag drop (defaults to false)
*/
draggable: false,
/**
* @cfg {Number} duration Animation duration if animate = true (defaults to 0.35)
*/
duration : 0.35,
/**
* @cfg {Boolean} dynamic True to resize the element while dragging instead of using a proxy (defaults to false)
*/
dynamic : false,
/**
* @cfg {String} easing Animation easing if animate = true (defaults to <tt>'easingOutStrong'</tt>)
*/
easing : 'easeOutStrong',
/**
* @cfg {Boolean} enabled False to disable resizing (defaults to true)
*/
enabled : true,
/**
* @property enabled Writable. False if resizing is disabled.
* @type Boolean
*/
/**
* @cfg {String} handles String consisting of the resize handles to display (defaults to undefined).
* Specify either <tt>'all'</tt> or any of <tt>'n s e w ne nw se sw'</tt>.
*/
handles : false,
/**
* @cfg {Boolean} multiDirectional <b>Deprecated</b>. Deprecated style of adding multi-direction resize handles.
*/
multiDirectional : false,
/**
* @cfg {Number} height The height of the element in pixels (defaults to null)
*/
height : null,
/**
* @cfg {Number} width The width of the element in pixels (defaults to null)
*/
width : null,
/**
* @cfg {Number} heightIncrement The increment to snap the height resize in pixels
* (only applies if <code>{@link #dynamic}==true</code>). Defaults to <tt>0</tt>.
*/
heightIncrement : 0,
/**
* @cfg {Number} widthIncrement The increment to snap the width resize in pixels
* (only applies if <code>{@link #dynamic}==true</code>). Defaults to <tt>0</tt>.
*/
widthIncrement : 0,
/**
* @cfg {Number} minHeight The minimum height for the element (defaults to 5)
*/
minHeight : 5,
/**
* @cfg {Number} minWidth The minimum width for the element (defaults to 5)
*/
minWidth : 5,
/**
* @cfg {Number} maxHeight The maximum height for the element (defaults to 10000)
*/
maxHeight : 10000,
/**
* @cfg {Number} maxWidth The maximum width for the element (defaults to 10000)
*/
maxWidth : 10000,
/**
* @cfg {Number} minX The minimum x for the element (defaults to 0)
*/
minX: 0,
/**
* @cfg {Number} minY The minimum x for the element (defaults to 0)
*/
minY: 0,
/**
* @cfg {Boolean} pinned True to ensure that the resize handles are always visible, false to display them only when the
* user mouses over the resizable borders. This is only applied at config time. (defaults to false)
*/
pinned : false,
/**
* @cfg {Boolean} preserveRatio True to preserve the original ratio between height
* and width during resize (defaults to false)
*/
preserveRatio : false,
/**
* @cfg {Ext.lib.Region} resizeRegion Constrain the resize to a particular region
*/


/**
* Perform a manual resize and fires the 'resize' event.
* @param {Number} width
* @param {Number} height
*/
resizeTo : function(width, height){
this.el.setSize(width, height);
this.fireEvent('resize', this, width, height, null);
},

// private
startSizing : function(e, handle){
this.fireEvent('beforeresize', this, e);
if(this.enabled){ // 2nd enabled check in case disabled before beforeresize handler
e.stopEvent();

Ext.getDoc().on({
scope: this,
mousemove: this.onMouseMove,
mouseup: {
fn: this.onMouseUp,
single: true,
scope: this
}
});
Ext.getBody().addClass('ux-resizable-handle-' + handle.position);

this.resizing = true;
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]];

if(this.constrainTo) {
var ct = Ext.get(this.constrainTo);
this.resizeRegion = ct.getRegion().adjust(
ct.getFrameWidth('t'),
ct.getFrameWidth('l'),
-ct.getFrameWidth('b'),
-ct.getFrameWidth('r')
);
}

this.proxy.setStyle('visibility', 'hidden'); // workaround display none
this.proxy.show();
this.proxy.setBox(this.startBox);
if(!this.dynamic){
this.proxy.setStyle('visibility', 'visible');
}
}
},

// private
onMouseDown : function(handle, e){
if(this.enabled && !this.activeHandle){
e.stopEvent();
this.activeHandle = handle;
this.startSizing(e, handle);
}
},

// private
onMouseUp : function(e){
Ext.getBody().removeClass('ux-resizable-handle-' + this.activeHandle.position)
.un('mousemove', this.onMouseMove, this);
var size = this.resizeElement();
this.resizing = false;
this.handleOut(this.activeHandle);
this.proxy.hide();
this.fireEvent('resize', this, size.width, size.height, e);
this.activeHandle = null;
},

// private
snap : function(value, inc, min){
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 Math.max(min, newValue);
},

/**
* <p>Performs resizing of the associated Element. This method is called internally by this
* class, and should not be called by user code.</p>
* <p>If a Resizable is being used to resize an Element which encapsulates a more complex UI
* component such as a Panel, this method may be overridden by specifying an implementation
* as a config option to provide appropriate behaviour at the end of the resize operation on
* mouseup, for example resizing the Panel, and relaying the Panel's content.</p>
* <p>The new area to be resized to is available by examining the state of the {@link #proxy}
* Element. Example:
<pre><code>
new Ext.Panel({
title: 'Resize me',
x: 100,
y: 100,
renderTo: Ext.getBody(),
floating: true,
frame: true,
width: 400,
height: 200,
listeners: {
render: function(p) {
new Ext.Resizable(p.getEl(), {
handles: 'all',
pinned: true,
transparent: true,
resizeElement: function() {
var box = this.proxy.getBox();
p.updateBox(box);
if (p.layout) {
p.doLayout();
}
return box;
}
});
}
}
}).show();
</code></pre>
*/
resizeElement : function(){
var box = this.proxy.getBox();
if(this.updateBox){
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);
}
if(!this.dynamic){
this.proxy.hide();
}
return box;
},

// private
constrain : function(v, diff, m, mx){
if(v - diff < m){
diff = v - m;
}else if(v - diff > mx){
diff = v - mx;
}
return diff;
},

// private
onMouseMove : function(e){
if(this.enabled && this.activeHandle){
try{// try catch so if something goes wrong the user doesn't get hung

if(this.resizeRegion && !this.resizeRegion.contains(e.getPoint())) {
return;
}

//var curXY = this.startPoint;
var curSize = this.curSize || this.startBox,
x = this.startBox.x, y = this.startBox.y,
ox = x,
oy = y,
w = curSize.width,
h = curSize.height,
ow = w,
oh = h,
mw = this.minWidth,
mh = this.minHeight,
mxw = this.maxWidth,
mxh = this.maxHeight,
wi = this.widthIncrement,
hi = this.heightIncrement,
eventXY = e.getXY(),
diffX = -(this.startPoint[0] - Math.max(this.minX, eventXY[0])),
diffY = -(this.startPoint[1] - Math.max(this.minY, eventXY[1])),
pos = this.activeHandle.position,
tw,
th;

switch(pos){
case 'east':
w += diffX;
w = Math.min(Math.max(mw, w), mxw);
break;
case 'south':
h += diffY;
h = Math.min(Math.max(mh, h), mxh);
break;
case 'southeast':
w += diffX;
h += diffY;
w = Math.min(Math.max(mw, w), mxw);
h = Math.min(Math.max(mh, h), mxh);
break;
case 'north':
diffY = this.constrain(h, diffY, mh, mxh);
y += diffY;
h -= diffY;
break;
case 'west':
diffX = this.constrain(w, diffX, mw, mxw);
x += diffX;
w -= diffX;
break;
case 'northeast':
w += diffX;
w = Math.min(Math.max(mw, w), mxw);
diffY = this.constrain(h, diffY, mh, mxh);
y += diffY;
h -= diffY;
break;
case 'northwest':
diffX = this.constrain(w, diffX, mw, mxw);
diffY = this.constrain(h, diffY, mh, mxh);
y += diffY;
h -= diffY;
x += diffX;
w -= diffX;
break;
case 'southwest':
diffX = this.constrain(w, diffX, mw, mxw);
h += diffY;
h = Math.min(Math.max(mh, h), mxh);
x += diffX;
w -= diffX;
break;
}

var sw = this.snap(w, wi, mw);
var sh = this.snap(h, hi, mh);
if(sw != w || sh != h){
switch(pos){
case 'northeast':
y -= sh - h;
break;
case 'north':
y -= sh - h;
break;
case 'southwest':
x -= sw - w;
break;
case 'west':
x -= sw - w;
break;
case 'northwest':
x -= sw - w;
y -= sh - h;
break;
}
w = sw;
h = sh;
}

if(this.preserveRatio){
switch(pos){
case 'southeast':
case 'east':
h = oh * (w/ow);
h = Math.min(Math.max(mh, h), mxh);
w = ow * (h/oh);
break;
case 'south':
w = ow * (h/oh);
w = Math.min(Math.max(mw, w), mxw);
h = oh * (w/ow);
break;
case 'northeast':
w = ow * (h/oh);
w = Math.min(Math.max(mw, w), mxw);
h = oh * (w/ow);
break;
case 'north':
tw = w;
w = ow * (h/oh);
w = Math.min(Math.max(mw, w), mxw);
h = oh * (w/ow);
x += (tw - w) / 2;
break;
case 'southwest':
h = oh * (w/ow);
h = Math.min(Math.max(mh, h), mxh);
tw = w;
w = ow * (h/oh);
x += tw - w;
break;
case 'west':
th = h;
h = oh * (w/ow);
h = Math.min(Math.max(mh, h), mxh);
y += (th - h) / 2;
tw = w;
w = ow * (h/oh);
x += tw - w;
break;
case 'northwest':
tw = w;
th = h;
h = oh * (w/ow);
h = Math.min(Math.max(mh, h), mxh);
w = ow * (h/oh);
y += th - h;
x += tw - w;
break;

}
}
this.proxy.setBounds(x, y, w, h);
if(this.dynamic){
this.resizeElement();
}
}catch(ex){}
}
},

// private
handleOver : function(handle){
if(this.enabled){
Ext.getBody().addClass('ux-resizable-handle-' + handle.position);
}
},

// private
handleOut : function(handle){
if(!this.resizing){
Ext.getBody().removeClass('ux-resizable-handle-' + handle.position);
}
},

/**
* Returns the element this component is bound to.
* @return {Ext.Element}
*/
getEl : function(){
return this.el;
},

/**
* Destroys this resizable. If the element was wrapped and
* removeEl is not true then the element remains.
* @param {Boolean} removeEl (optional) true to remove the element from the DOM
*/
destroy : function(removeEl){
Ext.destroy(this.dd, this.proxy);
this.proxy = null;

var ps = Ext.Resizable.positions;
for(var k in ps){
if(typeof ps[k] != 'function' && this[ps[k]]){
this[ps[k]].destroy();
}
}
if(removeEl){
this.el.update('');
Ext.destroy(this.el);
this.el = null;
}
this.purgeListeners();
},

syncHandleHeight : function(){
var h = this.el.getHeight(true);
if(this.west){
this.west.el.setHeight(h);
}
if(this.east){
this.east.el.setHeight(h);
}
}
});

// private
// hash to map config positions to true positions
Ext.Resizable.positions = {
n: 'north', s: 'south', e: 'east', w: 'west', se: 'southeast', sw: 'southwest', nw: 'northwest', ne: 'northeast'
};
Ext.Resizable.cfg = {
north: {left: 7, right: -7, height: 7},
south: {left: 7, right: -7, top: -7},
east: {top: 7, bottom: -7, left: -7},
west: {top: 7, bottom: -7, width: 7},
southeast: {top: -7, left: -7},
southwest: {top: -7, width: 7},
northwest: {height: 7, width: 7},
northeast: {left: -7, height: 7}
};

// private
Ext.Resizable.Handle = function(rz, pos){
this.position = pos;
this.rz = rz;
var cfg = Ext.Resizable.cfg[pos] || Ext.Resizable.cfg[Ext.Resizable.positions[pos]];
this.ez = new Ext.ux.EventZone(Ext.apply({
position: pos,
el: rz.el
}, cfg));
this.ez.on({
mousedown: this.onMouseDown,
mouseenter: this.onMouseOver,
mouseleave: this.onMouseOut,
scope: this
});
};

// private
Ext.Resizable.Handle.prototype = {
cursor: 'move',

// private
afterResize : function(rz){
// do nothing
},
// private
onMouseDown : function(e){
this.rz.onMouseDown(this, e);
},
// private
onMouseOver : function(e){
this.rz.handleOver(this, e);
},
// private
onMouseOut : function(e){
this.rz.handleOut(this, e);
},
// private
destroy : function(){
Ext.destroy(this.el);
this.el = null;
}
};

Ext.onReady(function() {
var zoneResizer = new Ext.Resizable('the-image', {
minWidth:50,
minHeight: 50,
preserveRatio: true
});

var dwrapped = new Ext.Resizable('not-wrapped', {
width:450,
height:150,
minWidth:200,
minHeight: 50,
dynamic: true
});

new Ext.ux.EventZone({
el: 'my-trigger',
left: -17,
listeners: {
click: function() {
alert("Trigger Clicked");
},
mouseenter: function(e, z) {
z.el.dom.style.cursor = 'pointer';
},
mouseleave: function(e, z) {
z.el.dom.style.cursor = '';
}
}
});

// An active triangle
new Ext.ux.EventZone({
el: 'the-code',
points: [
[40, 0],
[5, 60],
[75, 60]
],
listeners: {
click: function() {
alert("Triangle Clicked");
},
mousemove: function(e, z) {
var p = e.getPoint();
var XY = z.el.getXY();
p.x -= XY[0];
p.y -= XY[1];
Ext.getDom('my-trigger').value = '[' + p.x + ', ' + p.y + ']';
},
mouseenter: function(e, z) {
z.el.dom.style.cursor = 'pointer';
},
mouseleave: function(e, z) {
z.el.dom.style.cursor = '';
}
}
});
});
</script>
<style type="text/css">
.ux-resizable-handle-east, .ux-resizable-handle-east textarea {
cursor: e-resize;
}

.ux-resizable-handle-south, .ux-resizable-handle-south textarea {
cursor: s-resize;
}

.ux-resizable-handle-west, .ux-resizable-handle-west textarea {
cursor: w-resize;
}

.ux-resizable-handle-north, .ux-resizable-handle-north textarea {
cursor: n-resize;
}

.ux-resizable-handle-southeast, .ux-resizable-handle-southeast textarea {
cursor: se-resize;
}

.ux-resizable-handle-northwest, .ux-resizable-handle-northwest textarea {
cursor: nw-resize;
}

.ux-resizable-handle-northeast, .ux-resizable-handle-northeast textarea {
cursor: ne-resize;
}

.ux-resizable-handle-southwest, .ux-resizable-handle-southwest textarea {
cursor: sw-resize;
}
</style>
</head>
<body>
<h1>Zone-based Resizable Example</h1>
<p>An image being resized <b>without</b> extra "handle" elements.</p>
<img id="the-image" src="http://extjs.com/deploy/dev/examples/resizable/zack.jpg" width="100" height="176"/>
<pre class="code"><code>var zoneResizer = new Ext.Resizable('the-image', {
minWidth:50,
minHeight: 50,
preserveRatio: true
});</code></pre>
<p>Here's a textarea that has dynamic sizing turned on.</p>
<textarea id="not-wrapped">
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Sed metus nibh, sodales a, porta at, vulputate eget, dui. Pellentesque ut nisl. Maecenas tortor turpis, interdum non, sodales non, iaculis ac, lacus. Vestibulum auctor, tortor quis iaculis malesuada, libero lectus bibendum purus, sit amet tincidunt quam turpis vel lacus. In pellentesque nisl non sem. Suspendisse nunc sem, pretium eget, cursus a, fringilla vel, urna. Aliquam commodo ullamcorper erat. Nullam vel justo in neque porttitor laoreet. Aenean lacus dui, consequat eu, adipiscing eget, nonummy non, nisi. Morbi nunc est, dignissim non, ornare sed, luctus eu, massa.
Vivamus eget quam. Vivamus tincidunt diam nec urna. Curabitur velit. Quisque dolor magna, ornare sed, elementum porta, luctus in, leo.
</textarea><br /><br />
And look how simple the code is, even my grandma could write it. Note that the below div also has an "active" triangle in it bounded by points
[40, 0], [5, 60], [75, 60]. When the mouse is over that area of the div, the trigger field below
reflects the element-relative mouse positions and the cursor is a hand. And the triangle is clickable.
<pre class="code" id="the-code"><code>var dwrapped = new Ext.Resizable('not-wrapped', {
width:450,
height:150,
minWidth:200,
minHeight: 50,
dynamic: true
});</code></pre>
<p>Here's a trigger. The sprite will have to have a left border, be made <b>vertical</b>, and then mouseover states will change the <b>top</b> position,
and the <b>y position</b> will be 100%. But I've no effing <b>clue</b> how to create sprites... Anyone?</p>
<input id="my-trigger" class="x-form-text x-form-field" style="width: 117px;background: url(http://extjs.com/deploy/dev/resources/images/default/form/trigger.gif) no-repeat 100px -1px; padding-right: 17px">
</body>
</html>

steffenk
22 Aug 2009, 2:56 AM
Hi Animal,

i would like to have a grandmother like you :D

Thx for this ux, just tested and works excellent! I will give it a try rezising textareas in TYPO3.

Animal
22 Aug 2009, 3:09 AM
Browsers don't seem to like processing mouse events over a scrollbar, so mousedown when on the right scrollbar doesn't work. You have to be right on the border for it to pick it up.

Oh well, you can't have everything.

steffenk
22 Aug 2009, 3:34 AM
Scrollbars ever were specific. I tested your ux also inside a scrolled area and works fine, i wonder why because you didn't use any scroll-offsets. I remember having problems with drag&drop in scrollarea. Why has it no influence in your script?

Animal
22 Aug 2009, 3:56 AM
I'm not sure what you are asking.

steffenk
22 Aug 2009, 4:38 AM
my question is why you don't need scrollTop/scrollLeft etc for calculating the mouse position, is it covered by Ext.util.Observable?

Animal
22 Aug 2009, 4:49 AM
EventObject.getXY returns the absolute page X,Y position, Element.getBox returns the absolute box area, so Ext covers it all.

Animal
23 Aug 2009, 7:27 AM
Latest version, as well as taking top, right, bottom and left modifiers to create a rectangular area, takes a points config which defines an internal polygon.

Events are then relayed just from the defined polygon.

The example page has been updated to exhibit this.

A bit like HTML image maps, but on any element.

steffenk
23 Aug 2009, 8:29 AM
Hi Animal,

one strange behaviour: the text in textarea isn't selectable anymore. Bug?

Animal
23 Aug 2009, 9:04 AM
Ah, I think that's always been a fault in my demo. It's



this.ez.el.unselectable();


In the Handle class. I don't think it's needed.

steffenk
23 Aug 2009, 10:31 AM
yes, works fine without this line.


Why does it loose selection while resizing? Seems like mousedown fires a blur for textarea, is there a way to prevent this?
When i look exact the selection isn't lost but only not visible. When i resize textarea with textselection and click inside after resize i see the old selection shortly.
(But this isn't really important ;) )

steffenk
27 Aug 2009, 2:50 PM
Hi Animal,

i discovered a small bug, seen while resize textarea.

If i resize the bottom right corner of textarea and shrink size, after release mouse button and moving cursor to body of textarea, the curser remains as resizer amd doesn't change to textcursor.

Animal
28 Aug 2009, 10:54 AM
Yeas, that's strange. I don't know what it could be.

steffenk
30 Aug 2009, 10:13 AM
Hi Animal,

as i want to integrate your code in TYPO3 (RFC is already done) i want to ask you about license and copyright. So
1) could you add your copyright to the code?
2) say if it's ok for you to integrate it in TYPO3 (and if license is the same like ExtJS)

That would be nice, thanks!

Animal
30 Aug 2009, 10:30 AM
OK, what do I do? What form of words should I insert into the header comment? I'll release the code under some explicit licence so you can use it.

steffenk
30 Aug 2009, 11:56 AM
i thought some thing like a signature


**
* Ext.ux.EventZone Extension Class for Ext 3.x Library
*
* @author <your name>
* @version $Id$
*
* @license Ext.ux.EventZone is licensed under the terms of
* the Open Source LGPL 3.0 license. Commercial use is permitted to the extent
* that the code/component(s) do NOT become part of another Open Source or Commercially
* licensed development library or toolkit without explicit permission.
*
* License details: http://www.gnu.org/licenses/lgpl.html
*/
(this is what Saki does)

Animal
30 Aug 2009, 1:19 PM
OK, I added that to the code in post 1.

steffenk
30 Aug 2009, 1:35 PM
Thanks!