2013-04-18 01:07:59 +00:00
|
|
|
/*!
|
|
|
|
* VisualEditor ContentEditable ResizableNode class.
|
|
|
|
*
|
|
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ContentEditable resizable node.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @abstract
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @param {jQuery} [$resizable=this.$] Resizable DOM element
|
2013-09-25 16:20:53 +00:00
|
|
|
* @param {Object} [config] Configuration options
|
|
|
|
* @param {number|null} [config.snapToGrid=10] Snap to a grid of size X when the shift key is held. Null disables.
|
2013-04-18 01:07:59 +00:00
|
|
|
*/
|
2013-09-25 16:20:53 +00:00
|
|
|
ve.ce.ResizableNode = function VeCeResizableNode( $resizable, config ) {
|
2013-04-18 01:07:59 +00:00
|
|
|
// Properties
|
|
|
|
this.$resizable = $resizable || this.$;
|
|
|
|
this.ratio = this.model.getAttribute( 'width' ) / this.model.getAttribute( 'height' );
|
|
|
|
this.resizing = false;
|
2013-05-28 16:36:31 +00:00
|
|
|
this.$resizeHandles = this.$$( '<div>' );
|
2013-09-25 16:20:53 +00:00
|
|
|
this.snapToGrid = ( config && config.snapToGrid !== undefined ) ? config.snapToGrid : 10;
|
2013-04-18 01:07:59 +00:00
|
|
|
|
|
|
|
// Events
|
2013-05-22 10:52:53 +00:00
|
|
|
this.connect( this, {
|
|
|
|
'focus': 'onResizableFocus',
|
|
|
|
'blur': 'onResizableBlur',
|
2013-06-05 20:59:19 +00:00
|
|
|
'live': 'onResizableLive',
|
|
|
|
'resize': 'onResizableFocus'
|
2013-05-22 10:52:53 +00:00
|
|
|
} );
|
2013-04-18 01:07:59 +00:00
|
|
|
|
|
|
|
// Initialization
|
|
|
|
this.$resizeHandles
|
|
|
|
.addClass( 've-ce-resizableNode-handles' )
|
2013-05-28 16:36:31 +00:00
|
|
|
.append( this.$$( '<div>' ).addClass( 've-ce-resizableNode-nwHandle' ) )
|
|
|
|
.append( this.$$( '<div>' ).addClass( 've-ce-resizableNode-neHandle' ) )
|
|
|
|
.append( this.$$( '<div>' ).addClass( 've-ce-resizableNode-seHandle' ) )
|
|
|
|
.append( this.$$( '<div>' ).addClass( 've-ce-resizableNode-swHandle' ) );
|
2013-04-18 01:07:59 +00:00
|
|
|
};
|
|
|
|
|
2013-06-07 00:51:00 +00:00
|
|
|
/* Static Properties */
|
|
|
|
|
|
|
|
ve.ce.ResizableNode.static = {};
|
|
|
|
|
2013-04-18 01:07:59 +00:00
|
|
|
/* Methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle node focus.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.onResizableFocus = function () {
|
2013-07-01 20:23:51 +00:00
|
|
|
this.$resizeHandles.appendTo( this.root.getSurface().getSurface().$localOverlayControls );
|
2013-04-18 01:07:59 +00:00
|
|
|
|
2013-07-01 20:23:51 +00:00
|
|
|
this.setResizableHandlesSizeAndPosition();
|
2013-04-18 01:07:59 +00:00
|
|
|
|
2013-07-01 21:16:22 +00:00
|
|
|
this.$resizeHandles
|
|
|
|
.find( '.ve-ce-resizableNode-neHandle' )
|
|
|
|
.css( { 'margin-right': -this.$resizable.width() } )
|
|
|
|
.end()
|
|
|
|
.find( '.ve-ce-resizableNode-swHandle' )
|
|
|
|
.css( { 'margin-bottom': -this.$resizable.height() } )
|
|
|
|
.end()
|
|
|
|
.find( '.ve-ce-resizableNode-seHandle' )
|
|
|
|
.css( {
|
|
|
|
'margin-right': -this.$resizable.width(),
|
|
|
|
'margin-bottom': -this.$resizable.height()
|
|
|
|
} );
|
|
|
|
|
|
|
|
this.$resizeHandles.children()
|
|
|
|
.off( '.ve-ui-resizableNode' )
|
|
|
|
.on(
|
|
|
|
'mousedown.ve-ui-resizableNode',
|
|
|
|
ve.bind( this.onResizeHandlesCornerMouseDown, this )
|
|
|
|
);
|
2013-04-18 01:07:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle node blur.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.onResizableBlur = function () {
|
2013-07-01 21:16:22 +00:00
|
|
|
this.$resizeHandles.detach();
|
2013-04-18 01:07:59 +00:00
|
|
|
};
|
|
|
|
|
2013-05-22 10:52:53 +00:00
|
|
|
/**
|
|
|
|
* Handle live event.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
2013-07-01 20:23:51 +00:00
|
|
|
ve.ce.ResizableNode.prototype.onResizableLive = function () {
|
|
|
|
var surfaceModel = this.getRoot().getSurface().getModel();
|
|
|
|
|
|
|
|
if ( this.live ) {
|
|
|
|
surfaceModel.connect( this, { 'history': 'setResizableHandlesSizeAndPosition' } );
|
|
|
|
} else {
|
|
|
|
surfaceModel.disconnect( this, { 'history': 'setResizableHandlesSizeAndPosition' } );
|
|
|
|
this.onResizableBlur();
|
2013-05-22 10:52:53 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-04-18 01:07:59 +00:00
|
|
|
/**
|
|
|
|
* Handle bounding box handle mousedown.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {jQuery.Event} e Click event
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.onResizeHandlesCornerMouseDown = function ( e ) {
|
2013-04-19 19:56:55 +00:00
|
|
|
// Hide context menu
|
|
|
|
// TODO: Maybe there's a more generic way to handle this sort of thing? For relocation it's
|
|
|
|
// handled in ve.ce.Surface
|
|
|
|
this.root.getSurface().getSurface().getContext().hide();
|
2013-04-18 01:07:59 +00:00
|
|
|
|
|
|
|
// Set bounding box width and undo the handle margins
|
|
|
|
this.$resizeHandles
|
2013-06-18 05:04:56 +00:00
|
|
|
.addClass( 've-ui-resizableNode-handles-resizing' )
|
2013-04-18 01:07:59 +00:00
|
|
|
.css( {
|
2013-04-19 19:56:55 +00:00
|
|
|
'width': this.$resizable.width(),
|
|
|
|
'height': this.$resizable.height()
|
2013-04-18 01:07:59 +00:00
|
|
|
} );
|
|
|
|
|
|
|
|
this.$resizeHandles.children().css( 'margin', 0 );
|
|
|
|
|
|
|
|
// Values to calculate adjusted bounding box size
|
|
|
|
this.resizeInfo = {
|
|
|
|
'mouseX': e.screenX,
|
|
|
|
'mouseY': e.screenY,
|
|
|
|
'top': this.$resizeHandles.position().top,
|
|
|
|
'left': this.$resizeHandles.position().left,
|
|
|
|
'height': this.$resizeHandles.height(),
|
|
|
|
'width': this.$resizeHandles.width(),
|
2013-06-05 10:47:47 +00:00
|
|
|
'handle': e.target.className
|
2013-04-18 01:07:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Bind resize events
|
|
|
|
this.resizing = true;
|
The Great ve.ui.Surface refactor of 2013
Prologue:
Farewell ve.Editor my good chap… Oh, hey there HTML frames - I didn't
see you there! In a world where iframes are outlaws, and symbols like
document and window are global, there were more than a few assumptions
about which document or window was being used. But fear not - for this
commit (probably) tracks them all down, leaving a trail of
iframe-compatible awesomeness in its wake. With the great ve.ui.Surface
now able to be used inside of iframes, let the reference editing
commence. But there, lurking in the darkness is a DM issue so fierce it
may take Roan and/or Ed up to 3 whole hours to sort it out.
Note to Roan and/or Ed:
Editing references seems to work fine, but when saving the page there
are "no changes" which is a reasonable indication to the contrary.
Objectives:
* Make it possible to have multiple surfaces be instantiated, get along
nicely, and be embedded inside of iframes if needed.
* Make reference content editable within a dialog
Approach:
* Move what's left of ve.Editor to ve.ui.Surface and essentially
obliterate all use of it
* Make even more stuff inherit from ve.Element (long live this.$$)
* Use the correct document or window anywhere it was being assumed to be
the top level one
* Resolve stacking order issues by removing the excessive use of z-index
and introducing global and local overlay elements for each editor
* Add a surface to the reference dialog, load up the reference contents
and save them back on apply
* Actually destroy what we create in ce and ui surfaces
* Add recursive frame offset calculation method to ve.Element
* Moved ve.ce.Surface's getSelectionRect method to the prototype
Bonus:
* Move ve.ce.DocumentNode.css contents to ve.ce.Node.css (not sure why it
was separate in the first place, but I'm likely the one to blame)
* Fix blatant lies in documentation
* Whitespace cleanup here and there
* Get rid of ve.ui.Window overlays - not used or needed
Change-Id: Iede83e7d24f7cb249b6ba3dc45d770445b862e08
2013-05-20 22:45:50 +00:00
|
|
|
$( this.getElementDocument() ).on( {
|
2013-04-18 01:07:59 +00:00
|
|
|
'mousemove.ve-ce-resizableNode': ve.bind( this.onDocumentMouseMove, this ),
|
|
|
|
'mouseup.ve-ce-resizableNode': ve.bind( this.onDocumentMouseUp, this )
|
|
|
|
} );
|
|
|
|
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2013-07-01 20:23:51 +00:00
|
|
|
/**
|
|
|
|
* Set the proper size and position for resize handles
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.setResizableHandlesSizeAndPosition = function () {
|
2013-06-27 17:25:28 +00:00
|
|
|
var offset = ve.Element.getRelativePosition(
|
|
|
|
this.$resizable, this.getRoot().getSurface().getSurface().$
|
|
|
|
);
|
2013-07-01 20:23:51 +00:00
|
|
|
|
|
|
|
this.$resizeHandles
|
|
|
|
.css( {
|
|
|
|
'width': 0,
|
|
|
|
'height': 0,
|
|
|
|
'top': offset.top,
|
|
|
|
'left': offset.left
|
|
|
|
} )
|
|
|
|
.find( '.ve-ce-resizableNode-neHandle' )
|
|
|
|
.css( { 'margin-right': -this.$resizable.width() } )
|
|
|
|
.end()
|
|
|
|
.find( '.ve-ce-resizableNode-swHandle' )
|
|
|
|
.css( { 'margin-bottom': -this.$resizable.height() } )
|
|
|
|
.end()
|
|
|
|
.find( '.ve-ce-resizableNode-seHandle' )
|
|
|
|
.css( {
|
|
|
|
'margin-right': -this.$resizable.width(),
|
|
|
|
'margin-bottom': -this.$resizable.height()
|
|
|
|
} );
|
|
|
|
};
|
|
|
|
|
2013-04-18 01:07:59 +00:00
|
|
|
/**
|
|
|
|
* Handle body mousemove.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
* @param {jQuery.Event} e Click event
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.onDocumentMouseMove = function ( e ) {
|
2013-09-25 16:20:53 +00:00
|
|
|
var newWidth, newHeight, newRatio, snapMin, snapMax, snap,
|
2013-04-18 01:07:59 +00:00
|
|
|
// TODO: Make these configurable
|
|
|
|
min = 1,
|
|
|
|
max = 1000,
|
|
|
|
diff = {},
|
|
|
|
dimensions = {
|
|
|
|
'width': 0,
|
|
|
|
'height': 0,
|
|
|
|
'top': this.resizeInfo.top,
|
|
|
|
'left': this.resizeInfo.left
|
|
|
|
};
|
|
|
|
|
|
|
|
if ( this.resizing ) {
|
|
|
|
// X and Y diff
|
|
|
|
switch ( this.resizeInfo.handle ) {
|
|
|
|
case 've-ce-resizableNode-seHandle':
|
|
|
|
diff.x = e.screenX - this.resizeInfo.mouseX;
|
|
|
|
diff.y = e.screenY - this.resizeInfo.mouseY;
|
|
|
|
break;
|
|
|
|
case 've-ce-resizableNode-nwHandle':
|
|
|
|
diff.x = this.resizeInfo.mouseX - e.screenX;
|
|
|
|
diff.y = this.resizeInfo.mouseY - e.screenY;
|
|
|
|
break;
|
|
|
|
case 've-ce-resizableNode-neHandle':
|
|
|
|
diff.x = e.screenX - this.resizeInfo.mouseX;
|
|
|
|
diff.y = this.resizeInfo.mouseY - e.screenY;
|
|
|
|
break;
|
|
|
|
case 've-ce-resizableNode-swHandle':
|
|
|
|
diff.x = this.resizeInfo.mouseX - e.screenX;
|
|
|
|
diff.y = e.screenY - this.resizeInfo.mouseY;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unconstrained dimensions and ratio
|
2013-04-19 19:56:55 +00:00
|
|
|
newWidth = Math.max( Math.min( this.$resizable.width() + diff.x, max ), min );
|
|
|
|
newHeight = Math.max( Math.min( this.$resizable.height() + diff.y, max ), min );
|
2013-04-18 01:07:59 +00:00
|
|
|
newRatio = newWidth / newHeight;
|
|
|
|
|
|
|
|
// Fix the ratio
|
|
|
|
if ( this.ratio > newRatio ) {
|
|
|
|
dimensions.width = newWidth;
|
2013-04-19 19:56:55 +00:00
|
|
|
dimensions.height = this.$resizable.height() +
|
|
|
|
( newWidth - this.$resizable.width() ) / this.ratio;
|
2013-04-18 01:07:59 +00:00
|
|
|
} else {
|
2013-04-19 19:56:55 +00:00
|
|
|
dimensions.width = this.$resizable.width() +
|
|
|
|
( newHeight - this.$resizable.height() ) * this.ratio;
|
2013-04-18 01:07:59 +00:00
|
|
|
dimensions.height = newHeight;
|
|
|
|
}
|
|
|
|
|
2013-09-25 16:20:53 +00:00
|
|
|
if ( this.snapToGrid && e.shiftKey ) {
|
|
|
|
snapMin = Math.ceil( min / this.snapToGrid );
|
|
|
|
snapMax = Math.floor( max / this.snapToGrid );
|
|
|
|
snap = Math.round( dimensions.width / this.snapToGrid );
|
|
|
|
dimensions.width = Math.max( Math.min( snap, snapMax ), snapMin ) * this.snapToGrid;
|
|
|
|
dimensions.height = dimensions.width / this.ratio;
|
|
|
|
}
|
|
|
|
|
2013-04-18 01:07:59 +00:00
|
|
|
// Fix the position
|
|
|
|
switch ( this.resizeInfo.handle ) {
|
|
|
|
case 've-ce-resizableNode-neHandle':
|
|
|
|
dimensions.top = this.resizeInfo.top +
|
|
|
|
( this.resizeInfo.height - dimensions.height );
|
|
|
|
break;
|
|
|
|
case 've-ce-resizableNode-swHandle':
|
|
|
|
dimensions.left = this.resizeInfo.left +
|
|
|
|
( this.resizeInfo.width - dimensions.width );
|
|
|
|
break;
|
|
|
|
case 've-ce-resizableNode-nwHandle':
|
|
|
|
dimensions.top = this.resizeInfo.top +
|
|
|
|
( this.resizeInfo.height - dimensions.height );
|
|
|
|
dimensions.left = this.resizeInfo.left +
|
|
|
|
( this.resizeInfo.width - dimensions.width );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update bounding box
|
|
|
|
this.$resizeHandles.css( dimensions );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle body mouseup.
|
|
|
|
*
|
|
|
|
* @method
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.onDocumentMouseUp = function () {
|
2013-07-16 14:06:47 +00:00
|
|
|
var attrChanges,
|
|
|
|
offset = this.model.getOffset(),
|
2013-04-18 01:07:59 +00:00
|
|
|
width = this.$resizeHandles.outerWidth(),
|
|
|
|
height = this.$resizeHandles.outerHeight(),
|
|
|
|
surfaceModel = this.getRoot().getSurface().getModel(),
|
|
|
|
documentModel = surfaceModel.getDocument(),
|
2013-07-16 14:06:47 +00:00
|
|
|
selection = surfaceModel.getSelection();
|
2013-04-18 01:07:59 +00:00
|
|
|
|
2013-06-18 05:04:56 +00:00
|
|
|
this.$resizeHandles.removeClass( 've-ui-resizableNode-handles-resizing' );
|
The Great ve.ui.Surface refactor of 2013
Prologue:
Farewell ve.Editor my good chap… Oh, hey there HTML frames - I didn't
see you there! In a world where iframes are outlaws, and symbols like
document and window are global, there were more than a few assumptions
about which document or window was being used. But fear not - for this
commit (probably) tracks them all down, leaving a trail of
iframe-compatible awesomeness in its wake. With the great ve.ui.Surface
now able to be used inside of iframes, let the reference editing
commence. But there, lurking in the darkness is a DM issue so fierce it
may take Roan and/or Ed up to 3 whole hours to sort it out.
Note to Roan and/or Ed:
Editing references seems to work fine, but when saving the page there
are "no changes" which is a reasonable indication to the contrary.
Objectives:
* Make it possible to have multiple surfaces be instantiated, get along
nicely, and be embedded inside of iframes if needed.
* Make reference content editable within a dialog
Approach:
* Move what's left of ve.Editor to ve.ui.Surface and essentially
obliterate all use of it
* Make even more stuff inherit from ve.Element (long live this.$$)
* Use the correct document or window anywhere it was being assumed to be
the top level one
* Resolve stacking order issues by removing the excessive use of z-index
and introducing global and local overlay elements for each editor
* Add a surface to the reference dialog, load up the reference contents
and save them back on apply
* Actually destroy what we create in ce and ui surfaces
* Add recursive frame offset calculation method to ve.Element
* Moved ve.ce.Surface's getSelectionRect method to the prototype
Bonus:
* Move ve.ce.DocumentNode.css contents to ve.ce.Node.css (not sure why it
was separate in the first place, but I'm likely the one to blame)
* Fix blatant lies in documentation
* Whitespace cleanup here and there
* Get rid of ve.ui.Window overlays - not used or needed
Change-Id: Iede83e7d24f7cb249b6ba3dc45d770445b862e08
2013-05-20 22:45:50 +00:00
|
|
|
$( this.getElementDocument() ).off( '.ve-ce-resizableNode' );
|
2013-04-18 01:07:59 +00:00
|
|
|
this.resizing = false;
|
2013-04-19 19:56:55 +00:00
|
|
|
|
2013-06-30 04:47:58 +00:00
|
|
|
// Apply changes to the model
|
2013-07-16 14:06:47 +00:00
|
|
|
attrChanges = this.getAttributeChanges( width, height );
|
2013-06-30 04:47:58 +00:00
|
|
|
if ( !ve.isEmptyObject( attrChanges ) ) {
|
|
|
|
surfaceModel.change(
|
|
|
|
ve.dm.Transaction.newFromAttributeChanges( documentModel, offset, attrChanges ),
|
|
|
|
selection
|
|
|
|
);
|
|
|
|
}
|
2013-06-05 20:59:19 +00:00
|
|
|
|
2013-10-10 12:34:54 +00:00
|
|
|
// Update the context menu. This usually happens with the redraw, but not if the
|
2013-09-25 20:54:59 +00:00
|
|
|
// user doesn't perform a drag
|
2013-10-10 12:34:54 +00:00
|
|
|
this.root.getSurface().getSurface().getContext().update();
|
2013-09-25 20:54:59 +00:00
|
|
|
|
2013-06-30 04:47:58 +00:00
|
|
|
this.emit( 'resize' );
|
2013-04-18 01:07:59 +00:00
|
|
|
};
|
2013-07-16 14:06:47 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate an object of attributes changes from the new width and height.
|
|
|
|
*
|
|
|
|
* @param {number} width New image width
|
|
|
|
* @param {number} height New image height
|
|
|
|
* @returns {Object} Attribute changes
|
|
|
|
*/
|
|
|
|
ve.ce.ResizableNode.prototype.getAttributeChanges = function ( width, height ) {
|
|
|
|
var attrChanges = {};
|
|
|
|
if ( this.model.getAttribute( 'width' ) !== width ) {
|
|
|
|
attrChanges.width = width;
|
|
|
|
}
|
|
|
|
if ( this.model.getAttribute( 'height' ) !== height ) {
|
|
|
|
attrChanges.height = height;
|
|
|
|
}
|
|
|
|
return attrChanges;
|
2013-09-25 16:20:53 +00:00
|
|
|
};
|