Merge "ve.ui.MWTemplateDialog: Implement inferring of template data"

This commit is contained in:
jenkins-bot 2013-05-30 17:41:21 +00:00 committed by Gerrit Code Review
commit 09ed510659

View file

@ -27,7 +27,9 @@ ve.ui.MWTemplateDialog = function VeUiMWTemplateDialog( surface, config ) {
// Properties // Properties
this.node = null; this.node = null;
this.content = null; this.content = null;
this.specs = {}; // Buffer for getTemplateSpecs
this.fetchQueue = [];
this.fetchCallbacks = $.Callbacks();
}; };
/* Inheritance */ /* Inheritance */
@ -50,77 +52,111 @@ ve.ui.MWTemplateDialog.static.modelClasses = [ ve.dm.MWTemplateNode ];
* @method * @method
*/ */
ve.ui.MWTemplateDialog.prototype.onOpen = function () { ve.ui.MWTemplateDialog.prototype.onOpen = function () {
var i, len, template, title, var i, progress, len, template,
templates = []; dialog = this;
this.node = this.surface.getView().getFocusedNode(); function increaseProgress() {
if ( !this.node ) { progress++;
if ( progress === len ) {
dialog.setupPages();
}
}
function makeStoreTemplateSpec( template ) {
return function ( specs ) {
template.spec = specs[template.specId];
increaseProgress();
};
}
dialog.node = dialog.surface.getView().getFocusedNode();
if ( !dialog.node ) {
throw new Error( 'No focused node to edit' ); throw new Error( 'No focused node to edit' );
} }
// Get content values // Get content values and copy it so we can safely change it to our liking
this.content = ve.copyObject( this.node.getModel().getAttribute( 'mw' ) ); dialog.content = ve.copyObject( dialog.node.getModel().getAttribute( 'mw' ) );
// Convert single template format to multiple template format // Convert single template format to multiple template format
if ( this.content.params ) { if ( dialog.content.params ) {
this.content = { 'parts': [ { 'template': this.content } ] }; dialog.content = {
'parts': [
{
'template': dialog.content
}
]
};
} }
// Get all template data asynchronously
for ( i = 0, len = this.content.parts.length; i < len; i++ ) { progress = -1;
template = this.content.parts[i].template; len = dialog.content.parts.length;
// Get all template specs asynchronously
for ( i = 0; i < len; i++ ) {
template = dialog.content.parts[i].template;
if ( template ) { if ( template ) {
if ( template.target.url ) { // Method #getTemplateSpecs will use the part id instead of `target.url`
try { // if the target has no url property (which Parsoid omits if the target is
title = new mw.Title( template.target.url ); // dynamically generated from wikitext). In that case we want each template
templates.push( { // invocation to have its own inferred template spec.
'title': title.toString(), template.specId = template.target.url || ( '#!/part/' + i );
'params': template.params dialog.getTemplateSpecs( template, makeStoreTemplateSpec( template ) );
} );
} catch ( e ) {}
}
} else { } else {
// Wrap plain wikitext in object so editor has something to reference // This is a raw wikitext part (between two associated template invocations),
this.content.parts[i] = { 'wt': this.content.parts[i] }; // wrap in object so editor has something to reference
dialog.content.parts[i] = { 'wt': dialog.content.parts[i] };
increaseProgress();
} }
} }
if ( templates.length ) {
this.getTemplateData( templates ) increaseProgress();
.done( ve.bind( function ( specs ) {
this.specs = specs;
}, this ) )
.always( ve.bind( this.setupPages, this ) );
} else {
this.setupPages();
}
}; };
/** /**
* Handle window close events. * Handle window close events.
* *
* @method
* @param {string} action Action that caused the window to be closed * @param {string} action Action that caused the window to be closed
*/ */
ve.ui.MWTemplateDialog.prototype.onClose = function ( action ) { ve.ui.MWTemplateDialog.prototype.onClose = function ( action ) {
var i, len, wt, var i, len, parts,
surfaceModel = this.surface.getModel(); surfaceModel = this.surface.getModel();
// Save changes // Save changes
if ( action === 'apply' ) { if ( action === 'apply' ) {
// Expand wikitext content
for ( i = 0, len = this.content.parts.length; i < len; i++ ) { // Undo non-standard changes we made to the content model in #onOpen
wt = this.content.parts[i].wt; parts = this.content.parts;
if ( typeof wt === 'string' ) {
// Replace object wrapper with plain text for ( i = 0, len = parts.length; i < len; i++ ) {
this.content.parts[i] = wt;
// Convert object part with wt property back to string part
if ( typeof parts[i].wt === 'string' ) {
parts[i] = parts[i].wt;
}
// Remove the properties #onOpen put here
if ( parts[i].template ) {
if ( parts[i].template.spec ) {
delete parts[i].template.spec;
}
if ( parts[i].template.specId ) {
delete parts[i].template.specId;
}
} }
} }
// Restore single template format // Restore single template format
if ( this.content.parts.length === 1 ) { if ( this.content.parts.length === 1 ) {
this.content = this.content.parts[0].template; this.content = this.content.parts[0].template;
} }
// TODO: Wrap attribute changes in ve.dm.SurfaceFragment // TODO: Wrap attribute changes in ve.dm.SurfaceFragment
surfaceModel.change( surfaceModel.change(
ve.dm.Transaction.newFromAttributeChange( ve.dm.Transaction.newFromAttributeChange(
surfaceModel.getDocument(), this.node.getOffset(), 'mw', this.content surfaceModel.getDocument(),
this.node.getOffset(),
'mw',
this.content
) )
); );
} }
@ -128,7 +164,6 @@ ve.ui.MWTemplateDialog.prototype.onClose = function ( action ) {
this.clearPages(); this.clearPages();
this.node = null; this.node = null;
this.content = null; this.content = null;
this.specs = {};
// Parent method // Parent method
ve.ui.PagedDialog.prototype.onClose.call( this ); ve.ui.PagedDialog.prototype.onClose.call( this );
@ -142,8 +177,7 @@ ve.ui.MWTemplateDialog.prototype.onClose = function ( action ) {
ve.ui.MWTemplateDialog.prototype.setupPages = function () { ve.ui.MWTemplateDialog.prototype.setupPages = function () {
// Build pages from parts // Build pages from parts
var i, len, template, spec, param, var i, len, template, spec, param,
parts = this.content.parts, parts = this.content.parts;
specs = this.specs;
// Parent method // Parent method
ve.ui.PagedDialog.prototype.onOpen.call( this ); ve.ui.PagedDialog.prototype.onOpen.call( this );
@ -152,7 +186,7 @@ ve.ui.MWTemplateDialog.prototype.setupPages = function () {
for ( i = 0, len = parts.length; i < len; i++ ) { for ( i = 0, len = parts.length; i < len; i++ ) {
if ( parts[i].template ) { if ( parts[i].template ) {
template = parts[i].template; template = parts[i].template;
spec = specs[template.target.url]; spec = template.spec;
// Add template page // Add template page
this.addTemplatePage( 'part_' + i, template ); this.addTemplatePage( 'part_' + i, template );
// Add parameter pages // Add parameter pages
@ -172,23 +206,111 @@ ve.ui.MWTemplateDialog.prototype.setupPages = function () {
}; };
/** /**
* Get a promise for template data. * Backfill missing template data based on template invocation.
* * @param {Object} template Template invocation description
* TODO: Backfill template info from params objects * @return {Object} Template data blob
*
* @method
* @param {Object[]} templates Template information containing `title` and `params` properties
* @return {jQuery.Promise} Template data blob on success, or an error code on failure
*/ */
ve.ui.MWTemplateDialog.prototype.getTemplateData = function ( templates ) { ve.ui.MWTemplateDialog.static.makeTemplateSpec = function ( params ) {
var key, blob;
blob = {
description: null,
params: {},
sets: []
};
for ( key in params ) {
blob.params[key] = {
'label': {
en: key
},
'required': false,
'description': null,
'deprecated': false,
'aliases': [],
'default': '',
'type': 'string'
};
}
return blob;
};
/**
* Get template specs for one or more templates in the content model.
*
* @param {Object[]|undefined} templates List of template invocation descriptions. Contains `title` and
* `params` properties. Or undefined to handle the queue built so far.
* @param {Function} callback
* @param {Object} callback.blobs Object containing template data blobs keyed by page title.
*/
ve.ui.MWTemplateDialog.prototype.getTemplateSpecs = function ( templates, callback ) {
var fillTemplateSpecs,
dialog = this;
// Yield once with setTimeout before fetching to allow batching
if ( callback ) {
dialog.fetchCallbacks.add( callback );
}
if ( templates ) {
templates = ve.isArray( templates ) ? templates : [ templates ];
// Push into the queue
dialog.fetchQueue.push.apply( dialog.fetchQueue, templates );
setTimeout( function () {
dialog.getTemplateSpecs();
} );
return;
} else if ( dialog.fetchQueue.length ) {
// Handle batch queue
templates = dialog.fetchQueue.slice();
dialog.fetchQueue.length = 0;
} else {
// This a delayed call but a previous delayed call already
// cleared the queue for us. This call has become redundant.
return;
}
fillTemplateSpecs = function ( specs ) {
var i, len, template, specId;
for ( i = 0, len = templates.length; i < len; i++ ) {
template = templates[i];
specId = template.specId;
if ( !specs[specId] ) {
specs[specId] = dialog.constructor.static.makeTemplateSpec( template );
}
}
dialog.fetchCallbacks.fireWith( null, [ specs ] );
};
dialog.fetchTemplateSpecs( templates )
.done( fillTemplateSpecs )
.fail( function () {
fillTemplateSpecs( {} );
} );
};
/**
* Fetch template data from the TemplateData API.
*
* @param {Object[]} templates List of template invocation descriptions
* @return {jQuery.Promise}
*/
ve.ui.MWTemplateDialog.prototype.fetchTemplateSpecs = function ( templates ) {
var i, len, var i, len,
d = $.Deferred(),
titles = [], titles = [],
specs = {}, specs = {};
deferred = $.Deferred();
// Collect all titles // Collect all titles
for ( i = 0, len = templates.length; i < len; i++ ) { for ( i = 0, len = templates.length; i < len; i++ ) {
titles.push( templates[i].title ); if ( templates[i].target.url ) {
titles.push( templates[i].target.url );
}
}
// Optimise for empty lists
if ( !templates.length ) {
setTimeout( d.reject );
return d.promise();
} }
// Request template data from server // Request template data from server
@ -202,27 +324,31 @@ ve.ui.MWTemplateDialog.prototype.getTemplateData = function ( templates ) {
} }
} ) } )
.done( function ( data ) { .done( function ( data ) {
var id; var i, len, id;
if ( data && data.pages ) { if ( data && data.pages ) {
// Add template data to spec
for ( id in data.pages ) { for ( id in data.pages ) {
specs[data.pages[id].title] = data.pages[id]; specs[data.pages[id].title] = data.pages[id];
} }
deferred.resolve( specs ); if ( data.normalized ) {
for ( i = 0, len = data.normalized.length; i < len; i++ ) {
specs[ data.normalized[i].from ] = specs[ data.normalized[i].to ];
}
}
d.resolve( specs );
} else {
d.reject( 'unavailable', arguments );
} }
deferred.reject( 'unavailable', arguments );
} ) } )
.fail( function () { .fail( function () {
deferred.reject( 'http', arguments ); d.reject( 'http', arguments );
} ); } );
return deferred.promise(); return d.promise();
}; };
/** /**
* Add page for wikitext. * Add page for wikitext.
* *
* @method
* @param {string} page Unique page name * @param {string} page Unique page name
* @param {Object} value Parameter value * @param {Object} value Parameter value
*/ */
@ -250,7 +376,6 @@ ve.ui.MWTemplateDialog.prototype.addWikitextPage = function ( page, value ) {
/** /**
* Add page for a template. * Add page for a template.
* *
* @method
* @param {string} page Unique page name * @param {string} page Unique page name
* @param {Object} template Template info * @param {Object} template Template info
*/ */
@ -271,7 +396,6 @@ ve.ui.MWTemplateDialog.prototype.addTemplatePage = function ( page, template ) {
/** /**
* Add page for a parameter. * Add page for a parameter.
* *
* @method
* @param {string} page Unique page name * @param {string} page Unique page name
* @param {string} name Parameter name * @param {string} name Parameter name
* @param {Object} value Parameter value * @param {Object} value Parameter value