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

Reply via email to