mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-26 21:13:01 +00:00
9ffbfc77cd
Bug: T250843 Change-Id: I53ef4fa4a65b017eec90cc1f2cbb7e7f0681e317
1295 lines
35 KiB
JavaScript
1295 lines
35 KiB
JavaScript
/*!
|
|
* VisualEditor DataModel MWImageModel class.
|
|
*
|
|
* @copyright See AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* MediaWiki image model.
|
|
*
|
|
* @class
|
|
* @mixes OO.EventEmitter
|
|
*
|
|
* @constructor
|
|
* @param {ve.dm.Document} parentDoc Document that contains or will contain the image
|
|
* @param {Object} [config] Configuration options
|
|
* @param {string} [config.resourceName] The resource name of the given media file
|
|
* @param {Object} [config.currentDimensions={}] Current dimensions, width & height
|
|
* @param {Object} [config.minDimensions={}] Minimum dimensions, width & height
|
|
* @param {boolean} [config.isDefaultSize=false] Object is using its default size dimensions
|
|
*/
|
|
ve.dm.MWImageModel = function VeDmMWImageModel( parentDoc, config ) {
|
|
config = config || {};
|
|
|
|
// Mixin constructors
|
|
OO.EventEmitter.call( this );
|
|
|
|
// Properties
|
|
this.attributesCache = {};
|
|
|
|
// Image properties
|
|
this.parentDoc = parentDoc;
|
|
this.captionDoc = null;
|
|
this.caption = null;
|
|
this.mediaType = null;
|
|
this.altText = '';
|
|
this.type = null;
|
|
this.alignment = null;
|
|
this.scalable = null;
|
|
this.sizeType = null;
|
|
this.border = false;
|
|
this.borderable = false;
|
|
this.defaultDimensions = null;
|
|
this.changedImageSource = false;
|
|
|
|
this.imageSrc = '';
|
|
this.imageResourceName = '';
|
|
this.imageHref = '';
|
|
this.imageClassAttr = null;
|
|
|
|
// FIXME: This is blindly being preserved but may not apply if, say,
|
|
// a link is no longer pointing to a file description page. When support
|
|
// for editing the |link= media option is added, take it into account.
|
|
this.imgWrapperClassAttr = null;
|
|
|
|
this.boundingBox = null;
|
|
this.initialHash = {};
|
|
|
|
// Get wiki default thumbnail size
|
|
this.defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' )
|
|
.thumbLimits[ mw.user.options.get( 'thumbsize' ) ];
|
|
|
|
if ( config.resourceName ) {
|
|
this.setImageResourceName( config.resourceName );
|
|
}
|
|
|
|
// Create scalable
|
|
const currentDimensions = config.currentDimensions || {};
|
|
const minDimensions = config.minDimensions || {};
|
|
|
|
const scalable = new ve.dm.Scalable( {
|
|
currentDimensions: {
|
|
width: currentDimensions.width,
|
|
height: currentDimensions.height
|
|
},
|
|
minDimensions: {
|
|
width: minDimensions.width || 1,
|
|
height: minDimensions.height || 1
|
|
},
|
|
defaultSize: !!config.isDefaultSize
|
|
} );
|
|
// Set the initial scalable, connect it to events
|
|
// and request an update from the API
|
|
this.attachScalable( scalable );
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.mixinClass( ve.dm.MWImageModel, OO.EventEmitter );
|
|
|
|
/* Events */
|
|
|
|
/**
|
|
* Change of image alignment or of having alignment at all
|
|
*
|
|
* @event ve.dm.MWImageModel#alignmentChange
|
|
* @param {string} Alignment 'left', 'right', 'center' or 'none'
|
|
*/
|
|
|
|
/**
|
|
* Change in size type between default and custom
|
|
*
|
|
* @event ve.dm.MWImageModel#sizeDefaultChange
|
|
* @param {boolean} Image is default size
|
|
*/
|
|
|
|
/**
|
|
* Change in the image type
|
|
*
|
|
* @event ve.dm.MWImageModel#typeChange
|
|
* @param {string} Image type 'thumb', 'frame', 'frameless' or 'none'
|
|
*/
|
|
|
|
/* Static Properties */
|
|
|
|
ve.dm.MWImageModel.static.infoCache = {};
|
|
|
|
/* Static Methods */
|
|
|
|
/**
|
|
* Create a new image node based on given parameters.
|
|
*
|
|
* @param {Object} attributes Image attributes
|
|
* @param {string} [imageType] Image node type 'mwInlineImage' or 'mwBlockImage'.
|
|
* Defaults to 'mwBlockImage'
|
|
* @return {ve.dm.MWImageNode} An image node
|
|
*/
|
|
ve.dm.MWImageModel.static.createImageNode = function ( attributes, imageType ) {
|
|
const defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' )
|
|
.thumbLimits[ mw.user.options.get( 'thumbsize' ) ];
|
|
|
|
const attrs = ve.extendObject( {
|
|
mediaClass: 'File',
|
|
mediaTag: 'img',
|
|
type: 'thumb',
|
|
align: 'default',
|
|
width: defaultThumbSize,
|
|
mediaType: 'BITMAP',
|
|
defaultSize: true,
|
|
imageClassAttr: 'mw-file-element'
|
|
}, attributes );
|
|
|
|
if ( attrs.defaultSize ) {
|
|
const newDimensions = ve.dm.MWImageNode.static.scaleToThumbnailSize( attrs, attrs.mediaType );
|
|
if ( newDimensions ) {
|
|
attrs.width = newDimensions.width;
|
|
attrs.height = newDimensions.height;
|
|
}
|
|
}
|
|
|
|
imageType = imageType || 'mwBlockImage';
|
|
|
|
const newNode = ve.dm.nodeFactory.createFromElement( {
|
|
type: imageType,
|
|
attributes: attrs
|
|
} );
|
|
|
|
ve.dm.MWImageNode.static.syncScalableToType( attrs.type, attrs.mediaType, newNode.getScalable() );
|
|
|
|
return newNode;
|
|
};
|
|
|
|
/**
|
|
* Load from image data with scalable information.
|
|
*
|
|
* @param {Object} attrs Image node attributes
|
|
* @param {ve.dm.Document} parentDoc Document that contains or will contain the image
|
|
* @return {ve.dm.MWImageModel} Image model
|
|
*/
|
|
ve.dm.MWImageModel.static.newFromImageAttributes = function ( attrs, parentDoc ) {
|
|
const imgModel = new ve.dm.MWImageModel(
|
|
parentDoc,
|
|
{
|
|
resourceName: attrs.resource,
|
|
currentDimensions: {
|
|
width: attrs.width,
|
|
height: attrs.height
|
|
},
|
|
defaultSize: !!attrs.defaultSize
|
|
}
|
|
);
|
|
|
|
// Cache the attributes so we can create a new image without
|
|
// losing any existing information
|
|
imgModel.cacheOriginalImageAttributes( attrs );
|
|
|
|
imgModel.setImageSource( attrs.src );
|
|
imgModel.setFilename( new mw.Title( mw.libs.ve.normalizeParsoidResourceName( attrs.resource ) ).getMainText() );
|
|
imgModel.setImageHref( attrs.href );
|
|
imgModel.setImageClassAttr( attrs.imageClassAttr );
|
|
imgModel.setImgWrapperClassAttr( attrs.imgWrapperClassAttr );
|
|
|
|
// Set bounding box
|
|
imgModel.setBoundingBox( {
|
|
width: attrs.width,
|
|
height: attrs.height
|
|
} );
|
|
|
|
// Collect all the information
|
|
imgModel.toggleBorder( !!attrs.borderImage );
|
|
imgModel.setAltText( attrs.alt || '' );
|
|
|
|
imgModel.setType( attrs.type );
|
|
|
|
// 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
|
|
imgModel.setAlignment( attrs.align || 'default' );
|
|
|
|
// Default size
|
|
imgModel.toggleDefaultSize( !!attrs.defaultSize );
|
|
|
|
// TODO: When scale/upright is available, set the size
|
|
// type accordingly
|
|
imgModel.setSizeType( imgModel.isDefaultSize() ? 'default' : 'custom' );
|
|
|
|
return imgModel;
|
|
};
|
|
|
|
/**
|
|
* Load from existing image node.
|
|
*
|
|
* @param {ve.dm.MWImageNode} node Image node
|
|
* @return {ve.dm.MWImageModel} Image model
|
|
*/
|
|
ve.dm.MWImageModel.static.newFromImageNode = function ( node ) {
|
|
return ve.dm.MWImageModel.static.newFromImageAttributes( node.getAttributes(), node.getDocument() );
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Get the hash object of the current image model state.
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getHashObject = function () {
|
|
const hash = {
|
|
filename: this.getFilename(),
|
|
altText: this.getAltText(),
|
|
type: this.getType(),
|
|
alignment: this.getAlignment(),
|
|
sizeType: this.getSizeType(),
|
|
border: this.hasBorder(),
|
|
borderable: this.isBorderable()
|
|
};
|
|
|
|
if ( this.getScalable() ) {
|
|
hash.scalable = {
|
|
currentDimensions: ve.copy( this.getScalable().getCurrentDimensions() ),
|
|
isDefault: this.getScalable().isDefault()
|
|
};
|
|
}
|
|
return hash;
|
|
};
|
|
|
|
/**
|
|
* Normalize the source url by stripping the protocol off.
|
|
* This is done so when an image is replaced with the same image,
|
|
* the imageModel can recognize that nothing has actually changed.
|
|
*
|
|
* Example:
|
|
* 'http://upload.wikimedia.org/wikipedia/commons/0/Foo.png'
|
|
* to '//upload.wikimedia.org/wikipedia/commons/0/Foo.png'
|
|
*
|
|
* @return {string} Normalized image source
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getNormalizedImageSource = function () {
|
|
// Strip the url prefix 'http' / 'https' etc
|
|
return this.getImageSource().replace( /^https?:\/\//, '//' );
|
|
};
|
|
|
|
/**
|
|
* Adjust the model parameters based on a new image
|
|
*
|
|
* @param {Object} attrs New image source attributes
|
|
* @param {Object} [APIinfo] The image's API info
|
|
* @throws {Error} Image has insufficient details to compute the imageModel details.
|
|
*/
|
|
ve.dm.MWImageModel.prototype.changeImageSource = function ( attrs, APIinfo ) {
|
|
this.changedImageSource = true;
|
|
|
|
if ( attrs.mediaType ) {
|
|
this.setMediaType( attrs.mediaType );
|
|
}
|
|
if ( attrs.href ) {
|
|
this.setImageHref( attrs.href );
|
|
}
|
|
|
|
// FIXME: Account for falsey but present values
|
|
if ( attrs.imageClassAttr ) {
|
|
this.setImageClassAttr( attrs.imageClassAttr );
|
|
}
|
|
|
|
// FIXME: Account for falsey but present values
|
|
if ( attrs.imgWrapperClassAttr ) {
|
|
this.setImgWrapperClassAttr( attrs.imgWrapperClassAttr );
|
|
}
|
|
|
|
if ( attrs.resource ) {
|
|
this.setImageResourceName( attrs.resource );
|
|
this.setFilename( new mw.Title( mw.libs.ve.normalizeParsoidResourceName( attrs.resource ) ).getMainText() );
|
|
}
|
|
|
|
if ( attrs.src ) {
|
|
this.setImageSource( attrs.src );
|
|
}
|
|
|
|
// Remove the scalable default and original dimensions
|
|
this.scalable.clearOriginalDimensions();
|
|
this.scalable.clearDefaultDimensions();
|
|
this.scalable.clearMaxDimensions();
|
|
this.scalable.clearMinDimensions();
|
|
// This is a different image so clear the attributes cache
|
|
delete this.attributesCache.originalWidth;
|
|
delete this.attributesCache.originalHeight;
|
|
|
|
// If we already have dimensions from the API, use them
|
|
if ( APIinfo ) {
|
|
this.scalable.setOriginalDimensions( {
|
|
width: APIinfo.width,
|
|
height: APIinfo.height
|
|
} );
|
|
// Update media type
|
|
this.setMediaType( APIinfo.mediatype );
|
|
// Update defaults
|
|
ve.dm.MWImageNode.static.syncScalableToType(
|
|
this.getType(),
|
|
APIinfo.mediatype,
|
|
this.scalable
|
|
);
|
|
this.updateScalableDetails( {
|
|
width: APIinfo.width,
|
|
height: APIinfo.height
|
|
} );
|
|
} else {
|
|
// Call for updated scalable if we don't have dimensions from the API info
|
|
if ( this.getFilename() ) {
|
|
// Update anyway
|
|
ve.dm.MWImageNode.static.getScalablePromise( this.getFilename() ).done( ( info ) => {
|
|
this.scalable.setOriginalDimensions( {
|
|
width: info.width,
|
|
height: info.height
|
|
} );
|
|
// Update media type
|
|
this.setMediaType( info.mediatype );
|
|
// Update defaults
|
|
ve.dm.MWImageNode.static.syncScalableToType(
|
|
this.getType(),
|
|
info.mediatype,
|
|
this.scalable
|
|
);
|
|
this.updateScalableDetails( {
|
|
width: info.width,
|
|
height: info.height
|
|
} );
|
|
} );
|
|
} else {
|
|
throw new Error( 'Cannot compute details for an image without remote filename and without sizing info.' );
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the current image node type according to the attributes.
|
|
* If either of the parameters are given, the node type is tested
|
|
* against them, otherwise, it is tested against the current image
|
|
* parameters.
|
|
*
|
|
* @param {string} [imageType] Optional. Image type.
|
|
* @param {string} [align] Optional. Image alignment.
|
|
* @return {string} Node type 'mwInlineImage' or 'mwBlockImage'
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getImageNodeType = function ( imageType, align ) {
|
|
imageType = imageType || this.getType();
|
|
|
|
if (
|
|
( this.getType() === 'frameless' || this.getType() === 'none' ) &&
|
|
( !this.isAligned( align ) || this.isDefaultAligned( imageType, align ) )
|
|
) {
|
|
return 'mwInlineImage';
|
|
} else {
|
|
return 'mwBlockImage';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the original bounding box
|
|
*
|
|
* @return {Object} Bounding box with width and height
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getBoundingBox = function () {
|
|
return this.boundingBox;
|
|
};
|
|
|
|
/**
|
|
* Update an existing image node by changing its attributes
|
|
*
|
|
* @param {ve.dm.MWImageNode} node Image node to update
|
|
* @param {ve.dm.Surface} surfaceModel Surface model of main document
|
|
*/
|
|
ve.dm.MWImageModel.prototype.updateImageNode = function ( node, surfaceModel ) {
|
|
const doc = surfaceModel.getDocument();
|
|
|
|
// Update the caption
|
|
if ( node.getType() === 'mwBlockImage' ) {
|
|
let captionNode = node.getCaptionNode();
|
|
if ( !captionNode ) {
|
|
// There was no caption before, so insert one now
|
|
surfaceModel.getFragment()
|
|
.adjustLinearSelection( 1 )
|
|
.collapseToStart()
|
|
.insertContent( [ { type: 'mwImageCaption' }, { type: '/mwImageCaption' } ] );
|
|
// Update the caption node
|
|
captionNode = node.getCaptionNode();
|
|
}
|
|
|
|
const captionRange = captionNode.getRange();
|
|
|
|
// Remove contents of old caption
|
|
surfaceModel.change(
|
|
ve.dm.TransactionBuilder.static.newFromRemoval(
|
|
doc,
|
|
captionRange,
|
|
true
|
|
)
|
|
);
|
|
|
|
// Add contents of new caption
|
|
surfaceModel.change(
|
|
ve.dm.TransactionBuilder.static.newFromDocumentInsertion(
|
|
doc,
|
|
captionRange.start,
|
|
this.getCaptionDocument()
|
|
)
|
|
);
|
|
}
|
|
|
|
// Update attributes
|
|
surfaceModel.change(
|
|
ve.dm.TransactionBuilder.static.newFromAttributeChanges(
|
|
doc,
|
|
node.getOffset(),
|
|
this.getUpdatedAttributes()
|
|
)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Insert image into a surface.
|
|
*
|
|
* Image is inserted at the current fragment position.
|
|
*
|
|
* @param {ve.dm.SurfaceFragment} fragment Fragment covering range to insert at
|
|
* @return {ve.dm.SurfaceFragment} Fragment covering inserted image
|
|
* @throws {Error} Unknown image node type
|
|
*/
|
|
ve.dm.MWImageModel.prototype.insertImageNode = function ( fragment ) {
|
|
const nodeType = this.getImageNodeType(),
|
|
surfaceModel = fragment.getSurface();
|
|
|
|
if ( !( fragment.getSelection() instanceof ve.dm.LinearSelection ) ) {
|
|
return fragment;
|
|
}
|
|
|
|
const selectedNode = fragment.getSelectedNode();
|
|
|
|
// If there was a previous node, remove it first
|
|
if ( selectedNode ) {
|
|
// Remove the old image
|
|
fragment.removeContent();
|
|
}
|
|
|
|
const contentToInsert = this.getData();
|
|
|
|
let offset;
|
|
switch ( nodeType ) {
|
|
case 'mwInlineImage':
|
|
if ( selectedNode && selectedNode.type === 'mwBlockImage' ) {
|
|
// If converting from a block image, create a wrapper paragraph for the inline image to go in.
|
|
fragment.insertContent( [ { type: 'paragraph', internal: { generated: 'wrapper' } }, { type: '/paragraph' } ] );
|
|
offset = fragment.getSelection().getRange().start + 1;
|
|
} else {
|
|
// Try to put the image inside the nearest content node
|
|
offset = fragment.getDocument().data.getNearestContentOffset( fragment.getSelection().getRange().start );
|
|
}
|
|
if ( offset > -1 ) {
|
|
fragment = fragment.clone( new ve.dm.LinearSelection( new ve.Range( offset ) ) );
|
|
}
|
|
fragment.insertContent( contentToInsert );
|
|
return fragment;
|
|
|
|
case 'mwBlockImage':
|
|
// Try to put the image in front of the structural node
|
|
offset = fragment.getDocument().data.getNearestStructuralOffset( fragment.getSelection().getRange().start, -1 );
|
|
if ( offset > -1 ) {
|
|
fragment = fragment.clone( new ve.dm.LinearSelection( new ve.Range( offset ) ) );
|
|
}
|
|
fragment.insertContent( contentToInsert );
|
|
// Add contents of new caption
|
|
surfaceModel.change(
|
|
ve.dm.TransactionBuilder.static.newFromDocumentInsertion(
|
|
surfaceModel.getDocument(),
|
|
fragment.getSelection().getRange().start + 2,
|
|
this.getCaptionDocument()
|
|
)
|
|
);
|
|
return fragment;
|
|
|
|
default:
|
|
throw new Error( 'Unknown image node type ' + nodeType );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get linear data representation of the image
|
|
*
|
|
* @return {Array} Linear data
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getData = function () {
|
|
const originalAttrs = ve.copy( this.getOriginalImageAttributes() ),
|
|
editAttributes = ve.extendObject( originalAttrs, this.getUpdatedAttributes() ),
|
|
nodeType = this.getImageNodeType();
|
|
|
|
// Remove old classes
|
|
delete editAttributes.originalClasses;
|
|
delete editAttributes.unrecognizedClasses;
|
|
// Newly created images must have valid URLs, so remove the error attribute
|
|
if ( this.isChangedImageSource() ) {
|
|
delete editAttributes.isError;
|
|
}
|
|
|
|
const data = [
|
|
{
|
|
type: nodeType,
|
|
attributes: editAttributes
|
|
},
|
|
{ type: '/' + nodeType }
|
|
];
|
|
|
|
if ( nodeType === 'mwBlockImage' ) {
|
|
data.splice( 1, 0, { type: 'mwImageCaption' }, { type: '/mwImageCaption' } );
|
|
}
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* Return all updated attributes that belong to the node.
|
|
*
|
|
* @return {Object} Updated attributes
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getUpdatedAttributes = function () {
|
|
const origAttrs = this.getOriginalImageAttributes();
|
|
|
|
let currentDimensions;
|
|
// Adjust default dimensions if size is set to default
|
|
if ( this.scalable.isDefault() && this.scalable.getDefaultDimensions() ) {
|
|
currentDimensions = this.scalable.getDefaultDimensions();
|
|
} else {
|
|
currentDimensions = this.getCurrentDimensions();
|
|
}
|
|
|
|
const attrs = {
|
|
mediaClass: 'File',
|
|
mediaTag: this.getMediaTag(),
|
|
type: this.getType(),
|
|
width: currentDimensions.width,
|
|
height: currentDimensions.height,
|
|
defaultSize: this.isDefaultSize(),
|
|
borderImage: this.hasBorder()
|
|
};
|
|
|
|
if ( this.getAltText() || typeof origAttrs.alt === 'string' ) {
|
|
attrs.alt = this.getAltText();
|
|
}
|
|
|
|
if ( this.isDefaultAligned() ) {
|
|
attrs.align = 'default';
|
|
} else if ( !this.isAligned() ) {
|
|
attrs.align = 'none';
|
|
} else {
|
|
attrs.align = this.getAlignment();
|
|
}
|
|
|
|
attrs.src = this.getImageSource();
|
|
attrs.href = this.getImageHref();
|
|
attrs.imageClassAttr = this.getImageClassAttr();
|
|
attrs.imgWrapperClassAttr = this.getImgWrapperClassAttr();
|
|
attrs.resource = this.getImageResourceName();
|
|
|
|
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 image file source
|
|
*
|
|
* @param {string} src The source of the given media file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setImageSource = function ( src ) {
|
|
this.imageSrc = src;
|
|
};
|
|
|
|
/**
|
|
* Set the image file resource name
|
|
*
|
|
* @param {string} resourceName The resource name of the given image file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setImageResourceName = function ( resourceName ) {
|
|
this.imageResourceName = resourceName;
|
|
};
|
|
|
|
/**
|
|
* Set the image href value
|
|
*
|
|
* @param {string} href The destination href of the given media file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setImageHref = function ( href ) {
|
|
this.imageHref = href;
|
|
};
|
|
|
|
/**
|
|
* Set the original bounding box
|
|
*
|
|
* @param {Object} box Bounding box with width and height
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setBoundingBox = function ( box ) {
|
|
this.boundingBox = box;
|
|
};
|
|
|
|
/**
|
|
* Set the initial hash object of the image to be compared to when
|
|
* checking if the model is modified.
|
|
*
|
|
* @param {Object} hash The initial hash object
|
|
*/
|
|
ve.dm.MWImageModel.prototype.storeInitialHash = function ( hash ) {
|
|
this.initialHash = hash;
|
|
};
|
|
|
|
/**
|
|
* Set symbolic name of media type.
|
|
*
|
|
* Example values: "BITMAP" for JPEG or PNG images; "DRAWING" for SVG graphics
|
|
*
|
|
* @param {string|undefined} type Symbolic media type name, or undefined if empty
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setMediaType = function ( type ) {
|
|
this.mediaType = type;
|
|
};
|
|
|
|
/**
|
|
* Check whether the image is set to default size
|
|
*
|
|
* @return {boolean} Default size flag on or off
|
|
*/
|
|
ve.dm.MWImageModel.prototype.isDefaultSize = function () {
|
|
// An image with 'frame' always ignores the size specification
|
|
return this.scalable.isDefault() || this.getType() === 'frame';
|
|
};
|
|
|
|
/**
|
|
* 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 source is changed
|
|
*
|
|
* @return {boolean} changedImageSource flag on or off
|
|
*/
|
|
ve.dm.MWImageModel.prototype.isChangedImageSource = function () {
|
|
return this.changedImageSource;
|
|
};
|
|
|
|
/**
|
|
* Check whether the image has floating alignment set
|
|
*
|
|
* @param {string} [align] Optional. Alignment value to test against.
|
|
* @return {boolean} hasAlignment flag on or off
|
|
*/
|
|
ve.dm.MWImageModel.prototype.isAligned = function ( align ) {
|
|
align = align || this.alignment;
|
|
// The image is aligned if it has alignment (not undefined and not null)
|
|
// and if its alignment is not 'none'.
|
|
// Inline images initially have null alignment value (and are not aligned)
|
|
return align && align !== 'none';
|
|
};
|
|
|
|
/**
|
|
* Check whether the image is set to default alignment
|
|
* We explicitly repeat tests so to avoid recursively calling
|
|
* the other methods.
|
|
*
|
|
* @param {string} [imageType] Type of the image.
|
|
* @param {string} [align] Optional alignment value to test against.
|
|
* Supplying this parameter would test whether this align parameter
|
|
* would mean the image is aligned to its default position.
|
|
* @return {boolean} defaultAlignment flag on or off
|
|
*/
|
|
ve.dm.MWImageModel.prototype.isDefaultAligned = function ( imageType, align ) {
|
|
const alignment = align || this.getAlignment(),
|
|
defaultAlignment = ( this.parentDoc.getDir() === 'rtl' ) ? 'left' : 'right';
|
|
|
|
imageType = imageType || this.getType();
|
|
// No alignment specified means default alignment always
|
|
// Inline images have no align attribute; during the initialization
|
|
// stage of the model we have to account for that option. Later the
|
|
// model creates a faux alignment for inline images ('none' for default)
|
|
// but if initially the alignment is null or undefined, it means the image
|
|
// is inline without explicit alignment (which makes it default aligned)
|
|
if ( !alignment ) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
(
|
|
( imageType === 'frameless' || imageType === 'none' ) &&
|
|
alignment === 'none'
|
|
) ||
|
|
(
|
|
( imageType === 'thumb' || imageType === 'frame' ) &&
|
|
alignment === defaultAlignment
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* 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 file resource name
|
|
*
|
|
* @return {string} resourceName The resource name of the given media file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getResourceName = function () {
|
|
return this.imageResourceName;
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @return {string} Size type
|
|
*/
|
|
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|undefined} Symbolic media type name, or undefined if empty
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getMediaType = function () {
|
|
return this.mediaType;
|
|
};
|
|
|
|
/**
|
|
* Get media tag: img, video or audio
|
|
*
|
|
* @return {string} Tag name
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getMediaTag = function () {
|
|
const mediaType = this.getMediaType();
|
|
|
|
if ( mediaType === 'VIDEO' ) {
|
|
return 'video';
|
|
}
|
|
if ( mediaType === 'AUDIO' ) {
|
|
return 'audio';
|
|
}
|
|
return 'img';
|
|
};
|
|
|
|
/**
|
|
* Get image alignment 'left', 'right', 'center', 'none' or 'default'
|
|
*
|
|
* @return {string|null} Image alignment. Inline images have initial alignment
|
|
* value of null.
|
|
*/
|
|
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}
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getScalable = function () {
|
|
return this.scalable;
|
|
};
|
|
|
|
/**
|
|
* @typedef {Object} Dimensions
|
|
* @memberof ve.ui.DimensionsWidget
|
|
* @property {number} width The value of the width input
|
|
* @property {number} height The value of the height input
|
|
*/
|
|
|
|
/**
|
|
* Get the image current dimensions
|
|
*
|
|
* @return {ve.ui.DimensionsWidget.Dimensions} Current dimensions width/height
|
|
*/
|
|
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 = this.parentDoc.cloneWithData( [
|
|
{ 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 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 size flag of the image
|
|
*
|
|
* @param {boolean} [isDefault] Default size flag. Omit to toggle current value.
|
|
* @fires ve.dm.MWImageModel#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 ) {
|
|
const normalizedDimensions = ve.dm.Scalable.static.getDimensionsFromValue( dimensions, this.scalable.getRatio() );
|
|
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 ve.dm.MWImageModel#typeChange
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setType = function ( type ) {
|
|
const isDefaultAligned = this.isDefaultAligned( this.imageCurrentType );
|
|
|
|
this.type = type;
|
|
|
|
// If we're switching between inline and block or vice versa,
|
|
// check if the old type image was default aligned
|
|
if ( isDefaultAligned && this.imageCurrentType !== this.type ) {
|
|
if ( this.type === 'none' || this.type === 'frameless' ) {
|
|
// Reset default alignment for switching to inline images
|
|
this.setAlignment( 'none' );
|
|
} else {
|
|
// Reset default alignment for all other images
|
|
this.setAlignment( 'default' );
|
|
}
|
|
}
|
|
|
|
// Cache the current type for next check
|
|
this.imageCurrentType = type;
|
|
|
|
if ( type === 'frame' || type === 'thumb' ) {
|
|
// Disable border option
|
|
this.toggleBorderable( false );
|
|
} else {
|
|
// Enable border option
|
|
this.toggleBorderable( true );
|
|
}
|
|
|
|
// If type is frame, set to 'default' size
|
|
if ( type === 'frame' ) {
|
|
this.toggleDefaultSize( true );
|
|
}
|
|
|
|
// Let the image node update scalable considerations
|
|
// for default and max dimensions as per the new type.
|
|
ve.dm.MWImageNode.static.syncScalableToType( type, this.getMediaType(), this.getScalable() );
|
|
|
|
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 () {
|
|
const originalDimensions = this.scalable.getOriginalDimensions();
|
|
|
|
if ( !ve.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(
|
|
ve.dm.Scalable.static.getDimensionsFromValue( {
|
|
width: this.defaultThumbSize
|
|
}, this.scalable.getRatio() )
|
|
);
|
|
}
|
|
} else {
|
|
// Default is original size
|
|
this.scalable.setDefaultDimensions( originalDimensions );
|
|
}
|
|
} else {
|
|
this.scalable.clearDefaultDimensions();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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 image alignment
|
|
*
|
|
* @see #getAlignment
|
|
*
|
|
* @param {string} align Alignment
|
|
* @fires ve.dm.MWImageModel#alignmentChange
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setAlignment = function ( align ) {
|
|
if ( align === 'default' ) {
|
|
// If default, set the alignment to language dir default
|
|
align = this.getDefaultDir();
|
|
}
|
|
|
|
this.alignment = align;
|
|
this.emit( 'alignmentChange', align );
|
|
};
|
|
|
|
/**
|
|
* Set image vertical alignment
|
|
*
|
|
* @see #getVerticalAlignment
|
|
*
|
|
* @param {string} valign Alignment
|
|
* @fires ve.dm.MWImageModel#alignmentChange
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setVerticalAlignment = function ( valign ) {
|
|
this.verticalAlignment = valign;
|
|
this.emit( 'alignmentChange', valign );
|
|
};
|
|
|
|
/**
|
|
* Get the default alignment according to the document direction
|
|
*
|
|
* @param {string} [imageNodeType] Optional. The image node type that we would
|
|
* like to get the default direction for. Supplying this parameter allows us
|
|
* to check what the default alignment of a specific type of node would be.
|
|
* If the parameter is not supplied, the default alignment will be calculated
|
|
* based on the current node type.
|
|
* @return {string} Node alignment based on document direction
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getDefaultDir = function ( imageNodeType ) {
|
|
imageNodeType = imageNodeType || this.getImageNodeType();
|
|
|
|
if ( this.parentDoc.getDir() === 'rtl' ) {
|
|
// Assume position is 'left'
|
|
return ( imageNodeType === 'mwBlockImage' ) ? 'left' : 'none';
|
|
} else {
|
|
// Assume position is 'right'
|
|
return ( imageNodeType === 'mwBlockImage' ) ? 'right' : 'none';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the image file source
|
|
* The image file source that points to the location of the
|
|
* file on the Web.
|
|
* For instance, '//upload.wikimedia.org/wikipedia/commons/0/0f/Foo.jpg'
|
|
*
|
|
* @return {string} The source of the given media file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getImageSource = function () {
|
|
return this.imageSrc;
|
|
};
|
|
|
|
/**
|
|
* Get the image file resource name.
|
|
* The resource name represents the filename without the full
|
|
* source url.
|
|
* For example, './File:Foo.jpg'
|
|
*
|
|
* @return {string} The resource name of the given media file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getImageResourceName = function () {
|
|
return this.imageResourceName;
|
|
};
|
|
|
|
/**
|
|
* Get the image href value.
|
|
* This is the link that the image leads to. It usually contains
|
|
* the link to the source of the image in commons or locally, but
|
|
* may hold an alternative link if link= is supplied in the wikitext.
|
|
* For example, './File:Foo.jpg' or 'http://www.wikipedia.org'
|
|
*
|
|
* @return {string} The destination href of the given media file
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getImageHref = function () {
|
|
return this.imageHref;
|
|
};
|
|
|
|
/**
|
|
* @param {string|null} classAttr
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setImageClassAttr = function ( classAttr ) {
|
|
this.imageClassAttr = classAttr;
|
|
};
|
|
|
|
/**
|
|
* @return {string|null}
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getImageClassAttr = function () {
|
|
return this.imageClassAttr;
|
|
};
|
|
|
|
/**
|
|
* @param {string|null} classAttr
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setImgWrapperClassAttr = function ( classAttr ) {
|
|
this.imgWrapperClassAttr = classAttr;
|
|
};
|
|
|
|
/**
|
|
* @return {string|null}
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getImgWrapperClassAttr = function () {
|
|
return this.imgWrapperClassAttr;
|
|
};
|
|
|
|
/**
|
|
* Attach a new scalable object to the model and request the
|
|
* information from the API.
|
|
*
|
|
* @param {ve.dm.Scalable} scalable
|
|
*/
|
|
ve.dm.MWImageModel.prototype.attachScalable = function ( scalable ) {
|
|
const imageName = mw.libs.ve.normalizeParsoidResourceName( this.getResourceName() );
|
|
|
|
if ( this.scalable instanceof ve.dm.Scalable ) {
|
|
this.scalable.disconnect( this );
|
|
}
|
|
this.scalable = scalable;
|
|
|
|
// Events
|
|
this.scalable.connect( this, { defaultSizeChange: 'onScalableDefaultSizeChange' } );
|
|
|
|
// Call for updated scalable
|
|
if ( imageName ) {
|
|
ve.dm.MWImageNode.static.getScalablePromise( imageName ).done( ( info ) => {
|
|
this.scalable.setOriginalDimensions( {
|
|
width: info.width,
|
|
height: info.height
|
|
} );
|
|
// Update media type
|
|
this.setMediaType( info.mediatype );
|
|
// Update according to type
|
|
ve.dm.MWImageNode.static.syncScalableToType(
|
|
this.getType(),
|
|
this.getMediaType(),
|
|
this.getScalable()
|
|
);
|
|
|
|
// We have to adjust the details in the initial hash if the original
|
|
// image was 'default' since we didn't have default until now and the
|
|
// default dimensions that were 'recorded' were wrong
|
|
if ( !ve.isEmptyObject( this.initialHash ) && this.initialHash.scalable.isDefault ) {
|
|
this.initialHash.scalable.currentDimensions = this.scalable.getDefaultDimensions();
|
|
}
|
|
|
|
} );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the filename of the current image
|
|
*
|
|
* @param {string} filename Image filename (without namespace)
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setFilename = function ( filename ) {
|
|
this.filename = filename;
|
|
};
|
|
|
|
/**
|
|
* Get the filename of the current image
|
|
*
|
|
* @return {string} filename Image filename (without namespace)
|
|
*/
|
|
ve.dm.MWImageModel.prototype.getFilename = function () {
|
|
return this.filename;
|
|
};
|
|
|
|
/**
|
|
* If the image changed, update scalable definitions.
|
|
*
|
|
* @param {Object} originalDimensions Image original dimensions
|
|
*/
|
|
ve.dm.MWImageModel.prototype.updateScalableDetails = function ( originalDimensions ) {
|
|
let newDimensions;
|
|
|
|
// Resize the new image's current dimensions to default or based on the bounding box
|
|
if ( this.isDefaultSize() ) {
|
|
// Scale to default
|
|
newDimensions = ve.dm.MWImageNode.static.scaleToThumbnailSize( originalDimensions );
|
|
} else {
|
|
if ( this.getBoundingBox() ) {
|
|
// Scale the new image by its width
|
|
newDimensions = ve.dm.MWImageNode.static.resizeToBoundingBox(
|
|
originalDimensions,
|
|
{
|
|
width: this.boundingBox.width,
|
|
height: Infinity
|
|
}
|
|
);
|
|
} else {
|
|
newDimensions = originalDimensions;
|
|
}
|
|
}
|
|
|
|
if ( newDimensions ) {
|
|
this.getScalable().setCurrentDimensions( newDimensions );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set image caption document.
|
|
*
|
|
* @param {ve.dm.Document} doc Image caption document
|
|
*/
|
|
ve.dm.MWImageModel.prototype.setCaptionDocument = function ( doc ) {
|
|
this.captionDoc = doc;
|
|
};
|
|
|
|
/**
|
|
* Check if the model attributes and parameters have been modified by
|
|
* comparing the current hash to the new hash object.
|
|
*
|
|
* @return {boolean} Model has been modified
|
|
*/
|
|
ve.dm.MWImageModel.prototype.hasBeenModified = function () {
|
|
if ( this.initialHash ) {
|
|
return !ve.compare( this.initialHash, this.getHashObject() );
|
|
}
|
|
return true;
|
|
};
|