wp/wp-admin/js/revisions.js
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 22 8c2e4d02f4ef
equal deleted inserted replaced
15:3d4e9c994f10 16:a86126ab1dd4
    17 	revisions = wp.revisions = { model: {}, view: {}, controller: {} };
    17 	revisions = wp.revisions = { model: {}, view: {}, controller: {} };
    18 
    18 
    19 	// Link post revisions data served from the back end.
    19 	// Link post revisions data served from the back end.
    20 	revisions.settings = window._wpRevisionsSettings || {};
    20 	revisions.settings = window._wpRevisionsSettings || {};
    21 
    21 
    22 	// For debugging
    22 	// For debugging.
    23 	revisions.debug = false;
    23 	revisions.debug = false;
    24 
    24 
    25 	/**
    25 	/**
    26 	 * wp.revisions.log
    26 	 * wp.revisions.log
    27 	 *
    27 	 *
    32 		if ( window.console && revisions.debug ) {
    32 		if ( window.console && revisions.debug ) {
    33 			window.console.log.apply( window.console, arguments );
    33 			window.console.log.apply( window.console, arguments );
    34 		}
    34 		}
    35 	};
    35 	};
    36 
    36 
    37 	// Handy functions to help with positioning
    37 	// Handy functions to help with positioning.
    38 	$.fn.allOffsets = function() {
    38 	$.fn.allOffsets = function() {
    39 		var offset = this.offset() || {top: 0, left: 0}, win = $(window);
    39 		var offset = this.offset() || {top: 0, left: 0}, win = $(window);
    40 		return _.extend( offset, {
    40 		return _.extend( offset, {
    41 			right:  win.width()  - offset.left - this.outerWidth(),
    41 			right:  win.width()  - offset.left - this.outerWidth(),
    42 			bottom: win.height() - offset.top  - this.outerHeight()
    42 			bottom: win.height() - offset.top  - this.outerHeight()
    69 
    69 
    70 		initialize: function( options ) {
    70 		initialize: function( options ) {
    71 			this.frame = options.frame;
    71 			this.frame = options.frame;
    72 			this.revisions = options.revisions;
    72 			this.revisions = options.revisions;
    73 
    73 
    74 			// Listen for changes to the revisions or mode from outside
    74 			// Listen for changes to the revisions or mode from outside.
    75 			this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
    75 			this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
    76 			this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
    76 			this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
    77 
    77 
    78 			// Listen for internal changes
    78 			// Listen for internal changes.
    79 			this.on( 'change:from', this.handleLocalChanges );
    79 			this.on( 'change:from', this.handleLocalChanges );
    80 			this.on( 'change:to', this.handleLocalChanges );
    80 			this.on( 'change:to', this.handleLocalChanges );
    81 			this.on( 'change:compareTwoMode', this.updateSliderSettings );
    81 			this.on( 'change:compareTwoMode', this.updateSliderSettings );
    82 			this.on( 'update:revisions', this.updateSliderSettings );
    82 			this.on( 'update:revisions', this.updateSliderSettings );
    83 
    83 
    84 			// Listen for changes to the hovered revision
    84 			// Listen for changes to the hovered revision.
    85 			this.on( 'change:hoveredRevision', this.hoverRevision );
    85 			this.on( 'change:hoveredRevision', this.hoverRevision );
    86 
    86 
    87 			this.set({
    87 			this.set({
    88 				max:   this.revisions.length - 1,
    88 				max:   this.revisions.length - 1,
    89 				compareTwoMode: this.frame.get('compareTwoMode'),
    89 				compareTwoMode: this.frame.get('compareTwoMode'),
   103 					values: [
   103 					values: [
   104 						this.getSliderValue( 'to', 'from' ),
   104 						this.getSliderValue( 'to', 'from' ),
   105 						this.getSliderValue( 'from', 'to' )
   105 						this.getSliderValue( 'from', 'to' )
   106 					],
   106 					],
   107 					value: null,
   107 					value: null,
   108 					range: true // ensures handles cannot cross
   108 					range: true // Ensures handles cannot cross.
   109 				});
   109 				});
   110 			} else {
   110 			} else {
   111 				this.set({
   111 				this.set({
   112 					value: this.getSliderValue( 'to', 'to' ),
   112 					value: this.getSliderValue( 'to', 'to' ),
   113 					values: null,
   113 					values: null,
   115 				});
   115 				});
   116 			}
   116 			}
   117 			this.trigger( 'update:slider' );
   117 			this.trigger( 'update:slider' );
   118 		},
   118 		},
   119 
   119 
   120 		// Called when a revision is hovered
   120 		// Called when a revision is hovered.
   121 		hoverRevision: function( model, value ) {
   121 		hoverRevision: function( model, value ) {
   122 			this.trigger( 'hovered:revision', value );
   122 			this.trigger( 'hovered:revision', value );
   123 		},
   123 		},
   124 
   124 
   125 		// Called when `compareTwoMode` changes
   125 		// Called when `compareTwoMode` changes.
   126 		updateMode: function( model, value ) {
   126 		updateMode: function( model, value ) {
   127 			this.set({ compareTwoMode: value });
   127 			this.set({ compareTwoMode: value });
   128 		},
   128 		},
   129 
   129 
   130 		// Called when `from` or `to` changes in the local model
   130 		// Called when `from` or `to` changes in the local model.
   131 		handleLocalChanges: function() {
   131 		handleLocalChanges: function() {
   132 			this.frame.set({
   132 			this.frame.set({
   133 				from: this.get('from'),
   133 				from: this.get('from'),
   134 				to: this.get('to')
   134 				to: this.get('to')
   135 			});
   135 			});
   136 		},
   136 		},
   137 
   137 
   138 		// Receives revisions changes from outside the model
   138 		// Receives revisions changes from outside the model.
   139 		receiveRevisions: function( from, to ) {
   139 		receiveRevisions: function( from, to ) {
   140 			// Bail if nothing changed
   140 			// Bail if nothing changed.
   141 			if ( this.get('from') === from && this.get('to') === to ) {
   141 			if ( this.get('from') === from && this.get('to') === to ) {
   142 				return;
   142 				return;
   143 			}
   143 			}
   144 
   144 
   145 			this.set({ from: from, to: to }, { silent: true });
   145 			this.set({ from: from, to: to }, { silent: true });
   150 
   150 
   151 	revisions.model.Tooltip = Backbone.Model.extend({
   151 	revisions.model.Tooltip = Backbone.Model.extend({
   152 		defaults: {
   152 		defaults: {
   153 			revision: null,
   153 			revision: null,
   154 			offset: {},
   154 			offset: {},
   155 			hovering: false, // Whether the mouse is hovering
   155 			hovering: false, // Whether the mouse is hovering.
   156 			scrubbing: false // Whether the mouse is scrubbing
   156 			scrubbing: false // Whether the mouse is scrubbing.
   157 		},
   157 		},
   158 
   158 
   159 		initialize: function( options ) {
   159 		initialize: function( options ) {
   160 			this.frame = options.frame;
   160 			this.frame = options.frame;
   161 			this.revisions = options.revisions;
   161 			this.revisions = options.revisions;
   253 			if ( diff ) {
   253 			if ( diff ) {
   254 				deferred.resolveWith( context, [ diff ] );
   254 				deferred.resolveWith( context, [ diff ] );
   255 			} else {
   255 			} else {
   256 				this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
   256 				this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
   257 				_.each( ids, _.bind( function( id ) {
   257 				_.each( ids, _.bind( function( id ) {
   258 					// Remove anything that has an ongoing request
   258 					// Remove anything that has an ongoing request.
   259 					if ( this.requests[ id ] ) {
   259 					if ( this.requests[ id ] ) {
   260 						delete ids[ id ];
   260 						delete ids[ id ];
   261 					}
   261 					}
   262 					// Remove anything we already have
   262 					// Remove anything we already have.
   263 					if ( this.get( id ) ) {
   263 					if ( this.get( id ) ) {
   264 						delete ids[ id ];
   264 						delete ids[ id ];
   265 					}
   265 					}
   266 				}, this ) );
   266 				}, this ) );
   267 				if ( ! request ) {
   267 				if ( ! request ) {
   268 					// Always include the ID that started this ensure
   268 					// Always include the ID that started this ensure.
   269 					ids[ id ] = true;
   269 					ids[ id ] = true;
   270 					request   = this.load( _.keys( ids ) );
   270 					request   = this.load( _.keys( ids ) );
   271 				}
   271 				}
   272 
   272 
   273 				request.done( _.bind( function() {
   273 				request.done( _.bind( function() {
   278 			}
   278 			}
   279 
   279 
   280 			return deferred.promise();
   280 			return deferred.promise();
   281 		},
   281 		},
   282 
   282 
   283 		// Returns an array of proximal diffs
   283 		// Returns an array of proximal diffs.
   284 		getClosestUnloaded: function( ids, centerId ) {
   284 		getClosestUnloaded: function( ids, centerId ) {
   285 			var self = this;
   285 			var self = this;
   286 			return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) {
   286 			return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) {
   287 				return Math.abs( centerId - pair[1] );
   287 				return Math.abs( centerId - pair[1] );
   288 			}).map( function( pair ) {
   288 			}).map( function( pair ) {
   301 						deferred.resolve();
   301 						deferred.resolve();
   302 					});
   302 					});
   303 				}).fail( function() {
   303 				}).fail( function() {
   304 					if ( 1 === num ) { // Already tried 1. This just isn't working. Give up.
   304 					if ( 1 === num ) { // Already tried 1. This just isn't working. Give up.
   305 						deferred.reject();
   305 						deferred.reject();
   306 					} else { // Request fewer diffs this time
   306 					} else { // Request fewer diffs this time.
   307 						self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() {
   307 						self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() {
   308 							deferred.resolve();
   308 							deferred.resolve();
   309 						});
   309 						});
   310 					}
   310 					}
   311 				});
   311 				});
   315 			return deferred;
   315 			return deferred;
   316 		},
   316 		},
   317 
   317 
   318 		load: function( comparisons ) {
   318 		load: function( comparisons ) {
   319 			wp.revisions.log( 'load', comparisons );
   319 			wp.revisions.log( 'load', comparisons );
   320 			// Our collection should only ever grow, never shrink, so remove: false
   320 			// Our collection should only ever grow, never shrink, so `remove: false`.
   321 			return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() {
   321 			return this.fetch({ data: { compare: comparisons }, remove: false }).done( function() {
   322 				wp.revisions.log( 'load:complete', comparisons );
   322 				wp.revisions.log( 'load:complete', comparisons );
   323 			});
   323 			});
   324 		},
   324 		},
   325 
   325 
   392 			} );
   392 			} );
   393 
   393 
   394 			// Set the initial diffs collection.
   394 			// Set the initial diffs collection.
   395 			this.diffs.set( this.get( 'diffData' ) );
   395 			this.diffs.set( this.get( 'diffData' ) );
   396 
   396 
   397 			// Set up internal listeners
   397 			// Set up internal listeners.
   398 			this.listenTo( this, 'change:from', this.changeRevisionHandler );
   398 			this.listenTo( this, 'change:from', this.changeRevisionHandler );
   399 			this.listenTo( this, 'change:to', this.changeRevisionHandler );
   399 			this.listenTo( this, 'change:to', this.changeRevisionHandler );
   400 			this.listenTo( this, 'change:compareTwoMode', this.changeMode );
   400 			this.listenTo( this, 'change:compareTwoMode', this.changeMode );
   401 			this.listenTo( this, 'update:revisions', this.updatedRevisions );
   401 			this.listenTo( this, 'update:revisions', this.updatedRevisions );
   402 			this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
   402 			this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
   408 				to : this.revisions.get( state.to ),
   408 				to : this.revisions.get( state.to ),
   409 				from : this.revisions.get( state.from ),
   409 				from : this.revisions.get( state.from ),
   410 				compareTwoMode : state.compareTwoMode
   410 				compareTwoMode : state.compareTwoMode
   411 			} );
   411 			} );
   412 
   412 
   413 			// Start the router if browser supports History API
   413 			// Start the router if browser supports History API.
   414 			if ( window.history && window.history.pushState ) {
   414 			if ( window.history && window.history.pushState ) {
   415 				this.router = new revisions.Router({ model: this });
   415 				this.router = new revisions.Router({ model: this });
   416 				if ( Backbone.History.started ) {
   416 				if ( Backbone.History.started ) {
   417 					Backbone.history.stop();
   417 					Backbone.history.stop();
   418 				}
   418 				}
   427 
   427 
   428 		changeMode: function( model, value ) {
   428 		changeMode: function( model, value ) {
   429 			var toIndex = this.revisions.indexOf( this.get( 'to' ) );
   429 			var toIndex = this.revisions.indexOf( this.get( 'to' ) );
   430 
   430 
   431 			// If we were on the first revision before switching to two-handled mode,
   431 			// If we were on the first revision before switching to two-handled mode,
   432 			// bump the 'to' position over one
   432 			// bump the 'to' position over one.
   433 			if ( value && 0 === toIndex ) {
   433 			if ( value && 0 === toIndex ) {
   434 				this.set({
   434 				this.set({
   435 					from: this.revisions.at( toIndex ),
   435 					from: this.revisions.at( toIndex ),
   436 					to:   this.revisions.at( toIndex + 1 )
   436 					to:   this.revisions.at( toIndex + 1 )
   437 				});
   437 				});
   438 			}
   438 			}
   439 
   439 
   440 			// When switching back to single-handled mode, reset 'from' model to
   440 			// When switching back to single-handled mode, reset 'from' model to
   441 			// one position before the 'to' model
   441 			// one position before the 'to' model.
   442 			if ( ! value && 0 !== toIndex ) { // '! value' means switching to single-handled mode
   442 			if ( ! value && 0 !== toIndex ) { // '! value' means switching to single-handled mode.
   443 				this.set({
   443 				this.set({
   444 					from: this.revisions.at( toIndex - 1 ),
   444 					from: this.revisions.at( toIndex - 1 ),
   445 					to:   this.revisions.at( toIndex )
   445 					to:   this.revisions.at( toIndex )
   446 				});
   446 				});
   447 			}
   447 			}
   448 		},
   448 		},
   449 
   449 
   450 		updatedRevisions: function( from, to ) {
   450 		updatedRevisions: function( from, to ) {
   451 			if ( this.get( 'compareTwoMode' ) ) {
   451 			if ( this.get( 'compareTwoMode' ) ) {
   452 				// TODO: compare-two loading strategy
   452 				// @todo Compare-two loading strategy.
   453 			} else {
   453 			} else {
   454 				this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 );
   454 				this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 );
   455 			}
   455 			}
   456 		},
   456 		},
   457 
   457 
   458 		// Fetch the currently loaded diff.
   458 		// Fetch the currently loaded diff.
   459 		diff: function() {
   459 		diff: function() {
   460 			return this.diffs.get( this._diffId );
   460 			return this.diffs.get( this._diffId );
   461 		},
   461 		},
   462 
   462 
   463 		// So long as `from` and `to` are changed at the same time, the diff
   463 		/*
   464 		// will only be updated once. This is because Backbone updates all of
   464 		 * So long as `from` and `to` are changed at the same time, the diff
   465 		// the changed attributes in `set`, and then fires the `change` events.
   465 		 * will only be updated once. This is because Backbone updates all of
       
   466 		 * the changed attributes in `set`, and then fires the `change` events.
       
   467 		 */
   466 		updateDiff: function( options ) {
   468 		updateDiff: function( options ) {
   467 			var from, to, diffId, diff;
   469 			var from, to, diffId, diff;
   468 
   470 
   469 			options = options || {};
   471 			options = options || {};
   470 			from = this.get('from');
   472 			from = this.get('from');
   507 			if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) {
   509 			if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) {
   508 				this.set({
   510 				this.set({
   509 					loading: false,
   511 					loading: false,
   510 					error: true
   512 					error: true
   511 				});
   513 				});
   512 			} else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change
   514 			} else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change.
   513 				this.trigger( 'update:diff', diff );
   515 				this.trigger( 'update:diff', diff );
   514 			}
   516 			}
   515 		},
   517 		},
   516 
   518 
   517 		_ensureDiff: function() {
   519 		_ensureDiff: function() {
   591 		className: 'revisions-controls',
   593 		className: 'revisions-controls',
   592 
   594 
   593 		initialize: function() {
   595 		initialize: function() {
   594 			_.bindAll( this, 'setWidth' );
   596 			_.bindAll( this, 'setWidth' );
   595 
   597 
   596 			// Add the button view
   598 			// Add the button view.
   597 			this.views.add( new revisions.view.Buttons({
   599 			this.views.add( new revisions.view.Buttons({
   598 				model: this.model
   600 				model: this.model
   599 			}) );
   601 			}) );
   600 
   602 
   601 			// Add the checkbox view
   603 			// Add the checkbox view.
   602 			this.views.add( new revisions.view.Checkbox({
   604 			this.views.add( new revisions.view.Checkbox({
   603 				model: this.model
   605 				model: this.model
   604 			}) );
   606 			}) );
   605 
   607 
   606 			// Prep the slider model
   608 			// Prep the slider model.
   607 			var slider = new revisions.model.Slider({
   609 			var slider = new revisions.model.Slider({
   608 				frame: this.model,
   610 				frame: this.model,
   609 				revisions: this.model.revisions
   611 				revisions: this.model.revisions
   610 			}),
   612 			}),
   611 
   613 
   612 			// Prep the tooltip model
   614 			// Prep the tooltip model.
   613 			tooltip = new revisions.model.Tooltip({
   615 			tooltip = new revisions.model.Tooltip({
   614 				frame: this.model,
   616 				frame: this.model,
   615 				revisions: this.model.revisions,
   617 				revisions: this.model.revisions,
   616 				slider: slider
   618 				slider: slider
   617 			});
   619 			});
   618 
   620 
   619 			// Add the tooltip view
   621 			// Add the tooltip view.
   620 			this.views.add( new revisions.view.Tooltip({
   622 			this.views.add( new revisions.view.Tooltip({
   621 				model: tooltip
   623 				model: tooltip
   622 			}) );
   624 			}) );
   623 
   625 
   624 			// Add the tickmarks view
   626 			// Add the tickmarks view.
   625 			this.views.add( new revisions.view.Tickmarks({
   627 			this.views.add( new revisions.view.Tickmarks({
   626 				model: tooltip
   628 				model: tooltip
   627 			}) );
   629 			}) );
   628 
   630 
   629 			// Add the slider view
   631 			// Add the slider view.
   630 			this.views.add( new revisions.view.Slider({
   632 			this.views.add( new revisions.view.Slider({
   631 				model: slider
   633 				model: slider
   632 			}) );
   634 			}) );
   633 
   635 
   634 			// Add the Metabox view
   636 			// Add the Metabox view.
   635 			this.views.add( new revisions.view.Metabox({
   637 			this.views.add( new revisions.view.Metabox({
   636 				model: this.model
   638 				model: this.model
   637 			}) );
   639 			}) );
   638 		},
   640 		},
   639 
   641 
   670 		setWidth: function() {
   672 		setWidth: function() {
   671 			this.$el.css('width', this.$el.parent().width() + 'px');
   673 			this.$el.css('width', this.$el.parent().width() + 'px');
   672 		}
   674 		}
   673 	});
   675 	});
   674 
   676 
   675 	// The tickmarks view
   677 	// The tickmarks view.
   676 	revisions.view.Tickmarks = wp.Backbone.View.extend({
   678 	revisions.view.Tickmarks = wp.Backbone.View.extend({
   677 		className: 'revisions-tickmarks',
   679 		className: 'revisions-tickmarks',
   678 		direction: isRtl ? 'right' : 'left',
   680 		direction: isRtl ? 'right' : 'left',
   679 
   681 
   680 		initialize: function() {
   682 		initialize: function() {
   684 		reportTickPosition: function( model, revision ) {
   686 		reportTickPosition: function( model, revision ) {
   685 			var offset, thisOffset, parentOffset, tick, index = this.model.revisions.indexOf( revision );
   687 			var offset, thisOffset, parentOffset, tick, index = this.model.revisions.indexOf( revision );
   686 			thisOffset = this.$el.allOffsets();
   688 			thisOffset = this.$el.allOffsets();
   687 			parentOffset = this.$el.parent().allOffsets();
   689 			parentOffset = this.$el.parent().allOffsets();
   688 			if ( index === this.model.revisions.length - 1 ) {
   690 			if ( index === this.model.revisions.length - 1 ) {
   689 				// Last one
   691 				// Last one.
   690 				offset = {
   692 				offset = {
   691 					rightPlusWidth: thisOffset.left - parentOffset.left + 1,
   693 					rightPlusWidth: thisOffset.left - parentOffset.left + 1,
   692 					leftPlusWidth: thisOffset.right - parentOffset.right + 1
   694 					leftPlusWidth: thisOffset.right - parentOffset.right + 1
   693 				};
   695 				};
   694 			} else {
   696 			} else {
   695 				// Normal tick
   697 				// Normal tick.
   696 				tick = this.$('div:nth-of-type(' + (index + 1) + ')');
   698 				tick = this.$('div:nth-of-type(' + (index + 1) + ')');
   697 				offset = tick.allPositions();
   699 				offset = tick.allPositions();
   698 				_.extend( offset, {
   700 				_.extend( offset, {
   699 					left: offset.left + thisOffset.left - parentOffset.left,
   701 					left: offset.left + thisOffset.left - parentOffset.left,
   700 					right: offset.right + thisOffset.right - parentOffset.right
   702 					right: offset.right + thisOffset.right - parentOffset.right
   717 				this.$el.append( '<div style="' + this.direction + ': ' + ( 100 * tickWidth * index ) + '%"></div>' );
   719 				this.$el.append( '<div style="' + this.direction + ': ' + ( 100 * tickWidth * index ) + '%"></div>' );
   718 			}, this );
   720 			}, this );
   719 		}
   721 		}
   720 	});
   722 	});
   721 
   723 
   722 	// The metabox view
   724 	// The metabox view.
   723 	revisions.view.Metabox = wp.Backbone.View.extend({
   725 	revisions.view.Metabox = wp.Backbone.View.extend({
   724 		className: 'revisions-meta',
   726 		className: 'revisions-meta',
   725 
   727 
   726 		initialize: function() {
   728 		initialize: function() {
   727 			// Add the 'from' view
   729 			// Add the 'from' view.
   728 			this.views.add( new revisions.view.MetaFrom({
   730 			this.views.add( new revisions.view.MetaFrom({
   729 				model: this.model,
   731 				model: this.model,
   730 				className: 'diff-meta diff-meta-from'
   732 				className: 'diff-meta diff-meta-from'
   731 			}) );
   733 			}) );
   732 
   734 
   733 			// Add the 'to' view
   735 			// Add the 'to' view.
   734 			this.views.add( new revisions.view.MetaTo({
   736 			this.views.add( new revisions.view.MetaTo({
   735 				model: this.model
   737 				model: this.model
   736 			}) );
   738 			}) );
   737 		}
   739 		}
   738 	});
   740 	});
   739 
   741 
   740 	// The revision meta view (to be extended)
   742 	// The revision meta view (to be extended).
   741 	revisions.view.Meta = wp.Backbone.View.extend({
   743 	revisions.view.Meta = wp.Backbone.View.extend({
   742 		template: wp.template('revisions-meta'),
   744 		template: wp.template('revisions-meta'),
   743 
   745 
   744 		events: {
   746 		events: {
   745 			'click .restore-revision': 'restoreRevision'
   747 			'click .restore-revision': 'restoreRevision'
   758 		restoreRevision: function() {
   760 		restoreRevision: function() {
   759 			document.location = this.model.get('to').attributes.restoreUrl;
   761 			document.location = this.model.get('to').attributes.restoreUrl;
   760 		}
   762 		}
   761 	});
   763 	});
   762 
   764 
   763 	// The revision meta 'from' view
   765 	// The revision meta 'from' view.
   764 	revisions.view.MetaFrom = revisions.view.Meta.extend({
   766 	revisions.view.MetaFrom = revisions.view.Meta.extend({
   765 		className: 'diff-meta diff-meta-from',
   767 		className: 'diff-meta diff-meta-from',
   766 		type: 'from'
   768 		type: 'from'
   767 	});
   769 	});
   768 
   770 
   769 	// The revision meta 'to' view
   771 	// The revision meta 'to' view.
   770 	revisions.view.MetaTo = revisions.view.Meta.extend({
   772 	revisions.view.MetaTo = revisions.view.Meta.extend({
   771 		className: 'diff-meta diff-meta-to',
   773 		className: 'diff-meta diff-meta-to',
   772 		type: 'to'
   774 		type: 'to'
   773 	});
   775 	});
   774 
   776 
   878 
   880 
   879 		ready: function() {
   881 		ready: function() {
   880 			this.disabledButtonCheck();
   882 			this.disabledButtonCheck();
   881 		},
   883 		},
   882 
   884 
   883 		// Go to a specific model index
   885 		// Go to a specific model index.
   884 		gotoModel: function( toIndex ) {
   886 		gotoModel: function( toIndex ) {
   885 			var attributes = {
   887 			var attributes = {
   886 				to: this.model.revisions.at( toIndex )
   888 				to: this.model.revisions.at( toIndex )
   887 			};
   889 			};
   888 			// If we're at the first revision, unset 'from'.
   890 			// If we're at the first revision, unset 'from'.
   893 			}
   895 			}
   894 
   896 
   895 			this.model.set( attributes );
   897 			this.model.set( attributes );
   896 		},
   898 		},
   897 
   899 
   898 		// Go to the 'next' revision
   900 		// Go to the 'next' revision.
   899 		nextRevision: function() {
   901 		nextRevision: function() {
   900 			var toIndex = this.model.revisions.indexOf( this.model.get('to') ) + 1;
   902 			var toIndex = this.model.revisions.indexOf( this.model.get('to') ) + 1;
   901 			this.gotoModel( toIndex );
   903 			this.gotoModel( toIndex );
   902 		},
   904 		},
   903 
   905 
   904 		// Go to the 'previous' revision
   906 		// Go to the 'previous' revision.
   905 		previousRevision: function() {
   907 		previousRevision: function() {
   906 			var toIndex = this.model.revisions.indexOf( this.model.get('to') ) - 1;
   908 			var toIndex = this.model.revisions.indexOf( this.model.get('to') ) - 1;
   907 			this.gotoModel( toIndex );
   909 			this.gotoModel( toIndex );
   908 		},
   910 		},
   909 
   911 
   954 
   956 
   955 			this.applySliderSettings();
   957 			this.applySliderSettings();
   956 		},
   958 		},
   957 
   959 
   958 		mouseMove: function( e ) {
   960 		mouseMove: function( e ) {
   959 			var zoneCount         = this.model.revisions.length - 1, // One fewer zone than models
   961 			var zoneCount         = this.model.revisions.length - 1,       // One fewer zone than models.
   960 				sliderFrom        = this.$el.allOffsets()[this.direction], // "From" edge of slider
   962 				sliderFrom        = this.$el.allOffsets()[this.direction], // "From" edge of slider.
   961 				sliderWidth       = this.$el.width(), // Width of slider
   963 				sliderWidth       = this.$el.width(),                      // Width of slider.
   962 				tickWidth         = sliderWidth / zoneCount, // Calculated width of zone
   964 				tickWidth         = sliderWidth / zoneCount,               // Calculated width of zone.
   963 				actualX           = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom;
   965 				actualX           = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom.
   964 				currentModelIndex = Math.floor( ( actualX  + ( tickWidth / 2 )  ) / tickWidth ); // Calculate the model index
   966 				currentModelIndex = Math.floor( ( actualX  + ( tickWidth / 2 )  ) / tickWidth );    // Calculate the model index.
   965 
   967 
   966 			// Ensure sane value for currentModelIndex.
   968 			// Ensure sane value for currentModelIndex.
   967 			if ( currentModelIndex < 0 ) {
   969 			if ( currentModelIndex < 0 ) {
   968 				currentModelIndex = 0;
   970 				currentModelIndex = 0;
   969 			} else if ( currentModelIndex >= this.model.revisions.length ) {
   971 			} else if ( currentModelIndex >= this.model.revisions.length ) {
   970 				currentModelIndex = this.model.revisions.length - 1;
   972 				currentModelIndex = this.model.revisions.length - 1;
   971 			}
   973 			}
   972 
   974 
   973 			// Update the tooltip mode
   975 			// Update the tooltip mode.
   974 			this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
   976 			this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
   975 		},
   977 		},
   976 
   978 
   977 		mouseLeave: function() {
   979 		mouseLeave: function() {
   978 			this.model.set({ hovering: false });
   980 			this.model.set({ hovering: false });
   985 		applySliderSettings: function() {
   987 		applySliderSettings: function() {
   986 			this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) );
   988 			this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) );
   987 			var handles = this.$('a.ui-slider-handle');
   989 			var handles = this.$('a.ui-slider-handle');
   988 
   990 
   989 			if ( this.model.get('compareTwoMode') ) {
   991 			if ( this.model.get('compareTwoMode') ) {
   990 				// in RTL mode the 'left handle' is the second in the slider, 'right' is first
   992 				// In RTL mode the 'left handle' is the second in the slider, 'right' is first.
   991 				handles.first()
   993 				handles.first()
   992 					.toggleClass( 'to-handle', !! isRtl )
   994 					.toggleClass( 'to-handle', !! isRtl )
   993 					.toggleClass( 'from-handle', ! isRtl );
   995 					.toggleClass( 'from-handle', ! isRtl );
   994 				handles.last()
   996 				handles.last()
   995 					.toggleClass( 'from-handle', !! isRtl )
   997 					.toggleClass( 'from-handle', !! isRtl )
  1017 
  1019 
  1018 				// In two handle mode, ensure handles can't be dragged past each other.
  1020 				// In two handle mode, ensure handles can't be dragged past each other.
  1019 				// Adjust left/right boundaries and reset points.
  1021 				// Adjust left/right boundaries and reset points.
  1020 				if ( view.model.get('compareTwoMode') ) {
  1022 				if ( view.model.get('compareTwoMode') ) {
  1021 					handles = handle.parent().find('.ui-slider-handle');
  1023 					handles = handle.parent().find('.ui-slider-handle');
  1022 					if ( handle.is( handles.first() ) ) { // We're the left handle
  1024 					if ( handle.is( handles.first() ) ) {
       
  1025 						// We're the left handle.
  1023 						rightDragBoundary = handles.last().offset().left;
  1026 						rightDragBoundary = handles.last().offset().left;
  1024 						rightDragReset    = rightDragBoundary - sliderOffset;
  1027 						rightDragReset    = rightDragBoundary - sliderOffset;
  1025 					} else { // We're the right handle
  1028 					} else {
       
  1029 						// We're the right handle.
  1026 						leftDragBoundary = handles.first().offset().left + handles.first().width();
  1030 						leftDragBoundary = handles.first().offset().left + handles.first().width();
  1027 						leftDragReset    = leftDragBoundary - sliderOffset;
  1031 						leftDragReset    = leftDragBoundary - sliderOffset;
  1028 					}
  1032 					}
  1029 				}
  1033 				}
  1030 
  1034 
  1041 
  1045 
  1042 		getPosition: function( position ) {
  1046 		getPosition: function( position ) {
  1043 			return isRtl ? this.model.revisions.length - position - 1: position;
  1047 			return isRtl ? this.model.revisions.length - position - 1: position;
  1044 		},
  1048 		},
  1045 
  1049 
  1046 		// Responds to slide events
  1050 		// Responds to slide events.
  1047 		slide: function( event, ui ) {
  1051 		slide: function( event, ui ) {
  1048 			var attributes, movedRevision;
  1052 			var attributes, movedRevision;
  1049 			// Compare two revisions mode
  1053 			// Compare two revisions mode.
  1050 			if ( this.model.get('compareTwoMode') ) {
  1054 			if ( this.model.get('compareTwoMode') ) {
  1051 				// Prevent sliders from occupying same spot
  1055 				// Prevent sliders from occupying same spot.
  1052 				if ( ui.values[1] === ui.values[0] ) {
  1056 				if ( ui.values[1] === ui.values[0] ) {
  1053 					return false;
  1057 					return false;
  1054 				}
  1058 				}
  1055 				if ( isRtl ) {
  1059 				if ( isRtl ) {
  1056 					ui.values.reverse();
  1060 					ui.values.reverse();
  1070 					attributes.from = undefined;
  1074 					attributes.from = undefined;
  1071 				}
  1075 				}
  1072 			}
  1076 			}
  1073 			movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
  1077 			movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
  1074 
  1078 
  1075 			// If we are scrubbing, a scrub to a revision is considered a hover
  1079 			// If we are scrubbing, a scrub to a revision is considered a hover.
  1076 			if ( this.model.get('scrubbing') ) {
  1080 			if ( this.model.get('scrubbing') ) {
  1077 				attributes.hoveredRevision = movedRevision;
  1081 				attributes.hoveredRevision = movedRevision;
  1078 			}
  1082 			}
  1079 
  1083 
  1080 			this.model.set( attributes );
  1084 			this.model.set( attributes );
  1081 		},
  1085 		},
  1082 
  1086 
  1083 		stop: function() {
  1087 		stop: function() {
  1084 			$( window ).off('mousemove.wp.revisions');
  1088 			$( window ).off('mousemove.wp.revisions');
  1085 			this.model.updateSliderSettings(); // To snap us back to a tick mark
  1089 			this.model.updateSliderSettings(); // To snap us back to a tick mark.
  1086 			this.model.set({ scrubbing: false });
  1090 			this.model.set({ scrubbing: false });
  1087 		}
  1091 		}
  1088 	});
  1092 	});
  1089 
  1093 
  1090 	// The diff view.
  1094 	// The diff view.
  1103 	// Maintains the URL routes so browser URL matches state.
  1107 	// Maintains the URL routes so browser URL matches state.
  1104 	revisions.Router = Backbone.Router.extend({
  1108 	revisions.Router = Backbone.Router.extend({
  1105 		initialize: function( options ) {
  1109 		initialize: function( options ) {
  1106 			this.model = options.model;
  1110 			this.model = options.model;
  1107 
  1111 
  1108 			// Maintain state and history when navigating
  1112 			// Maintain state and history when navigating.
  1109 			this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
  1113 			this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
  1110 			this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
  1114 			this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
  1111 		},
  1115 		},
  1112 
  1116 
  1113 		baseUrl: function( url ) {
  1117 		baseUrl: function( url ) {