mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 09:26:37 +00:00
cb657e0bdf
Change-Id: I9dbc12335016a326f00bf5b9a960551f828c685d
525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
/*!
|
|
* VisualEditor user interface MWTemplateDialog class.
|
|
*
|
|
* @copyright See AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* Abstract base class for dialogs that allow to insert and edit MediaWiki transclusions, i.e. a
|
|
* sequence of one or more template invocations that strictly belong to each other (e.g. because
|
|
* they are unbalanced), possibly mixed with raw wikitext snippets. Currently used for:
|
|
* - {@see ve.ui.MWTransclusionDialog} for arbitrary transclusions. Registered via the name
|
|
* "transclusion".
|
|
* - {@see ve.ui.MWCitationDialog} in the Cite extension for the predefined citation types from
|
|
* [[MediaWiki:visualeditor-cite-tool-definition.json]]. These are strictly limited to a single
|
|
* template invocation. Registered via the name "cite".
|
|
*
|
|
* @class
|
|
* @abstract
|
|
* @extends ve.ui.NodeDialog
|
|
*
|
|
* @constructor
|
|
* @param {Object} [config] Configuration options
|
|
* @property {ve.dm.MWTransclusionModel|null} transclusionModel
|
|
* @property {ve.ui.MWTransclusionOutlineWidget} sidebar
|
|
* @property {boolean} [canGoBack=false]
|
|
*/
|
|
ve.ui.MWTemplateDialog = function VeUiMWTemplateDialog( config ) {
|
|
// Parent constructor
|
|
ve.ui.MWTemplateDialog.super.call( this, config );
|
|
|
|
// Properties
|
|
this.transclusionModel = null;
|
|
this.loaded = false;
|
|
this.altered = false;
|
|
this.canGoBack = false;
|
|
this.preventReselection = false;
|
|
|
|
this.confirmDialogs = new ve.ui.WindowManager( { factory: ve.ui.windowFactory, isolate: true } );
|
|
$( OO.ui.getTeleportTarget() ).append( this.confirmDialogs.$element );
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.inheritClass( ve.ui.MWTemplateDialog, ve.ui.NodeDialog );
|
|
|
|
/* Static Properties */
|
|
|
|
ve.ui.MWTemplateDialog.static.modelClasses = [ ve.dm.MWTransclusionNode ];
|
|
|
|
/**
|
|
* Configuration for the {@see ve.ui.MWTwoPaneTransclusionDialogLayout} used in this dialog.
|
|
*
|
|
* @static
|
|
* @property {Object}
|
|
* @inheritable
|
|
*/
|
|
ve.ui.MWTemplateDialog.static.bookletLayoutConfig = {};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getReadyProcess = function ( data ) {
|
|
return ve.ui.MWTemplateDialog.super.prototype.getReadyProcess.call( this, data )
|
|
.next( () => {
|
|
if ( this.transclusionModel.isEmpty() ) {
|
|
// Focus the template placeholder input field.
|
|
this.bookletLayout.focus();
|
|
}
|
|
|
|
this.bookletLayout.getPagesOrdered().forEach( ( page ) => {
|
|
if ( page instanceof ve.ui.MWParameterPage ) {
|
|
page.updateSize();
|
|
}
|
|
} );
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Update dialog actions whenever the content changes.
|
|
*
|
|
* @private
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.touch = function () {
|
|
if ( this.loaded ) {
|
|
this.altered = true;
|
|
this.setApplicableStatus();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle parts being replaced.
|
|
*
|
|
* @protected
|
|
* @param {ve.dm.MWTransclusionPartModel|null} removed Removed part
|
|
* @param {ve.dm.MWTransclusionPartModel|null} added Added part
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.onReplacePart = function ( removed, added ) {
|
|
const removePages = [];
|
|
|
|
if ( removed ) {
|
|
// Remove parameter pages of removed templates
|
|
if ( removed instanceof ve.dm.MWTemplateModel ) {
|
|
const params = removed.getParameters();
|
|
for ( const name in params ) {
|
|
removePages.push( params[ name ].getId() );
|
|
}
|
|
removed.disconnect( this );
|
|
}
|
|
removePages.push( removed.getId() );
|
|
this.bookletLayout.removePages( removePages );
|
|
}
|
|
|
|
if ( added ) {
|
|
const page = this.getPageFromPart( added );
|
|
if ( page ) {
|
|
let reselect;
|
|
|
|
this.bookletLayout.addPages( [ page ], this.transclusionModel.getIndex( added ) );
|
|
if ( removed ) {
|
|
// When we're replacing a part, it can only be a template placeholder
|
|
// becoming an actual template. Focus this new template.
|
|
reselect = added.getId();
|
|
}
|
|
|
|
if ( added instanceof ve.dm.MWTemplateModel ) {
|
|
// Prevent selection changes while parameters are added
|
|
this.preventReselection = true;
|
|
|
|
// Add existing params to templates (the template might be being moved)
|
|
const names = added.getOrderedParameterNames();
|
|
for ( let i = 0; i < names.length; i++ ) {
|
|
this.onAddParameter( added.getParameter( names[ i ] ) );
|
|
}
|
|
added.connect( this, { add: 'onAddParameter', remove: 'onRemoveParameter' } );
|
|
|
|
this.preventReselection = false;
|
|
|
|
if ( this.loaded ) {
|
|
if ( reselect ) {
|
|
this.bookletLayout.focusPart( reselect );
|
|
}
|
|
}
|
|
|
|
const documentedParameters = added.getSpec().getDocumentedParameterOrder(),
|
|
undocumentedParameters = added.getSpec().getUndocumentedParameterNames();
|
|
|
|
if ( !documentedParameters.length || undocumentedParameters.length ) {
|
|
page.addPlaceholderParameter();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( added || removed ) {
|
|
this.touch();
|
|
}
|
|
this.updateTitle();
|
|
};
|
|
|
|
/**
|
|
* Handle add param events.
|
|
*
|
|
* @private
|
|
* @param {ve.dm.MWParameterModel} param Added param
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.onAddParameter = function ( param ) {
|
|
let page;
|
|
|
|
if ( param.getName() ) {
|
|
page = new ve.ui.MWParameterPage( param, {
|
|
$overlay: this.$overlay, readOnly: this.isReadOnly()
|
|
} )
|
|
.connect( this, {
|
|
hasValueChange: 'onHasValueChange'
|
|
} );
|
|
} else {
|
|
// Create parameter placeholder.
|
|
page = new ve.ui.MWAddParameterPage( param, param.getId(), {
|
|
$overlay: this.$overlay
|
|
} )
|
|
.connect( this, {
|
|
templateParameterAdded: this.bookletLayout.focusPart.bind( this.bookletLayout )
|
|
} );
|
|
}
|
|
this.bookletLayout.addPages( [ page ], this.transclusionModel.getIndex( param ) );
|
|
if ( this.loaded ) {
|
|
this.touch();
|
|
|
|
if ( page instanceof ve.ui.MWParameterPage ) {
|
|
page.updateSize();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle remove param events.
|
|
*
|
|
* @private
|
|
* @param {ve.dm.MWParameterModel} param Removed param
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.onRemoveParameter = function ( param ) {
|
|
this.bookletLayout.removePages( [ param.getId() ] );
|
|
|
|
this.touch();
|
|
};
|
|
|
|
/**
|
|
* Sets transclusion applicable status
|
|
*
|
|
* If the transclusion is empty or only contains a placeholder it will not be insertable.
|
|
* If the transclusion only contains a placeholder it will not be editable.
|
|
*
|
|
* @private
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.setApplicableStatus = function () {
|
|
const canSave = !this.transclusionModel.isEmpty();
|
|
this.actions.setAbilities( { done: canSave && this.altered } );
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getBodyHeight = function () {
|
|
return 400;
|
|
};
|
|
|
|
/**
|
|
* Get a page for a transclusion part.
|
|
*
|
|
* @protected
|
|
* @param {ve.dm.MWTransclusionModel} part Part to get page for
|
|
* @return {OO.ui.PageLayout|null} Page for part, null if no matching page could be found
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getPageFromPart = function ( part ) {
|
|
if ( part instanceof ve.dm.MWTemplateModel ) {
|
|
return new ve.ui.MWTemplatePage( part, part.getId(), { $overlay: this.$overlay, isReadOnly: this.isReadOnly() } );
|
|
} else if ( part instanceof ve.dm.MWTemplatePlaceholderModel ) {
|
|
return new ve.ui.MWTemplatePlaceholderPage(
|
|
part,
|
|
part.getId(),
|
|
{ $overlay: this.$overlay }
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getSelectedNode = function ( data ) {
|
|
const selectedNode = ve.ui.MWTemplateDialog.super.prototype.getSelectedNode.call( this );
|
|
|
|
// Data initialization
|
|
data = data || {};
|
|
|
|
// Require template to match if specified
|
|
if ( selectedNode && data.template && !selectedNode.isSingleTemplate( data.template ) ) {
|
|
return null;
|
|
}
|
|
|
|
return selectedNode;
|
|
};
|
|
|
|
/**
|
|
* Update the dialog title.
|
|
*
|
|
* @protected
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.updateTitle = function () {
|
|
let title = ve.msg( 'visualeditor-dialog-transclusion-loading' );
|
|
|
|
if ( this.transclusionModel.isSingleTemplate() ) {
|
|
const part = this.transclusionModel.getParts()[ 0 ];
|
|
if ( part instanceof ve.dm.MWTemplateModel ) {
|
|
title = ve.msg(
|
|
this.getMode() === 'insert' ?
|
|
'visualeditor-dialog-transclusion-title-insert-known-template' :
|
|
'visualeditor-dialog-transclusion-title-edit-known-template',
|
|
part.getSpec().getLabel()
|
|
);
|
|
} else {
|
|
title = ve.msg( 'visualeditor-dialog-transclusion-title-insert-template' );
|
|
}
|
|
}
|
|
this.title.setLabel( title );
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.initialize = function () {
|
|
// Parent method
|
|
ve.ui.MWTemplateDialog.super.prototype.initialize.call( this );
|
|
|
|
// Properties
|
|
this.bookletLayout = new ve.ui.MWTwoPaneTransclusionDialogLayout( this.constructor.static.bookletLayoutConfig );
|
|
// TODO: Remove once all references are gone.
|
|
this.sidebar = this.bookletLayout.sidebar;
|
|
|
|
// Initialization
|
|
this.$content.addClass( 've-ui-mwTemplateDialog' );
|
|
// bookletLayout is appended after the form has been built in getSetupProcess for performance
|
|
};
|
|
|
|
/**
|
|
* If the user has left blank required parameters, confirm that they actually want to do this.
|
|
* If no required parameters were left blank, or if they were but the user decided to go ahead
|
|
* anyway, the returned deferred will be resolved.
|
|
* Otherwise, the returned deferred will be rejected.
|
|
*
|
|
* @private
|
|
* @return {jQuery.Deferred}
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.checkRequiredParameters = function () {
|
|
const blankRequired = [],
|
|
deferred = ve.createDeferred();
|
|
|
|
this.bookletLayout.stackLayout.getItems().forEach( ( page ) => {
|
|
if ( !( page instanceof ve.ui.MWParameterPage ) ) {
|
|
return;
|
|
}
|
|
if ( page.parameter.isRequired() && !page.valueInput.getValue() ) {
|
|
blankRequired.push( mw.msg(
|
|
'quotation-marks',
|
|
page.parameter.template.getSpec().getParameterLabel( page.parameter.getName() )
|
|
) );
|
|
}
|
|
} );
|
|
if ( blankRequired.length ) {
|
|
this.confirmDialogs.openWindow( 'requiredparamblankconfirm', {
|
|
message: mw.msg(
|
|
'visualeditor-dialog-transclusion-required-parameter-is-blank',
|
|
mw.language.listToText( blankRequired ),
|
|
blankRequired.length
|
|
),
|
|
title: mw.msg(
|
|
'visualeditor-dialog-transclusion-required-parameter-dialog-title',
|
|
blankRequired.length
|
|
)
|
|
} ).closed.then( ( data ) => {
|
|
if ( data && data.action === 'ok' ) {
|
|
deferred.resolve();
|
|
} else {
|
|
deferred.reject();
|
|
}
|
|
} );
|
|
} else {
|
|
deferred.resolve();
|
|
}
|
|
return deferred.promise();
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getActionProcess = function ( action ) {
|
|
if ( action === 'done' ) {
|
|
return new OO.ui.Process( () => {
|
|
const deferred = ve.createDeferred();
|
|
this.checkRequiredParameters().done( () => {
|
|
const surfaceModel = this.getFragment().getSurface(),
|
|
obj = this.transclusionModel.getPlainObject();
|
|
|
|
this.pushPending();
|
|
|
|
let modelPromise = ve.createDeferred().resolve().promise();
|
|
if ( this.selectedNode instanceof ve.dm.MWTransclusionNode ) {
|
|
this.transclusionModel.updateTransclusionNode( surfaceModel, this.selectedNode );
|
|
// TODO: updating the node could result in the inline/block state change
|
|
} else if ( obj !== null ) {
|
|
// Collapse returns a new fragment, so update this.fragment
|
|
this.fragment = this.getFragment().collapseToEnd();
|
|
modelPromise = this.transclusionModel.insertTransclusionNode( this.getFragment() );
|
|
}
|
|
|
|
// TODO tracking will only be implemented temporarily to answer questions on
|
|
// template usage for the Technical Wishes topic area see T258917
|
|
const templateEvent = {
|
|
action: 'save',
|
|
// eslint-disable-next-line camelcase
|
|
template_names: []
|
|
};
|
|
const editCountBucket = mw.config.get( 'wgUserEditCountBucket' );
|
|
if ( editCountBucket !== null ) {
|
|
// eslint-disable-next-line camelcase
|
|
templateEvent.user_edit_count_bucket = editCountBucket;
|
|
}
|
|
const parts = this.transclusionModel.getParts();
|
|
for ( let i = 0; i < parts.length; i++ ) {
|
|
// Only {@see ve.dm.MWTemplateModel} have a title
|
|
const title = parts[ i ].getTitle && parts[ i ].getTitle();
|
|
if ( title ) {
|
|
templateEvent.template_names.push( title );
|
|
}
|
|
}
|
|
mw.track( 'event.VisualEditorTemplateDialogUse', templateEvent );
|
|
|
|
return modelPromise.then( () => {
|
|
this.close( { action: action } ).closed.always( this.popPending.bind( this ) );
|
|
} );
|
|
} ).always( deferred.resolve );
|
|
|
|
return deferred;
|
|
} );
|
|
}
|
|
|
|
return ve.ui.MWTemplateDialog.super.prototype.getActionProcess.call( this, action );
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getSetupProcess = function ( data ) {
|
|
data = data || {};
|
|
return ve.ui.MWTemplateDialog.super.prototype.getSetupProcess.call( this, data )
|
|
.next( () => {
|
|
let promise;
|
|
|
|
// Properties
|
|
this.loaded = false;
|
|
this.altered = false;
|
|
this.transclusionModel = new ve.dm.MWTransclusionModel( this.getFragment().getDocument() );
|
|
|
|
// Events
|
|
this.transclusionModel.connect( this, {
|
|
replace: 'onReplacePart',
|
|
change: 'touch'
|
|
} );
|
|
|
|
// Detach the form while building for performance
|
|
this.bookletLayout.$element.detach();
|
|
|
|
this.transclusionModel.connect( this.bookletLayout, { replace: 'onReplacePart' } );
|
|
|
|
// Initialization
|
|
if ( !this.selectedNode ) {
|
|
if ( data.template ) {
|
|
// The template name is from MediaWiki:Visualeditor-cite-tool-definition.json,
|
|
// passed via a ve.ui.Command, which triggers a ve.ui.MWCitationAction, which
|
|
// executes ve.ui.WindowAction.open(), which opens this dialog.
|
|
const template = ve.dm.MWTemplateModel.newFromName(
|
|
this.transclusionModel, data.template
|
|
);
|
|
promise = this.transclusionModel.addPart( template );
|
|
} else {
|
|
// Open the dialog to add a new template, always starting with a placeholder
|
|
const placeholderPage = new ve.dm.MWTemplatePlaceholderModel( this.transclusionModel );
|
|
promise = this.transclusionModel.addPart( placeholderPage );
|
|
promise.then( () => {
|
|
this.bookletLayout.setPage( placeholderPage.getId() );
|
|
} );
|
|
this.canGoBack = true;
|
|
}
|
|
} else {
|
|
// Open the dialog to edit an existing template
|
|
|
|
// TODO tracking will only be implemented temporarily to answer questions on
|
|
// template usage for the Technical Wishes topic area see T258917
|
|
const templateEvent = {
|
|
action: 'edit',
|
|
// eslint-disable-next-line camelcase
|
|
template_names: []
|
|
};
|
|
const editCountBucket = mw.config.get( 'wgUserEditCountBucket' );
|
|
if ( editCountBucket !== null ) {
|
|
// eslint-disable-next-line camelcase
|
|
templateEvent.user_edit_count_bucket = editCountBucket;
|
|
}
|
|
for ( let i = 0; i < this.selectedNode.partsList.length; i++ ) {
|
|
if ( this.selectedNode.partsList[ i ].templatePage ) {
|
|
templateEvent.template_names.push( this.selectedNode.partsList[ i ].templatePage );
|
|
}
|
|
}
|
|
mw.track( 'event.VisualEditorTemplateDialogUse', templateEvent );
|
|
|
|
promise = this.transclusionModel
|
|
.load( ve.copy( this.selectedNode.getAttribute( 'mw' ) ) )
|
|
.then( this.initializeTemplateParameters.bind( this ) );
|
|
}
|
|
this.actions.setAbilities( { done: false } );
|
|
|
|
return promise.then( () => {
|
|
// Add missing required and suggested parameters to each transclusion.
|
|
this.transclusionModel.addPromptedParameters();
|
|
|
|
this.$body.append( this.bookletLayout.$element );
|
|
this.$element.addClass( 've-ui-mwTemplateDialog-ready' );
|
|
this.loaded = true;
|
|
} );
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Intentionally empty. This is provided for Wikia extensibility.
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.initializeTemplateParameters = function () {};
|
|
|
|
/**
|
|
* @private
|
|
* @param {string} pageName
|
|
* @param {boolean} hasValue
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.onHasValueChange = function ( pageName, hasValue ) {
|
|
this.sidebar.toggleHasValueByPageName( pageName, hasValue );
|
|
};
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
ve.ui.MWTemplateDialog.prototype.getTeardownProcess = function ( data ) {
|
|
return ve.ui.MWTemplateDialog.super.prototype.getTeardownProcess.call( this, data )
|
|
.first( () => {
|
|
// Cleanup
|
|
this.$element.removeClass( 've-ui-mwTemplateDialog-ready' );
|
|
this.transclusionModel.disconnect( this );
|
|
this.transclusionModel.abortAllApiRequests();
|
|
this.transclusionModel = null;
|
|
this.bookletLayout.clearPages();
|
|
this.content = null;
|
|
} );
|
|
};
|