%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ce/ |
Current File : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ce/ve.ce.ResizableNode.js |
/*! * VisualEditor ContentEditable ResizableNode class. * * @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org */ /** * ContentEditable resizable node. * * @class * @abstract * * @constructor * @param {jQuery} [$resizable=this.$element] Resizable DOM element * @param {Object} [config] Configuration options * @cfg {number|null} [snapToGrid=10] Snap to a grid of size X when the shift key is held. Null disables. * @cfg {boolean} [outline=false] Resize using an outline of the element only, don't live preview. * @cfg {boolean} [showSizeLabel=true] Show a label with the current dimensions while resizing * @cfg {boolean} [showScaleLabel=true] Show a label with the current scale while resizing */ ve.ce.ResizableNode = function VeCeResizableNode( $resizable, config ) { config = config || {}; // Properties this.$resizable = $resizable || this.$element; this.resizing = false; this.enabled = !!this.$resizable.length; this.$resizeHandles = $( '<div>' ); this.snapToGrid = config.snapToGrid !== undefined ? config.snapToGrid : 10; this.outline = !!config.outline; this.showSizeLabel = config.showSizeLabel !== false; this.showScaleLabel = config.showScaleLabel !== false; // Only gets enabled when the original dimensions are provided this.canShowScaleLabel = false; if ( this.showSizeLabel || this.showScaleLabel ) { this.$sizeText = $( '<span>' ).addClass( 've-ce-resizableNode-sizeText' ); this.$sizeLabel = $( '<div>' ).addClass( 've-ce-resizableNode-sizeLabel' ).append( this.$sizeText ); } this.resizableOffset = null; this.resizableSurface = null; if ( !this.enabled ) { return; } // Events this.connect( this, { focus: 'onResizableFocus', blur: 'onResizableBlur', setup: 'onResizableSetup', teardown: 'onResizableTeardown', resizing: 'onResizableResizing', resizeEnd: 'onResizableFocus', rerender: 'onResizableFocus', align: 'onResizableAlign' } ); this.model.connect( this, { attributeChange: 'onResizableAttributeChange' } ); // Initialization this.$resizeHandles .addClass( 've-ce-resizableNode-handles' ) .append( $( '<div>' ) .addClass( 've-ce-resizableNode-nwHandle' ) .data( 'handle', 'nw' ) ) .append( $( '<div>' ) .addClass( 've-ce-resizableNode-neHandle' ) .data( 'handle', 'ne' ) ) .append( $( '<div>' ) .addClass( 've-ce-resizableNode-seHandle' ) .data( 'handle', 'se' ) ) .append( $( '<div>' ) .addClass( 've-ce-resizableNode-swHandle' ) .data( 'handle', 'sw' ) ); }; /* Inheritance */ OO.initClass( ve.ce.ResizableNode ); /* Events */ /** * @event resizeStart */ /** * @event resizing * @param {Object} dimensions Dimension object containing width & height */ /** * @event resizeEnd */ /* Methods */ /** * Get and cache the relative offset of the $resizable node * * @return {Object} Position coordinates, containing top & left */ ve.ce.ResizableNode.prototype.getResizableOffset = function () { if ( !this.resizableOffset ) { this.resizableOffset = OO.ui.Element.static.getRelativePosition( this.$resizable, this.resizableSurface.getSurface().$element ); } return this.resizableOffset; }; /** */ ve.ce.ResizableNode.prototype.setOriginalDimensions = function ( dimensions ) { var scalable; if ( !this.enabled ) { return; } scalable = this.model.getScalable(); scalable.setOriginalDimensions( dimensions ); // If dimensions are valid and the scale label is desired, enable it this.canShowScaleLabel = this.showScaleLabel && scalable.getOriginalDimensions().width && scalable.getOriginalDimensions().height; }; /** * Hide the size label */ ve.ce.ResizableNode.prototype.hideSizeLabel = function () { var node = this; if ( !this.enabled ) { return; } // 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' ); } ); // Actually hide the size label after it's done animating setTimeout( function () { node.$sizeLabel.addClass( 'oo-ui-element-hidden' ); }, 200 ); }; /** * Update the contents and position of the size label */ ve.ce.ResizableNode.prototype.updateSizeLabel = function () { var top, height, scalable, dimensions, offset, minWidth; if ( !this.enabled ) { return; } if ( !this.showSizeLabel && !this.canShowScaleLabel ) { return; } scalable = this.model.getScalable(); dimensions = scalable.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 .removeClass( 'oo-ui-element-hidden' ) .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 * scalable.getCurrentScale() ) + '%' ) ); } this.$sizeText.toggleClass( 've-ce-resizableNode-sizeText-warning', scalable.isTooSmall() || scalable.isTooLarge() ); }; /** * Show specific resize handles * * @param {string[]} [handles] List of handles to show: 'nw', 'ne', 'sw', 'se'. Show all if undefined. */ ve.ce.ResizableNode.prototype.showHandles = function ( handles ) { var i, len, add = [], remove = [], allDirections = [ 'nw', 'ne', 'sw', 'se' ]; if ( !this.enabled ) { return; } for ( i = 0, len = allDirections.length; i < len; i++ ) { if ( handles === undefined || handles.indexOf( allDirections[ i ] ) !== -1 ) { remove.push( 've-ce-resizableNode-hide-' + allDirections[ i ] ); } else { add.push( 've-ce-resizableNode-hide-' + allDirections[ i ] ); } } this.$resizeHandles .addClass( add.join( ' ' ) ) .removeClass( remove.join( ' ' ) ); }; /** * Handle node focus. * * @method */ ve.ce.ResizableNode.prototype.onResizableFocus = function () { this.$resizeHandles.appendTo( this.resizableSurface.getSurface().$controls ); if ( this.$sizeLabel ) { this.$sizeLabel.appendTo( this.resizableSurface.getSurface().$controls ); } // Call getScalable to pre-fetch the extended data this.model.getScalable(); this.setResizableHandlesSizeAndPosition(); this.$resizeHandles .find( '.ve-ce-resizableNode-neHandle' ) .css( { marginRight: -this.$resizable.width() } ) .end() .find( '.ve-ce-resizableNode-swHandle' ) .css( { marginBottom: -this.$resizable.height() } ) .end() .find( '.ve-ce-resizableNode-seHandle' ) .css( { marginRight: -this.$resizable.width(), marginBottom: -this.$resizable.height() } ); this.$resizeHandles.children() .off( '.ve-ce-resizableNode' ) .on( 'mousedown.ve-ce-resizableNode', this.onResizeHandlesCornerMouseDown.bind( this ) ); this.resizableSurface.connect( this, { position: 'setResizableHandlesSizeAndPosition' } ); }; /** * Handle node blur. * * @method */ ve.ce.ResizableNode.prototype.onResizableBlur = function () { // Node may have already been torn down, e.g. after delete if ( !this.isResizableSetup || !this.root ) { return; } this.$resizeHandles.detach(); if ( this.$sizeLabel ) { this.$sizeLabel.detach(); } this.resizableSurface.disconnect( this, { position: 'setResizableHandlesSizeAndPosition' } ); }; /** * Respond to AlignableNodes changing their alignment by hiding useless resize handles. * * @param {string} align Alignment */ ve.ce.ResizableNode.prototype.onResizableAlign = function ( align ) { switch ( align ) { case 'right': this.showHandles( [ 'sw' ] ); break; case 'left': this.showHandles( [ 'se' ] ); break; case 'center': this.showHandles( [ 'sw', 'se' ] ); break; default: this.showHandles(); break; } }; /** * Handle setup event. * * @method */ ve.ce.ResizableNode.prototype.onResizableSetup = function () { // Exit if already setup or not attached if ( this.isResizableSetup || !this.root ) { return; } this.resizableSurface = this.root.getSurface(); this.isResizableSetup = true; }; /** * Handle teardown event. * * @method */ ve.ce.ResizableNode.prototype.onResizableTeardown = function () { // Exit if not setup or not attached if ( !this.isResizableSetup || !this.root ) { return; } this.onResizableBlur(); this.resizableSurface = null; this.isResizableSetup = false; }; /** * Handle resizing event. * * @method * @param {Object} dimensions Dimension object containing width & height */ ve.ce.ResizableNode.prototype.onResizableResizing = function ( dimensions ) { // Clear cached resizable offset position as it may have changed this.resizableOffset = null; this.model.getScalable().setCurrentDimensions( dimensions ); if ( !this.outline ) { this.$resizable.css( this.model.getScalable().getCurrentDimensions() ); this.setResizableHandlesPosition(); } this.updateSizeLabel(); }; /** * Handle attribute change events from the model. * * @method * @param {string} key Attribute key * @param {string} from Old value * @param {string} to New value */ ve.ce.ResizableNode.prototype.onResizableAttributeChange = function () { this.$resizable.css( this.model.getCurrentDimensions() ); }; /** * Handle bounding box handle mousedown. * * @method * @param {jQuery.Event} e Click event * @fires resizeStart */ ve.ce.ResizableNode.prototype.onResizeHandlesCornerMouseDown = function ( e ) { // 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().toggle( false ); // Set bounding box width and undo the handle margins this.$resizeHandles .addClass( 've-ce-resizableNode-handles-resizing' ) .css( { width: this.$resizable.width(), height: this.$resizable.height() } ); 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(), handle: $( e.target ).data( 'handle' ) }; // Bind resize events this.resizing = true; this.root.getSurface().resizing = true; this.model.getScalable().setCurrentDimensions( { width: this.resizeInfo.width, height: this.resizeInfo.height } ); this.updateSizeLabel(); $( this.getElementDocument() ).on( { 'mousemove.ve-ce-resizableNode': this.onDocumentMouseMove.bind( this ), 'mouseup.ve-ce-resizableNode': this.onDocumentMouseUp.bind( this ) } ); this.emit( 'resizeStart' ); return false; }; /** * Set the proper size and position for resize handles * * @method */ ve.ce.ResizableNode.prototype.setResizableHandlesSizeAndPosition = function () { var width, height; if ( !this.enabled ) { return; } width = this.$resizable.width(); height = this.$resizable.height(); // Clear cached resizable offset position as it may have changed this.resizableOffset = null; this.setResizableHandlesPosition(); this.$resizeHandles .css( { width: 0, height: 0 } ) .find( '.ve-ce-resizableNode-neHandle' ) .css( { marginRight: -width } ) .end() .find( '.ve-ce-resizableNode-swHandle' ) .css( { marginBottom: -height } ) .end() .find( '.ve-ce-resizableNode-seHandle' ) .css( { marginRight: -width, marginBottom: -height } ); }; /** * Set the proper position for resize handles * * @method */ ve.ce.ResizableNode.prototype.setResizableHandlesPosition = function () { var offset; if ( !this.enabled ) { return; } offset = this.getResizableOffset(); this.$resizeHandles.css( { top: offset.top, left: offset.left } ); }; /** * Handle body mousemove. * * @method * @param {jQuery.Event} e Click event * @fires resizing */ ve.ce.ResizableNode.prototype.onDocumentMouseMove = function ( e ) { var 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 'se': diff.x = e.screenX - this.resizeInfo.mouseX; diff.y = e.screenY - this.resizeInfo.mouseY; break; case 'nw': diff.x = this.resizeInfo.mouseX - e.screenX; diff.y = this.resizeInfo.mouseY - e.screenY; break; case 'ne': diff.x = e.screenX - this.resizeInfo.mouseX; diff.y = this.resizeInfo.mouseY - e.screenY; break; case 'sw': diff.x = this.resizeInfo.mouseX - e.screenX; diff.y = e.screenY - this.resizeInfo.mouseY; break; } dimensions = this.model.getScalable().getBoundedDimensions( { width: this.resizeInfo.width + diff.x, height: this.resizeInfo.height + diff.y }, e.shiftKey && this.snapToGrid ); // Fix the position switch ( this.resizeInfo.handle ) { case 'ne': dimensions.top = this.resizeInfo.top + ( this.resizeInfo.height - dimensions.height ); break; case 'sw': dimensions.left = this.resizeInfo.left + ( this.resizeInfo.width - dimensions.width ); break; case 'nw': 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 ); this.emit( 'resizing', { width: dimensions.width, height: dimensions.height } ); } }; /** * Handle body mouseup. * * @method * @fires resizeEnd */ ve.ce.ResizableNode.prototype.onDocumentMouseUp = function () { var attrChanges, width = this.$resizeHandles.outerWidth(), height = this.$resizeHandles.outerHeight(); this.$resizeHandles.removeClass( 've-ce-resizableNode-handles-resizing' ); $( this.getElementDocument() ).off( '.ve-ce-resizableNode' ); this.resizing = false; this.root.getSurface().resizing = false; this.hideSizeLabel(); // Apply changes to the model attrChanges = this.getAttributeChanges( width, height ); if ( !ve.isEmptyObject( attrChanges ) ) { this.resizableSurface.getModel().getFragment().changeAttributes( attrChanges ); } // Update the context menu. This usually happens with the redraw, but not if the // user doesn't perform a drag this.root.getSurface().getSurface().getContext().updateDimensions(); this.emit( 'resizeEnd' ); }; /** * Generate an object of attributes changes from the new width and height. * * @param {number} width New image width * @param {number} height New image height * @return {Object} Attribute changes */ ve.ce.ResizableNode.prototype.getAttributeChanges = function ( width, height ) { var attrChanges = {}, currentDimensions = this.model.getCurrentDimensions(); if ( currentDimensions.width !== width ) { attrChanges.width = width; } if ( currentDimensions.height !== height ) { attrChanges.height = height; } return attrChanges; };