mediawiki-extensions-Visual.../modules/ve-mw/ui/dialogs/ve.ui.MWGalleryDialog.js
Thalia 6fdbe9fd7e Make dialog for editing galleries
Make new graphical interface for editing existing
galleries and adding new galleries.

NB The dialog does not yet support rich text in the
image captions, nor does it provide separate fields for
e.g. link, alt text, etc. These are dependent on parsing
the text within the tag, which is yet to be implemented
by Parsoid. For now, these attributes should be
specified in wikitext in the image-specific caption
field.

Bug: T45037
Change-Id: I2b4082e991268241a15b9bbd6d85c94cdc2185f2
2016-06-07 13:53:02 +01:00

622 lines
18 KiB
JavaScript

/*!
* VisualEditor user interface MWGalleryDialog class.
*
* @copyright 2016 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Dialog for editing MediaWiki galleries.
*
* @class
* @extends ve.ui.MWExtensionDialog
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.MWGalleryDialog = function VeUiMWGalleryDialog() {
// Parent constructor
ve.ui.MWGalleryDialog.super.apply( this, arguments );
this.$element.addClass( 've-ui-mwGalleryDialog' );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWGalleryDialog, ve.ui.MWExtensionDialog );
/* Static properties */
ve.ui.MWGalleryDialog.static.name = 'gallery';
ve.ui.MWGalleryDialog.static.size = 'large';
ve.ui.MWGalleryDialog.static.title =
OO.ui.deferMsg( 'visualeditor-mwgallerydialog-title' );
ve.ui.MWGalleryDialog.static.modelClasses = [ ve.dm.MWGalleryNode ];
/* Methods */
/**
* @inheritdoc
*/
ve.ui.MWGalleryDialog.prototype.initialize = function () {
var indexLayout, imagesCard, optionsCard, menuLayout, menuPanel,
modeField, captionField, widthsField, heightsField,
perrowField, showFilenameField, classesField, stylesField;
// Parent method
ve.ui.MWGalleryDialog.super.prototype.initialize.call( this );
// States
this.highlightedItem = null;
this.searchPanelVisible = false;
// Images and options cards
indexLayout = new OO.ui.IndexLayout( {
scrollable: false,
expanded: true
} );
imagesCard = new OO.ui.CardLayout( 'images', {
label: ve.msg( 'visualeditor-mwgallerydialog-card-images' ),
expandable: false,
scrollable: false,
padded: true
} );
optionsCard = new OO.ui.CardLayout( 'options', {
label: ve.msg( 'visualeditor-mwgallerydialog-card-options' ),
expandable: false,
scrollable: false,
padded: true
} );
// Images card
// General layout
menuLayout = new OO.ui.MenuLayout();
menuPanel = new OO.ui.PanelLayout( {
padded: true,
expanded: true,
scrollable: true
} );
this.editPanel = new OO.ui.PanelLayout( {
padded: true,
expanded: true,
scrollable: true
} );
this.searchPanel = new OO.ui.PanelLayout( {
padded: true,
expanded: true,
scrollable: true
} ).toggle( false );
// Menu
this.$emptyGalleryMessage = $( '<div>' )
.addClass( 'oo-ui-element-hidden' )
.text( ve.msg( 'visualeditor-mwgallerydialog-empty-gallery-message' ) );
this.galleryGroup = new ve.ui.MWGalleryGroupWidget();
this.showSearchPanelButton = new OO.ui.ButtonWidget( {
label: ve.msg( 'visualeditor-mwgallerydialog-search-button-label' ),
flags: [ 'progressive' ],
classes: [ 've-ui-mwGalleryDialog-show-search-panel-button' ]
} );
// Edit panel
this.$highlightedImage = $( '<div>' )
.addClass( 've-ui-mwGalleryDialog-highlighted-image' );
// TODO: make into a ve.ui.MWTargetWidget once Parsoid handles galleries
this.highlightedCaptionInput = new OO.ui.TextInputWidget( {
placeholder: 'Image caption'
} );
this.removeButton = new OO.ui.ButtonWidget( {
label: ve.msg( 'visualeditor-mwgallerydialog-remove-button-label' ),
flags: [ 'destructive' ],
classes: [ 've-ui-mwGalleryDialog-remove-button' ]
} );
// Search panel
this.searchWidget = new ve.ui.MWMediaSearchWidget();
// Options card
// Input widgets
this.modeDropdown = new OO.ui.DropdownWidget( {
menu: {
items: [
new OO.ui.OptionWidget( {
data: 'traditional',
label: ve.msg( 'visualeditor-mwgallerydialog-mode-dropdown-label-traditional' )
} ),
new OO.ui.OptionWidget( {
data: 'nolines',
label: ve.msg( 'visualeditor-mwgallerydialog-mode-dropdown-label-nolines' )
} ),
new OO.ui.OptionWidget( {
data: 'packed',
label: ve.msg( 'visualeditor-mwgallerydialog-mode-dropdown-label-packed' )
} ),
new OO.ui.OptionWidget( {
data: 'packed-overlay',
label: ve.msg( 'visualeditor-mwgallerydialog-mode-dropdown-label-packed-overlay' )
} ),
new OO.ui.OptionWidget( {
data: 'packed-hover',
label: ve.msg( 'visualeditor-mwgallerydialog-mode-dropdown-label-packed-hover' )
} )
]
}
} );
this.captionInput = new OO.ui.TextInputWidget( {
placeholder: ve.msg( 'visualeditor-mwgallerydialog-caption-input-placeholder' )
} );
this.widthsInput = new OO.ui.NumberInputWidget( {
min: 0,
showButtons: false
} );
this.heightsInput = new OO.ui.NumberInputWidget( {
min: 0,
showButtons: false
} );
this.perrowInput = new OO.ui.NumberInputWidget( {
min: 0,
showButtons: false
} );
this.showFilenameCheckbox = new OO.ui.CheckboxInputWidget( {
value: 'yes'
} );
this.classesInput = new OO.ui.TextInputWidget( {
placeholder: ve.msg( 'visualeditor-mwgallerydialog-classes-input-placeholder' )
} );
this.stylesInput = new OO.ui.TextInputWidget( {
placeholder: ve.msg( 'visualeditor-mwgallerydialog-styles-input-placeholder' )
} );
// Field layouts
modeField = new OO.ui.FieldLayout( this.modeDropdown, {
label: ve.msg( 'visualeditor-mwgallerydialog-mode-field-label' )
} );
captionField = new OO.ui.FieldLayout( this.captionInput, {
label: ve.msg( 'visualeditor-mwgallerydialog-caption-field-label' )
} );
widthsField = new OO.ui.FieldLayout( this.widthsInput, {
label: ve.msg( 'visualeditor-mwgallerydialog-widths-field-label' )
} );
heightsField = new OO.ui.FieldLayout( this.heightsInput, {
label: ve.msg( 'visualeditor-mwgallerydialog-heights-field-label' )
} );
perrowField = new OO.ui.FieldLayout( this.perrowInput, {
label: ve.msg( 'visualeditor-mwgallerydialog-perrow-field-label' )
} );
showFilenameField = new OO.ui.FieldLayout( this.showFilenameCheckbox, {
label: ve.msg( 'visualeditor-mwgallerydialog-show-filename-field-label' )
} );
classesField = new OO.ui.FieldLayout( this.classesInput, {
label: ve.msg( 'visualeditor-mwgallerydialog-classes-field-label' )
} );
stylesField = new OO.ui.FieldLayout( this.stylesInput, {
label: ve.msg( 'visualeditor-mwgallerydialog-styles-field-label' )
} );
// Append everything
menuLayout.$menu.append(
menuPanel.$element.append(
this.$emptyGalleryMessage,
this.galleryGroup.$element,
this.showSearchPanelButton.$element
)
);
menuLayout.$content.append(
this.editPanel.$element.append(
this.$highlightedImage,
this.highlightedCaptionInput.$element,
this.removeButton.$element
),
this.searchPanel.$element.append(
this.searchWidget.$element
)
);
imagesCard.$element.append(
menuLayout.$element
);
optionsCard.$element.append(
modeField.$element,
captionField.$element,
widthsField.$element,
heightsField.$element,
perrowField.$element,
showFilenameField.$element,
classesField.$element,
stylesField.$element
);
indexLayout.addCards( [
imagesCard,
optionsCard
] );
this.$body.append( indexLayout.$element );
};
/**
* @inheritdoc
*/
ve.ui.MWGalleryDialog.prototype.getSetupProcess = function ( data ) {
return ve.ui.MWGalleryDialog.super.prototype.getSetupProcess.call( this, data )
.next( function () {
var titlesString, title, titleText, imageTitles, mode,
caption, widths, heights, perrow,
showFilename, classes, styles,
dialog = this,
attributes = this.selectedNode && this.selectedNode.getAttribute( 'mw' ).attrs;
// Images card
// If editing an existing gallery, populate with the images...
if ( this.selectedNode ) {
imageTitles = [];
this.imageData = [];
// Get image and caption data
// TODO: Can be multiple pipes. See parser.php -> renderImageGallery in MediaWiki
$.trim( this.selectedNode.getAttribute( 'mw' ).body.extsrc )
.split( '\n' ).forEach( function ( line ) {
var matches;
// Match lines like:
// Image:someimage.jpg|This is some image
matches = line.match( /^([^|]+)(\|(.*))?$/ );
// Ignore any empty lines
if ( matches ) {
title = mw.Title.newFromText( matches[ 1 ] );
// Ignore any invalid titles
// (which will result in title being null)
if ( title ) {
titleText = title.getPrefixedText();
imageTitles.push( titleText );
dialog.imageData.push( {
title: titleText,
caption: matches[ 3 ]
} );
}
}
}
);
// Populate menu and edit panels
titlesString = imageTitles.join( '|' );
this.imagesPromise = this.requestImages( {
titlesString: titlesString
} ).done( function () {
dialog.onHighlightItem();
} );
// ...Otherwise show the search panel
} else {
this.toggleEmptyGalleryMessage( true );
this.showSearchPanelButton.toggle( false );
this.toggleSearchPanel( true );
this.updateActions();
}
// Options card
// Set options
mode = attributes && attributes.mode || 'traditional';
caption = attributes && attributes.caption || '';
widths = attributes && parseInt( attributes.widths ) || '';
heights = attributes && parseInt( attributes.heights ) || '';
perrow = attributes && attributes.perrow || '';
showFilename = attributes && attributes.showfilename === 'yes';
classes = attributes && attributes.class || '';
styles = attributes && attributes.style || '';
// Populate options panel
this.modeDropdown.getMenu().selectItemByData( mode );
this.captionInput.setValue( caption );
this.widthsInput.setValue( widths );
this.heightsInput.setValue( heights );
this.perrowInput.setValue( perrow );
this.showFilenameCheckbox.setSelected( showFilename );
this.classesInput.setValue( classes );
this.stylesInput.setValue( styles );
// Add event handlers
this.searchWidget.getResults().connect( this, { choose: 'onSearchResultsChoose' } );
this.showSearchPanelButton.connect( this, { click: 'onShowSearchPanelButtonClick' } );
this.galleryGroup.connect( this, { editItem: 'onHighlightItem' } );
this.removeButton.connect( this, { click: 'onRemoveItem' } );
this.modeDropdown.getMenu().connect( this, { choose: 'onModeDropdownChange' } );
// Hack: Give the input a value so that this.insertOrUpdateNode gets called
this.input.setValue( 'gallery' );
}, this );
};
/**
* @inheritdoc
*/
ve.ui.MWGalleryDialog.prototype.getReadyProcess = function ( data ) {
return ve.ui.MWGalleryDialog.super.prototype.getReadyProcess.call( this, data )
.next( function () {
this.searchWidget.getQuery().focus();
return this.imagesPromise;
}, this );
};
/**
* @inheritdoc
*/
ve.ui.MWGalleryDialog.prototype.getTeardownProcess = function ( data ) {
return ve.ui.MWGalleryDialog.super.prototype.getTeardownProcess.call( this, data )
.first( function () {
this.galleryGroup.clearItems();
this.highlightedItem = null;
this.searchWidget.getQuery().setValue( '' );
this.searchWidget.teardown();
this.searchPanelVisible = false;
// Disconnect events
this.searchWidget.getResults().disconnect( this );
this.showSearchPanelButton.disconnect( this );
this.galleryGroup.disconnect( this );
this.removeButton.disconnect( this );
this.modeDropdown.disconnect( this );
}, this );
};
/**
* @inheritdoc
*/
ve.ui.MWGalleryDialog.prototype.getBodyHeight = function () {
return 600;
};
/**
* Request the images for the images card menu
*
* @param {Object} options Options for the request
*/
ve.ui.MWGalleryDialog.prototype.requestImages = function ( options ) {
return new mw.Api().get( {
action: 'query',
prop: 'imageinfo',
iiprop: 'url',
iiurlwidth: options.width || 200,
// Matches height of this.$highlightedImage
iiurlheight: options.height || 200,
titles: options.titlesString
} ).done( this.onRequestImagesSuccess.bind( this ) );
};
/**
* Create items for the returned images and add them to the gallery group
*
* @param {Object} deferred jQuery deferred object
* @param {Object} response jQuery response object
*/
ve.ui.MWGalleryDialog.prototype.onRequestImagesSuccess = function ( deferred, response ) {
var index,
thumbUrls = {},
items = [],
pages = response.responseJSON.query.pages;
// Store object of titles to thumbUrls
for ( index in pages ) {
if ( pages[ index ].imageinfo ) {
thumbUrls[ pages[ index ].title ] = pages[ index ].imageinfo[ 0 ].thumburl;
}
}
// Make items for every image in imageData
this.imageData.forEach( function ( image ) {
image.thumbUrl = thumbUrls[ image.title ];
items.push( new ve.ui.MWGalleryItemWidget( image ) );
} );
this.galleryGroup.addItems( items );
// Gallery is no longer empty
this.updateActions();
this.toggleEmptyGalleryMessage( false );
this.showSearchPanelButton.toggle( true );
};
/**
* Request a new image and highlight it
*
* @param {string} title File name for the new image
*/
ve.ui.MWGalleryDialog.prototype.addNewImage = function ( title ) {
var dialog = this;
// Reset this.imageData, for onRequestImagesSuccess
this.imageData = [ {
title: title,
caption: ''
} ];
// Request image
this.requestImages( {
titlesString: title
} ).done( function () {
// populate edit panel with the new image
var items = dialog.galleryGroup.items;
dialog.onHighlightItem( items[ items.length - 1 ] );
dialog.highlightedCaptionInput.focus();
} );
};
/**
* Handle search results choose event.
*
* @param {ve.ui.MWMediaResultWidget} item Chosen item
*/
ve.ui.MWGalleryDialog.prototype.onSearchResultsChoose = function ( item ) {
this.addNewImage( item.getData().title );
};
/**
* Handle click event for the remove button
*/
ve.ui.MWGalleryDialog.prototype.onRemoveItem = function () {
// Remove the highlighted item
this.galleryGroup.removeItems( [ this.highlightedItem ] );
// Show the search panel if the gallery is now empty...
if ( this.galleryGroup.items.length === 0 ) {
this.toggleEmptyGalleryMessage( true );
this.showSearchPanelButton.toggle( false );
this.toggleSearchPanel( true );
// ...Otherwise highlight the first item in the gallery
} else {
this.onHighlightItem();
}
this.updateActions();
};
/**
* Handle clicking on an image in the menu
*
* @param {ve.ui.MWGalleryItemWidget} item The item that was clicked on
*/
ve.ui.MWGalleryDialog.prototype.onHighlightItem = function ( item ) {
// Unhighlight previous item
if ( this.highlightedItem ) {
this.highlightedItem.toggleHighlighted( false );
}
// Show edit panel
this.toggleSearchPanel( false );
// Highlight new item
item = item ? item : this.galleryGroup.items[ 0 ];
item.toggleHighlighted( true );
this.highlightedItem = item;
// Populate edit panel
this.$highlightedImage
.css( 'background-image', 'url(' + item.thumbUrl + ')' );
this.highlightedCaptionInput
.setValue( item.caption );
};
/**
* Handle change event for this.modeDropdown
*/
ve.ui.MWGalleryDialog.prototype.onModeDropdownChange = function () {
var mode = this.modeDropdown.getMenu().getSelectedItem().getData(),
disabled = mode === 'packed' || mode === 'packed-overlay' || mode === 'packed-hover';
this.widthsInput.setDisabled( disabled );
this.perrowInput.setDisabled( disabled );
};
/**
* Handle click event for showSearchPanelButton
*/
ve.ui.MWGalleryDialog.prototype.onShowSearchPanelButtonClick = function () {
this.toggleSearchPanel( true );
};
/**
* Toggle the search panel (and the edit panel, the opposite way)
*
* @param {boolean} visible The search panel is visible
*/
ve.ui.MWGalleryDialog.prototype.toggleSearchPanel = function ( visible ) {
visible = visible !== undefined ? visible : !this.searchPanelVisible;
// If currently visible panel is an edit panel, store the caption
if ( !this.searchPanelVisible && this.highlightedItem ) {
this.highlightedItem.setCaption(
this.highlightedCaptionInput.getValue()
);
}
// Record the state of the search panel
this.searchPanelVisible = visible;
// Toggle the search panel, and do the opposite for the edit panel
this.searchPanel.toggle( visible );
this.editPanel.toggle( !visible );
// If the edit panel is visible, focus the caption input
if ( !visible ) {
this.highlightedCaptionInput.focus();
} else {
this.searchWidget.getQuery().focus();
}
};
/**
* Toggle the empty gallery message
*
* @param {boolean} empty The gallery is empty
*/
ve.ui.MWGalleryDialog.prototype.toggleEmptyGalleryMessage = function ( empty ) {
if ( empty ) {
this.$emptyGalleryMessage.removeClass( 'oo-ui-element-hidden' );
} else {
this.$emptyGalleryMessage.addClass( 'oo-ui-element-hidden' );
}
};
/**
* Disable the "Done" button if the gallery is empty, otherwise enable it
*/
ve.ui.MWGalleryDialog.prototype.updateActions = function () {
this.actions.setAbilities( { done: this.galleryGroup.items.length > 0 } );
};
/**
* @inheritdoc
*/
ve.ui.MWGalleryDialog.prototype.updateMwData = function ( mwData ) {
var i, ilen, mode, caption, widths, heights, perrow,
showFilename, classes, styles,
selectedNode = this.selectedNode,
oldMode = selectedNode && selectedNode.getAttribute( 'mw' ).attrs.mode,
extsrc = '',
items = this.galleryGroup.items;
// Parent method
ve.ui.MWGalleryDialog.super.prototype.updateMwData.call( this, mwData );
// Get titles and captions from gallery group
this.highlightedItem.setCaption( this.highlightedCaptionInput.getValue() );
for ( i = 0, ilen = items.length; i < ilen; i++ ) {
extsrc += '\n' + items[ i ].imageTitle + '|' + items[ i ].caption;
}
// Get data from options card
mode = this.modeDropdown.getMenu().getSelectedItem().getData();
if ( oldMode === undefined && mode === 'traditional' ) {
mode = undefined;
}
caption = this.captionInput.getValue();
widths = this.widthsInput.getValue();
heights = this.heightsInput.getValue();
perrow = this.perrowInput.getValue();
showFilename = this.showFilenameCheckbox.isSelected();
classes = this.classesInput.getValue();
styles = this.stylesInput.getValue();
// Update extsrc and attributes
mwData.body.extsrc = extsrc + '\n';
mwData.attrs.mode = mode || undefined;
mwData.attrs.caption = caption || undefined;
mwData.attrs.widths = widths + 'px' || undefined;
mwData.attrs.heights = heights + 'px' || undefined;
mwData.attrs.perrow = perrow || undefined;
mwData.attrs.showfilename = showFilename ? 'yes' : undefined;
mwData.attrs.classes = classes || undefined;
mwData.attrs.styles = styles || undefined;
};
/* Registration */
ve.ui.windowFactory.register( ve.ui.MWGalleryDialog );