/*!
* VisualEditor user interface MWMediaEditDialog class.
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/*global mw */
/**
* Dialog for editing MediaWiki media objects.
*
* @class
* @extends ve.ui.Dialog
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.MWMediaEditDialog = function VeUiMWMediaEditDialog( config ) {
// Parent constructor
ve.ui.Dialog.call( this, config );
// Properties
this.mediaNode = null;
this.captionNode = null;
this.store = null;
this.scalable = null;
// Events
this.connect( this, { 'open': 'onOpen' } );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWMediaEditDialog, ve.ui.Dialog );
/* Static Properties */
ve.ui.MWMediaEditDialog.static.name = 'mediaEdit';
ve.ui.MWMediaEditDialog.static.title =
OO.ui.deferMsg( 'visualeditor-dialog-media-title' );
ve.ui.MWMediaEditDialog.static.icon = 'picture';
ve.ui.MWMediaEditDialog.static.toolbarGroups = [
// History
{ 'include': [ 'undo', 'redo' ] },
// No formatting
/* {
'type': 'menu',
'indicator': 'down',
'title': OO.ui.deferMsg( 'visualeditor-toolbar-format-tooltip' ),
'include': [ { 'group': 'format' } ],
'promote': [ 'paragraph' ],
'demote': [ 'preformatted', 'heading1' ]
},*/
// Style
{
'type': 'list',
'icon': 'text-style',
'indicator': 'down',
'title': OO.ui.deferMsg( 'visualeditor-toolbar-style-tooltip' ),
'include': [ { 'group': 'textStyle' }, 'clear' ],
'promote': [ 'bold', 'italic' ],
'demote': [ 'strikethrough', 'code', 'underline', 'clear' ]
},
// Link
{ 'include': [ 'link' ] },
// Cite
{
'type': 'list',
'label': 'Cite',
'indicator': 'down',
'include': [ { 'group': 'cite' } ]
},
// No structure
/* {
'type': 'bar',
'include': [ 'number', 'bullet', 'outdent', 'indent' ]
},*/
// Insert
{
'label': OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
'indicator': 'down',
'include': '*',
'exclude': [
{ 'group': 'format' },
{ 'group': 'structure' },
'referenceList',
'gallery'
],
'promote': [ 'reference', 'mediaInsert' ],
'demote': [ 'language', 'specialcharacter' ]
}
];
ve.ui.MWMediaEditDialog.static.surfaceCommands = [
'undo',
'redo',
'bold',
'italic',
'link',
'clear',
'underline',
'subscript',
'superscript',
'pasteSpecial'
];
ve.ui.MWMediaEditDialog.static.pasteRules = ve.extendObject(
ve.copy( ve.init.mw.Target.static.pasteRules ),
{
'all': {
'blacklist': OO.simpleArrayUnion(
ve.getProp( ve.init.mw.Target.static.pasteRules, 'all', 'blacklist' ) || [],
[
// Tables (but not lists) are possible in wikitext with a leading
// line break but we prevent creating these with the UI
'list', 'listItem', 'definitionList', 'definitionListItem',
'table', 'tableCaption', 'tableSection', 'tableRow', 'tableCell'
]
),
// Headings are also possible, but discouraged
'conversions': {
'mwHeading': 'paragraph'
}
}
}
);
/* Methods */
/**
* @inheritdoc
*/
ve.ui.MWMediaEditDialog.prototype.initialize = function () {
var altTextFieldset, positionFieldset, borderField, positionField;
// Parent method
ve.ui.Dialog.prototype.initialize.call( this );
this.$spinner = this.$( '
' ).addClass( 've-specialchar-spinner' );
// Set up the booklet layout
this.bookletLayout = new OO.ui.BookletLayout( {
'$': this.$,
'outlined': true
} );
this.generalSettingsPage = new OO.ui.PageLayout( 'general', { '$': this.$ } );
this.advancedSettingsPage = new OO.ui.PageLayout( 'advanced', { '$': this.$ } );
this.bookletLayout.addPages( [
this.generalSettingsPage, this.advancedSettingsPage
] );
this.generalSettingsPage.getOutlineItem()
.setIcon( 'parameter' )
.setLabel( ve.msg( 'visualeditor-dialog-media-page-general' ) );
this.advancedSettingsPage.getOutlineItem()
.setIcon( 'parameter' )
.setLabel( ve.msg( 'visualeditor-dialog-media-page-advanced' ) );
// Define fieldsets for image settings
// Caption
this.captionFieldset = new OO.ui.FieldsetLayout( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-media-content-section' ),
'icon': 'parameter'
} );
// Alt text
altTextFieldset = new OO.ui.FieldsetLayout( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-media-alttext-section' ),
'icon': 'parameter'
} );
this.altTextInput = new OO.ui.TextInputWidget( {
'$': this.$
} );
this.altTextInput.$element.addClass( 've-ui-mwMediaEditDialog-altText' );
// Build alt text fieldset
altTextFieldset.$element
.append( this.altTextInput.$element );
// Position
this.positionInput = new OO.ui.ButtonSelectWidget( {
'$': this.$
} );
this.positionInput.addItems( [
new OO.ui.ButtonOptionWidget( 'left', {
'$': this.$,
'icon': 'align-float-left',
'label': ve.msg( 'visualeditor-dialog-media-position-left' )
} ),
new OO.ui.ButtonOptionWidget( 'center', {
'$': this.$,
'icon': 'align-center',
'label': ve.msg( 'visualeditor-dialog-media-position-center' )
} ),
new OO.ui.ButtonOptionWidget( 'right', {
'$': this.$,
'icon': 'align-float-right',
'label': ve.msg( 'visualeditor-dialog-media-position-right' )
} ),
], 0 );
this.positionCheckbox = new OO.ui.CheckboxInputWidget( {
'$': this.$
} );
positionField = new OO.ui.FieldLayout( this.positionCheckbox, {
'$': this.$,
'align': 'inline',
'label': ve.msg( 'visualeditor-dialog-media-position-checkbox' )
} );
positionFieldset = new OO.ui.FieldsetLayout( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-media-position-section' ),
'icon': 'parameter'
} );
// Build position fieldset
positionFieldset.$element.append( [
positionField.$element,
this.positionInput.$element
] );
// Type
this.typeFieldset = new OO.ui.FieldsetLayout( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-media-type-section' ),
'icon': 'parameter'
} );
this.typeInput = new OO.ui.ButtonSelectWidget( {
'$': this.$
} );
this.typeInput.addItems( [
// TODO: Inline images require a bit of further work, will be coming soon
new OO.ui.ButtonOptionWidget( 'thumb', {
'$': this.$,
'icon': 'image-thumbnail',
'label': ve.msg( 'visualeditor-dialog-media-type-thumb' )
} ),
new OO.ui.ButtonOptionWidget( 'frameless', {
'$': this.$,
'icon': 'image-frameless',
'label': ve.msg( 'visualeditor-dialog-media-type-frameless' )
} ),
new OO.ui.ButtonOptionWidget( 'frame', {
'$': this.$,
'icon': 'image-frame',
'label': ve.msg( 'visualeditor-dialog-media-type-frame' )
} ),
new OO.ui.ButtonOptionWidget( 'none', {
'$': this.$,
'icon': 'image-none',
'label': ve.msg( 'visualeditor-dialog-media-type-none' )
} )
] );
this.borderCheckbox = new OO.ui.CheckboxInputWidget( {
'$': this.$
} );
borderField = new OO.ui.FieldLayout( this.borderCheckbox, {
'$': this.$,
'align': 'inline',
'label': ve.msg( 'visualeditor-dialog-media-type-border' )
} );
// Build type fieldset
this.typeFieldset.$element.append( [
this.typeInput.$element,
borderField.$element
] );
// Size
this.sizeFieldset = new OO.ui.FieldsetLayout( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-media-size-section' ),
'icon': 'parameter'
} );
this.sizeErrorLabel = new OO.ui.LabelWidget( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-media-size-originalsize-error' )
} );
this.sizeWidget = new ve.ui.MediaSizeWidget( {}, {
'$': this.$
} );
this.$sizeWidgetElements = this.$( '
' ).append( [
this.sizeWidget.$element,
this.sizeErrorLabel.$element
] );
this.sizeFieldset.$element.append( [
this.$spinner,
this.$sizeWidgetElements
] );
// Get wiki default thumbnail size
this.defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' ).defaultUserOptions.defaultthumbsize;
this.applyButton = new OO.ui.ButtonWidget( {
'$': this.$,
'label': ve.msg( 'visualeditor-dialog-action-apply' ),
'flags': ['primary']
} );
// Events
this.applyButton.connect( this, { 'click': [ 'close', { 'action': 'apply' } ] } );
this.positionCheckbox.connect( this, { 'change': 'onPositionCheckboxChange' } );
this.sizeWidget.connect( this, { 'change': 'onSizeWidgetChange' } );
this.typeInput.connect( this, { 'select': 'onTypeChange' } );
this.bookletLayout.connect( this, { 'set': 'onBookletLayoutSet' } );
// Initialization
this.generalSettingsPage.$element.append( [
this.captionFieldset.$element,
altTextFieldset.$element
] );
this.advancedSettingsPage.$element.append( [
positionFieldset.$element,
this.typeFieldset.$element,
this.sizeFieldset.$element
] );
this.$body.append( this.bookletLayout.$element );
this.$foot.append( this.applyButton.$element );
};
/**
* Handle change event on the sizeWidget. Switch the size select
* from default to custom and vise versa based on the values in
* the widget.
*/
ve.ui.MWMediaEditDialog.prototype.onSizeWidgetChange = function () {
// Switch to 'default' or 'custom' size
if ( this.sizeWidget.isEmpty() ) {
this.sizeWidget.setSizeType( 'default' );
} else {
this.sizeWidget.setSizeType( 'custom' );
}
};
/**
* Handle type change, particularly to and from 'thumb' to make
* sure size is limited.
* @param {OO.ui.ButtonOptionWidget} item Selected item
*/
ve.ui.MWMediaEditDialog.prototype.onTypeChange = function ( item ) {
var selectedType = item ? item.getData() : '',
thumbOrFrameless = selectedType === 'thumb' || selectedType === 'frameless',
originalDimensions = this.scalable.getOriginalDimensions();
// As per wikitext docs, both 'thumb' and 'frameless' images are
// limited in max size to their original size
if ( thumbOrFrameless ) {
if ( originalDimensions ) {
// Set original dimensions as the max. In the future we may
// want to switch between original dimensions (in frameless
// and thumb) and perhaps some preset maximum dimensions for
// basic and frameless.
this.scalable.setMaxDimensions( originalDimensions );
this.scalable.setEnforcedMax( true );
} else {
// We don't have maximum dimensions available, so we can't
// enforce any max size
this.scalable.setEnforcedMax( false );
}
} else {
// Don't limit maximum dimensions on basic and frameless images
this.scalable.setEnforcedMax( false );
}
// Disable border checkbox for thumb and framed images
// According to documentation:
// https://en.wikipedia.org/wiki/Wikipedia:Extended_image_syntax#Border
if ( selectedType === 'thumb' || selectedType === 'frame' ) {
// Disable border option
this.borderCheckbox.setDisabled( true );
this.borderCheckbox.setValue( false );
} else {
// Enable border option
this.borderCheckbox.setDisabled( false );
}
// Re-validate the existing dimensions
this.sizeWidget.validateDimensions();
};
/**
* Handle change event on the positionCheckbox element. If an option
* is selected, mark the checkbox
*/
ve.ui.MWMediaEditDialog.prototype.onPositionCheckboxChange = function () {
var checked = this.positionCheckbox.getValue();
if ( !checked ) {
// If unchecked, remove selection
this.positionInput.selectItem( null );
} else {
// If checked, choose default position
if ( this.getFragment().getDocument().getDir() === 'ltr' ) {
// Assume default is 'right'
this.positionInput.selectItem(
this.positionInput.getItemFromData( 'right' )
);
} else {
// Assume default is 'left'
this.positionInput.selectItem(
this.positionInput.getItemFromData( 'left' )
);
}
}
this.positionInput.setDisabled( !checked );
};
ve.ui.MWMediaEditDialog.prototype.onBookletLayoutSet = function ( page ) {
// When switching to the general settings page, focus the surface
if ( page === this.generalSettingsPage ) {
this.captionSurface.focus();
}
};
/**
* @inheritdoc
*/
ve.ui.MWMediaEditDialog.prototype.setup = function ( data ) {
var newDoc,
dialog = this,
doc = this.getFragment().getSurface().getDocument();
// Parent method
ve.ui.Dialog.prototype.setup.call( this, data );
// Properties
this.mediaNode = this.getFragment().getSelectedNode();
this.captionNode = this.mediaNode.getCaptionNode();
this.store = doc.getStore();
if ( this.captionNode && this.captionNode.getLength() > 0 ) {
newDoc = doc.cloneFromRange( this.captionNode.getRange() );
} else {
newDoc = new ve.dm.Document( [
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
{ 'type': '/paragraph' },
{ 'type': 'internalList' },
{ 'type': '/internalList' }
] );
}
this.captionSurface = new ve.ui.SurfaceWidget(
newDoc,
{
'$': this.$,
'tools': this.constructor.static.toolbarGroups,
'commands': this.constructor.static.surfaceCommands,
'pasteRules': this.constructor.static.pasteRules
}
);
this.$spinner.show();
this.$sizeWidgetElements.hide();
this.sizeErrorLabel.$element.hide();
// Ask for the asynchronous call to get a full scalable object
// with original dimensions and imageinfo from the API
this.mediaNode.getScalablePromise()
.done( ve.bind( function () {
this.scalable = this.mediaNode.getScalable();
this.$spinner.hide();
this.$sizeWidgetElements.show();
if (
this.mediaNode.getAttribute( 'type' ) === 'thumb' &&
this.scalable.getOriginalDimensions()
) {
// Set the max dimensions to the image's original dimensions
this.scalable.setMaxDimensions(
this.scalable.getOriginalDimensions()
);
// Tell the size widget to limit maxDimensions to image's original dimensions
this.scalable.setEnforcedMax( true );
} else {
this.scalable.setEnforcedMax( false );
}
// Send the scalable object to the size widget
this.sizeWidget.setScalable( this.scalable );
this.scalable.setDefaultDimensions(
this.scalable.getDimensionsFromValue( { 'width': this.defaultThumbSize } )
);
}, this ) )
.fail( ve.bind( function () {
dialog.sizeErrorLabel.$element.show();
}, this ) );
// Initialize size
this.sizeWidget.setSizeType(
this.mediaNode.getAttribute( 'defaultSize' ) ?
'default' :
'custom'
);
// Set initial alt text
this.altTextInput.setValue( this.mediaNode.getAttribute( 'alt' ) || '' );
// Set initial position
if (
!this.mediaNode.getAttribute( 'align' ) ||
this.mediaNode.getAttribute( 'align' ) === 'none'
) {
this.positionCheckbox.setValue( false );
this.positionInput.setDisabled( true );
this.positionInput.selectItem( null );
} else {
this.positionCheckbox.setValue( true );
this.positionInput.setDisabled( false );
if ( this.mediaNode.getAttribute( 'align' ) === 'default' ) {
// Assume wiki default according to wiki dir
if ( this.getFragment().getDocument().getDir() === 'ltr' ) {
// Assume default is 'right'
this.positionInput.selectItem(
this.positionInput.getItemFromData( 'right' )
);
} else {
// Assume default is 'left'
this.positionInput.selectItem(
this.positionInput.getItemFromData( 'left' )
);
}
} else {
this.positionInput.selectItem(
this.positionInput.getItemFromData( this.mediaNode.getAttribute( 'align' ) )
);
}
}
// Border flag
this.borderCheckbox.setValue( !!this.mediaNode.getAttribute( 'borderImage' ) );
// Set image type
this.typeInput.selectItem( null );
if ( this.mediaNode.getAttribute( 'type' ) !== undefined ) {
this.typeInput.selectItem(
this.typeInput.getItemFromData( this.mediaNode.getAttribute( 'type' ) )
);
} else {
// Explicitly show 'none' if no type was specified
this.typeInput.selectItem(
this.typeInput.getItemFromData( 'none' )
);
}
// Initialization
this.captionFieldset.$element.append( this.captionSurface.$element );
this.captionSurface.initialize();
};
/**
* Respond to 'open' event. Fires once the dialog has finished opening.
*/
ve.ui.MWMediaEditDialog.prototype.onOpen = function () {
// Focus the caption surface
this.captionSurface.focus();
};
/**
* @inheritdoc
*/
ve.ui.MWMediaEditDialog.prototype.teardown = function ( data ) {
var newDoc, doc, originalAlt, attr, transactionAttributes = {},
imageSizeType, imageType, imageAlignmentCheckbox,
imageAlignmentValue, originalDimensions,
surfaceModel = this.getFragment().getSurface();
// Data initialization
data = data || {};
if ( data.action === 'apply' ) {
newDoc = this.captionSurface.getSurface().getModel().getDocument();
doc = surfaceModel.getDocument();
if ( !this.captionNode ) {
// Insert a new caption at the beginning of the image node
surfaceModel.getFragment()
.adjustRange( 1 )
.collapseRangeToStart()
.insertContent( [ { 'type': 'mwImageCaption' }, { 'type': '/mwImageCaption' } ] );
this.captionNode = this.mediaNode.getCaptionNode();
}
// Replace the contents of the caption
surfaceModel.change(
ve.dm.Transaction.newFromRemoval( doc, this.captionNode.getRange(), true )
);
surfaceModel.change(
ve.dm.Transaction.newFromDocumentInsertion( doc, this.captionNode.getRange().start, newDoc )
);
// Get all the details and their fallbacks
imageSizeType = this.sizeWidget.getSizeType() || 'default';
imageType = this.typeInput.getSelectedItem() ? this.typeInput.getSelectedItem().getData() : '';
imageAlignmentCheckbox = this.positionCheckbox.getValue();
if ( imageAlignmentCheckbox && this.positionInput.getSelectedItem() ) {
imageAlignmentValue = this.positionInput.getSelectedItem().getData();
}
// Size and scalabletravaganza
attr = null;
if ( imageSizeType === 'default' ) {
transactionAttributes.defaultSize = true;
originalDimensions = this.scalable.getOriginalDimensions();
// Figure out the default size
if ( imageType === 'thumb' || imageType === 'frame' ) {
// Default is thumb-default unless the image is originally smaller
if ( originalDimensions.width > this.defaultThumbSize ) {
attr = this.scalable.getDimensionsFromValue( { 'width': this.defaultThumbSize } );
} else {
attr = originalDimensions;
}
} else {
// Default is full size
if ( originalDimensions ) {
attr = originalDimensions;
}
}
// Apply
if ( attr ) {
transactionAttributes.width = attr.width;
transactionAttributes.height = attr.height;
}
// Upright is not yet implemented in Parsoid. When it is,
// the scale properties should be implemented here
//} else if ( imageSizeType === 'scale' ) {
} else if ( imageSizeType === 'custom' && this.sizeWidget.isValid() ) {
attr = this.sizeWidget.getCurrentDimensions();
transactionAttributes.width = attr.width;
transactionAttributes.height = attr.height;
transactionAttributes.defaultSize = false;
}
// Set alternate text
attr = $.trim( this.altTextInput.getValue() );
originalAlt = this.mediaNode.getAttribute( 'alt' );
// Allow the user to submit an empty alternate text but
// not if there was no alternate text originally to avoid
// dirty diffing images with empty |alt=
if (
// If there was no original alternate text but there
// is a value now, update
( originalAlt === undefined && attr ) ||
// If original alternate text was defined, always
// update, even if the input is empty to allow the
// user to unset it
originalAlt !== undefined
) {
transactionAttributes.alt = attr;
}
if ( !imageAlignmentCheckbox ) {
// Only change to 'none' if alignment was originally
// set to anything else
if (
this.mediaNode.getAttribute( 'align' ) &&
this.mediaNode.getAttribute( 'align' ) !== 'none'
) {
transactionAttributes.align = 'none';
}
} else {
// If alignment was originally default and is still
// set to the default position according to the wiki
// content direction, do not change it
if (
(
this.mediaNode.getAttribute( 'align' ) === 'default' &&
(
this.getFragment().getDocument().getDir() === 'ltr' &&
imageAlignmentValue !== 'right'
) ||
(
this.getFragment().getDocument().getDir() === 'rtl' &&
imageAlignmentValue !== 'left'
)
) ||
this.mediaNode.getAttribute( 'align' ) !== 'default'
) {
transactionAttributes.align = imageAlignmentValue;
}
}
// Border
if (
!this.borderCheckbox.isDisabled() &&
this.borderCheckbox.getValue() === true
) {
transactionAttributes.borderImage = true;
} else {
transactionAttributes.borderImage = false;
}
// Image type
if ( imageType ) {
transactionAttributes.type = imageType;
}
surfaceModel.change(
ve.dm.Transaction.newFromAttributeChanges( doc, this.mediaNode.getOffset(), transactionAttributes )
);
}
// Cleanup
this.captionSurface.destroy();
this.captionSurface = null;
this.captionNode = null;
// Parent method
ve.ui.Dialog.prototype.teardown.call( this, data );
};
/* Registration */
ve.ui.windowFactory.register( ve.ui.MWMediaEditDialog );