| Current File : /home/jvzmxxx/wiki1/extensions/Flow/modules/engine/misc/flow-handlebars.js |
/*!
* Implements a Handlebars layer for FlowBoard.TemplateEngine
*/
( function ( mw, $, moment, Handlebars ) {
mw.flow = mw.flow || {}; // create mw.flow globally
var _tplcache = {},
_timestamp = {
list: [],
currentIndex: 0
};
/**
* Instantiates a FlowHandlebars instance for TemplateEngine.
* @param {Object} FlowStorageEngine
* @return {FlowHandlebars}
* @constructor
*/
function FlowHandlebars( FlowStorageEngine ) {
return this;
}
mw.flow.FlowHandlebars = FlowHandlebars;
/**
* Returns a given template function. If template is missing, the template function is noop with mw.flow.debug.
* @param {string|Function} templateName
* @return {Function}
*/
FlowHandlebars.prototype.getTemplate = function ( templateName ) {
// If a template is already being passed, use it
if ( typeof templateName === 'function' ) {
return templateName;
}
if ( _tplcache[ templateName ] ) {
// Return cached compiled template
return _tplcache[ templateName ];
}
_tplcache[ templateName ] = mw.template.get( 'ext.flow.templating', 'handlebars/' + templateName + '.handlebars' );
if ( _tplcache[ templateName ] ) {
// Try to get this template
_tplcache[ templateName ] = _tplcache[ templateName ].render;
}
return _tplcache[ templateName ] || function () { mw.flow.debug( '[Handlebars] Missing template', arguments ); };
};
/**
* Processes a given template and returns the HTML generated by it.
* @param {string} templateName
* @param {*} [args]
* @return {string}
*/
FlowHandlebars.prototype.processTemplate = function ( templateName, args ) {
return FlowHandlebars.prototype.getTemplate( templateName )( args );
};
/**
* Runs processTemplate inside, but returns a DocumentFragment instead of an HTML string.
* This should be used for runtime parsing of a template, as it triggers processProgressiveEnhancement on the
* fragment, which allows progressiveEnhancement blocks to be instantiated.
* @param {string} templateName
* @param {*} [args]
* @return {DocumentFragment}
*/
FlowHandlebars.prototype.processTemplateGetFragment = function ( templateName, args ) {
var fragment = document.createDocumentFragment(),
div = document.createElement( 'div' );
div.innerHTML = FlowHandlebars.prototype.processTemplate( templateName, args );
FlowHandlebars.prototype.processProgressiveEnhancement( div );
while ( div.firstChild ) {
fragment.appendChild( div.firstChild );
}
div = null;
return fragment;
};
/**
* A method to call helper functions from outside templates. This removes Handlebars.SafeString wrappers.
* @param {string} helperName
* @param {...*} [args]
* @return mixed
*/
FlowHandlebars.prototype.callHelper = function ( helperName, args ) {
var result = this[ helperName ].apply( this, Array.prototype.slice.call( arguments, 1 ) );
if ( result && result.string ) {
return result.string;
}
return result;
};
/**
* Finds scripts of x-handlebars-template-progressive-enhancement type, compiles its innerHTML as a Handlebars
* template, and then replaces the whole script tag with it. This is used to "progressively enhance" a page with
* elements that are only necessary with JavaScript. On a non-JS page, these elements are never rendered at all.
* @param {HTMLElement|jQuery} target
* @todo Lacks args, lacks functionality, full support. (see also FlowHandlebars#progressiveEnhancement)
*/
FlowHandlebars.prototype.processProgressiveEnhancement = function ( target ) {
$( target ).find( 'script' ).addBack( 'script' ).filter( '[type="text/x-handlebars-template-progressive-enhancement"]' ).each( function () {
var $this = $( this ),
data = $this.data(),
target = $.trim( data.target ),
$target = $this,
content, $prevTarg, $nextTarg;
// Find new target, if not the script tag itself
if ( target ) {
$target = $this.findWithParent( target );
if ( !$target.length ) {
mw.flow.debug( '[processProgressiveEnhancement] Failed to find target', target, arguments );
return;
}
}
// Replace the nested flowprogressivescript tag with a real script tag for recursive progressiveEnhancement
content = this.innerHTML.replace( /<\/flowprogressivescript>/g, '</script>' );
// Inject the content
switch ( data.type ) {
case 'content':
// Insert
$target.empty().append( content );
// Get all new nodes
$target = $target.children();
break;
case 'insert':
// Store sibling before adding new content
$prevTarg = $target.prev();
// Insert
$target.before( content );
// Get all new nodes
$target = $target.prevUntil( $prevTarg );
break;
case 'replace':
/* falls through */
default:
// Store siblings before adding new content
$prevTarg = $target.prev();
$nextTarg = $target.next();
// Insert
$target.replaceWith( content );
// Get all new nodes
$target = $prevTarg.nextUntil( $nextTarg );
}
// $target now contains all the new elements inserted; let's recursively do progressiveEnhancement if needed
FlowHandlebars.prototype.processProgressiveEnhancement( $target );
// Remove script tag
$this.remove();
} );
};
/**
* Parameters could be Message::rawParam (in PHP) object, which will
* translate into a { raw: "string" } object in JS.
* @todo: this does not exactly match the behavior in PHP yet (no parse,
* no escape), but at least it won't print an [Object object] param.
*
* @param {Array} parameters
* @return {Array}
*/
function flowNormalizeL10nParameters( parameters ) {
return $.map( parameters, function ( arg ) {
return arg ? ( arg.raw || arg.plaintext || arg ) : '';
} );
}
/**
* Calls flowMessages to get localized message strings.
*
* Example: `{{l10n "reply_count" 12}}`
*
* @todo use mw.message
* @param {string} str
* @param {...*} [args]
* @param {Object} [options]
* @return {string}
*/
FlowHandlebars.prototype.l10n = function ( str /*, args..., options */ ) {
// chop off str and options leaving just args
var args = flowNormalizeL10nParameters( Array.prototype.slice.call( arguments, 1, -1 ) );
return mw.message( str ).params( args ).text();
};
/**
* HTML-safe version of l10n.
* @return {string|Handlebars.SafeString}
*/
FlowHandlebars.prototype.l10nParse = function ( str /*, args..., options */ ) {
var args = flowNormalizeL10nParameters( Array.prototype.slice.call( arguments, 1, -1 ) );
return FlowHandlebars.prototype.html(
mw.message( str ).params( args ).parse()
);
};
/**
* Parses the timestamp out of a base-36 UUID, and calls timestamp with it.
*
* Example: `{{uuidTimestamp id "flow-message-x-"}}`
*
* @param {string} uuid id
* @param {boolean} [timeAgoOnly]
* @return {string}
*/
FlowHandlebars.prototype.uuidTimestamp = function ( uuid, timeAgoOnly ) {
var timestamp = mw.flow.uuidToTime( uuid );
return FlowHandlebars.prototype.timestamp( timestamp, timeAgoOnly );
};
/**
* Generates markup for an "nnn sssss ago" and date/time string.
*
* Example: `{{timestamp start_time}}`
*
* @param {number} timestamp milliseconds
* @return {string}
*/
FlowHandlebars.prototype.timestamp = function ( timestamp ) {
if ( isNaN( timestamp ) ) {
mw.flow.debug( '[timestamp] Invalid arguments', arguments );
return;
}
var guid,
formatter = moment( timestamp );
// Generate a GUID for this element to find it later
guid = ( Math.random() + 1 ).toString( 36 ).substring( 2 );
// Store this in the timestamps auto-updater array
_timestamp.list.push( { guid: guid, timestamp: timestamp, failcount: 0 } );
// Render the timestamp template
return FlowHandlebars.prototype.html(
FlowHandlebars.prototype.processTemplate(
'timestamp',
{
time_iso: timestamp,
time_ago: formatter.fromNow(),
time_readable: formatter.format( 'LLL' ),
guid: guid
}
)
);
};
/**
* Updates one flow-timestamp node at a time every 100ms, until finishing, and then sleeps 5s.
* Nodes do not get updated again until they have changed.
* @todo Perhaps only update elements within the viewport?
* @todo Maybe updating elements every few seconds is distracting? Think about this.
*/
function timestampAutoUpdate() {
var arrayItem, $ago, failed, secondsAgo, text, formatter,
currentTime = +new Date() / 1000;
// Only update elements that need updating (eg. only update minutes every 60s)
do {
arrayItem = _timestamp.list[ _timestamp.list._currentIndex ];
if ( !arrayItem || !arrayItem.nextUpdate || currentTime >= arrayItem.nextUpdate ) {
break;
}
// Find the next array item
_timestamp.list._currentIndex++;
} while ( arrayItem );
if ( !arrayItem ) {
// Finished array; reset loop
_timestamp.list._currentIndex = 0;
// Run again in 5s
setTimeout( timestampAutoUpdate, 5000 );
return;
}
$ago = $( document.getElementById( arrayItem.guid ) );
failed = true;
secondsAgo = currentTime - ( arrayItem.timestamp / 1000 );
if ( $ago && $ago.length ) {
formatter = moment( arrayItem.timestamp );
text = formatter.fromNow();
// Returned a valid "n ago" string?
if ( text ) {
// Reset the failcount
failed = arrayItem.failcount = 0;
// Set the next update time
arrayItem.nextUpdate = currentTime + ( secondsAgo > 604800 ? 604800 - currentTime % 604800 : ( secondsAgo > 86400 ? 86400 - currentTime % 86400 : ( secondsAgo > 3600 ? 3600 - currentTime % 3600 : ( secondsAgo > 60 ? 60 - currentTime % 60 : 1 ) ) ) );
// Only touch the DOM if the text has actually changed
if ( $ago.text() !== text ) {
$ago.text( text );
}
}
}
if ( failed && ++arrayItem.failcount > 9 ) {
// Remove this array item if we failed this 10 times in a row
_timestamp.list.splice( _timestamp.list._currentIndex, 1 );
} else {
// Go to next item
_timestamp.list._currentIndex++;
}
// Run every 100ms until we update all nodes
setTimeout( timestampAutoUpdate, 100 );
}
$( document ).ready( timestampAutoUpdate );
/**
* Do not escape HTML string. Used as a Handlebars helper.
*
* Example: `{{html "<div/>"}}`
*
* @param {string} string
* @return {string|Handlebars.SafeString}
*/
FlowHandlebars.prototype.html = function ( string ) {
return new Handlebars.SafeString( string );
};
/**
*
* Example: `{{block this}}`
*
* @param {Object} context
* @param {Object} options
* @return {string}
*/
FlowHandlebars.prototype.workflowBlock = function ( context, options ) {
return FlowHandlebars.prototype.html( FlowHandlebars.prototype.processTemplate(
'flow_block_' + context.type + ( context[ 'block-action-template' ] || '' ),
context
) );
};
/**
* Example: `{{post ../../../../rootBlock this}}`
* @param {Object} context
* @param {Object} revision
* @param {Object} options
* @return {string}
*/
FlowHandlebars.prototype.postBlock = function ( context, revision, options ) {
return FlowHandlebars.prototype.html( FlowHandlebars.prototype.processTemplate(
'flow_post',
{
revision: revision,
rootBlock: context
}
) );
};
/**
* Example: `{{#each topics}}{{#eachPost this}}{{content}}{{/eachPost}}{{/each}}`
* @param {string} context
* @param {string} postId
* @param {Object} options
* @return {string}
* @todo support multiple postIds in an array
*/
FlowHandlebars.prototype.eachPost = function ( context, postId, options ) {
var revId = ( context.posts && context.posts[ postId ] && context.posts[ postId ][ 0 ] ),
revision = ( context.revisions && context.revisions[ revId ] ) || { content: null };
if ( revision.content === null ) {
mw.flow.debug( '[eachPost] Failed to find revision object', arguments );
}
return options.fn ? options.fn( revision ) : revision;
};
/**
* The progressiveEnhancement helper essentially does one of replace things:
* 1. type="replace": (target="selector") Replaces target entirely with rendered template.
* 2. type="content": (target="selector") Replaces target's content with rendered template.
* 3. type="insert": Inserts rendered template at the helper's location.
*
* This template is used to simplify server-side and client-side rendering. Client-side renders a
* progressiveEnhancement helper instantly, in the post-process stage. The server-side renders only a script tag
* with a template inside. This script tag is found ondomready, and then the post-processing occurs at that time.
*
* Option keys:
* * type=String (replace, content, insert)
* * target=String (jQuery selector; needed for replace and content -- defaults to self)
* * id=String
*
* Example: `{{#progressiveEnhancement type="content"}}{{> ok}}{{/progressiveEnhancement}}`
*
* @param {Object} options
* @return {string}
* @todo Implement support for full functionality, perhaps revisit the implementation.
*/
FlowHandlebars.prototype.progressiveEnhancement = function ( options ) {
var hash = options.hash,
// Replace nested script tag with placeholder tag for
// recursive progresiveEnhancement
inner = options.fn( this ).replace( /<\/script>/g, '</flowprogressivescript>' );
if ( !hash.type ) {
hash.type = 'insert';
}
return FlowHandlebars.prototype.html(
'<scr' + 'ipt' +
' type="text/x-handlebars-template-progressive-enhancement"' +
' data-type="' + hash.type + '"' +
( hash.target ? ' data-target="' + hash.target + '"' : '' ) +
( hash.id ? ' id="' + hash.id + '"' : '' ) +
'>' +
inner +
'</scr' + 'ipt>'
);
};
/**
* Runs a callback when user is anonymous
* @param array $options which must contain fn and inverse key mapping to functions.
*
* @return mixed result of callback
*/
FlowHandlebars.prototype.ifAnonymous = function ( options ) {
if ( mw.user.isAnon() ) {
return options.fn( this );
}
return options.inverse( this );
};
/**
* Adds returnto parameter pointing to given Title to an existing URL
* @param string $title
*
* @return string modified url
*/
FlowHandlebars.prototype.linkWithReturnTo = function ( title ) {
return mw.util.getUrl( title, {
returntoquery: encodeURIComponent( window.location.search ),
returnto: mw.config.get( 'wgPageName' )
} );
};
/**
* Accepts the contentType and content properties returned from the api
* for individual revisions and ensures that content is included in the
* final html page in an XSS safe manner.
*
* It is expected that all content with contentType of html has been
* processed by parsoid and is safe for direct output into the document.
*
* Usage:
* {{escapeContent revision.contentType revision.content}}
*
* @param {string} contentType
* @param {string} content
* @return {string}
*/
FlowHandlebars.prototype.escapeContent = function ( contentType, content ) {
if ( contentType === 'html' || contentType === 'fixed-html' || contentType === 'topic-title-html' ) {
return FlowHandlebars.prototype.html( content );
}
return content;
};
/**
* Renders a tooltip node.
*
* Example: `{{#tooltip positionClass="up" contextClass="progressive" extraClass="flow-my-tooltip"}}what{{/tooltip}}`
*
* @param {Object} options
* @return {string}
*/
FlowHandlebars.prototype.tooltip = function ( options ) {
var params = options.hash;
return FlowHandlebars.prototype.html( FlowHandlebars.prototype.processTemplate(
'flow_tooltip',
{
positionClass: params.positionClass ? 'flow-ui-tooltip-' + params.positionClass : null,
contextClass: params.contextClass ? 'mw-ui-' + params.contextClass : null,
extraClass: params.extraClass,
blockClass: params.isBlock ? 'flow-ui-tooltip-block' : null,
content: options.fn( this )
}
) );
};
/**
* Return url for putting post into the specified moderation state. If the user
* cannot put the post into the specified state a blank string is returned.
*
* @param {Object} actions
* @param {string} moderationState
* @return {string}
*/
FlowHandlebars.prototype.moderationAction = function ( actions, moderationState ) {
return actions[ moderationState ] ? actions[ moderationState ].url : '';
};
/**
* Concatenate all unnamed handlebars arguments
*
* @return {string}
*/
FlowHandlebars.prototype.concat = function () {
// handlebars puts an options argument at the end of
// user supplied parameters, pop that off
return Array.prototype.slice.call( arguments, 0, -1 ).join( '' );
};
/**
* Renders block if condition is true
*
* @param {string} value
* @param {string} operator supported values: 'or'
* @param {string} value2
*/
FlowHandlebars.prototype.ifCond = function ( value, operator, value2, options ) {
if ( operator === 'or' ) {
return value || value2 ? options.fn( this ) : options.inverse( this );
}
if ( operator === '===' ) {
return value === value2 ? options.fn( this ) : options.inverse( this );
}
if ( operator === '!==' ) {
return value !== value2 ? options.fn( this ) : options.inverse( this );
}
return '';
};
/**
* Outputs debugging information
*
* For development use only
*/
FlowHandlebars.prototype.debug = function () {
mw.flow.debug( '[Handlebars] debug', arguments );
};
// Load partials
$.each( mw.templates.values, function ( moduleName ) {
$.each( this, function ( name ) {
// remove extension
var partialMatch, partialName;
partialMatch = name.match( /handlebars\/(.*)\.partial\.handlebars$/ );
if ( partialMatch ) {
partialName = partialMatch[ 1 ];
Handlebars.partials[ partialName ] = mw.template.get( moduleName, name ).render;
}
} );
} );
// Register helpers
Handlebars.registerHelper( 'l10n', FlowHandlebars.prototype.l10n );
Handlebars.registerHelper( 'l10nParse', FlowHandlebars.prototype.l10nParse );
Handlebars.registerHelper( 'uuidTimestamp', FlowHandlebars.prototype.uuidTimestamp );
Handlebars.registerHelper( 'timestamp', FlowHandlebars.prototype.timestamp );
Handlebars.registerHelper( 'html', FlowHandlebars.prototype.html );
Handlebars.registerHelper( 'block', FlowHandlebars.prototype.workflowBlock );
Handlebars.registerHelper( 'post', FlowHandlebars.prototype.postBlock );
Handlebars.registerHelper( 'eachPost', FlowHandlebars.prototype.eachPost );
Handlebars.registerHelper( 'progressiveEnhancement', FlowHandlebars.prototype.progressiveEnhancement );
Handlebars.registerHelper( 'ifAnonymous', FlowHandlebars.prototype.ifAnonymous );
Handlebars.registerHelper( 'linkWithReturnTo', FlowHandlebars.prototype.linkWithReturnTo );
Handlebars.registerHelper( 'escapeContent', FlowHandlebars.prototype.escapeContent );
Handlebars.registerHelper( 'tooltip', FlowHandlebars.prototype.tooltip );
Handlebars.registerHelper( 'moderationAction', FlowHandlebars.prototype.moderationAction );
Handlebars.registerHelper( 'concat', FlowHandlebars.prototype.concat );
Handlebars.registerHelper( 'ifCond', FlowHandlebars.prototype.ifCond );
Handlebars.registerHelper( 'debug', FlowHandlebars.prototype.debug );
}( mediaWiki, jQuery, moment, Handlebars ) );