mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-28 16:20:52 +00:00
be199c0bf2
Objectives: * Allow users on-wiki to create tools and dialogs for citation templates of their choosing * Allow editing of citation templates directly, without having to go through the reference dialog * Provide citation template tools within reference editing that use the same titles and icons as the citation tools do, but don't wrap the inserted content in a ref tag Changes: * Reference list was cloning the DOM element it was inserting into its view before the generated content node could finish rendering, so it never ended up showing the finished rendering in the reference list * Documenting hack about use of reference list node's destroy method, and how we are depending on destroy not canceling generated content rendering * Introduced reference model * Added saving/updating method to transclusion model * Added getPartsList method to dm transclusion node, which caches the result and invalidates the cache on update * Added citation dialog, which extends transclusion dialog * Added cite group to toolbars, cite-template in reference dialog toolbar * Factored out getting the node to edit and saving changes procedures in transclusion dialog so they could be extended in citation dialog * Updated uses of autoAdd as per changes in oojs-ui (Ic353f91) * Renamed MWDialogTool file since there was only one tool in it * Expanded TransclusionDialogTool file out since there is now more logic to it * Switched to using ve.dm.MWReferenceModel instead of plain objects in reference search widget Configuration: If you add to MediaWiki:Visualeditor-cite-tool-definition.json the following code you will magically be presented with a delightful array of citation options: [ { "name": "web", "icon": "ref-cite-web", "template": "Cite web" }, { "name": "book", "icon": "ref-cite-book", "template": "Cite book" }, { "name": "news", "icon": "ref-cite-news", "template": "Cite news" }, { "name": "journal", "icon": "ref-cite-journal", "template": "Cite journal" } ] ...or... [ { "name": "any-name", "icon": "any-ooui-icon", "template": "Any template", "title": "Any title text" } ] The title text is derived either from the title property or from the name property by pre-pending the string 'visualeditor-cite-tool-name-' to generate a message key. Titles for 'web', 'book', 'news' and 'journal' are provided. The icon is a normal oo-ui-icon name, and more icons can be added, as usual, by adding a class called .oo-ui-icon-{icon name} to MediaWiki:Common.css. 'ref-cite-web', 'ref-cite-book', 'ref-cite-news' and 'ref-cite-journal' are provided. The template name is simply the name of the template without its namespace prefix. Depends on Ic353f91 in oojs-ui Bug: 50110 Bug: 50768 Change-Id: Id401d973b8d5fe2faec481cc777c17a24fd19dd4
475 lines
12 KiB
JavaScript
475 lines
12 KiB
JavaScript
/*!
|
|
* VisualEditor DataModel MWTransclusionModel class.
|
|
*
|
|
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/*global mw */
|
|
|
|
( function () {
|
|
var hasOwn = Object.hasOwnProperty,
|
|
specCache = {};
|
|
|
|
/**
|
|
* MediaWiki transclusion model.
|
|
*
|
|
* @class
|
|
* @mixins OO.EventEmitter
|
|
*
|
|
* @constructor
|
|
*/
|
|
ve.dm.MWTransclusionModel = function VeDmMWTransclusionModel() {
|
|
// Mixin constructors
|
|
OO.EventEmitter.call( this );
|
|
|
|
// Properties
|
|
this.parts = [];
|
|
this.uid = 0;
|
|
this.requests = [];
|
|
this.queue = [];
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.mixinClass( ve.dm.MWTransclusionModel, OO.EventEmitter );
|
|
|
|
/* Events */
|
|
|
|
/**
|
|
* @event replace
|
|
* @param {ve.dm.MWTransclusionPartModel|null} removed Removed part
|
|
* @param {ve.dm.MWTransclusionPartModel|null} added Added part
|
|
*/
|
|
|
|
/**
|
|
* @event change
|
|
*/
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Insert transclusion into a surface.
|
|
*
|
|
* Transclusion is inserted at the current cursor position in `surfaceModel`.
|
|
*
|
|
* @param {ve.dm.Surface} surfaceModel Surface model of main document
|
|
* @param {ve.Range} [at] Location to insert at
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.insertTransclusionNode = function ( surfaceModel, at ) {
|
|
surfaceModel
|
|
.getFragment( at || surfaceModel.getSelection().clone(), true )
|
|
.insertContent( [
|
|
{
|
|
'type': 'mwTransclusionInline',
|
|
'attributes': {
|
|
'mw': this.getPlainObject()
|
|
}
|
|
},
|
|
{ 'type': '/mwTransclusionInline' }
|
|
] );
|
|
};
|
|
|
|
/**
|
|
* Update transclusion node in a document.
|
|
*
|
|
* @param {ve.dm.Surface} surfaceModel Surface model of main document
|
|
* @param {ve.dm.MWTransclusionNode} node Transclusion node to update
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.updateTransclusionNode = function ( surfaceModel, node ) {
|
|
var obj = this.getPlainObject();
|
|
|
|
if ( obj !== null ) {
|
|
surfaceModel.getFragment( node.getOuterRange(), true )
|
|
.changeAttributes( { 'mw': obj } );
|
|
} else {
|
|
surfaceModel.getFragment( node.getOuterRange(), true )
|
|
.removeContent();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Load from transclusion data, and fetch spec from server.
|
|
*
|
|
* @param {Object} data Transclusion data
|
|
* @returns {jQuery.Promise} Promise, resolved when spec is loaded
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.load = function ( data ) {
|
|
var i, len, part, deferred,
|
|
promises = [];
|
|
|
|
// Convert single part format to multi-part format
|
|
// Parsoid doesn't use this format any more, but we accept it for backwards compatibility
|
|
if ( data.params && data.target ) {
|
|
data = { 'parts': [ { 'template': data } ] };
|
|
}
|
|
|
|
if ( ve.isArray( data.parts ) ) {
|
|
for ( i = 0, len = data.parts.length; i < len; i++ ) {
|
|
part = data.parts[i];
|
|
if ( part.template ) {
|
|
deferred = $.Deferred();
|
|
promises.push( deferred.promise() );
|
|
this.queue.push( {
|
|
'add': ve.dm.MWTemplateModel.newFromData( this, part.template ),
|
|
'deferred': deferred
|
|
} );
|
|
} else if ( typeof part === 'string' ) {
|
|
deferred = $.Deferred();
|
|
promises.push( deferred.promise() );
|
|
this.queue.push( {
|
|
'add': new ve.dm.MWTransclusionContentModel( this, part, 'data' ),
|
|
'deferred': deferred
|
|
} );
|
|
}
|
|
}
|
|
setTimeout( ve.bind( this.fetch, this ) );
|
|
}
|
|
|
|
return $.when.apply( $, promises );
|
|
};
|
|
|
|
/**
|
|
* Process one or more queue items.
|
|
*
|
|
* @param {Object[]} queue List of objects containing parts to add and optionally indexes to add
|
|
* them at, if no index is given parts will be added at the end
|
|
* @fires replace For each item added
|
|
* @fires change
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.process = function ( queue ) {
|
|
var i, len, item, title, index, remove, existing;
|
|
|
|
for ( i = 0, len = queue.length; i < len; i++ ) {
|
|
remove = 0;
|
|
item = queue[i];
|
|
|
|
if ( item.add instanceof ve.dm.MWTemplateModel ) {
|
|
title = item.add.getTitle();
|
|
if ( hasOwn.call( specCache, title ) && specCache[title] ) {
|
|
item.add.getSpec().extend( specCache[title] );
|
|
}
|
|
}
|
|
|
|
// Use specified index
|
|
index = item.index;
|
|
// Auto-remove if already existing, preserving index
|
|
existing = ve.indexOf( item.add, this.parts );
|
|
if ( existing !== -1 ) {
|
|
this.removePart( item.add );
|
|
if ( index && existing + 1 < index ) {
|
|
index--;
|
|
}
|
|
}
|
|
// Derive index from removal if given
|
|
if ( index === undefined && item.remove ) {
|
|
index = ve.indexOf( item.remove, this.parts );
|
|
if ( index !== -1 ) {
|
|
remove = 1;
|
|
}
|
|
}
|
|
// Use last index as a last resort
|
|
if ( index === undefined || index === -1 ) {
|
|
index = this.parts.length;
|
|
}
|
|
|
|
this.parts.splice( index, remove, item.add );
|
|
if ( item.add ) {
|
|
item.add.connect( this, { 'change': [ 'emit', 'change' ] } );
|
|
}
|
|
if ( item.remove ) {
|
|
item.remove.disconnect( this );
|
|
}
|
|
this.emit( 'replace', item.remove || null, item.add );
|
|
|
|
// Resolve promises
|
|
if ( item.deferred ) {
|
|
item.deferred.resolve();
|
|
}
|
|
}
|
|
this.emit( 'change' );
|
|
};
|
|
|
|
/** */
|
|
ve.dm.MWTransclusionModel.prototype.fetch = function () {
|
|
if ( !this.queue.length ) {
|
|
return;
|
|
}
|
|
|
|
var i, len, item, title, request,
|
|
titles = [],
|
|
specs = {},
|
|
queue = this.queue.slice();
|
|
|
|
// Clear shared queue for future calls
|
|
this.queue.length = 0;
|
|
|
|
// Get unique list of template titles that aren't already loaded
|
|
for ( i = 0, len = queue.length; i < len; i++ ) {
|
|
item = queue[i];
|
|
if ( item.add instanceof ve.dm.MWTemplateModel ) {
|
|
title = item.add.getTitle();
|
|
if (
|
|
// Skip titles that don't have a resolvable href
|
|
title &&
|
|
// Skip titles outside the template namespace
|
|
title.charAt( 0 ) !== ':' &&
|
|
// Skip already cached data
|
|
!hasOwn.call( specCache, title ) &&
|
|
// Skip duplicate titles in the same batch
|
|
ve.indexOf( title, titles ) === -1
|
|
) {
|
|
titles.push( title );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bypass server for empty lists
|
|
if ( !titles.length ) {
|
|
setTimeout( ve.bind( this.process, this, queue ) );
|
|
return;
|
|
}
|
|
|
|
// Request template specs from server
|
|
request = ve.init.mw.Target.static.apiRequest( {
|
|
'action': 'templatedata',
|
|
'titles': titles.join( '|' ),
|
|
'lang': mw.config.get( 'wgUserLanguage' )
|
|
} )
|
|
.done( function ( data ) {
|
|
var i, len, id;
|
|
|
|
if ( data && data.pages ) {
|
|
// Keep spec data on hand for future use
|
|
for ( id in data.pages ) {
|
|
specs[data.pages[id].title] = data.pages[id];
|
|
}
|
|
// Cross-reference under normalized titles
|
|
if ( data.normalized ) {
|
|
for ( i = 0, len = data.normalized.length; i < len; i++ ) {
|
|
// Only define the alias if the target exists, otherwise
|
|
// we create a new property with an invalid "undefined" value.
|
|
if ( hasOwn.call( specs, data.normalized[i].to ) ) {
|
|
specs[data.normalized[i].from] = specs[data.normalized[i].to];
|
|
}
|
|
}
|
|
}
|
|
// Prevent asking again for templates that have no specs
|
|
for ( i = 0, len = titles.length; i < len; i++ ) {
|
|
title = titles[i];
|
|
if ( !specs[title] ) {
|
|
specs[title] = null;
|
|
}
|
|
}
|
|
|
|
ve.extendObject( specCache, specs );
|
|
}
|
|
} )
|
|
.always( ve.bind( function () {
|
|
// Prune completed request
|
|
var index = ve.indexOf( request, this.requests );
|
|
if ( index !== -1 ) {
|
|
this.requests.splice( index, 1 );
|
|
}
|
|
// Actually add queued items
|
|
this.process( queue );
|
|
}, this ) );
|
|
|
|
this.requests.push( request );
|
|
};
|
|
|
|
/**
|
|
* Abort any pending requests.
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.abortRequests = function () {
|
|
var i, len;
|
|
|
|
for ( i = 0, len = this.requests.length; i < len; i++ ) {
|
|
this.requests[i].abort();
|
|
}
|
|
this.requests.length = 0;
|
|
};
|
|
|
|
/**
|
|
* Get plain object representation of template transclusion.
|
|
*
|
|
* @returns {Object|null} Plain object representation, or null if empty
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.getPlainObject = function () {
|
|
var i, len, part, serialization,
|
|
obj = { 'parts': [] };
|
|
|
|
for ( i = 0, len = this.parts.length; i < len; i++ ) {
|
|
part = this.parts[i];
|
|
serialization = part.serialize();
|
|
if ( serialization !== undefined ) {
|
|
obj.parts.push( serialization );
|
|
}
|
|
}
|
|
|
|
if ( obj.parts.length === 0 ) {
|
|
return null;
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
|
|
/**
|
|
* Get the wikitext for this transclusion.
|
|
*
|
|
* @returns {string} Wikitext like `{{foo|1=bar|baz=quux}}`
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.getWikitext = function () {
|
|
var i, len,
|
|
wikitext = '';
|
|
|
|
for ( i = 0, len = this.parts.length; i < len; i++ ) {
|
|
wikitext += this.parts[i].getWikitext();
|
|
}
|
|
|
|
return wikitext;
|
|
};
|
|
|
|
/**
|
|
* Get a unique ID for a part in the transclusion.
|
|
*
|
|
* This is used to give parts unique IDs, and returns a different value each time it's called.
|
|
*
|
|
* @returns {number} Unique ID
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.getUniquePartId = function () {
|
|
return this.uid++;
|
|
};
|
|
|
|
/**
|
|
* Replace part.
|
|
*
|
|
* Replace asynchonously.
|
|
*
|
|
* @param {ve.dm.MWTransclusionPartModel} remove Part to remove
|
|
* @param {ve.dm.MWTransclusionPartModel} add Part to add
|
|
* @throws {Error} If part to remove is not valid
|
|
* @throws {Error} If part to add is not valid
|
|
* @returns {jQuery.Promise} Promise, resolved when part is added
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.replacePart = function ( remove, add ) {
|
|
var deferred = $.Deferred();
|
|
if (
|
|
!( remove instanceof ve.dm.MWTransclusionPartModel ) ||
|
|
!( add instanceof ve.dm.MWTransclusionPartModel )
|
|
) {
|
|
throw new Error( 'Invalid transclusion part' );
|
|
}
|
|
this.queue.push( { 'remove': remove, 'add': add, 'deferred': deferred } );
|
|
|
|
// Fetch on next yield to process items in the queue together, subsequent calls to fetch will
|
|
// have no effect because the queue will be clear
|
|
setTimeout( ve.bind( this.fetch, this ) );
|
|
|
|
return deferred.promise();
|
|
};
|
|
|
|
/**
|
|
* Add part.
|
|
*
|
|
* Added asynchronously, but order is preserved.
|
|
*
|
|
* @param {ve.dm.MWTransclusionPartModel} part Part to add
|
|
* @param {number} [index] Specific index to add content at, defaults to the end
|
|
* @throws {Error} If part is not valid
|
|
* @returns {jQuery.Promise} Promise, resolved when part is added
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.addPart = function ( part, index ) {
|
|
var deferred = $.Deferred();
|
|
if ( !( part instanceof ve.dm.MWTransclusionPartModel ) ) {
|
|
throw new Error( 'Invalid transclusion part' );
|
|
}
|
|
this.queue.push( { 'add': part, 'index': index, 'deferred': deferred } );
|
|
|
|
// Fetch on next yield to process items in the queue together, subsequent calls to fetch will
|
|
// have no effect because the queue will be clear
|
|
setTimeout( ve.bind( this.fetch, this ) );
|
|
|
|
return deferred.promise();
|
|
};
|
|
|
|
/**
|
|
* Remove a part.
|
|
*
|
|
* @param {ve.dm.MWTransclusionPartModel} part Part to remove
|
|
* @fires replace
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.removePart = function ( part ) {
|
|
var index = ve.indexOf( part, this.parts );
|
|
if ( index !== -1 ) {
|
|
this.parts.splice( index, 1 );
|
|
part.disconnect( this );
|
|
this.emit( 'replace', part, null );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get all parts.
|
|
*
|
|
* @returns {ve.dm.MWTransclusionPartModel[]} Parts in transclusion
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.getParts = function () {
|
|
return this.parts;
|
|
};
|
|
|
|
/**
|
|
* Get part by its ID.
|
|
*
|
|
* Matching is performed against the first section of the `id`, delimited by a '/'.
|
|
*
|
|
* @param {string} id Part ID
|
|
* @returns {ve.dm.MWTransclusionPartModel|null} Part with matching ID, if found
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.getPartFromId = function ( id ) {
|
|
var i, len,
|
|
// For ids from ve.dm.MWParameterModel, compare against the part id
|
|
// of the parameter instead of the entire model id (e.g. "part_1" instead of "part_1/foo").
|
|
partId = id.split( '/' )[0];
|
|
|
|
for ( i = 0, len = this.parts.length; i < len; i++ ) {
|
|
if ( this.parts[i].getId() === partId ) {
|
|
return this.parts[i];
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get the index of a part or parameter.
|
|
*
|
|
* Indexes are linear depth-first addresses in the transclusion tree.
|
|
*
|
|
* @param {ve.dm.MWTransclusionPartModel|ve.dm.MWParameterModel} model Part or parameter
|
|
* @returns {number} Page index of model
|
|
*/
|
|
ve.dm.MWTransclusionModel.prototype.getIndex = function ( model ) {
|
|
var i, iLen, j, jLen, part, names,
|
|
parts = this.parts,
|
|
index = 0;
|
|
|
|
for ( i = 0, iLen = parts.length; i < iLen; i++ ) {
|
|
part = parts[i];
|
|
if ( part === model ) {
|
|
return index;
|
|
}
|
|
index++;
|
|
if ( part instanceof ve.dm.MWTemplateModel ) {
|
|
names = part.getParameterNames();
|
|
for ( j = 0, jLen = names.length; j < jLen; j++ ) {
|
|
if ( part.getParameter( names[j] ) === model ) {
|
|
return index;
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
|
|
}() );
|