* @file Revisions interface functions, Backbone classes and
* the revisions.php document.ready bootstrap.
* @output wp-admin/js/revisions.js
window.wp = window.wp || {};
* Expose the module in window.wp.revisions.
revisions = wp.revisions = { model: {}, view: {}, controller: {} };
// Link post revisions data served from the back end.
revisions.settings = window._wpRevisionsSettings || {};
* A debugging utility for revisions. Works only when a
* debug flag is on and the browser supports it.
revisions.log = function() {
if ( window.console && revisions.debug ) {
window.console.log.apply( window.console, arguments );
// Handy functions to help with positioning.
$.fn.allOffsets = function() {
var offset = this.offset() || {top: 0, left: 0}, win = $(window);
return _.extend( offset, {
right: win.width() - offset.left - this.outerWidth(),
bottom: win.height() - offset.top - this.outerHeight()
$.fn.allPositions = function() {
var position = this.position() || {top: 0, left: 0}, parent = this.parent();
return _.extend( position, {
right: parent.outerWidth() - position.left - this.outerWidth(),
bottom: parent.outerHeight() - position.top - this.outerHeight()
* ========================================================================
* ========================================================================
revisions.model.Slider = Backbone.Model.extend({
initialize: function( options ) {
this.frame = options.frame;
this.revisions = options.revisions;
// Listen for changes to the revisions or mode from outside.
this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
// Listen for internal changes.
this.on( 'change:from', this.handleLocalChanges );
this.on( 'change:to', this.handleLocalChanges );
this.on( 'change:compareTwoMode', this.updateSliderSettings );
this.on( 'update:revisions', this.updateSliderSettings );
// Listen for changes to the hovered revision.
this.on( 'change:hoveredRevision', this.hoverRevision );
max: this.revisions.length - 1,
compareTwoMode: this.frame.get('compareTwoMode'),
from: this.frame.get('from'),
this.updateSliderSettings();
getSliderValue: function( a, b ) {
return isRtl ? this.revisions.length - this.revisions.indexOf( this.get(a) ) - 1 : this.revisions.indexOf( this.get(b) );
updateSliderSettings: function() {
if ( this.get('compareTwoMode') ) {
this.getSliderValue( 'to', 'from' ),
this.getSliderValue( 'from', 'to' )
range: true // Ensures handles cannot cross.
value: this.getSliderValue( 'to', 'to' ),
this.trigger( 'update:slider' );
// Called when a revision is hovered.
hoverRevision: function( model, value ) {
this.trigger( 'hovered:revision', value );
// Called when `compareTwoMode` changes.
updateMode: function( model, value ) {
this.set({ compareTwoMode: value });
// Called when `from` or `to` changes in the local model.
handleLocalChanges: function() {
// Receives revisions changes from outside the model.
receiveRevisions: function( from, to ) {
// Bail if nothing changed.
if ( this.get('from') === from && this.get('to') === to ) {
this.set({ from: from, to: to }, { silent: true });
this.trigger( 'update:revisions', from, to );
revisions.model.Tooltip = Backbone.Model.extend({
hovering: false, // Whether the mouse is hovering.
scrubbing: false // Whether the mouse is scrubbing.
initialize: function( options ) {
this.frame = options.frame;
this.revisions = options.revisions;
this.slider = options.slider;
this.listenTo( this.slider, 'hovered:revision', this.updateRevision );
this.listenTo( this.slider, 'change:hovering', this.setHovering );
this.listenTo( this.slider, 'change:scrubbing', this.setScrubbing );
updateRevision: function( revision ) {
this.set({ revision: revision });
setHovering: function( model, value ) {
this.set({ hovering: value });
setScrubbing: function( model, value ) {
this.set({ scrubbing: value });
revisions.model.Revision = Backbone.Model.extend({});
* wp.revisions.model.Revisions
* A collection of post revisions.
revisions.model.Revisions = Backbone.Collection.extend({
model: revisions.model.Revision,
_.bindAll( this, 'next', 'prev' );
next: function( revision ) {
var index = this.indexOf( revision );
if ( index !== -1 && index !== this.length - 1 ) {
return this.at( index + 1 );
prev: function( revision ) {
var index = this.indexOf( revision );
if ( index !== -1 && index !== 0 ) {
return this.at( index - 1 );
revisions.model.Field = Backbone.Model.extend({});
revisions.model.Fields = Backbone.Collection.extend({
model: revisions.model.Field
revisions.model.Diff = Backbone.Model.extend({
var fields = this.get('fields');
this.fields = new revisions.model.Fields( fields );
revisions.model.Diffs = Backbone.Collection.extend({
initialize: function( models, options ) {
_.bindAll( this, 'getClosestUnloaded' );
this.loadAll = _.once( this._loadAll );
this.revisions = options.revisions;
this.postId = options.postId;
model: revisions.model.Diff,
ensure: function( id, context ) {
var diff = this.get( id ),
request = this.requests[ id ],
wp.revisions.log( 'ensure', id );
this.trigger( 'ensure', ids, from, to, deferred.promise() );
deferred.resolveWith( context, [ diff ] );
this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
_.each( ids, _.bind( function( id ) {
// Remove anything that has an ongoing request.
if ( this.requests[ id ] ) {
// Remove anything we already have.
// Always include the ID that started this ensure.
request = this.load( _.keys( ids ) );
request.done( _.bind( function() {
deferred.resolveWith( context, [ this.get( id ) ] );
}, this ) ).fail( _.bind( function() {
return deferred.promise();
// Returns an array of proximal diffs.
getClosestUnloaded: function( ids, centerId ) {
return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) {
return Math.abs( centerId - pair[1] );
}).map( function( pair ) {
}).filter( function( diffId ) {
return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ];
_loadAll: function( allRevisionIds, centerId, num ) {
var self = this, deferred = $.Deferred(),
diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num );
if ( _.size( diffs ) > 0 ) {
this.load( diffs ).done( function() {
self._loadAll( allRevisionIds, centerId, num ).done( function() {
if ( 1 === num ) { // Already tried 1. This just isn't working. Give up.
} else { // Request fewer diffs this time.
self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() {
load: function( comparisons ) {
wp.revisions.log( 'load', comparisons );
// Our collection should only ever grow, never shrink, so `remove: false`.
return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() {
wp.revisions.log( 'load:complete', comparisons );
sync: function( method, model, options ) {
if ( 'read' === method ) {
options.data = _.extend( options.data || {}, {
action: 'get-revision-diffs',
var deferred = wp.ajax.send( options ),
requests = this.requests;
// Record that we're requesting each diff.
if ( options.data.compare ) {
_.each( options.data.compare, function( id ) {
requests[ id ] = deferred;
// When the request completes, clear the stored request.
deferred.always( function() {
if ( options.data.compare ) {
_.each( options.data.compare, function( id ) {
// Otherwise, fall back to `Backbone.sync()`.
return Backbone.Model.prototype.sync.apply( this, arguments );
* wp.revisions.model.FrameState
* @see wp.revisions.view.Frame
* @param {object} attributes Model attributes - none are required.
* @param {object} options Options for the model.
* @param {revisions.model.Revisions} options.revisions A collection of revisions.
revisions.model.FrameState = Backbone.Model.extend({
initialize: function( attributes, options ) {
var state = this.get( 'initialDiffState' );
_.bindAll( this, 'receiveDiff' );
this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 );
this.revisions = options.revisions;
this.diffs = new revisions.model.Diffs( [], {
revisions: this.revisions,
postId: this.get( 'postId' )
// Set the initial diffs collection.
this.diffs.set( this.get( 'diffData' ) );
// Set up internal listeners.
this.listenTo( this, 'change:from', this.changeRevisionHandler );
this.listenTo( this, 'change:to', this.changeRevisionHandler );
this.listenTo( this, 'change:compareTwoMode', this.changeMode );
this.listenTo( this, 'update:revisions', this.updatedRevisions );
this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
this.listenTo( this, 'update:diff', this.updateLoadingStatus );
// Set the initial revisions, baseUrl, and mode as provided through attributes.
to : this.revisions.get( state.to ),
from : this.revisions.get( state.from ),
compareTwoMode : state.compareTwoMode
// Start the router if browser supports History API.
if ( window.history && window.history.pushState ) {
this.router = new revisions.Router({ model: this });
if ( Backbone.History.started ) {
Backbone.history.start({ pushState: true });
updateLoadingStatus: function() {
this.set( 'error', false );
this.set( 'loading', ! this.diff() );
changeMode: function( model, value ) {
var toIndex = this.revisions.indexOf( this.get( 'to' ) );
// If we were on the first revision before switching to two-handled mode,
// bump the 'to' position over one.
if ( value && 0 === toIndex ) {
from: this.revisions.at( toIndex ),
to: this.revisions.at( toIndex + 1 )
// When switching back to single-handled mode, reset 'from' model to
// one position before the 'to' model.
if ( ! value && 0 !== toIndex ) { // '! value' means switching to single-handled mode.
from: this.revisions.at( toIndex - 1 ),
to: this.revisions.at( toIndex )
updatedRevisions: function( from, to ) {
if ( this.get( 'compareTwoMode' ) ) {
// @todo Compare-two loading strategy.
this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 );
// Fetch the currently loaded diff.
return this.diffs.get( this._diffId );
* So long as `from` and `to` are changed at the same time, the diff
* will only be updated once. This is because Backbone updates all of
* the changed attributes in `set`, and then fires the `change` events.
updateDiff: function( options ) {
var from, to, diffId, diff;
diffId = ( from ? from.id : 0 ) + ':' + to.id;
// Check if we're actually changing the diff id.
if ( this._diffId === diffId ) {
return $.Deferred().reject().promise();
this.trigger( 'update:revisions', from, to );
diff = this.diffs.get( diffId );
// If we already have the diff, then immediately trigger the update.
this.receiveDiff( diff );
return $.Deferred().resolve().promise();
// Otherwise, fetch the diff.
if ( options.immediate ) {
return this._ensureDiff();
this._debouncedEnsureDiff();
return $.Deferred().reject().promise();