wp/wp-admin/js/revisions.js
changeset 5 5e2f62d02dcd
parent 0 d970ebf37754
child 7 cf61fcea0001
equal deleted inserted replaced
4:346c88efed21 5:5e2f62d02dcd
       
     1 /* global isRtl */
       
     2 /**
       
     3  * @file Revisions interface functions, Backbone classes and
       
     4  * the revisions.php document.ready bootstrap.
       
     5  *
       
     6  */
       
     7 
     1 window.wp = window.wp || {};
     8 window.wp = window.wp || {};
     2 
     9 
     3 (function($) {
    10 (function($) {
     4 	var revisions;
    11 	var revisions;
     5 
    12 	/**
       
    13 	 * Expose the module in window.wp.revisions.
       
    14 	 */
     6 	revisions = wp.revisions = { model: {}, view: {}, controller: {} };
    15 	revisions = wp.revisions = { model: {}, view: {}, controller: {} };
     7 
    16 
     8 	// Link settings.
    17 	// Link post revisions data served from the back-end.
     9 	revisions.settings = _.isUndefined( _wpRevisionsSettings ) ? {} : _wpRevisionsSettings;
    18 	revisions.settings = window._wpRevisionsSettings || {};
    10 
    19 
    11 	// For debugging
    20 	// For debugging
    12 	revisions.debug = false;
    21 	revisions.debug = false;
    13 
    22 
       
    23 	/**
       
    24 	 * wp.revisions.log
       
    25 	 *
       
    26 	 * A debugging utility for revisions. Works only when a
       
    27 	 * debugĀ flag is on and the browser supports it.
       
    28 	 */
    14 	revisions.log = function() {
    29 	revisions.log = function() {
    15 		if ( window.console && revisions.debug )
    30 		if ( window.console && revisions.debug ) {
    16 			console.log.apply( console, arguments );
    31 			window.console.log.apply( window.console, arguments );
       
    32 		}
    17 	};
    33 	};
    18 
    34 
    19 	// Handy functions to help with positioning
    35 	// Handy functions to help with positioning
    20 	$.fn.allOffsets = function() {
    36 	$.fn.allOffsets = function() {
    21 		var offset = this.offset() || {top: 0, left: 0}, win = $(window);
    37 		var offset = this.offset() || {top: 0, left: 0}, win = $(window);
    30 		return _.extend( position, {
    46 		return _.extend( position, {
    31 			right:  parent.outerWidth()  - position.left - this.outerWidth(),
    47 			right:  parent.outerWidth()  - position.left - this.outerWidth(),
    32 			bottom: parent.outerHeight() - position.top  - this.outerHeight()
    48 			bottom: parent.outerHeight() - position.top  - this.outerHeight()
    33 		});
    49 		});
    34 	};
    50 	};
    35 
       
    36 	// wp_localize_script transforms top-level numbers into strings. Undo that.
       
    37 	if ( revisions.settings.to )
       
    38 		revisions.settings.to = parseInt( revisions.settings.to, 10 );
       
    39 	if ( revisions.settings.from )
       
    40 		revisions.settings.from = parseInt( revisions.settings.from, 10 );
       
    41 
       
    42 	// wp_localize_script does not allow for top-level booleans. Fix that.
       
    43 	if ( revisions.settings.compareTwoMode )
       
    44 		revisions.settings.compareTwoMode = revisions.settings.compareTwoMode === '1';
       
    45 
    51 
    46 	/**
    52 	/**
    47 	 * ========================================================================
    53 	 * ========================================================================
    48 	 * MODELS
    54 	 * MODELS
    49 	 * ========================================================================
    55 	 * ========================================================================
    66 			// Listen for changes to the revisions or mode from outside
    72 			// Listen for changes to the revisions or mode from outside
    67 			this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
    73 			this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
    68 			this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
    74 			this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
    69 
    75 
    70 			// Listen for internal changes
    76 			// Listen for internal changes
    71 			this.listenTo( this, 'change:from', this.handleLocalChanges );
    77 			this.on( 'change:from', this.handleLocalChanges );
    72 			this.listenTo( this, 'change:to', this.handleLocalChanges );
    78 			this.on( 'change:to', this.handleLocalChanges );
    73 			this.listenTo( this, 'change:compareTwoMode', this.updateSliderSettings );
    79 			this.on( 'change:compareTwoMode', this.updateSliderSettings );
    74 			this.listenTo( this, 'update:revisions', this.updateSliderSettings );
    80 			this.on( 'update:revisions', this.updateSliderSettings );
    75 
    81 
    76 			// Listen for changes to the hovered revision
    82 			// Listen for changes to the hovered revision
    77 			this.listenTo( this, 'change:hoveredRevision', this.hoverRevision );
    83 			this.on( 'change:hoveredRevision', this.hoverRevision );
    78 
    84 
    79 			this.set({
    85 			this.set({
    80 				max:   this.revisions.length - 1,
    86 				max:   this.revisions.length - 1,
    81 				compareTwoMode: this.frame.get('compareTwoMode'),
    87 				compareTwoMode: this.frame.get('compareTwoMode'),
    82 				from: this.frame.get('from'),
    88 				from: this.frame.get('from'),
   128 		},
   134 		},
   129 
   135 
   130 		// Receives revisions changes from outside the model
   136 		// Receives revisions changes from outside the model
   131 		receiveRevisions: function( from, to ) {
   137 		receiveRevisions: function( from, to ) {
   132 			// Bail if nothing changed
   138 			// Bail if nothing changed
   133 			if ( this.get('from') === from && this.get('to') === to )
   139 			if ( this.get('from') === from && this.get('to') === to ) {
   134 				return;
   140 				return;
       
   141 			}
   135 
   142 
   136 			this.set({ from: from, to: to }, { silent: true });
   143 			this.set({ from: from, to: to }, { silent: true });
   137 			this.trigger( 'update:revisions', from, to );
   144 			this.trigger( 'update:revisions', from, to );
   138 		}
   145 		}
   139 
   146 
   171 		}
   178 		}
   172 	});
   179 	});
   173 
   180 
   174 	revisions.model.Revision = Backbone.Model.extend({});
   181 	revisions.model.Revision = Backbone.Model.extend({});
   175 
   182 
       
   183 	/**
       
   184 	 * wp.revisions.model.Revisions
       
   185 	 *
       
   186 	 * A collection of post revisions.
       
   187 	 */
   176 	revisions.model.Revisions = Backbone.Collection.extend({
   188 	revisions.model.Revisions = Backbone.Collection.extend({
   177 		model: revisions.model.Revision,
   189 		model: revisions.model.Revision,
   178 
   190 
   179 		initialize: function() {
   191 		initialize: function() {
   180 			_.bindAll( this, 'next', 'prev' );
   192 			_.bindAll( this, 'next', 'prev' );
   181 		},
   193 		},
   182 
   194 
   183 		next: function( revision ) {
   195 		next: function( revision ) {
   184 			var index = this.indexOf( revision );
   196 			var index = this.indexOf( revision );
   185 
   197 
   186 			if ( index !== -1 && index !== this.length - 1 )
   198 			if ( index !== -1 && index !== this.length - 1 ) {
   187 				return this.at( index + 1 );
   199 				return this.at( index + 1 );
       
   200 			}
   188 		},
   201 		},
   189 
   202 
   190 		prev: function( revision ) {
   203 		prev: function( revision ) {
   191 			var index = this.indexOf( revision );
   204 			var index = this.indexOf( revision );
   192 
   205 
   193 			if ( index !== -1 && index !== 0 )
   206 			if ( index !== -1 && index !== 0 ) {
   194 				return this.at( index - 1 );
   207 				return this.at( index - 1 );
       
   208 			}
   195 		}
   209 		}
   196 	});
   210 	});
   197 
   211 
   198 	revisions.model.Field = Backbone.Model.extend({});
   212 	revisions.model.Field = Backbone.Model.extend({});
   199 
   213 
   200 	revisions.model.Fields = Backbone.Collection.extend({
   214 	revisions.model.Fields = Backbone.Collection.extend({
   201 		model: revisions.model.Field
   215 		model: revisions.model.Field
   202 	});
   216 	});
   203 
   217 
   204 	revisions.model.Diff = Backbone.Model.extend({
   218 	revisions.model.Diff = Backbone.Model.extend({
   205 		initialize: function( attributes, options ) {
   219 		initialize: function() {
   206 			var fields = this.get('fields');
   220 			var fields = this.get('fields');
   207 			this.unset('fields');
   221 			this.unset('fields');
   208 
   222 
   209 			this.fields = new revisions.model.Fields( fields );
   223 			this.fields = new revisions.model.Fields( fields );
   210 		}
   224 		}
   213 	revisions.model.Diffs = Backbone.Collection.extend({
   227 	revisions.model.Diffs = Backbone.Collection.extend({
   214 		initialize: function( models, options ) {
   228 		initialize: function( models, options ) {
   215 			_.bindAll( this, 'getClosestUnloaded' );
   229 			_.bindAll( this, 'getClosestUnloaded' );
   216 			this.loadAll = _.once( this._loadAll );
   230 			this.loadAll = _.once( this._loadAll );
   217 			this.revisions = options.revisions;
   231 			this.revisions = options.revisions;
       
   232 			this.postId = options.postId;
   218 			this.requests  = {};
   233 			this.requests  = {};
   219 		},
   234 		},
   220 
   235 
   221 		model: revisions.model.Diff,
   236 		model: revisions.model.Diff,
   222 
   237 
   223 		ensure: function( id, context ) {
   238 		ensure: function( id, context ) {
   224 			var diff     = this.get( id );
   239 			var diff     = this.get( id ),
   225 			var request  = this.requests[ id ];
   240 				request  = this.requests[ id ],
   226 			var deferred = $.Deferred();
   241 				deferred = $.Deferred(),
   227 			var ids      = {};
   242 				ids      = {},
   228 			var from     = id.split(':')[0];
   243 				from     = id.split(':')[0],
   229 			var to       = id.split(':')[1];
   244 				to       = id.split(':')[1];
   230 			ids[id] = true;
   245 			ids[id] = true;
   231 
   246 
   232 			wp.revisions.log( 'ensure', id );
   247 			wp.revisions.log( 'ensure', id );
   233 
   248 
   234 			this.trigger( 'ensure', ids, from, to, deferred.promise() );
   249 			this.trigger( 'ensure', ids, from, to, deferred.promise() );
   237 				deferred.resolveWith( context, [ diff ] );
   252 				deferred.resolveWith( context, [ diff ] );
   238 			} else {
   253 			} else {
   239 				this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
   254 				this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
   240 				_.each( ids, _.bind( function( id ) {
   255 				_.each( ids, _.bind( function( id ) {
   241 					// Remove anything that has an ongoing request
   256 					// Remove anything that has an ongoing request
   242 					if ( this.requests[ id ] )
   257 					if ( this.requests[ id ] ) {
   243 						delete ids[ id ];
   258 						delete ids[ id ];
       
   259 					}
   244 					// Remove anything we already have
   260 					// Remove anything we already have
   245 					if ( this.get( id ) )
   261 					if ( this.get( id ) ) {
   246 						delete ids[ id ];
   262 						delete ids[ id ];
       
   263 					}
   247 				}, this ) );
   264 				}, this ) );
   248 				if ( ! request ) {
   265 				if ( ! request ) {
   249 					// Always include the ID that started this ensure
   266 					// Always include the ID that started this ensure
   250 					ids[ id ] = true;
   267 					ids[ id ] = true;
   251 					request   = this.load( _.keys( ids ) );
   268 					request   = this.load( _.keys( ids ) );
   272 				return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ];
   289 				return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ];
   273 			}).value();
   290 			}).value();
   274 		},
   291 		},
   275 
   292 
   276 		_loadAll: function( allRevisionIds, centerId, num ) {
   293 		_loadAll: function( allRevisionIds, centerId, num ) {
   277 			var self = this, deferred = $.Deferred();
   294 			var self = this, deferred = $.Deferred(),
   278 			diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num );
   295 				diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num );
   279 			if ( _.size( diffs ) > 0 ) {
   296 			if ( _.size( diffs ) > 0 ) {
   280 				this.load( diffs ).done( function() {
   297 				this.load( diffs ).done( function() {
   281 					self._loadAll( allRevisionIds, centerId, num ).done( function() {
   298 					self._loadAll( allRevisionIds, centerId, num ).done( function() {
   282 						deferred.resolve();
   299 						deferred.resolve();
   283 					});
   300 					});
   297 		},
   314 		},
   298 
   315 
   299 		load: function( comparisons ) {
   316 		load: function( comparisons ) {
   300 			wp.revisions.log( 'load', comparisons );
   317 			wp.revisions.log( 'load', comparisons );
   301 			// Our collection should only ever grow, never shrink, so remove: false
   318 			// Our collection should only ever grow, never shrink, so remove: false
   302 			return this.fetch({ data: { compare: comparisons }, remove: false }).done( function(){
   319 			return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() {
   303 				wp.revisions.log( 'load:complete', comparisons );
   320 				wp.revisions.log( 'load:complete', comparisons );
   304 			});
   321 			});
   305 		},
   322 		},
   306 
   323 
   307 		sync: function( method, model, options ) {
   324 		sync: function( method, model, options ) {
   308 			if ( 'read' === method ) {
   325 			if ( 'read' === method ) {
   309 				options = options || {};
   326 				options = options || {};
   310 				options.context = this;
   327 				options.context = this;
   311 				options.data = _.extend( options.data || {}, {
   328 				options.data = _.extend( options.data || {}, {
   312 					action: 'get-revision-diffs',
   329 					action: 'get-revision-diffs',
   313 					post_id: revisions.settings.postId
   330 					post_id: this.postId
   314 				});
   331 				});
   315 
   332 
   316 				var deferred = wp.ajax.send( options );
   333 				var deferred = wp.ajax.send( options ),
   317 				var requests = this.requests;
   334 					requests = this.requests;
   318 
   335 
   319 				// Record that we're requesting each diff.
   336 				// Record that we're requesting each diff.
   320 				if ( options.data.compare ) {
   337 				if ( options.data.compare ) {
   321 					_.each( options.data.compare, function( id ) {
   338 					_.each( options.data.compare, function( id ) {
   322 						requests[ id ] = deferred;
   339 						requests[ id ] = deferred;
   340 			}
   357 			}
   341 		}
   358 		}
   342 	});
   359 	});
   343 
   360 
   344 
   361 
       
   362 	/**
       
   363 	 * wp.revisions.model.FrameState
       
   364 	 *
       
   365 	 * The frame state.
       
   366 	 *
       
   367 	 * @see wp.revisions.view.Frame
       
   368 	 *
       
   369 	 * @param {object}                    attributes        Model attributes - none are required.
       
   370 	 * @param {object}                    options           Options for the model.
       
   371 	 * @param {revisions.model.Revisions} options.revisions A collection of revisions.
       
   372 	 */
   345 	revisions.model.FrameState = Backbone.Model.extend({
   373 	revisions.model.FrameState = Backbone.Model.extend({
   346 		defaults: {
   374 		defaults: {
   347 			loading: false,
   375 			loading: false,
   348 			error: false,
   376 			error: false,
   349 			compareTwoMode: false
   377 			compareTwoMode: false
   350 		},
   378 		},
   351 
   379 
   352 		initialize: function( attributes, options ) {
   380 		initialize: function( attributes, options ) {
   353 			var properties = {};
   381 			var state = this.get( 'initialDiffState' );
   354 
       
   355 			_.bindAll( this, 'receiveDiff' );
   382 			_.bindAll( this, 'receiveDiff' );
   356 			this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 );
   383 			this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 );
   357 
   384 
   358 			this.revisions = options.revisions;
   385 			this.revisions = options.revisions;
   359 			this.diffs = new revisions.model.Diffs( [], { revisions: this.revisions });
   386 
   360 
   387 			this.diffs = new revisions.model.Diffs( [], {
   361 			// Set the initial diffs collection provided through the settings
   388 				revisions: this.revisions,
   362 			this.diffs.set( revisions.settings.diffData );
   389 				postId: this.get( 'postId' )
       
   390 			} );
       
   391 
       
   392 			// Set the initial diffs collection.
       
   393 			this.diffs.set( this.get( 'diffData' ) );
   363 
   394 
   364 			// Set up internal listeners
   395 			// Set up internal listeners
   365 			this.listenTo( this, 'change:from', this.changeRevisionHandler );
   396 			this.listenTo( this, 'change:from', this.changeRevisionHandler );
   366 			this.listenTo( this, 'change:to', this.changeRevisionHandler );
   397 			this.listenTo( this, 'change:to', this.changeRevisionHandler );
   367 			this.listenTo( this, 'change:compareTwoMode', this.changeMode );
   398 			this.listenTo( this, 'change:compareTwoMode', this.changeMode );
   368 			this.listenTo( this, 'update:revisions', this.updatedRevisions );
   399 			this.listenTo( this, 'update:revisions', this.updatedRevisions );
   369 			this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
   400 			this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
   370 			this.listenTo( this, 'update:diff', this.updateLoadingStatus );
   401 			this.listenTo( this, 'update:diff', this.updateLoadingStatus );
   371 
   402 
   372 			// Set the initial revisions, baseUrl, and mode as provided through settings
   403 			// Set the initial revisions, baseUrl, and mode as provided through attributes.
   373 			properties.to = this.revisions.get( revisions.settings.to );
   404 
   374 			properties.from = this.revisions.get( revisions.settings.from );
   405 			this.set( {
   375 			properties.compareTwoMode = revisions.settings.compareTwoMode;
   406 				to : this.revisions.get( state.to ),
   376 			properties.baseUrl = revisions.settings.baseUrl;
   407 				from : this.revisions.get( state.from ),
   377 			this.set( properties );
   408 				compareTwoMode : state.compareTwoMode
       
   409 			} );
   378 
   410 
   379 			// Start the router if browser supports History API
   411 			// Start the router if browser supports History API
   380 			if ( window.history && window.history.pushState ) {
   412 			if ( window.history && window.history.pushState ) {
   381 				this.router = new revisions.Router({ model: this });
   413 				this.router = new revisions.Router({ model: this });
   382 				Backbone.history.start({ pushState: true });
   414 				Backbone.history.start({ pushState: true });
   387 			this.set( 'error', false );
   419 			this.set( 'error', false );
   388 			this.set( 'loading', ! this.diff() );
   420 			this.set( 'loading', ! this.diff() );
   389 		},
   421 		},
   390 
   422 
   391 		changeMode: function( model, value ) {
   423 		changeMode: function( model, value ) {
   392 			// If we were on the first revision before switching, we have to bump them over one
   424 			var toIndex = this.revisions.indexOf( this.get( 'to' ) );
   393 			if ( value && 0 === this.revisions.indexOf( this.get('to') ) ) {
   425 
       
   426 			// If we were on the first revision before switching to two-handled mode,
       
   427 			// bump the 'to' position over one
       
   428 			if ( value && 0 === toIndex ) {
   394 				this.set({
   429 				this.set({
   395 					from: this.revisions.at(0),
   430 					from: this.revisions.at( toIndex ),
   396 					to: this.revisions.at(1)
   431 					to:   this.revisions.at( toIndex + 1 )
       
   432 				});
       
   433 			}
       
   434 
       
   435 			// When switching back to single-handled mode, reset 'from' model to
       
   436 			// one position before the 'to' model
       
   437 			if ( ! value && 0 !== toIndex ) { // '! value' means switching to single-handled mode
       
   438 				this.set({
       
   439 					from: this.revisions.at( toIndex - 1 ),
       
   440 					to:   this.revisions.at( toIndex )
   397 				});
   441 				});
   398 			}
   442 			}
   399 		},
   443 		},
   400 
   444 
   401 		updatedRevisions: function( from, to ) {
   445 		updatedRevisions: function( from, to ) {
   421 			from = this.get('from');
   465 			from = this.get('from');
   422 			to = this.get('to');
   466 			to = this.get('to');
   423 			diffId = ( from ? from.id : 0 ) + ':' + to.id;
   467 			diffId = ( from ? from.id : 0 ) + ':' + to.id;
   424 
   468 
   425 			// Check if we're actually changing the diff id.
   469 			// Check if we're actually changing the diff id.
   426 			if ( this._diffId === diffId )
   470 			if ( this._diffId === diffId ) {
   427 				return $.Deferred().reject().promise();
   471 				return $.Deferred().reject().promise();
       
   472 			}
   428 
   473 
   429 			this._diffId = diffId;
   474 			this._diffId = diffId;
   430 			this.trigger( 'update:revisions', from, to );
   475 			this.trigger( 'update:revisions', from, to );
   431 
   476 
   432 			diff = this.diffs.get( diffId );
   477 			diff = this.diffs.get( diffId );
   446 			}
   491 			}
   447 		},
   492 		},
   448 
   493 
   449 		// A simple wrapper around `updateDiff` to prevent the change event's
   494 		// A simple wrapper around `updateDiff` to prevent the change event's
   450 		// parameters from being passed through.
   495 		// parameters from being passed through.
   451 		changeRevisionHandler: function( model, value, options ) {
   496 		changeRevisionHandler: function() {
   452 			this.updateDiff();
   497 			this.updateDiff();
   453 		},
   498 		},
   454 
   499 
   455 		receiveDiff: function( diff ) {
   500 		receiveDiff: function( diff ) {
   456 			// Did we actually get a diff?
   501 			// Did we actually get a diff?
   474 	 * ========================================================================
   519 	 * ========================================================================
   475 	 * VIEWS
   520 	 * VIEWS
   476 	 * ========================================================================
   521 	 * ========================================================================
   477 	 */
   522 	 */
   478 
   523 
   479 	// The frame view. This contains the entire page.
   524 	/**
       
   525 	 * wp.revisions.view.Frame
       
   526 	 *
       
   527 	 * Top level frame that orchestrates the revisions experience.
       
   528 	 *
       
   529 	 * @param {object}                     options       The options hash for the view.
       
   530 	 * @param {revisions.model.FrameState} options.model The frame state model.
       
   531 	 */
   480 	revisions.view.Frame = wp.Backbone.View.extend({
   532 	revisions.view.Frame = wp.Backbone.View.extend({
   481 		className: 'revisions',
   533 		className: 'revisions',
   482 		template: wp.template('revisions-frame'),
   534 		template: wp.template('revisions-frame'),
   483 
   535 
   484 		initialize: function() {
   536 		initialize: function() {
   521 		updateCompareTwoMode: function() {
   573 		updateCompareTwoMode: function() {
   522 			this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') );
   574 			this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') );
   523 		}
   575 		}
   524 	});
   576 	});
   525 
   577 
   526 	// The control view.
   578 	/**
   527 	// This contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
   579 	 * wp.revisions.view.Controls
       
   580 	 *
       
   581 	 * The controls view.
       
   582 	 *
       
   583 	 * Contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
       
   584 	 */
   528 	revisions.view.Controls = wp.Backbone.View.extend({
   585 	revisions.view.Controls = wp.Backbone.View.extend({
   529 		className: 'revisions-controls',
   586 		className: 'revisions-controls',
   530 
   587 
   531 		initialize: function() {
   588 		initialize: function() {
   532 			_.bindAll( this, 'setWidth' );
   589 			_.bindAll( this, 'setWidth' );
   543 
   600 
   544 			// Prep the slider model
   601 			// Prep the slider model
   545 			var slider = new revisions.model.Slider({
   602 			var slider = new revisions.model.Slider({
   546 				frame: this.model,
   603 				frame: this.model,
   547 				revisions: this.model.revisions
   604 				revisions: this.model.revisions
   548 			});
   605 			}),
   549 
   606 
   550 			// Prep the tooltip model
   607 			// Prep the tooltip model
   551 			var tooltip = new revisions.model.Tooltip({
   608 			tooltip = new revisions.model.Tooltip({
   552 				frame: this.model,
   609 				frame: this.model,
   553 				revisions: this.model.revisions,
   610 				revisions: this.model.revisions,
   554 				slider: slider
   611 				slider: slider
   555 			});
   612 			});
   556 
   613 
   577 
   634 
   578 		ready: function() {
   635 		ready: function() {
   579 			this.top = this.$el.offset().top;
   636 			this.top = this.$el.offset().top;
   580 			this.window = $(window);
   637 			this.window = $(window);
   581 			this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) {
   638 			this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) {
   582 				var controls = e.data.controls;
   639 				var controls  = e.data.controls,
   583 				var container = controls.$el.parent();
   640 					container = controls.$el.parent(),
   584 				var scrolled = controls.window.scrollTop();
   641 					scrolled  = controls.window.scrollTop(),
   585 				var frame = controls.views.parent;
   642 					frame     = controls.views.parent;
   586 
   643 
   587 				if ( scrolled >= controls.top ) {
   644 				if ( scrolled >= controls.top ) {
   588 					if ( ! frame.$el.hasClass('pinned') ) {
   645 					if ( ! frame.$el.hasClass('pinned') ) {
   589 						controls.setWidth();
   646 						controls.setWidth();
   590 						container.css('height', container.height() + 'px' );
   647 						container.css('height', container.height() + 'px' );
   722 		initialize: function() {
   779 		initialize: function() {
   723 			this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
   780 			this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
   724 		},
   781 		},
   725 
   782 
   726 		ready: function() {
   783 		ready: function() {
   727 			if ( this.model.revisions.length < 3 )
   784 			if ( this.model.revisions.length < 3 ) {
   728 				$('.revision-toggle-compare-mode').hide();
   785 				$('.revision-toggle-compare-mode').hide();
       
   786 			}
   729 		},
   787 		},
   730 
   788 
   731 		updateCompareTwoMode: function() {
   789 		updateCompareTwoMode: function() {
   732 			this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') );
   790 			this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') );
   733 		},
   791 		},
   734 
   792 
   735 		// Toggle the compare two mode feature when the compare two checkbox is checked.
   793 		// Toggle the compare two mode feature when the compare two checkbox is checked.
   736 		compareTwoToggle: function( event ) {
   794 		compareTwoToggle: function() {
   737 			// Activate compare two mode?
   795 			// Activate compare two mode?
   738 			this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
   796 			this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
   739 		}
   797 		}
   740 	});
   798 	});
   741 
   799 
   743 	// Encapsulates the tooltip.
   801 	// Encapsulates the tooltip.
   744 	revisions.view.Tooltip = wp.Backbone.View.extend({
   802 	revisions.view.Tooltip = wp.Backbone.View.extend({
   745 		className: 'revisions-tooltip',
   803 		className: 'revisions-tooltip',
   746 		template: wp.template('revisions-meta'),
   804 		template: wp.template('revisions-meta'),
   747 
   805 
   748 		initialize: function( options ) {
   806 		initialize: function() {
   749 			this.listenTo( this.model, 'change:offset', this.render );
   807 			this.listenTo( this.model, 'change:offset', this.render );
   750 			this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
   808 			this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
   751 			this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
   809 			this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
   752 		},
   810 		},
   753 
   811 
   754 		prepare: function() {
   812 		prepare: function() {
   755 			if ( _.isNull( this.model.get('revision') ) )
   813 			if ( _.isNull( this.model.get('revision') ) ) {
   756 				return;
   814 				return;
   757 			else
   815 			} else {
   758 				return _.extend( { type: 'tooltip' }, {
   816 				return _.extend( { type: 'tooltip' }, {
   759 					attributes: this.model.get('revision').toJSON()
   817 					attributes: this.model.get('revision').toJSON()
   760 				});
   818 				});
       
   819 			}
   761 		},
   820 		},
   762 
   821 
   763 		render: function() {
   822 		render: function() {
   764 			var direction, directionVal, flipped, css = {}, position = this.model.revisions.indexOf( this.model.get('revision') ) + 1;
   823 			var otherDirection,
       
   824 				direction,
       
   825 				directionVal,
       
   826 				flipped,
       
   827 				css      = {},
       
   828 				position = this.model.revisions.indexOf( this.model.get('revision') ) + 1;
       
   829 
   765 			flipped = ( position / this.model.revisions.length ) > 0.5;
   830 			flipped = ( position / this.model.revisions.length ) > 0.5;
   766 			if ( isRtl ) {
   831 			if ( isRtl ) {
   767 				direction = flipped ? 'left' : 'right';
   832 				direction = flipped ? 'left' : 'right';
   768 				directionVal = flipped ? 'leftPlusWidth' : direction;
   833 				directionVal = flipped ? 'leftPlusWidth' : direction;
   769 			} else {
   834 			} else {
   779 
   844 
   780 		visible: function() {
   845 		visible: function() {
   781 			return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
   846 			return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
   782 		},
   847 		},
   783 
   848 
   784 		toggleVisibility: function( options ) {
   849 		toggleVisibility: function() {
   785 			if ( this.visible() )
   850 			if ( this.visible() ) {
   786 				this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 );
   851 				this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 );
   787 			else
   852 			} else {
   788 				this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } );
   853 				this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } );
       
   854 			}
   789 			return;
   855 			return;
   790 		}
   856 		}
   791 	});
   857 	});
   792 
   858 
   793 	// The buttons view.
   859 	// The buttons view.
   813 		gotoModel: function( toIndex ) {
   879 		gotoModel: function( toIndex ) {
   814 			var attributes = {
   880 			var attributes = {
   815 				to: this.model.revisions.at( toIndex )
   881 				to: this.model.revisions.at( toIndex )
   816 			};
   882 			};
   817 			// If we're at the first revision, unset 'from'.
   883 			// If we're at the first revision, unset 'from'.
   818 			if ( toIndex )
   884 			if ( toIndex ) {
   819 				attributes.from = this.model.revisions.at( toIndex - 1 );
   885 				attributes.from = this.model.revisions.at( toIndex - 1 );
   820 			else
   886 			} else {
   821 				this.model.unset('from', { silent: true });
   887 				this.model.unset('from', { silent: true });
       
   888 			}
   822 
   889 
   823 			this.model.set( attributes );
   890 			this.model.set( attributes );
   824 		},
   891 		},
   825 
   892 
   826 		// Go to the 'next' revision
   893 		// Go to the 'next' revision
   835 			this.gotoModel( toIndex );
   902 			this.gotoModel( toIndex );
   836 		},
   903 		},
   837 
   904 
   838 		// Check to see if the Previous or Next buttons need to be disabled or enabled.
   905 		// Check to see if the Previous or Next buttons need to be disabled or enabled.
   839 		disabledButtonCheck: function() {
   906 		disabledButtonCheck: function() {
   840 			var maxVal = this.model.revisions.length - 1,
   907 			var maxVal   = this.model.revisions.length - 1,
   841 				minVal = 0,
   908 				minVal   = 0,
   842 				next = $('.revisions-next .button'),
   909 				next     = $('.revisions-next .button'),
   843 				previous = $('.revisions-previous .button'),
   910 				previous = $('.revisions-previous .button'),
   844 				val = this.model.revisions.indexOf( this.model.get('to') );
   911 				val      = this.model.revisions.indexOf( this.model.get('to') );
   845 
   912 
   846 			// Disable "Next" button if you're on the last node.
   913 			// Disable "Next" button if you're on the last node.
   847 			next.prop( 'disabled', ( maxVal === val ) );
   914 			next.prop( 'disabled', ( maxVal === val ) );
   848 
   915 
   849 			// Disable "Previous" button if you're on the first node.
   916 			// Disable "Previous" button if you're on the first node.
   882 
   949 
   883 			this.applySliderSettings();
   950 			this.applySliderSettings();
   884 		},
   951 		},
   885 
   952 
   886 		mouseMove: function( e ) {
   953 		mouseMove: function( e ) {
   887 			var zoneCount = this.model.revisions.length - 1, // One fewer zone than models
   954 			var zoneCount         = this.model.revisions.length - 1, // One fewer zone than models
   888 				sliderFrom = this.$el.allOffsets()[this.direction], // "From" edge of slider
   955 				sliderFrom        = this.$el.allOffsets()[this.direction], // "From" edge of slider
   889 				sliderWidth = this.$el.width(), // Width of slider
   956 				sliderWidth       = this.$el.width(), // Width of slider
   890 				tickWidth = sliderWidth / zoneCount, // Calculated width of zone
   957 				tickWidth         = sliderWidth / zoneCount, // Calculated width of zone
   891 				actualX = isRtl? $(window).width() - e.pageX : e.pageX; // Flipped for RTL - sliderFrom;
   958 				actualX           = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom;
   892 			actualX = actualX - sliderFrom; // Offset of mouse position in slider
   959 				currentModelIndex = Math.floor( ( actualX  + ( tickWidth / 2 )  ) / tickWidth ); // Calculate the model index
   893 			var currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 )  ) / tickWidth ); // Calculate the model index
       
   894 
   960 
   895 			// Ensure sane value for currentModelIndex.
   961 			// Ensure sane value for currentModelIndex.
   896 			if ( currentModelIndex < 0 )
   962 			if ( currentModelIndex < 0 ) {
   897 				currentModelIndex = 0;
   963 				currentModelIndex = 0;
   898 			else if ( currentModelIndex >= this.model.revisions.length )
   964 			} else if ( currentModelIndex >= this.model.revisions.length ) {
   899 				currentModelIndex = this.model.revisions.length - 1;
   965 				currentModelIndex = this.model.revisions.length - 1;
       
   966 			}
   900 
   967 
   901 			// Update the tooltip mode
   968 			// Update the tooltip mode
   902 			this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
   969 			this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
   903 		},
   970 		},
   904 
   971 
   931 			this.model.set({ scrubbing: true });
   998 			this.model.set({ scrubbing: true });
   932 
   999 
   933 			// Track the mouse position to enable smooth dragging,
  1000 			// Track the mouse position to enable smooth dragging,
   934 			// overrides default jQuery UI step behavior.
  1001 			// overrides default jQuery UI step behavior.
   935 			$( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
  1002 			$( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
   936 				var view              = e.data.view,
  1003 				var handles,
   937 				    leftDragBoundary  = view.$el.offset().left,
  1004 					view              = e.data.view,
   938 				    sliderOffset      = leftDragBoundary,
  1005 					leftDragBoundary  = view.$el.offset().left,
   939 				    sliderRightEdge   = leftDragBoundary + view.$el.width(),
  1006 					sliderOffset      = leftDragBoundary,
   940 				    rightDragBoundary = sliderRightEdge,
  1007 					sliderRightEdge   = leftDragBoundary + view.$el.width(),
   941 				    leftDragReset     = '0',
  1008 					rightDragBoundary = sliderRightEdge,
   942 				    rightDragReset    = '100%',
  1009 					leftDragReset     = '0',
   943 				    handle            = $( ui.handle );
  1010 					rightDragReset    = '100%',
       
  1011 					handle            = $( ui.handle );
   944 
  1012 
   945 				// In two handle mode, ensure handles can't be dragged past each other.
  1013 				// In two handle mode, ensure handles can't be dragged past each other.
   946 				// Adjust left/right boundaries and reset points.
  1014 				// Adjust left/right boundaries and reset points.
   947 				if ( view.model.get('compareTwoMode') ) {
  1015 				if ( view.model.get('compareTwoMode') ) {
   948 					var handles = handle.parent().find('.ui-slider-handle');
  1016 					handles = handle.parent().find('.ui-slider-handle');
   949 					if ( handle.is( handles.first() ) ) { // We're the left handle
  1017 					if ( handle.is( handles.first() ) ) { // We're the left handle
   950 						rightDragBoundary = handles.last().offset().left;
  1018 						rightDragBoundary = handles.last().offset().left;
   951 						rightDragReset    = rightDragBoundary - sliderOffset;
  1019 						rightDragReset    = rightDragBoundary - sliderOffset;
   952 					} else { // We're the right handle
  1020 					} else { // We're the right handle
   953 						leftDragBoundary = handles.first().offset().left + handles.first().width();
  1021 						leftDragBoundary = handles.first().offset().left + handles.first().width();
   974 		slide: function( event, ui ) {
  1042 		slide: function( event, ui ) {
   975 			var attributes, movedRevision;
  1043 			var attributes, movedRevision;
   976 			// Compare two revisions mode
  1044 			// Compare two revisions mode
   977 			if ( this.model.get('compareTwoMode') ) {
  1045 			if ( this.model.get('compareTwoMode') ) {
   978 				// Prevent sliders from occupying same spot
  1046 				// Prevent sliders from occupying same spot
   979 				if ( ui.values[1] === ui.values[0] )
  1047 				if ( ui.values[1] === ui.values[0] ) {
   980 					return false;
  1048 					return false;
   981 				if ( isRtl )
  1049 				}
       
  1050 				if ( isRtl ) {
   982 					ui.values.reverse();
  1051 					ui.values.reverse();
       
  1052 				}
   983 				attributes = {
  1053 				attributes = {
   984 					from: this.model.revisions.at( this.getPosition( ui.values[0] ) ),
  1054 					from: this.model.revisions.at( this.getPosition( ui.values[0] ) ),
   985 					to: this.model.revisions.at( this.getPosition( ui.values[1] ) )
  1055 					to: this.model.revisions.at( this.getPosition( ui.values[1] ) )
   986 				};
  1056 				};
   987 			} else {
  1057 			} else {
   988 				attributes = {
  1058 				attributes = {
   989 					to: this.model.revisions.at( this.getPosition( ui.value ) )
  1059 					to: this.model.revisions.at( this.getPosition( ui.value ) )
   990 				};
  1060 				};
   991 				// If we're at the first revision, unset 'from'.
  1061 				// If we're at the first revision, unset 'from'.
   992 				if ( this.getPosition( ui.value ) > 0 )
  1062 				if ( this.getPosition( ui.value ) > 0 ) {
   993 					attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 );
  1063 					attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 );
   994 				else
  1064 				} else {
   995 					attributes.from = undefined;
  1065 					attributes.from = undefined;
       
  1066 				}
   996 			}
  1067 			}
   997 			movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
  1068 			movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
   998 
  1069 
   999 			// If we are scrubbing, a scrub to a revision is considered a hover
  1070 			// If we are scrubbing, a scrub to a revision is considered a hover
  1000 			if ( this.model.get('scrubbing') )
  1071 			if ( this.model.get('scrubbing') ) {
  1001 				attributes.hoveredRevision = movedRevision;
  1072 				attributes.hoveredRevision = movedRevision;
       
  1073 			}
  1002 
  1074 
  1003 			this.model.set( attributes );
  1075 			this.model.set( attributes );
  1004 		},
  1076 		},
  1005 
  1077 
  1006 		stop: function( event, ui ) {
  1078 		stop: function() {
  1007 			$( window ).off('mousemove.wp.revisions');
  1079 			$( window ).off('mousemove.wp.revisions');
  1008 			this.model.updateSliderSettings(); // To snap us back to a tick mark
  1080 			this.model.updateSliderSettings(); // To snap us back to a tick mark
  1009 			this.model.set({ scrubbing: false });
  1081 			this.model.set({ scrubbing: false });
  1010 		}
  1082 		}
  1011 	});
  1083 	});
  1012 
  1084 
  1013 	// The diff view.
  1085 	// The diff view.
  1014 	// This is the view for the current active diff.
  1086 	// This is the view for the current active diff.
  1015 	revisions.view.Diff = wp.Backbone.View.extend({
  1087 	revisions.view.Diff = wp.Backbone.View.extend({
  1016 		className: 'revisions-diff',
  1088 		className: 'revisions-diff',
  1017 		template: wp.template('revisions-diff'),
  1089 		template:  wp.template('revisions-diff'),
  1018 
  1090 
  1019 		// Generate the options to be passed to the template.
  1091 		// Generate the options to be passed to the template.
  1020 		prepare: function() {
  1092 		prepare: function() {
  1021 			return _.extend({ fields: this.model.fields.toJSON() }, this.options );
  1093 			return _.extend({ fields: this.model.fields.toJSON() }, this.options );
  1022 		}
  1094 		}
  1023 	});
  1095 	});
  1024 
  1096 
  1025 	// The revisions router
  1097 	// The revisions router.
  1026 	// takes URLs with #hash fragments and routes them
  1098 	// Maintains the URL routes so browser URL matches state.
  1027 	revisions.Router = Backbone.Router.extend({
  1099 	revisions.Router = Backbone.Router.extend({
  1028 		initialize: function( options ) {
  1100 		initialize: function( options ) {
  1029 			this.model = options.model;
  1101 			this.model = options.model;
  1030 			this.routes = _.object([
  1102 
  1031 				[ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ],
       
  1032 				[ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ]
       
  1033 			]);
       
  1034 			// Maintain state and history when navigating
  1103 			// Maintain state and history when navigating
  1035 			this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
  1104 			this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
  1036 			this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
  1105 			this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
  1037 		},
  1106 		},
  1038 
  1107 
  1039 		baseUrl: function( url ) {
  1108 		baseUrl: function( url ) {
  1040 			return this.model.get('baseUrl') + url;
  1109 			return this.model.get('baseUrl') + url;
  1041 		},
  1110 		},
  1042 
  1111 
  1043 		updateUrl: function() {
  1112 		updateUrl: function() {
  1044 			var from = this.model.has('from') ? this.model.get('from').id : 0;
  1113 			var from = this.model.has('from') ? this.model.get('from').id : 0,
  1045 			var to = this.model.get('to').id;
  1114 				to   = this.model.get('to').id;
  1046 			if ( this.model.get('compareTwoMode' ) )
  1115 			if ( this.model.get('compareTwoMode' ) ) {
  1047 				this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ) );
  1116 				this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ), { replace: true } );
  1048 			else
  1117 			} else {
  1049 				this.navigate( this.baseUrl( '?revision=' + to ) );
  1118 				this.navigate( this.baseUrl( '?revision=' + to ), { replace: true } );
       
  1119 			}
  1050 		},
  1120 		},
  1051 
  1121 
  1052 		handleRoute: function( a, b ) {
  1122 		handleRoute: function( a, b ) {
  1053 			var from, to, compareTwo = _.isUndefined( b );
  1123 			var compareTwo = _.isUndefined( b );
  1054 
  1124 
  1055 			if ( ! compareTwo ) {
  1125 			if ( ! compareTwo ) {
  1056 				b = this.model.revisions.get( a );
  1126 				b = this.model.revisions.get( a );
  1057 				a = this.model.revisions.prev( b );
  1127 				a = this.model.revisions.prev( b );
  1058 				b = b ? b.id : 0;
  1128 				b = b ? b.id : 0;
  1059 				a = a ? a.id : 0;
  1129 				a = a ? a.id : 0;
  1060 			}
  1130 			}
  1061 
  1131 		}
  1062 			this.model.set({
  1132 	});
  1063 				from: this.model.revisions.get( parseInt( a, 10 ) ),
  1133 
  1064 				to: this.model.revisions.get( parseInt( a, 10 ) ),
  1134 	/**
  1065 				compareTwoMode: compareTwo
  1135 	 * Initialize the revisions UI for revision.php.
  1066 			});
  1136 	 */
  1067 		}
       
  1068 	});
       
  1069 
       
  1070 	// Initialize the revisions UI.
       
  1071 	revisions.init = function() {
  1137 	revisions.init = function() {
       
  1138 		var state;
       
  1139 
       
  1140 		// Bail if the current page is not revision.php.
       
  1141 		if ( ! window.adminpage || 'revision-php' !== window.adminpage ) {
       
  1142 			return;
       
  1143 		}
       
  1144 
       
  1145 		state = new revisions.model.FrameState({
       
  1146 			initialDiffState: {
       
  1147 				// wp_localize_script doesn't stringifies ints, so cast them.
       
  1148 				to: parseInt( revisions.settings.to, 10 ),
       
  1149 				from: parseInt( revisions.settings.from, 10 ),
       
  1150 				// wp_localize_script does not allow for top-level booleans so do a comparator here.
       
  1151 				compareTwoMode: ( revisions.settings.compareTwoMode === '1' )
       
  1152 			},
       
  1153 			diffData: revisions.settings.diffData,
       
  1154 			baseUrl: revisions.settings.baseUrl,
       
  1155 			postId: parseInt( revisions.settings.postId, 10 )
       
  1156 		}, {
       
  1157 			revisions: new revisions.model.Revisions( revisions.settings.revisionData )
       
  1158 		});
       
  1159 
  1072 		revisions.view.frame = new revisions.view.Frame({
  1160 		revisions.view.frame = new revisions.view.Frame({
  1073 			model: new revisions.model.FrameState({}, {
  1161 			model: state
  1074 				revisions: new revisions.model.Revisions( revisions.settings.revisionData )
       
  1075 			})
       
  1076 		}).render();
  1162 		}).render();
  1077 	};
  1163 	};
  1078 
  1164 
  1079 	$( revisions.init );
  1165 	$( revisions.init );
  1080 }(jQuery));
  1166 }(jQuery));