Esanders has uploaded a new change for review.
https://gerrit.wikimedia.org/r/109594
Change subject: The great image scaling rewrite of 2014
......................................................................
The great image scaling rewrite of 2014
Separate out logic for aspect ratio locked scaling so it can be
shared between ve.ce.ResizableNode and ve.ui.MediaSizeWidget.
Change-Id: I5b4f0f91b8534725130978875a5b4cabb6b98bd2
---
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/index.html
M modules/ve/ce/nodes/ve.ce.ImageNode.js
M modules/ve/ce/ve.ce.ResizableNode.js
M modules/ve/test/index.html
M modules/ve/ui/widgets/ve.ui.MediaSizeWidget.js
A modules/ve/ve.Scalable.js
8 files changed, 386 insertions(+), 285 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor
refs/changes/94/109594/1
diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index 77452d5..a2a2b37 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -92,6 +92,7 @@
<!-- visualEditor.core -->
<script src="../modules/ve/ve.Range.js"></script>
+ <script src="../modules/ve/ve.Scalable.js"></script>
<script src="../modules/ve/ve.Node.js"></script>
<script src="../modules/ve/ve.BranchNode.js"></script>
<script src="../modules/ve/ve.LeafNode.js"></script>
diff --git a/build/modules.json b/build/modules.json
index 782b44e..f58451e 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -81,6 +81,7 @@
"visualEditor.core": {
"scripts": [
"modules/ve/ve.Range.js",
+ "modules/ve/ve.Scalable.js",
"modules/ve/ve.Node.js",
"modules/ve/ve.BranchNode.js",
"modules/ve/ve.LeafNode.js",
diff --git a/demos/ve/index.html b/demos/ve/index.html
index 546a440..5a859e6 100644
--- a/demos/ve/index.html
+++ b/demos/ve/index.html
@@ -122,6 +122,7 @@
<!-- visualEditor.core -->
<script src="../../modules/ve/ve.Range.js"></script>
+ <script src="../../modules/ve/ve.Scalable.js"></script>
<script src="../../modules/ve/ve.Node.js"></script>
<script src="../../modules/ve/ve.BranchNode.js"></script>
<script src="../../modules/ve/ve.LeafNode.js"></script>
diff --git a/modules/ve/ce/nodes/ve.ce.ImageNode.js
b/modules/ve/ce/nodes/ve.ce.ImageNode.js
index 08c6530..395b4df 100644
--- a/modules/ve/ce/nodes/ve.ce.ImageNode.js
+++ b/modules/ve/ce/nodes/ve.ce.ImageNode.js
@@ -19,13 +19,17 @@
* @param {Object} [config] Configuration options
*/
ve.ce.ImageNode = function VeCeImageNode( model, config ) {
+ config = ve.extendObject( {
+ 'minDimensions': { 'width': 1, 'height': 1 }
+ }, config );
+
// Parent constructor
ve.ce.LeafNode.call( this, model, config );
// Mixin constructors
ve.ce.FocusableNode.call( this );
ve.ce.RelocatableNode.call( this );
- ve.ce.ResizableNode.call( this );
+ ve.ce.ResizableNode.call( this, null, config );
// Properties
this.$image = this.$element;
diff --git a/modules/ve/ce/ve.ce.ResizableNode.js
b/modules/ve/ce/ve.ce.ResizableNode.js
index 5f6be1b..d2fb333 100644
--- a/modules/ve/ce/ve.ce.ResizableNode.js
+++ b/modules/ve/ce/ve.ce.ResizableNode.js
@@ -9,6 +9,7 @@
* ContentEditable resizable node.
*
* @class
+ * @mixins ve.Scalable
* @abstract
*
* @constructor
@@ -18,14 +19,15 @@
* @param {boolean} [config.outline=false] Resize using an outline of the
element only, don't live preview.
* @param {boolean} [config.showSizeLabel=true] Show a label with the current
dimensions while resizing
* @param {boolean} [config.showScaleLabel=true] Show a label with the current
scale while resizing
- * @param {boolean} [config.min=1] Minimum size of longest edge
- * @param {boolean} [config.max=Infinity] Maximum size or longest edge
*/
ve.ce.ResizableNode = function VeCeResizableNode( $resizable, config ) {
config = config || {};
+
+ // Mixin constructors
+ ve.Scalable.call( this, config );
+
// Properties
this.$resizable = $resizable || this.$element;
- this.ratio = this.model.getAttribute( 'width' ) /
this.model.getAttribute( 'height' );
this.resizing = false;
this.$resizeHandles = this.$( '<div>' );
this.snapToGrid = config.snapToGrid !== undefined ? config.snapToGrid :
10;
@@ -39,10 +41,6 @@
this.$sizeLabel = this.$( '<div>' ).addClass(
've-ce-resizableNode-sizeLabel' ).append( this.$sizeText );
}
this.resizableOffset = null;
- this.originalDimensions = null;
-
- this.min = config.min !== undefined ? config.min : 1;
- this.max = config.max !== undefined ? config.max : Infinity;
// Events
this.connect( this, {
@@ -60,7 +58,16 @@
.append( this.$( '<div>' ).addClass(
've-ce-resizableNode-neHandle' ) )
.append( this.$( '<div>' ).addClass(
've-ce-resizableNode-seHandle' ) )
.append( this.$( '<div>' ).addClass(
've-ce-resizableNode-swHandle' ) );
+
+ this.setCurrentDimensions( {
+ 'width': this.model.getAttribute( 'width' ),
+ 'height': this.model.getAttribute( 'height' )
+ } );
};
+
+/* Inheritance */
+
+OO.mixinClass( ve.ce.ResizableNode, ve.Scalable );
/* Events */
@@ -97,69 +104,68 @@
return this.resizableOffset;
};
-/**
- * Set the orignal dimensions of an image
- *
- * @param {Object} dimensions Dimensions object with width & height
- */
+/** */
ve.ce.ResizableNode.prototype.setOriginalDimensions = function ( dimensions ) {
- this.originalDimensions = ve.copy( dimensions );
+ // Parent method
+ ve.Scalable.prototype.setOriginalDimensions.call( this, dimensions );
// If dimensions are valid and the scale label is desired, enable it
- this.canShowScaleLabel = this.showScaleLabel &&
this.originalDimensions.width && this.originalDimensions.height;
+ this.canShowScaleLabel = this.showScaleLabel &&
this.getOriginalDimensions().width && this.getOriginalDimensions().height;
+};
+
+/**
+ * Hide the size label
+ */
+ve.ce.ResizableNode.prototype.hideSizeLabel = function () {
+ var node = this;
+ // Defer the removal of this class otherwise other DOM changes may cause
+ // the opacity transition to not play out smoothly
+ setTimeout( function () {
+ node.$sizeLabel.removeClass(
've-ce-resizableNode-sizeLabel-resizing' );
+ } );
};
/**
* Update the contents and position of the size label
- *
- * Omitting the dimensions object will hide the size label.
- *
- * @param {Object} [dimensions] Dimensions object with width, height, top &
left, or undefined to hide
*/
-ve.ce.ResizableNode.prototype.updateSizeLabel = function ( dimensions ) {
+ve.ce.ResizableNode.prototype.updateSizeLabel = function () {
if ( !this.showSizeLabel && !this.canShowScaleLabel ) {
return;
}
- var offset, node, top, height, minWidth;
- if ( dimensions ) {
- offset = this.getResizableOffset();
+
+ var top, height,
+ dimensions = this.getCurrentDimensions(),
+ offset = this.getResizableOffset(),
minWidth = ( this.showSizeLabel ? 100 : 0 ) + (
this.showScaleLabel ? 30 : 0 );
- // Put the label on the outside when too narrow
- if ( dimensions.width < minWidth ) {
- top = offset.top + dimensions.height;
- height = 30;
- } else {
- top = offset.top;
- height = dimensions.height;
- }
- this.$sizeLabel
- .addClass( 've-ce-resizableNode-sizeLabel-resizing' )
- .css( {
- 'top': top,
- 'left': offset.left,
- 'width': dimensions.width,
- 'height': height,
- 'lineHeight': height + 'px'
- } );
- this.$sizeText.empty();
- if ( this.showSizeLabel ) {
- this.$sizeText.append( $( '<span>' )
- .addClass( 've-ce-resizableNode-sizeText-size' )
- .text( Math.round( dimensions.width ) + ' × ' +
Math.round( dimensions.height ) )
- );
- }
- if ( this.canShowScaleLabel ) {
- this.$sizeText.append( $( '<span>' )
- .addClass( 've-ce-resizableNode-sizeText-scale'
)
- .text( Math.round( 100 * dimensions.width /
this.originalDimensions.width ) + '%' )
- );
- }
+
+ // Put the label on the outside when too narrow
+ if ( dimensions.width < minWidth ) {
+ top = offset.top + dimensions.height;
+ height = 30;
} else {
- node = this;
- // Defer the removal of this class otherwise other DOM changes
may cause
- // the opacity transition to not play out smoothly
- setTimeout( function () {
- node.$sizeLabel.removeClass(
've-ce-resizableNode-sizeLabel-resizing' );
+ top = offset.top;
+ height = dimensions.height;
+ }
+ this.$sizeLabel
+ .addClass( 've-ce-resizableNode-sizeLabel-resizing' )
+ .css( {
+ 'top': top,
+ 'left': offset.left,
+ 'width': dimensions.width,
+ 'height': height,
+ 'lineHeight': height + 'px'
} );
+ this.$sizeText.empty();
+ if ( this.showSizeLabel ) {
+ this.$sizeText.append( $( '<span>' )
+ .addClass( 've-ce-resizableNode-sizeText-size' )
+ .text( Math.round( dimensions.width ) + ' × ' +
Math.round( dimensions.height ) )
+ );
+ }
+ if ( this.canShowScaleLabel ) {
+ this.$sizeText.append( $( '<span>' )
+ .addClass( 've-ce-resizableNode-sizeText-scale' )
+ .text( Math.round( 100 * this.getCurrentScale() ) + '%'
)
+ );
}
};
@@ -253,14 +259,12 @@
ve.ce.ResizableNode.prototype.onResizableResizing = function ( dimensions ) {
// Clear cached resizable offset position as it may have changed
this.resizableOffset = null;
+ this.setCurrentDimensions( dimensions );
if ( !this.outline ) {
- this.$resizable.css( {
- 'width': dimensions.width,
- 'height': dimensions.height
- } );
+ this.$resizable.css( this.getCurrentDimensions() );
this.setResizableHandlesPosition();
}
- this.updateSizeLabel( dimensions );
+ this.updateSizeLabel();
};
/**
@@ -300,7 +304,11 @@
// Bind resize events
this.resizing = true;
this.root.getSurface().resizing = true;
- this.updateSizeLabel( this.resizeInfo );
+ this.setCurrentDimensions( {
+ 'width': this.resizeInfo.width,
+ 'height': this.resizeInfo.height
+ } );
+ this.updateSizeLabel();
this.$( this.getElementDocument() ).on( {
'mousemove.ve-ce-resizableNode': ve.bind(
this.onDocumentMouseMove, this ),
'mouseup.ve-ce-resizableNode': ve.bind( this.onDocumentMouseUp,
this )
@@ -364,8 +372,7 @@
* @fires resizing
*/
ve.ce.ResizableNode.prototype.onDocumentMouseMove = function ( e ) {
- var newWidth, newHeight, newRatio, snapMin, snapMax, snap,
- diff = {},
+ var diff = {},
dimensions = {
'width': 0,
'height': 0,
@@ -394,29 +401,10 @@
break;
}
- // Unconstrained dimensions and ratio
- newWidth = Math.max( Math.min( this.resizeInfo.width + diff.x,
this.max ), this.min );
- newHeight = Math.max( Math.min( this.resizeInfo.height +
diff.y, this.max ), this.min );
- newRatio = newWidth / newHeight;
-
- // Fix the ratio
- if ( newRatio < this.ratio ) {
- dimensions.width = newWidth;
- dimensions.height = this.resizeInfo.height +
- ( newWidth - this.resizeInfo.width ) /
this.ratio;
- } else {
- dimensions.width = this.resizeInfo.width +
- ( newHeight - this.resizeInfo.height ) *
this.ratio;
- dimensions.height = newHeight;
- }
-
- if ( this.snapToGrid && e.shiftKey ) {
- snapMin = Math.ceil( this.min / this.snapToGrid );
- snapMax = Math.floor( this.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;
- }
+ dimensions = this.getBoundedDimensions( {
+ 'width': this.resizeInfo.width + diff.x,
+ 'height': this.resizeInfo.height + diff.y
+ }, e.shiftKey && this.snapToGrid );
// Fix the position
switch ( this.resizeInfo.handle ) {
@@ -438,7 +426,10 @@
// Update bounding box
this.$resizeHandles.css( dimensions );
- this.emit( 'resizing', dimensions );
+ this.emit( 'resizing', {
+ 'width': dimensions.width,
+ 'height': dimensions.height
+ } );
}
};
@@ -461,7 +452,7 @@
this.$( this.getElementDocument() ).off( '.ve-ce-resizableNode' );
this.resizing = false;
this.root.getSurface().resizing = false;
- this.updateSizeLabel();
+ this.hideSizeLabel();
// Apply changes to the model
attrChanges = this.getAttributeChanges( width, height );
diff --git a/modules/ve/test/index.html b/modules/ve/test/index.html
index 90c1c5d..e5f71b4 100644
--- a/modules/ve/test/index.html
+++ b/modules/ve/test/index.html
@@ -75,6 +75,7 @@
<!-- visualEditor.core -->
<script src="../../../modules/ve/ve.Range.js"></script>
+ <script src="../../../modules/ve/ve.Scalable.js"></script>
<script src="../../../modules/ve/ve.Node.js"></script>
<script src="../../../modules/ve/ve.BranchNode.js"></script>
<script src="../../../modules/ve/ve.LeafNode.js"></script>
diff --git a/modules/ve/ui/widgets/ve.ui.MediaSizeWidget.js
b/modules/ve/ui/widgets/ve.ui.MediaSizeWidget.js
index 505742f..d74821c 100644
--- a/modules/ve/ui/widgets/ve.ui.MediaSizeWidget.js
+++ b/modules/ve/ui/widgets/ve.ui.MediaSizeWidget.js
@@ -16,33 +16,22 @@
*
* @class
* @extends OO.ui.Widget
+ * @mixins ve.Scalable
*
* @constructor
* @param {Object} [config] Configuration options
- * @cfg {number} [width] Initial width value
- * @cfg {number} [height] Initial heigh value
- * @cfg {Object} [originalDimensions] Original dimensions (width and height)
- * @cfg {Object} [maxDimensions] Maximum dimensions the user is not allowed to
exceed
*/
ve.ui.MediaSizeWidget = function VeUiMediaSizeWidget( config ) {
var heightLabel, widthLabel;
- // Parent constructor
- OO.ui.Widget.call( this, config );
-
// Configuration
config = config || {};
- this.width = config.width || '';
- this.height = config.height || '';
- this.originalDimensions = null;
- this.maxDimensions = null;
+ // Parent constructor
+ OO.ui.Widget.call( this, config );
- // Cache for the aspect ratio, which is set by setOriginalDimensions()
- this.aspectRatio = null;
-
- // Validation
- this.valid = false;
+ // Mixin constructors
+ ve.Scalable.call( this, config );
// Define dimension input widgets
this.widthInput = new OO.ui.TextInputWidget( {
@@ -119,21 +108,9 @@
OO.inheritClass( ve.ui.MediaSizeWidget, OO.ui.Widget );
-/* Methods */
+OO.mixinClass( ve.ui.MediaSizeWidget, ve.Scalable );
-/**
- * Get the currently set rounded dimensions.
- *
- * @returns {Object} Current dimensions, rounded to integer values
- * @returns {number} return.width Width
- * @returns {number} return.height Height
- */
-ve.ui.MediaSizeWidget.prototype.getDimensions = function () {
- return {
- 'width': Number( this.widthInput.getValue() ),
- 'height': Number( this.heightInput.getValue() )
- };
-};
+/* Methods */
/**
* Set the current width and height dimensions.
@@ -163,8 +140,7 @@
* @param {number} [dimensions.width] Width to set
* @param {number} [dimensions.height] Height to set
*/
-ve.ui.MediaSizeWidget.prototype.setDimensions = function ( dimensions ) {
-
+ve.ui.MediaSizeWidget.prototype.setCurrentDimensions = function ( dimensions )
{
// Recursion protection
if ( this.preventChangeRecursion ) {
return;
@@ -172,137 +148,31 @@
this.preventChangeRecursion = true;
- if ( dimensions.width && dimensions.height ) {
- // If both dimensions are set up, use them directly
- this.width = dimensions.width;
- this.height = dimensions.height;
- } else if ( dimensions.width && !dimensions.height ) {
- // If only width is defined
- this.width = dimensions.width;
- if ( this.aspectRatio !== null ) {
- // If aspect ratio is available, calculate
- this.height = Math.round( this.width /
this.getAspectRatio() );
- }
- } else if ( dimensions.height && !dimensions.width ) {
- // If only height is defined
- this.height = dimensions.height;
- if ( this.aspectRatio !== null ) {
- // If aspect ratio is available, calculate
- this.width = Math.round( this.height *
this.getAspectRatio() );
- }
+ if ( !dimensions.height && this.getRatio() !== null && $.isNumeric(
dimensions.width ) ) {
+ dimensions.height = Math.round( dimensions.width /
this.getRatio() );
+ }
+ if ( !dimensions.width && this.getRatio() !== null && $.isNumeric(
dimensions.height ) ) {
+ dimensions.width = Math.round( dimensions.height /
this.getRatio() );
}
- // This will only update if the value has changed
- this.widthInput.setValue( this.width );
- this.heightInput.setValue( this.height );
+ ve.Scalable.prototype.setCurrentDimensions.call( this, dimensions );
- // Check if we need to notify the user that the dimensions
- // have a problem
- this.validateDimensions();
+ // This will only update if the value has changed
+ this.widthInput.setValue( this.getCurrentDimensions().width );
+ this.heightInput.setValue( this.getCurrentDimensions().height );
+
+ this.errorLabel.$element.toggle( !this.isCurrentDimensionsValid() );
+ this.$element.toggleClass( 've-ui-mediaSizeWidget-input-hasError',
!this.isCurrentDimensionsValid() );
this.preventChangeRecursion = false;
};
-/**
- * Get the height and width values of the maximum allowed dimensions, if set.
- *
- * @returns {Object} Maximum dimensions
- * @returns {number} [return.width] Maximum width, if set
- * @returns {number} [return.height] Maximum height, if set
- */
-ve.ui.MediaSizeWidget.prototype.getMaxDimensions = function () {
- return this.originalDimensions;
-};
-
-/**
- * Set maximum width and/or height.
- * @param {Object} dimensions Maximum dimensions
- * @param {number} [dimensions.width] Maximum width
- * @param {number} [dimensions.height] Maximum height
- */
-ve.ui.MediaSizeWidget.prototype.setMaxDimensions = function ( dimensions ) {
- this.maxDimensions = ve.copy( dimensions );
-};
-
-/**
- * Get the original dimensions of the image, if set.
- * @returns {Object} Original dimensions
- * @returns {number} [return.width] Original width, if set
- * @returns {number} [return.height] Original height, if set
- */
-ve.ui.MediaSizeWidget.prototype.getOriginalDimensions = function () {
- return this.originalDimensions;
-};
-
-/**
- * Set the original dimensions and cache the aspect ratio.
- * @param {Object} dimensions Original dimensions
- * @param {number} dimensions.width Original width
- * @param {number} dimensions.height Original height
- */
+/** */
ve.ui.MediaSizeWidget.prototype.setOriginalDimensions = function ( dimensions
) {
- this.originalDimensions = ve.copy( dimensions );
- // Cache the aspect ratio
- this.aspectRatio = this.originalDimensions.width /
this.originalDimensions.height;
+ // Parent method
+ ve.Scalable.prototype.setOriginalDimensions.call( this, dimensions );
// Enable the 'original dimensions' button
this.originalDimensionsButton.setDisabled( false );
-};
-
-/**
- * Explicitly set the aspect ratio, overriding what setOriginalDimensions()
computed.
- * @param {number} ratio Aspect ratio (width/height)
- */
-ve.ui.MediaSizeWidget.prototype.setAspectRatio = function ( ratio ) {
- this.aspectRatio = ratio;
-};
-
-/**
- * Retrieve the aspect ratio. This is only known if set through
setAspectRatio() or
- * computed by setOriginalDimensions().
- *
- * @returns {number|null} Aspect ratio (width/height)
- */
-ve.ui.MediaSizeWidget.prototype.getAspectRatio = function () {
- return this.aspectRatio;
-};
-
-/**
- * Checks whether the input values are valid. If the inputs are
- * not valid, an error class will be added to the inputs.
- */
-ve.ui.MediaSizeWidget.prototype.validateDimensions = function () {
-
- // Check for an error in the values
- if (
- !$.isNumeric( this.width ) ||
- !$.isNumeric( this.height ) ||
- Number( this.width ) <= 0 ||
- Number( this.height ) <= 0 ||
- // Check if the size exceeds max dimensions,
- // but only if the maxDimensions are set
- // TODO use a separate error message for this case,
- // and put the max dimensions in the error message
- (
- this.maxDimensions &&
- $.isNumeric( this.maxDimensions.width ) &&
- Number( this.width ) > this.maxDimensions.width
- ) || (
- this.maxDimensions &&
- $.isNumeric( this.maxDimensions.height ) &&
- Number( this.height ) > this.maxDimensions.height
- )
- ) {
- this.valid = false;
- // Show error message
- this.errorLabel.$element.show();
- } else {
- this.valid = true;
- // Hide the error message
- this.errorLabel.$element.hide();
- }
-
- // Add or remove the error class
- this.$element.toggleClass( 've-ui-mediaSizeWidget-input-hasError',
!this.valid );
};
/**
@@ -310,15 +180,7 @@
*/
ve.ui.MediaSizeWidget.prototype.onWidthChange = function () {
var val = this.widthInput.getValue();
- if ( $.isNumeric( val ) ) {
- // Calculate and update the corresponding value
- this.setDimensions( { 'width': val } );
- } else {
- this.width = val;
- // We didn't perform an actual change, but we should still
validate
- // the input values
- this.validateDimensions();
- }
+ this.setCurrentDimensions( { 'width': $.isNumeric( val ) ? Number( val
) : val } );
};
/**
@@ -326,15 +188,7 @@
*/
ve.ui.MediaSizeWidget.prototype.onHeightChange = function () {
var val = this.heightInput.getValue();
- if ( $.isNumeric( val ) ) {
- // Calculate and update the corresponding value
- this.setDimensions( { 'height': val } );
- } else {
- this.height = val;
- // We didn't perform an actual change, but we should still
validate
- // the input values
- this.validateDimensions();
- }
+ this.setCurrentDimensions( { 'height': $.isNumeric( val ) ? Number( val
) : val } );
};
/**
@@ -343,27 +197,5 @@
* @param {jQuery.Event} e Click event
*/
ve.ui.MediaSizeWidget.prototype.onButtonOriginalDimensionsClick = function () {
- this.setDimensions( this.originalDimensions );
-};
-
-/**
- * Checks whether there is an error with the widget
- * @returns {boolean} Values are valid
- */
-ve.ui.MediaSizeWidget.prototype.isValid = function () {
- return this.valid;
-};
-
-/**
- * Clear all values.
- * This is useful to update the widget values between different
- * images that have other dimensions or restrictions while the
- * widget is already instantiated.
- */
-ve.ui.MediaSizeWidget.prototype.clear = function () {
- this.aspectRatio = null;
- this.originalDimensions = {};
- this.maxDimensions = {};
- this.width = '';
- this.height = '';
+ this.setCurrentDimensions( this.getOriginalDimensions() );
};
diff --git a/modules/ve/ve.Scalable.js b/modules/ve/ve.Scalable.js
new file mode 100644
index 0000000..55dfd6a
--- /dev/null
+++ b/modules/ve/ve.Scalable.js
@@ -0,0 +1,270 @@
+/*!
+ * VisualEditor Scalable class.
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * Scalable object.
+ *
+ * @class
+ * @abstract
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @param {boolean} [config.fixedRatio=true] Object has a fixed aspect ratio
+ * @param {Object} [config.currentDimensions] Current dimensions, width &
height
+ * @param {Object} [config.originalDimensions] Original dimensions, width &
height
+ * @param {Object} [config.maxDimensions] Maximum dimensions, width & height
+ * @param {Object} [config.minDimensions] Minimum dimensions, width & height
+ */
+ve.Scalable = function VeScalable( config ) {
+ config = ve.extendObject( {
+ 'fixedRatio': true
+ }, config );
+
+ // Properties
+ this.fixedRatio = config.fixedRatio;
+ this.currentDimensions = null;
+ this.originalDimensions = null;
+ this.maxDimensions = null;
+ this.minDimensions = null;
+ this.ratio = null;
+ this.valid = null;
+
+ if ( config.currentDimensions ) {
+ this.setCurrentDimensions( config.currentDimensions );
+ }
+ if ( config.originalDimensions ) {
+ this.setOriginalDimensions( config.originalDimensions );
+ }
+ if ( config.maxDimensions ) {
+ this.setMaxDimensions( config.maxDimensions );
+ }
+ if ( config.minDimensions ) {
+ this.setMinDimensions( config.minDimensions );
+ }
+};
+
+/**
+ * Set the fixed aspect ratio from specified dimensions.
+ *
+ * @param {Object} dimensions Dimensions object with width & height
+ */
+ve.Scalable.prototype.setRatioFromDimensions = function ( dimensions ) {
+ if ( dimensions.width && dimensions.height ) {
+ this.ratio = dimensions.width / dimensions.height;
+ }
+};
+
+/**
+ * Set the orignal dimensions of an image
+ *
+ * @param {Object} dimensions Dimensions object with width & height
+ */
+ve.Scalable.prototype.setCurrentDimensions = function ( dimensions ) {
+ this.currentDimensions = ve.copy( dimensions );
+ // Only use current dimensions for ratio if it isn't set
+ if ( this.fixedRatio && !this.ratio ) {
+ this.setRatioFromDimensions( this.getCurrentDimensions() );
+ }
+ this.valid = null;
+};
+
+/**
+ * Set the orignal dimensions of an image
+ *
+ * @param {Object} dimensions Dimensions object with width & height
+ */
+ve.Scalable.prototype.setOriginalDimensions = function ( dimensions ) {
+ this.originalDimensions = ve.copy( dimensions );
+ // Always overwrite ratio
+ if ( this.fixedRatio ) {
+ this.setRatioFromDimensions( this.getOriginalDimensions() );
+ }
+};
+
+/**
+ * Set the maximum dimensions of an image
+ *
+ * @param {Object} dimensions Dimensions object with width & height
+ */
+ve.Scalable.prototype.setMaxDimensions = function ( dimensions ) {
+ this.maxDimensions = ve.copy( dimensions );
+ this.valid = null;
+};
+
+/**
+ * Set the minimum dimensions of an image
+ *
+ * @param {Object} dimensions Dimensions object with width & height
+ */
+ve.Scalable.prototype.setMinDimensions = function ( dimensions ) {
+ this.minDimensions = ve.copy( dimensions );
+ this.valid = null;
+};
+
+/**
+ * Get the orignal dimensions of an image
+ *
+ * @returns {Object} Dimensions object with width & height
+ */
+ve.Scalable.prototype.getCurrentDimensions = function () {
+ return this.currentDimensions;
+};
+
+/**
+ * Get the orignal dimensions of an image
+ *
+ * @returns {Object} Dimensions object with width & height
+ */
+ve.Scalable.prototype.getOriginalDimensions = function () {
+ return this.originalDimensions;
+};
+
+/**
+ * Get the maximum dimensions of an image
+ *
+ * @returns {Object} Dimensions object with width & height
+ */
+ve.Scalable.prototype.getMaxDimensions = function () {
+ return this.maxDimensions;
+};
+
+/**
+ * Get the minimum dimensions of an image
+ *
+ * @returns {Object} Dimensions object with width & height
+ */
+ve.Scalable.prototype.getMinDimensions = function () {
+ return this.minDimensions;
+};
+
+/**
+ * Get the ratio
+ *
+ * @returns {number} Ratio
+ */
+ve.Scalable.prototype.getRatio = function () {
+ return this.ratio;
+};
+
+/**
+ * Check if the object has a fixed ratio
+ *
+ * @returns {boolean} The object has a fixed ratio
+ */
+ve.Scalable.prototype.isFixedRatio = function () {
+ return this.fixedRatio;
+};
+
+/**
+ * Get the current scale of the object
+ *
+ * @returns {number|null} A scale (1=100%), or null if not applicable
+ */
+ve.Scalable.prototype.getCurrentScale = function () {
+ if ( !this.isFixedRatio() || !this.getCurrentDimensions() ||
!this.getOriginalDimensions() ) {
+ return null;
+ }
+ return this.getCurrentDimensions().width /
this.getOriginalDimensions().width;
+};
+
+/**
+ * Get a set of dimensions bounded by current restrictions, from specified
dimensions
+ *
+ * @param {Object} dimensions Dimensions object with width & height
+ * @param {number} [grid] Option grid size to snap to
+ * @returns {Object} Dimensions object with width & height
+ */
+ve.Scalable.prototype.getBoundedDimensions = function ( dimensions, grid ) {
+ var ratio, snap, snapMin, snapMax,
+ maxDimensions = this.getMaxDimensions(),
+ minDimensions = this.getMinDimensions();
+
+ // Don't modify the input
+ dimensions = ve.copy( dimensions );
+
+ // Bound to min/max
+ if ( minDimensions ) {
+ dimensions.width = Math.max( dimensions.width,
this.minDimensions.width );
+ dimensions.height = Math.max( dimensions.height,
this.minDimensions.height );
+ }
+ if ( maxDimensions ) {
+ dimensions.width = Math.min( dimensions.width,
this.maxDimensions.width );
+ dimensions.height = Math.min( dimensions.height,
this.maxDimensions.height );
+ }
+
+ // Bound to ratio
+ if ( this.isFixedRatio() ) {
+ ratio = dimensions.width / dimensions.height;
+ if ( ratio < this.getRatio() ) {
+ dimensions.height = dimensions.width / this.getRatio();
+ } else {
+ dimensions.width = dimensions.height * this.getRatio();
+ }
+ }
+
+ // Snap to grid
+ if ( grid ) {
+ snapMin = minDimensions ? Math.ceil( minDimensions.width / grid
) : -Infinity;
+ snapMax = maxDimensions ? Math.floor( maxDimensions.width /
grid ) : Infinity;
+ snap = Math.round( dimensions.width / grid );
+ dimensions.width = Math.max( Math.min( snap, snapMax ), snapMin
) * grid;
+ if ( this.isFixedRatio() ) {
+ // If the ratio is fixed we can't snap both to the
grid, so just snap the width
+ dimensions.height = dimensions.width / this.getRatio();
+ } else {
+ snapMin = minDimensions ? Math.ceil(
minDimensions.height / grid ) : -Infinity;
+ snapMax = maxDimensions ? Math.floor(
maxDimensions.height / grid ) : Infinity;
+ snap = Math.round( dimensions.height / grid );
+ dimensions.height = Math.max( Math.min( snap, snapMax
), snapMin ) * grid;
+ }
+ }
+
+ return dimensions;
+};
+
+/**
+ * Checks whether the current dimensions are numeric and within range
+ *
+ * @returns {boolean} Current dimensions are valid
+ */
+ve.Scalable.prototype.isCurrentDimensionsValid = function () {
+ if ( this.valid === null ) {
+ var dimensions = this.getCurrentDimensions(),
+ maxDimensions = this.getMaxDimensions(),
+ minDimensions = this.getMinDimensions();
+
+ this.valid = (
+ $.isNumeric( dimensions.width ) &&
+ $.isNumeric( dimensions.height ) &&
+ (
+ !this.minDimensions || (
+ dimensions.width >= minDimensions.width
&&
+ dimensions.height >=
minDimensions.height
+ )
+ ) &&
+ (
+ !this.maxDimensions || (
+ dimensions.width <= maxDimensions.width
&&
+ dimensions.height <=
maxDimensions.height
+ )
+ )
+ );
+ }
+ return this.valid;
+};
+
+/**
+ * Clear all values.
+ */
+ve.Scalable.prototype.clear = function () {
+ this.currentDimensions = null;
+ this.originalDimensions = null;
+ this.maxDimensions = null;
+ this.minDimensions = null;
+ this.ratio = null;
+ this.valid = null;
+};
--
To view, visit https://gerrit.wikimedia.org/r/109594
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I5b4f0f91b8534725130978875a5b4cabb6b98bd2
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits