2013-04-03 18:21:10 +00:00
|
|
|
/*!
|
2013-04-17 17:53:26 +00:00
|
|
|
* VisualEditor ContentEditable GeneratedContentNode class.
|
2013-04-03 18:21:10 +00:00
|
|
|
*
|
|
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ContentEditable generated content node.
|
|
|
|
*
|
|
|
|
* @class
|
|
|
|
* @abstract
|
2013-05-29 01:35:42 +00:00
|
|
|
*
|
2013-04-03 18:21:10 +00:00
|
|
|
* @constructor
|
|
|
|
*/
|
2013-05-29 01:35:42 +00:00
|
|
|
ve.ce.GeneratedContentNode = function VeCeGeneratedContentNode() {
|
2013-08-03 23:05:51 +00:00
|
|
|
// Properties
|
|
|
|
this.generatingPromise = null;
|
|
|
|
|
2013-08-28 22:55:35 +00:00
|
|
|
// DOM changes
|
2013-04-03 18:21:10 +00:00
|
|
|
this.$.addClass( 've-ce-generatedContentNode' );
|
|
|
|
|
|
|
|
// Events
|
2013-08-15 23:32:41 +00:00
|
|
|
this.model.connect( this, { 'update': 'onGeneratedContentNodeUpdate' } );
|
2013-04-03 18:21:10 +00:00
|
|
|
|
|
|
|
// Initialization
|
2013-08-03 23:05:51 +00:00
|
|
|
this.update();
|
2013-04-03 18:21:10 +00:00
|
|
|
};
|
|
|
|
|
2013-06-19 15:04:02 +00:00
|
|
|
/* Events */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event setup
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event teardown
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @event rerender
|
|
|
|
*/
|
|
|
|
|
2013-09-23 12:58:17 +00:00
|
|
|
/* Static members */
|
|
|
|
|
|
|
|
ve.ce.GeneratedContentNode.static = {};
|
|
|
|
|
|
|
|
// this.$ is just a wrapper for the real content, so don't duplicate attributes on it
|
|
|
|
ve.ce.GeneratedContentNode.static.renderHtmlAttributes = false;
|
|
|
|
|
2013-08-03 23:05:51 +00:00
|
|
|
/* Abstract methods */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start a deferred process to generate the contents of the node.
|
|
|
|
*
|
|
|
|
* If successful, the returned promise must be resolved with the generated DOM elements passed
|
|
|
|
* in as the first parameter, i.e. promise.resolve( domElements ); . Any other parameters to
|
|
|
|
* .resolve() are ignored.
|
|
|
|
*
|
|
|
|
* If the returned promise object is abortable (has an .abort() method), .abort() will be called if
|
|
|
|
* a newer update is started before the current update has finished. When a promise is aborted, it
|
|
|
|
* should cease its work and shouldn't be resolved or rejected. If an outdated update's promise
|
|
|
|
* is resolved or rejected anyway (which may happen if an aborted promise misbehaves, or if the
|
|
|
|
* promise wasn't abortable), this is ignored and doneGenerating()/failGenerating() is not called.
|
|
|
|
*
|
|
|
|
* Additional data may be passed in the config object to instruct this function to render something
|
|
|
|
* different than what's in the model. This data is implementation-specific and is passed through
|
|
|
|
* by forceUpdate().
|
|
|
|
*
|
|
|
|
* @abstract
|
|
|
|
* @param {Object} [config] Optional additional data
|
|
|
|
* @returns {jQuery.Promise} Promise object, may be abortable
|
|
|
|
*/
|
|
|
|
ve.ce.GeneratedContentNode.prototype.generateContents = function () {
|
|
|
|
throw new Error( 've.ce.GeneratedContentNode subclass must implement generateContents' );
|
|
|
|
};
|
|
|
|
|
2013-04-03 18:21:10 +00:00
|
|
|
/* Methods */
|
|
|
|
|
2013-08-15 23:32:41 +00:00
|
|
|
/**
|
|
|
|
* Handler for the update event
|
|
|
|
*/
|
|
|
|
ve.ce.GeneratedContentNode.prototype.onGeneratedContentNodeUpdate = function () {
|
|
|
|
this.update();
|
|
|
|
};
|
|
|
|
|
2013-04-03 18:21:10 +00:00
|
|
|
/**
|
2013-08-03 23:05:51 +00:00
|
|
|
* Rerender the contents of this node.
|
2013-04-03 18:21:10 +00:00
|
|
|
*
|
2013-10-15 17:37:03 +00:00
|
|
|
* @param {Object|string|Array} generatedContents Generated contents, in the default case an HTMLElement array
|
2013-10-22 17:54:59 +00:00
|
|
|
* @fires setup
|
|
|
|
* @fires teardown
|
2013-04-03 18:21:10 +00:00
|
|
|
*/
|
2013-10-15 17:37:03 +00:00
|
|
|
ve.ce.GeneratedContentNode.prototype.render = function ( generatedContents ) {
|
2013-08-27 16:21:30 +00:00
|
|
|
var $rendering, doc = this.getElementDocument();
|
2013-08-03 23:05:51 +00:00
|
|
|
if ( this.live ) {
|
|
|
|
this.emit( 'teardown' );
|
|
|
|
}
|
2013-08-27 16:21:30 +00:00
|
|
|
// Filter out link, meta and style tags for bug 50043
|
2013-10-15 17:37:03 +00:00
|
|
|
$rendering = $( ve.copyDomElements( generatedContents, doc ) ).not( 'link, meta, style' );
|
2013-08-27 16:21:30 +00:00
|
|
|
// Also remove link, meta and style tags nested inside other tags
|
|
|
|
$rendering.find( 'link, meta, style' ).remove();
|
|
|
|
this.$.empty().append( $rendering );
|
2013-08-03 23:05:51 +00:00
|
|
|
if ( this.live ) {
|
|
|
|
this.emit( 'setup' );
|
2013-10-15 17:37:03 +00:00
|
|
|
this.afterRender( generatedContents );
|
2013-08-03 23:05:51 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2013-09-26 19:35:43 +00:00
|
|
|
/**
|
|
|
|
* Trigger rerender events after rendering the contents of the node.
|
|
|
|
*
|
|
|
|
* Nodes may override this method if the rerender event needs to be deferred (e.g. until images have loaded)
|
|
|
|
*
|
2013-10-15 17:37:03 +00:00
|
|
|
* @param {Object|string|Array} generatedContents Generated contents
|
2013-10-22 17:54:59 +00:00
|
|
|
* @fires rerender
|
2013-09-26 19:35:43 +00:00
|
|
|
*/
|
|
|
|
ve.ce.GeneratedContentNode.prototype.afterRender = function () {
|
|
|
|
this.emit( 'rerender' );
|
|
|
|
};
|
|
|
|
|
2013-08-03 23:05:51 +00:00
|
|
|
/**
|
|
|
|
* Update the contents of this node based on the model and config data. If this combination of
|
|
|
|
* model and config data has been rendered before, the cached rendering in the store will be used.
|
|
|
|
*
|
|
|
|
* @param {Object} [config] Optional additional data to pass to generateContents()
|
|
|
|
*/
|
|
|
|
ve.ce.GeneratedContentNode.prototype.update = function ( config ) {
|
|
|
|
var store = this.model.doc.getStore(),
|
|
|
|
index = store.indexOfHash( ve.getHash( [ this.model, config ] ) );
|
2013-04-03 18:21:10 +00:00
|
|
|
if ( index !== null ) {
|
2013-08-03 23:05:51 +00:00
|
|
|
this.render( store.value( index ) );
|
2013-04-03 18:21:10 +00:00
|
|
|
} else {
|
2013-09-24 23:47:56 +00:00
|
|
|
this.forceUpdate( config );
|
2013-04-03 18:21:10 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2013-08-03 23:05:51 +00:00
|
|
|
* Force the contents to be updated. Like update(), but bypasses the store.
|
|
|
|
*
|
|
|
|
* @param {Object} [config] Optional additional data to pass to generateContents()
|
2013-04-03 18:21:10 +00:00
|
|
|
*/
|
2013-08-03 23:05:51 +00:00
|
|
|
ve.ce.GeneratedContentNode.prototype.forceUpdate = function ( config ) {
|
|
|
|
var promise, node = this;
|
|
|
|
if ( this.generatingPromise ) {
|
|
|
|
// Abort the currently pending generation process if possible
|
|
|
|
// Unset this.generatingPromise first so that if the promise is resolved or rejected
|
|
|
|
// when we abort, this is ignored as it should be
|
|
|
|
promise = this.generatingPromise;
|
|
|
|
this.generatingPromise = null;
|
|
|
|
if ( $.isFunction( promise.abort ) ) {
|
|
|
|
promise.abort();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Only call startGenerating() if we weren't generating before
|
|
|
|
this.startGenerating();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a new promise
|
|
|
|
promise = this.generatingPromise = this.generateContents( config );
|
|
|
|
promise
|
|
|
|
// If this promise is no longer the currently pending one, ignore it completely
|
2013-10-15 17:37:03 +00:00
|
|
|
.done( function ( generatedContents ) {
|
2013-08-03 23:05:51 +00:00
|
|
|
if ( node.generatingPromise === promise ) {
|
2013-10-15 17:37:03 +00:00
|
|
|
node.doneGenerating( generatedContents, config );
|
2013-08-03 23:05:51 +00:00
|
|
|
}
|
|
|
|
} )
|
|
|
|
.fail( function () {
|
|
|
|
if ( node.generatingPromise === promise ) {
|
|
|
|
node.failGenerating();
|
|
|
|
}
|
|
|
|
} );
|
2013-04-03 18:21:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when the node starts generating new content.
|
2013-08-03 23:05:51 +00:00
|
|
|
*
|
|
|
|
* This function is only called when the node wasn't already generating content. If a second update
|
|
|
|
* comes in, this function will only be called if the first update has already finished (i.e.
|
|
|
|
* doneGenerating or failGenerating has already been called).
|
|
|
|
*
|
2013-04-03 18:21:10 +00:00
|
|
|
* @method
|
|
|
|
*/
|
|
|
|
ve.ce.GeneratedContentNode.prototype.startGenerating = function () {
|
2013-09-23 12:54:36 +00:00
|
|
|
this.$.addClass( 've-ce-generatedContentNode-generating' );
|
2013-04-03 18:21:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when the node successfully finishes generating new content.
|
|
|
|
*
|
|
|
|
* @method
|
2013-10-15 17:37:03 +00:00
|
|
|
* @param {Object|string|Array} generatedContents Generated contents
|
2013-08-03 23:05:51 +00:00
|
|
|
* @param {Object} [config] Config object passed to forceUpdate()
|
2013-04-03 18:21:10 +00:00
|
|
|
*/
|
2013-10-15 17:37:03 +00:00
|
|
|
ve.ce.GeneratedContentNode.prototype.doneGenerating = function ( generatedContents, config ) {
|
2013-04-03 18:21:10 +00:00
|
|
|
var store = this.model.doc.getStore(),
|
2013-08-03 23:05:51 +00:00
|
|
|
hash = ve.getHash( [ this.model, config ] );
|
2013-10-15 17:37:03 +00:00
|
|
|
|
|
|
|
store.index( generatedContents, hash );
|
2013-09-23 12:54:36 +00:00
|
|
|
this.$.removeClass( 've-ce-generatedContentNode-generating' );
|
2013-08-03 23:05:51 +00:00
|
|
|
this.generatingPromise = null;
|
2013-10-15 17:37:03 +00:00
|
|
|
this.render( generatedContents );
|
2013-04-03 18:21:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when the has failed to generate new content.
|
2013-08-03 23:05:51 +00:00
|
|
|
*
|
2013-04-03 18:21:10 +00:00
|
|
|
* @method
|
|
|
|
*/
|
|
|
|
ve.ce.GeneratedContentNode.prototype.failGenerating = function () {
|
2013-09-23 12:54:36 +00:00
|
|
|
this.$.removeClass( 've-ce-generatedContentNode-generating' );
|
2013-08-03 23:05:51 +00:00
|
|
|
this.generatingPromise = null;
|
2013-08-29 01:55:19 +00:00
|
|
|
};
|