+/* global isRtl */
+/**
+ * @file Revisions interface functions, Backbone classes and
+ * the revisions.php document.ready bootstrap.
+ *
+ */
+
window.wp = window.wp || {};
(function($) {
var revisions;
-
+ /**
+ * Expose the module in window.wp.revisions.
+ */
revisions = wp.revisions = { model: {}, view: {}, controller: {} };
- // Link settings.
- revisions.settings = _.isUndefined( _wpRevisionsSettings ) ? {} : _wpRevisionsSettings;
+ // Link post revisions data served from the back-end.
+ revisions.settings = window._wpRevisionsSettings || {};
// For debugging
revisions.debug = false;
+ /**
+ * wp.revisions.log
+ *
+ * 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 )
- console.log.apply( console, arguments );
+ if ( window.console && revisions.debug ) {
+ window.console.log.apply( window.console, arguments );
+ }
};
// Handy functions to help with positioning
});
};
- // wp_localize_script transforms top-level numbers into strings. Undo that.
- if ( revisions.settings.to )
- revisions.settings.to = parseInt( revisions.settings.to, 10 );
- if ( revisions.settings.from )
- revisions.settings.from = parseInt( revisions.settings.from, 10 );
-
- // wp_localize_script does not allow for top-level booleans. Fix that.
- if ( revisions.settings.compareTwoMode )
- revisions.settings.compareTwoMode = revisions.settings.compareTwoMode === '1';
-
/**
* ========================================================================
* MODELS
this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
// Listen for internal changes
- this.listenTo( this, 'change:from', this.handleLocalChanges );
- this.listenTo( this, 'change:to', this.handleLocalChanges );
- this.listenTo( this, 'change:compareTwoMode', this.updateSliderSettings );
- this.listenTo( this, 'update:revisions', this.updateSliderSettings );
+ 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.listenTo( this, 'change:hoveredRevision', this.hoverRevision );
+ this.on( 'change:hoveredRevision', this.hoverRevision );
this.set({
max: this.revisions.length - 1,
// Receives revisions changes from outside the model
receiveRevisions: function( from, to ) {
// Bail if nothing changed
- if ( this.get('from') === from && this.get('to') === to )
+ if ( this.get('from') === from && this.get('to') === to ) {
return;
+ }
this.set({ from: from, to: to }, { silent: true });
this.trigger( 'update:revisions', from, to );
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,
next: function( revision ) {
var index = this.indexOf( revision );
- if ( index !== -1 && index !== this.length - 1 )
+ 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 )
+ if ( index !== -1 && index !== 0 ) {
return this.at( index - 1 );
+ }
}
});
});
revisions.model.Diff = Backbone.Model.extend({
- initialize: function( attributes, options ) {
+ initialize: function() {
var fields = this.get('fields');
this.unset('fields');
_.bindAll( this, 'getClosestUnloaded' );
this.loadAll = _.once( this._loadAll );
this.revisions = options.revisions;
+ this.postId = options.postId;
this.requests = {};
},
model: revisions.model.Diff,
ensure: function( id, context ) {
- var diff = this.get( id );
- var request = this.requests[ id ];
- var deferred = $.Deferred();
- var ids = {};
- var from = id.split(':')[0];
- var to = id.split(':')[1];
+ var diff = this.get( id ),
+ request = this.requests[ id ],
+ deferred = $.Deferred(),
+ ids = {},
+ from = id.split(':')[0],
+ to = id.split(':')[1];
ids[id] = true;
wp.revisions.log( 'ensure', id );
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 ] )
+ if ( this.requests[ id ] ) {
delete ids[ id ];
+ }
// Remove anything we already have
- if ( this.get( id ) )
+ if ( this.get( id ) ) {
delete ids[ id ];
+ }
}, this ) );
if ( ! request ) {
// Always include the ID that started this ensure
},
_loadAll: function( allRevisionIds, centerId, num ) {
- var self = this, deferred = $.Deferred();
- diffs = _.first( this.getClosestUnloaded( 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() {
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(){
+ return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() {
wp.revisions.log( 'load:complete', comparisons );
});
},
options.context = this;
options.data = _.extend( options.data || {}, {
action: 'get-revision-diffs',
- post_id: revisions.settings.postId
+ post_id: this.postId
});
- var deferred = wp.ajax.send( options );
- var requests = this.requests;
+ var deferred = wp.ajax.send( options ),
+ requests = this.requests;
// Record that we're requesting each diff.
if ( options.data.compare ) {
});
+ /**
+ * wp.revisions.model.FrameState
+ *
+ * The frame state.
+ *
+ * @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({
defaults: {
loading: false,
},
initialize: function( attributes, options ) {
- var properties = {};
-
+ 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 });
- // Set the initial diffs collection provided through the settings
- this.diffs.set( revisions.settings.diffData );
+ 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.diffs, 'ensure:load', this.updateLoadingStatus );
this.listenTo( this, 'update:diff', this.updateLoadingStatus );
- // Set the initial revisions, baseUrl, and mode as provided through settings
- properties.to = this.revisions.get( revisions.settings.to );
- properties.from = this.revisions.get( revisions.settings.from );
- properties.compareTwoMode = revisions.settings.compareTwoMode;
- properties.baseUrl = revisions.settings.baseUrl;
- this.set( properties );
+ // Set the initial revisions, baseUrl, and mode as provided through attributes.
+
+ this.set( {
+ 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 ) {
},
changeMode: function( model, value ) {
- // If we were on the first revision before switching, we have to bump them over one
- if ( value && 0 === this.revisions.indexOf( this.get('to') ) ) {
+ 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 ) {
+ this.set({
+ 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
this.set({
- from: this.revisions.at(0),
- to: this.revisions.at(1)
+ from: this.revisions.at( toIndex - 1 ),
+ to: this.revisions.at( toIndex )
});
}
},
diffId = ( from ? from.id : 0 ) + ':' + to.id;
// Check if we're actually changing the diff id.
- if ( this._diffId === diffId )
+ if ( this._diffId === diffId ) {
return $.Deferred().reject().promise();
+ }
this._diffId = diffId;
this.trigger( 'update:revisions', from, to );
// A simple wrapper around `updateDiff` to prevent the change event's
// parameters from being passed through.
- changeRevisionHandler: function( model, value, options ) {
+ changeRevisionHandler: function() {
this.updateDiff();
},
* ========================================================================
*/
- // The frame view. This contains the entire page.
+ /**
+ * wp.revisions.view.Frame
+ *
+ * Top level frame that orchestrates the revisions experience.
+ *
+ * @param {object} options The options hash for the view.
+ * @param {revisions.model.FrameState} options.model The frame state model.
+ */
revisions.view.Frame = wp.Backbone.View.extend({
className: 'revisions',
template: wp.template('revisions-frame'),
}
});
- // The control view.
- // This contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
+ /**
+ * wp.revisions.view.Controls
+ *
+ * The controls view.
+ *
+ * Contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
+ */
revisions.view.Controls = wp.Backbone.View.extend({
className: 'revisions-controls',
var slider = new revisions.model.Slider({
frame: this.model,
revisions: this.model.revisions
- });
+ }),
// Prep the tooltip model
- var tooltip = new revisions.model.Tooltip({
+ tooltip = new revisions.model.Tooltip({
frame: this.model,
revisions: this.model.revisions,
slider: slider
this.top = this.$el.offset().top;
this.window = $(window);
this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) {
- var controls = e.data.controls;
- var container = controls.$el.parent();
- var scrolled = controls.window.scrollTop();
- var frame = controls.views.parent;
+ var controls = e.data.controls,
+ container = controls.$el.parent(),
+ scrolled = controls.window.scrollTop(),
+ frame = controls.views.parent;
if ( scrolled >= controls.top ) {
if ( ! frame.$el.hasClass('pinned') ) {
},
ready: function() {
- if ( this.model.revisions.length < 3 )
+ if ( this.model.revisions.length < 3 ) {
$('.revision-toggle-compare-mode').hide();
+ }
},
updateCompareTwoMode: function() {
},
// Toggle the compare two mode feature when the compare two checkbox is checked.
- compareTwoToggle: function( event ) {
+ compareTwoToggle: function() {
// Activate compare two mode?
this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
}
className: 'revisions-tooltip',
template: wp.template('revisions-meta'),
- initialize: function( options ) {
+ initialize: function() {
this.listenTo( this.model, 'change:offset', this.render );
this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
},
prepare: function() {
- if ( _.isNull( this.model.get('revision') ) )
+ if ( _.isNull( this.model.get('revision') ) ) {
return;
- else
+ } else {
return _.extend( { type: 'tooltip' }, {
attributes: this.model.get('revision').toJSON()
});
+ }
},
render: function() {
- var direction, directionVal, flipped, css = {}, position = this.model.revisions.indexOf( this.model.get('revision') ) + 1;
+ var otherDirection,
+ direction,
+ directionVal,
+ flipped,
+ css = {},
+ position = this.model.revisions.indexOf( this.model.get('revision') ) + 1;
+
flipped = ( position / this.model.revisions.length ) > 0.5;
if ( isRtl ) {
direction = flipped ? 'left' : 'right';
return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
},
- toggleVisibility: function( options ) {
- if ( this.visible() )
+ toggleVisibility: function() {
+ if ( this.visible() ) {
this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 );
- else
+ } else {
this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } );
+ }
return;
}
});
to: this.model.revisions.at( toIndex )
};
// If we're at the first revision, unset 'from'.
- if ( toIndex )
+ if ( toIndex ) {
attributes.from = this.model.revisions.at( toIndex - 1 );
- else
+ } else {
this.model.unset('from', { silent: true });
+ }
this.model.set( attributes );
},
// Check to see if the Previous or Next buttons need to be disabled or enabled.
disabledButtonCheck: function() {
- var maxVal = this.model.revisions.length - 1,
- minVal = 0,
- next = $('.revisions-next .button'),
+ var maxVal = this.model.revisions.length - 1,
+ minVal = 0,
+ next = $('.revisions-next .button'),
previous = $('.revisions-previous .button'),
- val = this.model.revisions.indexOf( this.model.get('to') );
+ val = this.model.revisions.indexOf( this.model.get('to') );
// Disable "Next" button if you're on the last node.
next.prop( 'disabled', ( maxVal === val ) );
},
mouseMove: function( e ) {
- var zoneCount = this.model.revisions.length - 1, // One fewer zone than models
- sliderFrom = this.$el.allOffsets()[this.direction], // "From" edge of slider
- sliderWidth = this.$el.width(), // Width of slider
- tickWidth = sliderWidth / zoneCount, // Calculated width of zone
- actualX = isRtl? $(window).width() - e.pageX : e.pageX; // Flipped for RTL - sliderFrom;
- actualX = actualX - sliderFrom; // Offset of mouse position in slider
- var currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 ) ) / tickWidth ); // Calculate the model index
+ var zoneCount = this.model.revisions.length - 1, // One fewer zone than models
+ sliderFrom = this.$el.allOffsets()[this.direction], // "From" edge of slider
+ sliderWidth = this.$el.width(), // Width of slider
+ tickWidth = sliderWidth / zoneCount, // Calculated width of zone
+ actualX = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom;
+ currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 ) ) / tickWidth ); // Calculate the model index
// Ensure sane value for currentModelIndex.
- if ( currentModelIndex < 0 )
+ if ( currentModelIndex < 0 ) {
currentModelIndex = 0;
- else if ( currentModelIndex >= this.model.revisions.length )
+ } else if ( currentModelIndex >= this.model.revisions.length ) {
currentModelIndex = this.model.revisions.length - 1;
+ }
// Update the tooltip mode
this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
// Track the mouse position to enable smooth dragging,
// overrides default jQuery UI step behavior.
$( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
- var view = e.data.view,
- leftDragBoundary = view.$el.offset().left,
- sliderOffset = leftDragBoundary,
- sliderRightEdge = leftDragBoundary + view.$el.width(),
- rightDragBoundary = sliderRightEdge,
- leftDragReset = '0',
- rightDragReset = '100%',
- handle = $( ui.handle );
+ var handles,
+ view = e.data.view,
+ leftDragBoundary = view.$el.offset().left,
+ sliderOffset = leftDragBoundary,
+ sliderRightEdge = leftDragBoundary + view.$el.width(),
+ rightDragBoundary = sliderRightEdge,
+ leftDragReset = '0',
+ rightDragReset = '100%',
+ handle = $( ui.handle );
// In two handle mode, ensure handles can't be dragged past each other.
// Adjust left/right boundaries and reset points.
if ( view.model.get('compareTwoMode') ) {
- var handles = handle.parent().find('.ui-slider-handle');
+ handles = handle.parent().find('.ui-slider-handle');
if ( handle.is( handles.first() ) ) { // We're the left handle
rightDragBoundary = handles.last().offset().left;
rightDragReset = rightDragBoundary - sliderOffset;
// Compare two revisions mode
if ( this.model.get('compareTwoMode') ) {
// Prevent sliders from occupying same spot
- if ( ui.values[1] === ui.values[0] )
+ if ( ui.values[1] === ui.values[0] ) {
return false;
- if ( isRtl )
+ }
+ if ( isRtl ) {
ui.values.reverse();
+ }
attributes = {
from: this.model.revisions.at( this.getPosition( ui.values[0] ) ),
to: this.model.revisions.at( this.getPosition( ui.values[1] ) )
to: this.model.revisions.at( this.getPosition( ui.value ) )
};
// If we're at the first revision, unset 'from'.
- if ( this.getPosition( ui.value ) > 0 )
+ if ( this.getPosition( ui.value ) > 0 ) {
attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 );
- else
+ } else {
attributes.from = undefined;
+ }
}
movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
// If we are scrubbing, a scrub to a revision is considered a hover
- if ( this.model.get('scrubbing') )
+ if ( this.model.get('scrubbing') ) {
attributes.hoveredRevision = movedRevision;
+ }
this.model.set( attributes );
},
- stop: function( event, ui ) {
+ stop: function() {
$( window ).off('mousemove.wp.revisions');
this.model.updateSliderSettings(); // To snap us back to a tick mark
this.model.set({ scrubbing: false });
// This is the view for the current active diff.
revisions.view.Diff = wp.Backbone.View.extend({
className: 'revisions-diff',
- template: wp.template('revisions-diff'),
+ template: wp.template('revisions-diff'),
// Generate the options to be passed to the template.
prepare: function() {
}
});
- // The revisions router
- // takes URLs with #hash fragments and routes them
+ // The revisions router.
+ // Maintains the URL routes so browser URL matches state.
revisions.Router = Backbone.Router.extend({
initialize: function( options ) {
this.model = options.model;
- this.routes = _.object([
- [ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ],
- [ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ]
- ]);
+
// Maintain state and history when navigating
this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
},
updateUrl: function() {
- var from = this.model.has('from') ? this.model.get('from').id : 0;
- var to = this.model.get('to').id;
- if ( this.model.get('compareTwoMode' ) )
- this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ) );
- else
- this.navigate( this.baseUrl( '?revision=' + to ) );
+ var from = this.model.has('from') ? this.model.get('from').id : 0,
+ to = this.model.get('to').id;
+ if ( this.model.get('compareTwoMode' ) ) {
+ this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ), { replace: true } );
+ } else {
+ this.navigate( this.baseUrl( '?revision=' + to ), { replace: true } );
+ }
},
handleRoute: function( a, b ) {
- var from, to, compareTwo = _.isUndefined( b );
+ var compareTwo = _.isUndefined( b );
if ( ! compareTwo ) {
b = this.model.revisions.get( a );
b = b ? b.id : 0;
a = a ? a.id : 0;
}
-
- this.model.set({
- from: this.model.revisions.get( parseInt( a, 10 ) ),
- to: this.model.revisions.get( parseInt( a, 10 ) ),
- compareTwoMode: compareTwo
- });
}
});
- // Initialize the revisions UI.
+ /**
+ * Initialize the revisions UI for revision.php.
+ */
revisions.init = function() {
+ var state;
+
+ // Bail if the current page is not revision.php.
+ if ( ! window.adminpage || 'revision-php' !== window.adminpage ) {
+ return;
+ }
+
+ state = new revisions.model.FrameState({
+ initialDiffState: {
+ // wp_localize_script doesn't stringifies ints, so cast them.
+ to: parseInt( revisions.settings.to, 10 ),
+ from: parseInt( revisions.settings.from, 10 ),
+ // wp_localize_script does not allow for top-level booleans so do a comparator here.
+ compareTwoMode: ( revisions.settings.compareTwoMode === '1' )
+ },
+ diffData: revisions.settings.diffData,
+ baseUrl: revisions.settings.baseUrl,
+ postId: parseInt( revisions.settings.postId, 10 )
+ }, {
+ revisions: new revisions.model.Revisions( revisions.settings.revisionData )
+ });
+
revisions.view.frame = new revisions.view.Frame({
- model: new revisions.model.FrameState({}, {
- revisions: new revisions.model.Revisions( revisions.settings.revisionData )
- })
+ model: state
}).render();
};