mediawiki-extensions-Visual.../modules/ve-mw/dm/models/ve.dm.MWImageModel.js
Moriel Schottlender 37b3c07b26 Image model and the media edit dialog
Creating an MWImageModel that handles all image edit operations.
If the new attributes mean a change of image types from inline
to block or vice versa, the model will handle the creation and
insertion of the new node.

Change-Id: Ibe71bc8bd74e4ba5a024ac722432ccf0b8f65e71
2014-05-19 21:11:06 -04:00

750 lines
19 KiB
JavaScript

/*!
* VisualEditor DataModel MWImageModel class.
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/*global mw */
/**
* MediaWiki image model.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
*/
ve.dm.MWImageModel = function VeDmMWImageModel() {
// Mixin constructors
OO.EventEmitter.call( this );
// Properties
this.mediaNode = null;
this.attributesCache = null;
// Image properties
this.captionDoc = null;
this.caption = null;
this.altText = null;
this.type = null;
this.alignment = null;
this.defaultAlignment = false;
this.aligned = false;
this.scalable = null;
this.sizeType = null;
this.border = false;
this.borderable = false;
this.dir = 'ltr';
this.defaultDimensions = null;
// Get wiki default thumbnail size
this.defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' )
.defaultUserOptions.defaultthumbsize;
};
/* Inheritance */
OO.mixinClass( ve.dm.MWImageModel, OO.EventEmitter );
/* Events */
/**
* Change of image alignment or of having alignment at all
* @event alignmentChange
* @param {string} Alignment 'left', 'right', 'center' or 'none'
*/
/**
* Change in size type between default and custom
* @event sizeDefaultChange
* @param {boolean} Image is default size
*/
/**
* Change in the image type
* @event typeChange
* @param {string} Image type 'thumb', 'frame', 'frameless' or 'none'
*/
/* Static Properties */
ve.dm.MWImageModel.static.infoCache = {};
/* Static Methods */
/**
* Load from image data with scalable information.
*
* @param {ve.dm.MWImageNode} node Image node
* @return {ve.dm.MWImageModel} Image model
*/
ve.dm.MWImageModel.static.newFromImageNode = function ( node ) {
var doc = node.getDocument(),
captionNode,
attrs = node.getAttributes(),
imgModel = new ve.dm.MWImageModel();
imgModel.setMediaNode( node );
// Set scalable
imgModel.setScalable( node.getScalable() );
// Cache the attributes so we can create a new image without
// losing any existing information
imgModel.cacheOriginalImageAttributes( attrs );
// Collect all the information
imgModel.toggleBorder( !!attrs.borderImage );
imgModel.setAltText( attrs.alt );
imgModel.setDir( doc.getDir() );
imgModel.setMediaType( attrs.mediatype || 'BITMAP' );
imgModel.setType( attrs.type );
imgModel.toggleDefaultAlignment(
attrs.align === undefined ||
attrs.align === 'default'
);
// Fix cases where alignment is undefined
// Inline images have no 'align' (they have 'valign' instead)
// But we do want an alignment case for these in case they
// are transformed to block images
attrs.align = attrs.align !== undefined ? attrs.align : 'default';
imgModel.setAlignment( attrs.align );
imgModel.toggleAligned( attrs.align !== 'none' );
imgModel.setVerticalAlignment( attrs.valign );
// Default size
imgModel.toggleDefaultSize( !!attrs.defaultSize );
// TODO: When scale/upright is available, set the size
// type accordingly
imgModel.setSizeType(
imgModel.isDefaultSize() ?
'default' :
'custom'
);
// If this is a block image, get the caption
if ( imgModel.getImageNodeType() === 'mwBlockImage' ) {
captionNode = node.getCaptionNode();
if ( captionNode && captionNode.getLength() > 0 ) {
imgModel.setCaptionDocument( doc.cloneFromRange( captionNode.getRange() ) );
}
}
return imgModel;
};
/* Methods */
/**
* Get the current image node type according to the attributes.
*
* @return {string} Node type 'mwInlineImage' or 'mwBlockImage'
*/
ve.dm.MWImageModel.prototype.getImageNodeType = function () {
if (
( this.getType() === 'frame' || this.getType() === 'none' ) &&
( !this.isAligned() || this.isDefaultAligned() )
) {
return 'mwInlineImage';
} else {
return 'mwBlockImage';
}
};
/**
* Update an existing image node by changing its attributes
*
* @param {ve.dm.Surface} surfaceModel Surface model of main document
*/
ve.dm.MWImageModel.prototype.updateImageNode = function ( surfaceModel ) {
var captionRange,
doc = surfaceModel.getDocument(),
captionNode = this.getMediaNode().getCaptionNode();
// Update the caption
if ( !captionNode ) {
// There was no caption before, so insert one now
surfaceModel.getFragment()
.adjustRange( 1 )
.collapseRangeToStart()
.insertContent( [ { 'type': 'mwImageCaption' }, { 'type': '/mwImageCaption' } ] );
// Update the caption node
captionNode = this.getMediaNode().getCaptionNode();
}
captionRange = captionNode.getRange();
// Remove contents of old caption
surfaceModel.change(
ve.dm.Transaction.newFromRemoval(
doc,
captionRange,
true
)
);
// Add contents of new caption
surfaceModel.change(
ve.dm.Transaction.newFromDocumentInsertion(
doc,
captionRange.start,
this.getCaptionDocument()
)
);
// Update attributes
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChanges(
doc,
this.mediaNode.getOffset(),
this.getUpdatedAttributes()
)
);
};
/**
* Insert image into a surface.
*
* Image is inserted at the current fragment position.
*
* @param {ve.dm.SurfaceFragment} fragment Fragment of the node
*/
ve.dm.MWImageModel.prototype.insertImageNode = function ( fragment ) {
var editAttributes, coveredNodes, newNodeRange, newFragment, newNode, i,
contentToInsert = [],
nodeType = this.getImageNodeType(),
originalAttrs = ve.copy( this.getOriginalImageAttributes() ),
surfaceModel = fragment.getSurface();
editAttributes = $.extend( originalAttrs, this.getUpdatedAttributes() );
// Remove old classes
delete editAttributes.originalClasses;
delete editAttributes.unrecognizedClasses;
contentToInsert = [
{
// TODO: Add support for MWInlineImage type
'type': nodeType,
'attributes': editAttributes
}
];
if ( nodeType === 'mwBlockImage' ) {
contentToInsert.push( { 'type': 'mwImageCaption' } );
contentToInsert.push( { 'type': '/mwImageCaption' } );
}
contentToInsert.push( { 'type': '/' + nodeType } );
// Insert the new image
coveredNodes = fragment
.collapseRangeToEnd()
.insertContent( contentToInsert )
.getCoveredNodes();
// Get the new image node
for ( i = 0; i < coveredNodes.length; i++ ) {
if (
coveredNodes[i].node.type === 'mwBlockImage' ||
coveredNodes[i].node.type === 'mwInlineImage'
) {
newNodeRange = coveredNodes[i].nodeOuterRange;
break;
}
}
// Select the new node (without extras)
newFragment = surfaceModel.getFragment( newNodeRange );
newFragment.select();
// Check if there should be a caption
if ( nodeType === 'mwBlockImage' ) {
newNode = newFragment.getSelectedNode();
if ( this.getCaptionDocument().data.getLength() > 4 ) {
// Add contents of new caption
surfaceModel.change(
ve.dm.Transaction.newFromDocumentInsertion(
surfaceModel.getDocument(),
newNode.getCaptionNode().getRange().start,
this.getCaptionDocument()
)
);
}
}
};
/**
* Return all updated attributes that belong to the node.
* @return {Object} Updated attributes
*/
ve.dm.MWImageModel.prototype.getUpdatedAttributes = function () {
var attrs,
origAttrs = this.getOriginalImageAttributes(),
currentDimensions = this.getCurrentDimensions();
attrs = {
'type': this.getType(),
'width': currentDimensions.width,
'height': currentDimensions.height,
'defaultSize': this.isDefaultSize(),
'borderImage': this.hasBorder()
};
if ( origAttrs.alt !== undefined || this.getAltText() !== '' ) {
attrs.alt = this.getAltText();
}
if (
this.isDefaultAligned() ||
this.getAlignment() === this.getDefaultDir()
) {
attrs.align = 'default';
} else if ( !this.isAligned() ) {
attrs.align = 'none';
} else {
attrs.align = this.getAlignment();
}
return attrs;
};
/**
* Deal with default change on the scalable object
*
* @param {boolean} isDefault
*/
ve.dm.MWImageModel.prototype.onScalableDefaultSizeChange = function ( isDefault ) {
this.toggleDefaultSize( isDefault );
};
/**
* Set the media node
* @param {ve.dm.MWImageNode} node Node model
*/
ve.dm.MWImageModel.prototype.setMediaNode = function ( node ) {
this.mediaNode = node;
};
/**
* Retrieve the media node
* @return {ve.dm.MWImageNode} node Node model
*/
ve.dm.MWImageModel.prototype.getMediaNode = function () {
return this.mediaNode;
};
/**
* Check whether the image is set to default size
* @return {boolean} Default size flag on or off
*/
ve.dm.MWImageModel.prototype.isDefaultSize = function () {
return this.scalable.isDefault();
};
/**
* Check whether the image has the border flag set
* @return {boolean} Border flag on or off
*/
ve.dm.MWImageModel.prototype.hasBorder = function () {
return this.border;
};
/**
* Check whether the image has floating alignment set
* @return {boolean} hasAlignment flag on or off
*/
ve.dm.MWImageModel.prototype.isAligned = function () {
return this.aligned;
};
/**
* Check whether the image is set to default alignment
* @return {boolean} defaultAlignment flag on or off
*/
ve.dm.MWImageModel.prototype.isDefaultAligned = function () {
return this.defaultAlignment;
};
/**
* Check whether the image can have a border set on it
* @return {boolean} Border possible or not
*/
ve.dm.MWImageModel.prototype.isBorderable = function () {
return this.borderable;
};
/**
* Get the image alternate text
* @return {string} Alternate text
*/
ve.dm.MWImageModel.prototype.getAltText = function () {
return this.altText;
};
/**
* Get image wikitext type; 'thumb', 'frame', 'frameless' or 'none/inline'
* @return {string} Image type
*/
ve.dm.MWImageModel.prototype.getType = function () {
return this.type;
};
/**
* Get the image size type of the image
*/
ve.dm.MWImageModel.prototype.getSizeType = function () {
return this.sizeType;
};
/**
* Get symbolic name of media type.
*
* Example values: "BITMAP" for JPEG or PNG images; "DRAWING" for SVG graphics
*
* @return {string|null} Symbolic media type name, or null if empty
*/
ve.dm.MWImageModel.prototype.getMediaType = function () {
return this.mediaType;
};
/**
* Get image alignment 'left', 'right', 'center', 'none' or 'default'
* @return {string} Image alignment
*/
ve.dm.MWImageModel.prototype.getAlignment = function () {
return this.alignment;
};
/**
* Get image vertical alignment
* 'middle', 'baseline', 'sub', 'super', 'top', 'text-top', 'bottom', 'text-bottom' or 'default'
* @return {string} Image alignment
*/
ve.dm.MWImageModel.prototype.getVerticalAlignment = function () {
return this.verticalAlignment;
};
/**
* Get the scalable object responsible for size manipulations
* for the given image
* @return {ve.dm.Scalable} Scalable object
*/
ve.dm.MWImageModel.prototype.getScalable = function () {
return this.scalable;
};
/**
* Get the image current dimensions
* @return {Object} Current dimensions width/height
* @return {number} dimensions.width The width of the image
* @return {number} dimensions.height The height of the image
*/
ve.dm.MWImageModel.prototype.getCurrentDimensions = function () {
return this.scalable.getCurrentDimensions();
};
/**
* Get image caption document.
*
* Auto-generates a blank document if no document exists.
*
* @return {ve.dm.Document} Caption document
*/
ve.dm.MWImageModel.prototype.getCaptionDocument = function () {
if ( !this.captionDoc ) {
this.captionDoc = new ve.dm.Document( [
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
{ 'type': '/paragraph' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
] );
}
return this.captionDoc;
};
/**
* Toggle the option of whether this image can or cannot have
* a border set on it.
*
* @param {boolean} [borderable] Set or unset borderable. If not
* specified, the current state is toggled.
*/
ve.dm.MWImageModel.prototype.toggleBorderable = function ( borderable ) {
borderable = borderable !== undefined ? !!borderable : !this.isBorderable();
this.borderable = borderable;
};
/**
* Toggle whether the image has an floating alignment set
*
* @param {boolean} [aligned] Image has alignment
* @fires alignmentChange
*/
ve.dm.MWImageModel.prototype.toggleAligned = function ( aligned ) {
var currentAlignment;
aligned = aligned !== undefined ? !!aligned : !this.isAligned();
this.aligned = !!aligned;
currentAlignment = !!aligned ? this.getAlignment() : 'none';
this.emit( 'alignmentChange',
!!aligned ?
this.getAlignment() : 'none'
);
};
/**
* Toggle the border flag of the image
*
* @param {boolean} [hasBorder] Border flag. Omit to toggle current value.
*/
ve.dm.MWImageModel.prototype.toggleBorder = function ( hasBorder ) {
hasBorder = hasBorder !== undefined ? !!hasBorder : !this.hasBorder();
this.border = !!hasBorder;
};
/**
* Toggle the default alignment flag of the image
*
* @param {Boolean} [isDefault] Default alignment flat. Omit to toggle current value.
* @fires alignmentChange
*/
ve.dm.MWImageModel.prototype.toggleDefaultAlignment = function ( isDefault ) {
isDefault = isDefault !== undefined ? !!isDefault : !this.isDefaultAligned();
this.defaultAlignment = !!isDefault;
this.emit( 'alignmentChange', this.alignment );
};
/**
* Toggle the default size flag of the image
* @param {boolean} [isDefault] Default size flag. Omit to toggle current value.
* @fires sizeDefaultChange
*/
ve.dm.MWImageModel.prototype.toggleDefaultSize = function ( isDefault ) {
isDefault = isDefault !== undefined ? !!isDefault : !this.isDefaultSize();
if ( this.isDefaultSize() !== isDefault ) {
this.scalable.toggleDefault( !!isDefault );
this.resetDefaultDimensions();
this.emit( 'sizeDefaultChange', !!isDefault );
}
};
/**
* Cache all image attributes
* @param {Object} attrs Image attributes
*/
ve.dm.MWImageModel.prototype.cacheOriginalImageAttributes = function ( attrs ) {
this.attributesCache = attrs;
};
/**
* Get the cache of all image attributes
* @return {Object} attrs Image attributes
*/
ve.dm.MWImageModel.prototype.getOriginalImageAttributes = function () {
return this.attributesCache;
};
/**
* Set the current dimensions of the image.
* Normalize in case only one dimension is available.
* @param {Object} dimensions Dimensions width and height
* @param {number} dimensions.width The width of the image
* @param {number} dimensions.height The height of the image
*/
ve.dm.MWImageModel.prototype.setCurrentDimensions = function ( dimensions ) {
var normalizedDimensions = this.scalable.getDimensionsFromValue( dimensions );
this.scalable.setCurrentDimensions( normalizedDimensions );
};
/**
* Set alternate text
* @param {string} text Alternate text
*/
ve.dm.MWImageModel.prototype.setAltText = function ( text ) {
this.altText = text;
};
/**
* Set image type
* @see #getType
*
* @param {string} type Image type
* @fires typeChange
*/
ve.dm.MWImageModel.prototype.setType = function ( type ) {
this.type = type;
if ( type === 'frame' || type === 'thumb' ) {
// Disable border option
this.toggleBorderable( false );
} else {
// Enable border option
this.toggleBorderable( true );
}
// Let the image node update scalable considerations
// for default and max dimensions as per the new type.
this.getMediaNode().updateType( type );
this.emit( 'typeChange', type );
};
/**
* Reset the default dimensions of the image based on its type
* and on whether we have the originalDimensions object from
* the API
*/
ve.dm.MWImageModel.prototype.resetDefaultDimensions = function () {
var originalDimensions = this.scalable.getOriginalDimensions();
if ( !$.isEmptyObject( originalDimensions ) ) {
if ( this.getType() === 'thumb' || this.getType() === 'frameless' ) {
// Default is thumb size
if ( originalDimensions.width <= this.defaultThumbSize ) {
this.scalable.setDefaultDimensions( originalDimensions );
} else {
this.scalable.setDefaultDimensions(
this.scalable.getDimensionsFromValue( {
'width': this.defaultThumbSize
} ) );
}
} else {
// Default is original size
this.scalable.setDefaultDimensions( originalDimensions );
}
} else {
this.scalable.setDefaultDimensions( {} );
}
};
/**
* Retrieve the currently set default dimensions from the scalable
* object attached to the image.
*
* @return {Object} Image default dimensions
*/
ve.dm.MWImageModel.prototype.getDefaultDimensions = function () {
return this.scalable.getDefaultDimensions();
};
/**
* Change size type of the image
*
* @param {string} type Size type 'default', 'custom' or 'scale'
*/
ve.dm.MWImageModel.prototype.setSizeType = function ( type ) {
if ( this.sizeType !== type ) {
this.sizeType = type;
this.toggleDefaultSize( type === 'default' );
}
};
/**
* Set symbolic name of media type.
*
* @see #getMediaType
*
* @param {string} type Media type
*/
ve.dm.MWImageModel.prototype.setMediaType = function ( type ) {
this.mediaType = type;
};
/**
* Set image alignment
*
* @see #getAlignment
*
* @param {string} align Alignment
*/
ve.dm.MWImageModel.prototype.setAlignment = function ( align ) {
if ( align === 'default' ) {
// If default, set the alignment to language dir default
align = this.getDefaultDir();
this.toggleDefaultAlignment( true );
} else {
this.toggleDefaultAlignment( false );
}
this.alignment = align;
this.emit( 'alignmentChange', align );
};
/**
* Set image vertical alignment
*
* @see #getVerticalAlignment
*
* @param {string} valign Alignment
*/
ve.dm.MWImageModel.prototype.setVerticalAlignment = function ( valign ) {
this.verticalAlignment = valign;
this.emit( 'alignmentChange', valign );
};
/**
* Get the default alignment according to the document direction
*
* @return {string} Node alignment based on document direction
*/
ve.dm.MWImageModel.prototype.getDefaultDir = function () {
if ( this.getDir() === 'rtl' ) {
// Assume position is 'left'
return ( this.getImageNodeType() === 'mwBlockImage' ) ? 'left' : 'right';
} else {
// Assume position is 'right'
return ( this.getImageNodeType() === 'mwBlockImage' ) ? 'right' : 'left';
}
};
/**
* Get the directionality of the image, especially important for
* default alignment.
*
* @return {string} Current document direction 'rtl' or 'ltr'
*/
ve.dm.MWImageModel.prototype.getDir = function () {
return this.dir;
};
/**
* Set the directionality of the image, especially important for
* default alignment.
* @param {string} dir 'rtl' or 'ltr'
*/
ve.dm.MWImageModel.prototype.setDir = function ( dir ) {
this.dir = dir;
};
/**
* Set the scalable object relevant to the image node
*
* @param {ve.dm.Scalable} Scalable object
*/
ve.dm.MWImageModel.prototype.setScalable = function ( scalable ) {
if ( this.scalable instanceof ve.dm.Scalable ) {
this.scalable.disconnect( this );
}
this.scalable = scalable;
// Events
this.scalable.connect( this, { 'defaultSizeChange': 'onScalableDefaultSizeChange' } );
};
/**
* Set image caption document.
*
* @param {ve.dm.Document} Image caption document
*/
ve.dm.MWImageModel.prototype.setCaptionDocument = function ( doc ) {
this.captionDoc = doc;
};