mediawiki-extensions-Visual.../modules/ve-mw/ui/dialogs/ve.ui.MWTemplateDialog.js
Christian Williams 1fc13cce68 Make MWTransclusionModel and MWTemplateDialog extensible
Wikia has done some work on the template user experience, including
automatically showing all available parameters without the use of
TemplateData. In order to make our changes, we had to make some changes
to VE-MW.

ve.dm.MWTransclusionModel.js
* this.specCache is created so subclasses can reference it.
* Promise handlers in the fetch() method have been broken out as class
  methods so they can be overridden in subclasses.

ve.ui.MWTemplateDialog.js
* addPromptedParameters() has been moved to the
  initializeNewTemplateParameters() class method so subclasses can
  overwrite. In Wikia's implementation, we have a method of getting
  all parameters and a dialog that shows all of the parameters, so the
  request to addPromptedParameters is overwritten.
* Added a done() handler to the transclusionModel promise for Wikia
  extensibility.

Change-Id: I073c5850420e7719e82957f879423c2717af674a
2014-10-10 20:38:18 +00:00

503 lines
14 KiB
JavaScript

/*
* VisualEditor user interface MWTemplateDialog class.
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/* global mw */
/**
* Dialog for inserting and editing MediaWiki transclusions.
*
* @class
* @abstract
* @extends ve.ui.NodeDialog
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.ui.MWTemplateDialog = function VeUiMWTemplateDialog( config ) {
// Parent constructor
ve.ui.MWTemplateDialog.super.call( this, config );
// Properties
this.transclusionModel = null;
this.loaded = false;
this.preventReselection = false;
this.confirmOverlay = new ve.ui.Overlay( { classes: ['ve-ui-overlay-global'] } );
this.confirmDialogs = new OO.ui.WindowManager( { factory: ve.ui.windowFactory, isolate: true } );
this.confirmOverlay.$element.append( this.confirmDialogs.$element );
$( 'body' ).append( this.confirmOverlay.$element );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWTemplateDialog, ve.ui.NodeDialog );
/* Static Properties */
ve.ui.MWTemplateDialog.static.icon = 'template';
ve.ui.MWTemplateDialog.static.modelClasses = [ ve.dm.MWTransclusionNode ];
ve.ui.MWTemplateDialog.static.actions = [
{
action: 'apply',
label: OO.ui.deferMsg( 'visualeditor-dialog-action-apply' ),
flags: 'primary',
modes: 'edit'
},
{
action: 'insert',
label: OO.ui.deferMsg( 'visualeditor-dialog-action-insert' ),
flags: [ 'primary', 'constructive' ],
modes: 'insert'
},
{
label: OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),
flags: 'safe',
modes: [ 'insert', 'edit' ]
}
];
/**
* Configuration for booklet layout.
*
* @static
* @property {Object}
* @inheritable
*/
ve.ui.MWTemplateDialog.static.bookletLayoutConfig = {
continuous: true,
outlined: false
};
/* Methods */
/**
* Handle the transclusion being ready to use.
*/
ve.ui.MWTemplateDialog.prototype.onTransclusionReady = function () {
this.loaded = true;
this.$element.addClass( 've-ui-mwTemplateDialog-ready' );
this.popPending();
};
/**
* Handle parts being replaced.
*
* @param {ve.dm.MWTransclusionPartModel} removed Removed part
* @param {ve.dm.MWTransclusionPartModel} added Added part
*/
ve.ui.MWTemplateDialog.prototype.onReplacePart = function ( removed, added ) {
var i, len, page, name, names, params, partPage, reselect, insertable, addedCount,
removePages = [];
if ( removed ) {
// Remove parameter pages of removed templates
partPage = this.bookletLayout.getPage( removed.getId() );
if ( removed instanceof ve.dm.MWTemplateModel ) {
params = removed.getParameters();
for ( name in params ) {
removePages.push( this.bookletLayout.getPage( params[name].getId() ) );
}
removed.disconnect( this );
}
if ( this.loaded && !this.preventReselection && partPage.isActive() ) {
reselect = this.bookletLayout.getClosestPage( partPage );
}
removePages.push( partPage );
this.bookletLayout.removePages( removePages );
}
if ( added ) {
page = this.getPageFromPart( added );
if ( page ) {
this.bookletLayout.addPages( [ page ], this.transclusionModel.getIndex( added ) );
if ( reselect ) {
// Use added page instead of closest page
this.setPageByName( added.getId() );
}
// Add existing params to templates (the template might be being moved)
if ( added instanceof ve.dm.MWTemplateModel ) {
names = added.getParameterNames();
params = added.getParameters();
// Prevent selection changes
this.preventReselection = true;
for ( i = 0, len = names.length; i < len; i++ ) {
this.onAddParameter( params[names[i]] );
}
this.preventReselection = false;
added.connect( this, { add: 'onAddParameter', remove: 'onRemoveParameter' } );
if ( names.length ) {
this.setPageByName( params[names[0]].getId() );
}
}
// Add required and suggested params to user created templates
if ( added instanceof ve.dm.MWTemplateModel && this.loaded ) {
// Prevent selection changes
this.preventReselection = true;
addedCount = added.addPromptedParameters();
this.preventReselection = false;
names = added.getParameterNames();
params = added.getParameters();
if ( names.length ) {
this.setPageByName( params[names[0]].getId() );
} else if ( addedCount === 0 ) {
page.onAddButtonFocus();
}
}
}
} else if ( reselect ) {
this.setPageByName( reselect.getName() );
}
insertable = this.isInsertable();
this.actions.setAbilities( { apply: insertable, insert: insertable } );
this.updateTitle();
};
/**
* Handle add param events.
*
* @param {ve.dm.MWParameterModel} param Added param
*/
ve.ui.MWTemplateDialog.prototype.onAddParameter = function ( param ) {
var page;
if ( param.getName() ) {
page = new ve.ui.MWParameterPage( param, param.getId(), { $: this.$ } );
} else {
page = new ve.ui.MWParameterPlaceholderPage( param, param.getId(), { $: this.$ } );
}
this.bookletLayout.addPages( [ page ], this.transclusionModel.getIndex( param ) );
if ( this.loaded && !this.preventReselection ) {
this.setPageByName( param.getId() );
} else {
this.onAddParameterBeforeLoad( page );
}
// Recalculate tab indexes
this.$body.find( '.ve-ui-mwParameterPage' ).each( function ( index ) {
$( this )
.find( '.ve-ui-mwParameterPage-field > .oo-ui-textInputWidget > textarea' )
.attr( 'tabindex', index * 3 + 1 )
.end()
.find( '.ve-ui-mwParameterPage-infoButton' )
.attr( 'tabindex', index * 3 + 2 )
.end()
.find( '.ve-ui-mwParameterPage-removeButton' )
.attr( 'tabindex', index * 3 + 3 )
.end()
.find( '.ve-ui-mwParameterPage-more' )
.attr( 'tabindex', index * 3 + 4 );
} );
};
/**
* Additional handling of parameter addition events before loading.
*
* @param {ve.ui.MWParameterPage} page Parameter page object
*/
ve.ui.MWTemplateDialog.prototype.onAddParameterBeforeLoad = function () {};
/**
* Handle remove param events.
*
* @param {ve.dm.MWParameterModel} param Removed param
*/
ve.ui.MWTemplateDialog.prototype.onRemoveParameter = function ( param ) {
var page = this.bookletLayout.getPage( param.getId() ),
reselect = this.bookletLayout.getClosestPage( page );
this.bookletLayout.removePages( [ page ] );
if ( this.loaded && !this.preventReselection ) {
this.setPageByName( reselect.getName() );
}
};
/**
* Checks if transclusion is in a valid state for inserting into the document
*
* If the transclusion is empty or only contains a placeholder it will not be insertable.
*
* @returns {boolean} Transclusion can be inserted
*/
ve.ui.MWTemplateDialog.prototype.isInsertable = function () {
var parts = this.transclusionModel && this.transclusionModel.getParts();
return this.loading.state() === 'resolved' &&
parts.length &&
( parts.length > 1 || !( parts[0] instanceof ve.dm.MWTemplatePlaceholderModel ) );
};
/**
* @inheritdoc
*/
ve.ui.MWTemplateDialog.prototype.getBodyHeight = function () {
return 400;
};
/**
* Get a page for a transclusion part.
*
* @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(), { $: this.$ } );
} else if ( part instanceof ve.dm.MWTemplatePlaceholderModel ) {
return new ve.ui.MWTemplatePlaceholderPage( part, part.getId(), { $: this.$ } );
}
return null;
};
/**
* Get the label of a template or template placeholder.
*
* @param {ve.dm.MWTemplateModel|ve.dm.MWTemplatePlaceholderModel} part Part to check
* @returns {string} Label of template or template placeholder
*/
ve.ui.MWTemplateDialog.prototype.getTemplatePartLabel = function ( part ) {
return part instanceof ve.dm.MWTemplateModel ?
part.getSpec().getLabel() : ve.msg( 'visualeditor-dialog-transclusion-placeholder' );
};
/**
* @inheritdoc
*/
ve.ui.MWTemplateDialog.prototype.getSelectedNode = function ( data ) {
var 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;
};
/**
* Set the page by name.
*
* Page names are always the ID of the part or param they represent.
*
* @param {string} name Page name
*/
ve.ui.MWTemplateDialog.prototype.setPageByName = function ( name ) {
if ( this.bookletLayout.isOutlined() ) {
this.bookletLayout.getOutline().selectItem(
this.bookletLayout.getOutline().getItemFromData( name )
);
} else {
this.bookletLayout.setPage( name );
}
};
/**
* Update the dialog title.
*/
ve.ui.MWTemplateDialog.prototype.updateTitle = function () {
var parts = this.transclusionModel && this.transclusionModel.getParts();
this.title.setLabel(
parts && parts.length === 1 && parts[0] ?
this.getTemplatePartLabel( parts[0] ) :
ve.msg( 'visualeditor-dialog-transclusion-loading' )
);
};
/**
* @inheritdoc
*/
ve.ui.MWTemplateDialog.prototype.initialize = function () {
// Parent method
ve.ui.MWTemplateDialog.super.prototype.initialize.call( this );
// Properties
this.panels = new OO.ui.StackLayout( { $: this.$ } );
this.bookletLayout = new OO.ui.BookletLayout(
ve.extendObject(
{ $: this.$ },
this.constructor.static.bookletLayoutConfig
)
);
// Initialization
this.$content.addClass( 've-ui-mwTemplateDialog' );
this.$body.append( this.panels.$element );
this.panels.addItems( [ this.bookletLayout ] );
};
/**
* 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.
* @returns {jQuery.Deferred}
*/
ve.ui.MWTemplateDialog.prototype.checkRequiredParameters = function () {
var blankRequired = [], deferred = $.Deferred();
$.each( this.bookletLayout.pages, function () {
if ( !( this instanceof ve.ui.MWParameterPage ) ) {
return true;
}
if ( this.parameter.isRequired() && !this.valueInput.getValue() ) {
blankRequired.push( mw.msg(
'quotation-marks',
this.parameter.template.getSpec().getParameterLabel( this.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
)
} ).then( function ( opened ) {
opened.then( function ( closing ) {
closing.then( function ( data ) {
if ( data.action === 'ok' ) {
deferred.resolve();
} else {
deferred.reject();
}
} );
} );
} );
} else {
deferred.resolve();
}
return deferred.promise();
};
/**
* @inheritdoc
*/
ve.ui.MWTemplateDialog.prototype.getActionProcess = function ( action ) {
if ( action === 'apply' || action === 'insert' ) {
return new OO.ui.Process( function () {
var deferred = $.Deferred();
this.checkRequiredParameters().done( function () {
var surfaceModel = this.getFragment().getSurface(),
obj = this.transclusionModel.getPlainObject();
if ( this.selectedNode instanceof ve.dm.MWTransclusionNode ) {
this.transclusionModel.updateTransclusionNode( surfaceModel, this.selectedNode );
} else if ( obj !== null ) {
// Collapse returns a new fragment, so update this.fragment
this.fragment = this.getFragment().collapseRangeToEnd();
this.transclusionModel.insertTransclusionNode( this.getFragment() );
}
this.close( { action: action } );
}.bind( this ) ).always( function () {
deferred.resolve();
} );
return deferred;
}, this );
}
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( function () {
var template, promise;
// Properties
this.loaded = false;
this.transclusionModel = new ve.dm.MWTransclusionModel();
// Events
this.transclusionModel.connect( this, { replace: 'onReplacePart' } );
// Initialization
if ( !this.selectedNode ) {
this.actions.setMode( 'insert' );
if ( data.template ) {
// New specified template
template = ve.dm.MWTemplateModel.newFromName(
this.transclusionModel, data.template
);
promise = this.transclusionModel.addPart( template ).done( function () {
this.initializeNewTemplateParameters();
} );
} else {
// New template placeholder
promise = this.transclusionModel.addPart(
new ve.dm.MWTemplatePlaceholderModel( this.transclusionModel )
);
}
} else {
this.actions.setMode( 'edit' );
// Load existing template
promise = this.transclusionModel
.load( ve.copy( this.selectedNode.getAttribute( 'mw' ) ) )
.done( ve.bind( function () {
this.initializeTemplateParameters();
}, this ) );
}
this.actions.setAbilities( { apply: false, insert: false } );
this.pushPending();
promise.always( this.onTransclusionReady.bind( this ) );
}, this );
};
/**
* Initialize parameters for new template insertion
*
* @method
*/
ve.ui.MWTemplateDialog.prototype.initializeNewTemplateParameters = function () {
var i, parts = this.transclusionModel.getParts();
for ( i = 0; i < parts.length; i++ ) {
if ( parts[i] instanceof ve.dm.MWTemplateModel ) {
parts[i].addPromptedParameters();
}
}
};
/**
* Intentionally empty. This is provided for Wikia extensibility.
*
* @method
*/
ve.ui.MWTemplateDialog.prototype.initializeTemplateParameters = function () {};
/**
* @inheritdoc
*/
ve.ui.MWTemplateDialog.prototype.getTeardownProcess = function ( data ) {
return ve.ui.MWTemplateDialog.super.prototype.getTeardownProcess.call( this, data )
.first( function () {
// Cleanup
this.$element.removeClass( 've-ui-mwTemplateDialog-ready' );
this.transclusionModel.disconnect( this );
this.transclusionModel.abortRequests();
this.transclusionModel = null;
this.bookletLayout.clearPages();
this.content = null;
}, this );
};