PageRenderTime 79ms CodeModel.GetById 34ms app.highlight 36ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-admin/js/revisions.js

https://gitlab.com/Gashler/dp
JavaScript | 1080 lines | 802 code | 176 blank | 102 comment | 83 complexity | a005233637f6f2564e710a0e4a5b0e1f MD5 | raw file
   1window.wp = window.wp || {};
   2
   3(function($) {
   4	var revisions;
   5
   6	revisions = wp.revisions = { model: {}, view: {}, controller: {} };
   7
   8	// Link settings.
   9	revisions.settings = _.isUndefined( _wpRevisionsSettings ) ? {} : _wpRevisionsSettings;
  10
  11	// For debugging
  12	revisions.debug = false;
  13
  14	revisions.log = function() {
  15		if ( window.console && revisions.debug )
  16			console.log.apply( console, arguments );
  17	};
  18
  19	// Handy functions to help with positioning
  20	$.fn.allOffsets = function() {
  21		var offset = this.offset() || {top: 0, left: 0}, win = $(window);
  22		return _.extend( offset, {
  23			right:  win.width()  - offset.left - this.outerWidth(),
  24			bottom: win.height() - offset.top  - this.outerHeight()
  25		});
  26	};
  27
  28	$.fn.allPositions = function() {
  29		var position = this.position() || {top: 0, left: 0}, parent = this.parent();
  30		return _.extend( position, {
  31			right:  parent.outerWidth()  - position.left - this.outerWidth(),
  32			bottom: parent.outerHeight() - position.top  - this.outerHeight()
  33		});
  34	};
  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
  46	/**
  47	 * ========================================================================
  48	 * MODELS
  49	 * ========================================================================
  50	 */
  51	revisions.model.Slider = Backbone.Model.extend({
  52		defaults: {
  53			value: null,
  54			values: null,
  55			min: 0,
  56			max: 1,
  57			step: 1,
  58			range: false,
  59			compareTwoMode: false
  60		},
  61
  62		initialize: function( options ) {
  63			this.frame = options.frame;
  64			this.revisions = options.revisions;
  65
  66			// Listen for changes to the revisions or mode from outside
  67			this.listenTo( this.frame, 'update:revisions', this.receiveRevisions );
  68			this.listenTo( this.frame, 'change:compareTwoMode', this.updateMode );
  69
  70			// Listen for internal changes
  71			this.listenTo( this, 'change:from', this.handleLocalChanges );
  72			this.listenTo( this, 'change:to', this.handleLocalChanges );
  73			this.listenTo( this, 'change:compareTwoMode', this.updateSliderSettings );
  74			this.listenTo( this, 'update:revisions', this.updateSliderSettings );
  75
  76			// Listen for changes to the hovered revision
  77			this.listenTo( this, 'change:hoveredRevision', this.hoverRevision );
  78
  79			this.set({
  80				max:   this.revisions.length - 1,
  81				compareTwoMode: this.frame.get('compareTwoMode'),
  82				from: this.frame.get('from'),
  83				to: this.frame.get('to')
  84			});
  85			this.updateSliderSettings();
  86		},
  87
  88		getSliderValue: function( a, b ) {
  89			return isRtl ? this.revisions.length - this.revisions.indexOf( this.get(a) ) - 1 : this.revisions.indexOf( this.get(b) );
  90		},
  91
  92		updateSliderSettings: function() {
  93			if ( this.get('compareTwoMode') ) {
  94				this.set({
  95					values: [
  96						this.getSliderValue( 'to', 'from' ),
  97						this.getSliderValue( 'from', 'to' )
  98					],
  99					value: null,
 100					range: true // ensures handles cannot cross
 101				});
 102			} else {
 103				this.set({
 104					value: this.getSliderValue( 'to', 'to' ),
 105					values: null,
 106					range: false
 107				});
 108			}
 109			this.trigger( 'update:slider' );
 110		},
 111
 112		// Called when a revision is hovered
 113		hoverRevision: function( model, value ) {
 114			this.trigger( 'hovered:revision', value );
 115		},
 116
 117		// Called when `compareTwoMode` changes
 118		updateMode: function( model, value ) {
 119			this.set({ compareTwoMode: value });
 120		},
 121
 122		// Called when `from` or `to` changes in the local model
 123		handleLocalChanges: function() {
 124			this.frame.set({
 125				from: this.get('from'),
 126				to: this.get('to')
 127			});
 128		},
 129
 130		// Receives revisions changes from outside the model
 131		receiveRevisions: function( from, to ) {
 132			// Bail if nothing changed
 133			if ( this.get('from') === from && this.get('to') === to )
 134				return;
 135
 136			this.set({ from: from, to: to }, { silent: true });
 137			this.trigger( 'update:revisions', from, to );
 138		}
 139
 140	});
 141
 142	revisions.model.Tooltip = Backbone.Model.extend({
 143		defaults: {
 144			revision: null,
 145			offset: {},
 146			hovering: false, // Whether the mouse is hovering
 147			scrubbing: false // Whether the mouse is scrubbing
 148		},
 149
 150		initialize: function( options ) {
 151			this.frame = options.frame;
 152			this.revisions = options.revisions;
 153			this.slider = options.slider;
 154
 155			this.listenTo( this.slider, 'hovered:revision', this.updateRevision );
 156			this.listenTo( this.slider, 'change:hovering', this.setHovering );
 157			this.listenTo( this.slider, 'change:scrubbing', this.setScrubbing );
 158		},
 159
 160
 161		updateRevision: function( revision ) {
 162			this.set({ revision: revision });
 163		},
 164
 165		setHovering: function( model, value ) {
 166			this.set({ hovering: value });
 167		},
 168
 169		setScrubbing: function( model, value ) {
 170			this.set({ scrubbing: value });
 171		}
 172	});
 173
 174	revisions.model.Revision = Backbone.Model.extend({});
 175
 176	revisions.model.Revisions = Backbone.Collection.extend({
 177		model: revisions.model.Revision,
 178
 179		initialize: function() {
 180			_.bindAll( this, 'next', 'prev' );
 181		},
 182
 183		next: function( revision ) {
 184			var index = this.indexOf( revision );
 185
 186			if ( index !== -1 && index !== this.length - 1 )
 187				return this.at( index + 1 );
 188		},
 189
 190		prev: function( revision ) {
 191			var index = this.indexOf( revision );
 192
 193			if ( index !== -1 && index !== 0 )
 194				return this.at( index - 1 );
 195		}
 196	});
 197
 198	revisions.model.Field = Backbone.Model.extend({});
 199
 200	revisions.model.Fields = Backbone.Collection.extend({
 201		model: revisions.model.Field
 202	});
 203
 204	revisions.model.Diff = Backbone.Model.extend({
 205		initialize: function( attributes, options ) {
 206			var fields = this.get('fields');
 207			this.unset('fields');
 208
 209			this.fields = new revisions.model.Fields( fields );
 210		}
 211	});
 212
 213	revisions.model.Diffs = Backbone.Collection.extend({
 214		initialize: function( models, options ) {
 215			_.bindAll( this, 'getClosestUnloaded' );
 216			this.loadAll = _.once( this._loadAll );
 217			this.revisions = options.revisions;
 218			this.requests  = {};
 219		},
 220
 221		model: revisions.model.Diff,
 222
 223		ensure: function( id, context ) {
 224			var diff     = this.get( id );
 225			var request  = this.requests[ id ];
 226			var deferred = $.Deferred();
 227			var ids      = {};
 228			var from     = id.split(':')[0];
 229			var to       = id.split(':')[1];
 230			ids[id] = true;
 231
 232			wp.revisions.log( 'ensure', id );
 233
 234			this.trigger( 'ensure', ids, from, to, deferred.promise() );
 235
 236			if ( diff ) {
 237				deferred.resolveWith( context, [ diff ] );
 238			} else {
 239				this.trigger( 'ensure:load', ids, from, to, deferred.promise() );
 240				_.each( ids, _.bind( function( id ) {
 241					// Remove anything that has an ongoing request
 242					if ( this.requests[ id ] )
 243						delete ids[ id ];
 244					// Remove anything we already have
 245					if ( this.get( id ) )
 246						delete ids[ id ];
 247				}, this ) );
 248				if ( ! request ) {
 249					// Always include the ID that started this ensure
 250					ids[ id ] = true;
 251					request   = this.load( _.keys( ids ) );
 252				}
 253
 254				request.done( _.bind( function() {
 255					deferred.resolveWith( context, [ this.get( id ) ] );
 256				}, this ) ).fail( _.bind( function() {
 257					deferred.reject();
 258				}) );
 259			}
 260
 261			return deferred.promise();
 262		},
 263
 264		// Returns an array of proximal diffs
 265		getClosestUnloaded: function( ids, centerId ) {
 266			var self = this;
 267			return _.chain([0].concat( ids )).initial().zip( ids ).sortBy( function( pair ) {
 268				return Math.abs( centerId - pair[1] );
 269			}).map( function( pair ) {
 270				return pair.join(':');
 271			}).filter( function( diffId ) {
 272				return _.isUndefined( self.get( diffId ) ) && ! self.requests[ diffId ];
 273			}).value();
 274		},
 275
 276		_loadAll: function( allRevisionIds, centerId, num ) {
 277			var self = this, deferred = $.Deferred();
 278			diffs = _.first( this.getClosestUnloaded( allRevisionIds, centerId ), num );
 279			if ( _.size( diffs ) > 0 ) {
 280				this.load( diffs ).done( function() {
 281					self._loadAll( allRevisionIds, centerId, num ).done( function() {
 282						deferred.resolve();
 283					});
 284				}).fail( function() {
 285					if ( 1 === num ) { // Already tried 1. This just isn't working. Give up.
 286						deferred.reject();
 287					} else { // Request fewer diffs this time
 288						self._loadAll( allRevisionIds, centerId, Math.ceil( num / 2 ) ).done( function() {
 289							deferred.resolve();
 290						});
 291					}
 292				});
 293			} else {
 294				deferred.resolve();
 295			}
 296			return deferred;
 297		},
 298
 299		load: function( comparisons ) {
 300			wp.revisions.log( 'load', comparisons );
 301			// Our collection should only ever grow, never shrink, so remove: false
 302			return this.fetch({ data: { compare: comparisons }, remove: false }).done( function(){
 303				wp.revisions.log( 'load:complete', comparisons );
 304			});
 305		},
 306
 307		sync: function( method, model, options ) {
 308			if ( 'read' === method ) {
 309				options = options || {};
 310				options.context = this;
 311				options.data = _.extend( options.data || {}, {
 312					action: 'get-revision-diffs',
 313					post_id: revisions.settings.postId
 314				});
 315
 316				var deferred = wp.ajax.send( options );
 317				var requests = this.requests;
 318
 319				// Record that we're requesting each diff.
 320				if ( options.data.compare ) {
 321					_.each( options.data.compare, function( id ) {
 322						requests[ id ] = deferred;
 323					});
 324				}
 325
 326				// When the request completes, clear the stored request.
 327				deferred.always( function() {
 328					if ( options.data.compare ) {
 329						_.each( options.data.compare, function( id ) {
 330							delete requests[ id ];
 331						});
 332					}
 333				});
 334
 335				return deferred;
 336
 337			// Otherwise, fall back to `Backbone.sync()`.
 338			} else {
 339				return Backbone.Model.prototype.sync.apply( this, arguments );
 340			}
 341		}
 342	});
 343
 344
 345	revisions.model.FrameState = Backbone.Model.extend({
 346		defaults: {
 347			loading: false,
 348			error: false,
 349			compareTwoMode: false
 350		},
 351
 352		initialize: function( attributes, options ) {
 353			var properties = {};
 354
 355			_.bindAll( this, 'receiveDiff' );
 356			this._debouncedEnsureDiff = _.debounce( this._ensureDiff, 200 );
 357
 358			this.revisions = options.revisions;
 359			this.diffs = new revisions.model.Diffs( [], { revisions: this.revisions });
 360
 361			// Set the initial diffs collection provided through the settings
 362			this.diffs.set( revisions.settings.diffData );
 363
 364			// Set up internal listeners
 365			this.listenTo( this, 'change:from', this.changeRevisionHandler );
 366			this.listenTo( this, 'change:to', this.changeRevisionHandler );
 367			this.listenTo( this, 'change:compareTwoMode', this.changeMode );
 368			this.listenTo( this, 'update:revisions', this.updatedRevisions );
 369			this.listenTo( this.diffs, 'ensure:load', this.updateLoadingStatus );
 370			this.listenTo( this, 'update:diff', this.updateLoadingStatus );
 371
 372			// Set the initial revisions, baseUrl, and mode as provided through settings
 373			properties.to = this.revisions.get( revisions.settings.to );
 374			properties.from = this.revisions.get( revisions.settings.from );
 375			properties.compareTwoMode = revisions.settings.compareTwoMode;
 376			properties.baseUrl = revisions.settings.baseUrl;
 377			this.set( properties );
 378
 379			// Start the router if browser supports History API
 380			if ( window.history && window.history.pushState ) {
 381				this.router = new revisions.Router({ model: this });
 382				Backbone.history.start({ pushState: true });
 383			}
 384		},
 385
 386		updateLoadingStatus: function() {
 387			this.set( 'error', false );
 388			this.set( 'loading', ! this.diff() );
 389		},
 390
 391		changeMode: function( model, value ) {
 392			// If we were on the first revision before switching, we have to bump them over one
 393			if ( value && 0 === this.revisions.indexOf( this.get('to') ) ) {
 394				this.set({
 395					from: this.revisions.at(0),
 396					to: this.revisions.at(1)
 397				});
 398			}
 399		},
 400
 401		updatedRevisions: function( from, to ) {
 402			if ( this.get( 'compareTwoMode' ) ) {
 403				// TODO: compare-two loading strategy
 404			} else {
 405				this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 );
 406			}
 407		},
 408
 409		// Fetch the currently loaded diff.
 410		diff: function() {
 411			return this.diffs.get( this._diffId );
 412		},
 413
 414		// So long as `from` and `to` are changed at the same time, the diff
 415		// will only be updated once. This is because Backbone updates all of
 416		// the changed attributes in `set`, and then fires the `change` events.
 417		updateDiff: function( options ) {
 418			var from, to, diffId, diff;
 419
 420			options = options || {};
 421			from = this.get('from');
 422			to = this.get('to');
 423			diffId = ( from ? from.id : 0 ) + ':' + to.id;
 424
 425			// Check if we're actually changing the diff id.
 426			if ( this._diffId === diffId )
 427				return $.Deferred().reject().promise();
 428
 429			this._diffId = diffId;
 430			this.trigger( 'update:revisions', from, to );
 431
 432			diff = this.diffs.get( diffId );
 433
 434			// If we already have the diff, then immediately trigger the update.
 435			if ( diff ) {
 436				this.receiveDiff( diff );
 437				return $.Deferred().resolve().promise();
 438			// Otherwise, fetch the diff.
 439			} else {
 440				if ( options.immediate ) {
 441					return this._ensureDiff();
 442				} else {
 443					this._debouncedEnsureDiff();
 444					return $.Deferred().reject().promise();
 445				}
 446			}
 447		},
 448
 449		// A simple wrapper around `updateDiff` to prevent the change event's
 450		// parameters from being passed through.
 451		changeRevisionHandler: function( model, value, options ) {
 452			this.updateDiff();
 453		},
 454
 455		receiveDiff: function( diff ) {
 456			// Did we actually get a diff?
 457			if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) {
 458				this.set({
 459					loading: false,
 460					error: true
 461				});
 462			} else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change
 463				this.trigger( 'update:diff', diff );
 464			}
 465		},
 466
 467		_ensureDiff: function() {
 468			return this.diffs.ensure( this._diffId, this ).always( this.receiveDiff );
 469		}
 470	});
 471
 472
 473	/**
 474	 * ========================================================================
 475	 * VIEWS
 476	 * ========================================================================
 477	 */
 478
 479	// The frame view. This contains the entire page.
 480	revisions.view.Frame = wp.Backbone.View.extend({
 481		className: 'revisions',
 482		template: wp.template('revisions-frame'),
 483
 484		initialize: function() {
 485			this.listenTo( this.model, 'update:diff', this.renderDiff );
 486			this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
 487			this.listenTo( this.model, 'change:loading', this.updateLoadingStatus );
 488			this.listenTo( this.model, 'change:error', this.updateErrorStatus );
 489
 490			this.views.set( '.revisions-control-frame', new revisions.view.Controls({
 491				model: this.model
 492			}) );
 493		},
 494
 495		render: function() {
 496			wp.Backbone.View.prototype.render.apply( this, arguments );
 497
 498			$('html').css( 'overflow-y', 'scroll' );
 499			$('#wpbody-content .wrap').append( this.el );
 500			this.updateCompareTwoMode();
 501			this.renderDiff( this.model.diff() );
 502			this.views.ready();
 503
 504			return this;
 505		},
 506
 507		renderDiff: function( diff ) {
 508			this.views.set( '.revisions-diff-frame', new revisions.view.Diff({
 509				model: diff
 510			}) );
 511		},
 512
 513		updateLoadingStatus: function() {
 514			this.$el.toggleClass( 'loading', this.model.get('loading') );
 515		},
 516
 517		updateErrorStatus: function() {
 518			this.$el.toggleClass( 'diff-error', this.model.get('error') );
 519		},
 520
 521		updateCompareTwoMode: function() {
 522			this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') );
 523		}
 524	});
 525
 526	// The control view.
 527	// This contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
 528	revisions.view.Controls = wp.Backbone.View.extend({
 529		className: 'revisions-controls',
 530
 531		initialize: function() {
 532			_.bindAll( this, 'setWidth' );
 533
 534			// Add the button view
 535			this.views.add( new revisions.view.Buttons({
 536				model: this.model
 537			}) );
 538
 539			// Add the checkbox view
 540			this.views.add( new revisions.view.Checkbox({
 541				model: this.model
 542			}) );
 543
 544			// Prep the slider model
 545			var slider = new revisions.model.Slider({
 546				frame: this.model,
 547				revisions: this.model.revisions
 548			});
 549
 550			// Prep the tooltip model
 551			var tooltip = new revisions.model.Tooltip({
 552				frame: this.model,
 553				revisions: this.model.revisions,
 554				slider: slider
 555			});
 556
 557			// Add the tooltip view
 558			this.views.add( new revisions.view.Tooltip({
 559				model: tooltip
 560			}) );
 561
 562			// Add the tickmarks view
 563			this.views.add( new revisions.view.Tickmarks({
 564				model: tooltip
 565			}) );
 566
 567			// Add the slider view
 568			this.views.add( new revisions.view.Slider({
 569				model: slider
 570			}) );
 571
 572			// Add the Metabox view
 573			this.views.add( new revisions.view.Metabox({
 574				model: this.model
 575			}) );
 576		},
 577
 578		ready: function() {
 579			this.top = this.$el.offset().top;
 580			this.window = $(window);
 581			this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) {
 582				var controls = e.data.controls;
 583				var container = controls.$el.parent();
 584				var scrolled = controls.window.scrollTop();
 585				var frame = controls.views.parent;
 586
 587				if ( scrolled >= controls.top ) {
 588					if ( ! frame.$el.hasClass('pinned') ) {
 589						controls.setWidth();
 590						container.css('height', container.height() + 'px' );
 591						controls.window.on('resize.wp.revisions.pinning click.wp.revisions.pinning', {controls: controls}, function(e) {
 592							e.data.controls.setWidth();
 593						});
 594					}
 595					frame.$el.addClass('pinned');
 596				} else if ( frame.$el.hasClass('pinned') ) {
 597					controls.window.off('.wp.revisions.pinning');
 598					controls.$el.css('width', 'auto');
 599					frame.$el.removeClass('pinned');
 600					container.css('height', 'auto');
 601					controls.top = controls.$el.offset().top;
 602				} else {
 603					controls.top = controls.$el.offset().top;
 604				}
 605			});
 606		},
 607
 608		setWidth: function() {
 609			this.$el.css('width', this.$el.parent().width() + 'px');
 610		}
 611	});
 612
 613	// The tickmarks view
 614	revisions.view.Tickmarks = wp.Backbone.View.extend({
 615		className: 'revisions-tickmarks',
 616		direction: isRtl ? 'right' : 'left',
 617
 618		initialize: function() {
 619			this.listenTo( this.model, 'change:revision', this.reportTickPosition );
 620		},
 621
 622		reportTickPosition: function( model, revision ) {
 623			var offset, thisOffset, parentOffset, tick, index = this.model.revisions.indexOf( revision );
 624			thisOffset = this.$el.allOffsets();
 625			parentOffset = this.$el.parent().allOffsets();
 626			if ( index === this.model.revisions.length - 1 ) {
 627				// Last one
 628				offset = {
 629					rightPlusWidth: thisOffset.left - parentOffset.left + 1,
 630					leftPlusWidth: thisOffset.right - parentOffset.right + 1
 631				};
 632			} else {
 633				// Normal tick
 634				tick = this.$('div:nth-of-type(' + (index + 1) + ')');
 635				offset = tick.allPositions();
 636				_.extend( offset, {
 637					left: offset.left + thisOffset.left - parentOffset.left,
 638					right: offset.right + thisOffset.right - parentOffset.right
 639				});
 640				_.extend( offset, {
 641					leftPlusWidth: offset.left + tick.outerWidth(),
 642					rightPlusWidth: offset.right + tick.outerWidth()
 643				});
 644			}
 645			this.model.set({ offset: offset });
 646		},
 647
 648		ready: function() {
 649			var tickCount, tickWidth;
 650			tickCount = this.model.revisions.length - 1;
 651			tickWidth = 1 / tickCount;
 652			this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px');
 653
 654			_(tickCount).times( function( index ){
 655				this.$el.append( '<div style="' + this.direction + ': ' + ( 100 * tickWidth * index ) + '%"></div>' );
 656			}, this );
 657		}
 658	});
 659
 660	// The metabox view
 661	revisions.view.Metabox = wp.Backbone.View.extend({
 662		className: 'revisions-meta',
 663
 664		initialize: function() {
 665			// Add the 'from' view
 666			this.views.add( new revisions.view.MetaFrom({
 667				model: this.model,
 668				className: 'diff-meta diff-meta-from'
 669			}) );
 670
 671			// Add the 'to' view
 672			this.views.add( new revisions.view.MetaTo({
 673				model: this.model
 674			}) );
 675		}
 676	});
 677
 678	// The revision meta view (to be extended)
 679	revisions.view.Meta = wp.Backbone.View.extend({
 680		template: wp.template('revisions-meta'),
 681
 682		events: {
 683			'click .restore-revision': 'restoreRevision'
 684		},
 685
 686		initialize: function() {
 687			this.listenTo( this.model, 'update:revisions', this.render );
 688		},
 689
 690		prepare: function() {
 691			return _.extend( this.model.toJSON()[this.type] || {}, {
 692				type: this.type
 693			});
 694		},
 695
 696		restoreRevision: function() {
 697			document.location = this.model.get('to').attributes.restoreUrl;
 698		}
 699	});
 700
 701	// The revision meta 'from' view
 702	revisions.view.MetaFrom = revisions.view.Meta.extend({
 703		className: 'diff-meta diff-meta-from',
 704		type: 'from'
 705	});
 706
 707	// The revision meta 'to' view
 708	revisions.view.MetaTo = revisions.view.Meta.extend({
 709		className: 'diff-meta diff-meta-to',
 710		type: 'to'
 711	});
 712
 713	// The checkbox view.
 714	revisions.view.Checkbox = wp.Backbone.View.extend({
 715		className: 'revisions-checkbox',
 716		template: wp.template('revisions-checkbox'),
 717
 718		events: {
 719			'click .compare-two-revisions': 'compareTwoToggle'
 720		},
 721
 722		initialize: function() {
 723			this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
 724		},
 725
 726		ready: function() {
 727			if ( this.model.revisions.length < 3 )
 728				$('.revision-toggle-compare-mode').hide();
 729		},
 730
 731		updateCompareTwoMode: function() {
 732			this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') );
 733		},
 734
 735		// Toggle the compare two mode feature when the compare two checkbox is checked.
 736		compareTwoToggle: function( event ) {
 737			// Activate compare two mode?
 738			this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
 739		}
 740	});
 741
 742	// The tooltip view.
 743	// Encapsulates the tooltip.
 744	revisions.view.Tooltip = wp.Backbone.View.extend({
 745		className: 'revisions-tooltip',
 746		template: wp.template('revisions-meta'),
 747
 748		initialize: function( options ) {
 749			this.listenTo( this.model, 'change:offset', this.render );
 750			this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
 751			this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
 752		},
 753
 754		prepare: function() {
 755			if ( _.isNull( this.model.get('revision') ) )
 756				return;
 757			else
 758				return _.extend( { type: 'tooltip' }, {
 759					attributes: this.model.get('revision').toJSON()
 760				});
 761		},
 762
 763		render: function() {
 764			var direction, directionVal, flipped, css = {}, position = this.model.revisions.indexOf( this.model.get('revision') ) + 1;
 765			flipped = ( position / this.model.revisions.length ) > 0.5;
 766			if ( isRtl ) {
 767				direction = flipped ? 'left' : 'right';
 768				directionVal = flipped ? 'leftPlusWidth' : direction;
 769			} else {
 770				direction = flipped ? 'right' : 'left';
 771				directionVal = flipped ? 'rightPlusWidth' : direction;
 772			}
 773			otherDirection = 'right' === direction ? 'left': 'right';
 774			wp.Backbone.View.prototype.render.apply( this, arguments );
 775			css[direction] = this.model.get('offset')[directionVal] + 'px';
 776			css[otherDirection] = '';
 777			this.$el.toggleClass( 'flipped', flipped ).css( css );
 778		},
 779
 780		visible: function() {
 781			return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
 782		},
 783
 784		toggleVisibility: function( options ) {
 785			if ( this.visible() )
 786				this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 );
 787			else
 788				this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } );
 789			return;
 790		}
 791	});
 792
 793	// The buttons view.
 794	// Encapsulates all of the configuration for the previous/next buttons.
 795	revisions.view.Buttons = wp.Backbone.View.extend({
 796		className: 'revisions-buttons',
 797		template: wp.template('revisions-buttons'),
 798
 799		events: {
 800			'click .revisions-next .button': 'nextRevision',
 801			'click .revisions-previous .button': 'previousRevision'
 802		},
 803
 804		initialize: function() {
 805			this.listenTo( this.model, 'update:revisions', this.disabledButtonCheck );
 806		},
 807
 808		ready: function() {
 809			this.disabledButtonCheck();
 810		},
 811
 812		// Go to a specific model index
 813		gotoModel: function( toIndex ) {
 814			var attributes = {
 815				to: this.model.revisions.at( toIndex )
 816			};
 817			// If we're at the first revision, unset 'from'.
 818			if ( toIndex )
 819				attributes.from = this.model.revisions.at( toIndex - 1 );
 820			else
 821				this.model.unset('from', { silent: true });
 822
 823			this.model.set( attributes );
 824		},
 825
 826		// Go to the 'next' revision
 827		nextRevision: function() {
 828			var toIndex = this.model.revisions.indexOf( this.model.get('to') ) + 1;
 829			this.gotoModel( toIndex );
 830		},
 831
 832		// Go to the 'previous' revision
 833		previousRevision: function() {
 834			var toIndex = this.model.revisions.indexOf( this.model.get('to') ) - 1;
 835			this.gotoModel( toIndex );
 836		},
 837
 838		// Check to see if the Previous or Next buttons need to be disabled or enabled.
 839		disabledButtonCheck: function() {
 840			var maxVal = this.model.revisions.length - 1,
 841				minVal = 0,
 842				next = $('.revisions-next .button'),
 843				previous = $('.revisions-previous .button'),
 844				val = this.model.revisions.indexOf( this.model.get('to') );
 845
 846			// Disable "Next" button if you're on the last node.
 847			next.prop( 'disabled', ( maxVal === val ) );
 848
 849			// Disable "Previous" button if you're on the first node.
 850			previous.prop( 'disabled', ( minVal === val ) );
 851		}
 852	});
 853
 854
 855	// The slider view.
 856	revisions.view.Slider = wp.Backbone.View.extend({
 857		className: 'wp-slider',
 858		direction: isRtl ? 'right' : 'left',
 859
 860		events: {
 861			'mousemove' : 'mouseMove'
 862		},
 863
 864		initialize: function() {
 865			_.bindAll( this, 'start', 'slide', 'stop', 'mouseMove', 'mouseEnter', 'mouseLeave' );
 866			this.listenTo( this.model, 'update:slider', this.applySliderSettings );
 867		},
 868
 869		ready: function() {
 870			this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px');
 871			this.$el.slider( _.extend( this.model.toJSON(), {
 872				start: this.start,
 873				slide: this.slide,
 874				stop:  this.stop
 875			}) );
 876
 877			this.$el.hoverIntent({
 878				over: this.mouseEnter,
 879				out: this.mouseLeave,
 880				timeout: 800
 881			});
 882
 883			this.applySliderSettings();
 884		},
 885
 886		mouseMove: function( e ) {
 887			var zoneCount = this.model.revisions.length - 1, // One fewer zone than models
 888				sliderFrom = this.$el.allOffsets()[this.direction], // "From" edge of slider
 889				sliderWidth = this.$el.width(), // Width of slider
 890				tickWidth = sliderWidth / zoneCount, // Calculated width of zone
 891				actualX = isRtl? $(window).width() - e.pageX : e.pageX; // Flipped for RTL - sliderFrom;
 892			actualX = actualX - sliderFrom; // Offset of mouse position in slider
 893			var currentModelIndex = Math.floor( ( actualX + ( tickWidth / 2 )  ) / tickWidth ); // Calculate the model index
 894
 895			// Ensure sane value for currentModelIndex.
 896			if ( currentModelIndex < 0 )
 897				currentModelIndex = 0;
 898			else if ( currentModelIndex >= this.model.revisions.length )
 899				currentModelIndex = this.model.revisions.length - 1;
 900
 901			// Update the tooltip mode
 902			this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
 903		},
 904
 905		mouseLeave: function() {
 906			this.model.set({ hovering: false });
 907		},
 908
 909		mouseEnter: function() {
 910			this.model.set({ hovering: true });
 911		},
 912
 913		applySliderSettings: function() {
 914			this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) );
 915			var handles = this.$('a.ui-slider-handle');
 916
 917			if ( this.model.get('compareTwoMode') ) {
 918				// in RTL mode the 'left handle' is the second in the slider, 'right' is first
 919				handles.first()
 920					.toggleClass( 'to-handle', !! isRtl )
 921					.toggleClass( 'from-handle', ! isRtl );
 922				handles.last()
 923					.toggleClass( 'from-handle', !! isRtl )
 924					.toggleClass( 'to-handle', ! isRtl );
 925			} else {
 926				handles.removeClass('from-handle to-handle');
 927			}
 928		},
 929
 930		start: function( event, ui ) {
 931			this.model.set({ scrubbing: true });
 932
 933			// Track the mouse position to enable smooth dragging,
 934			// overrides default jQuery UI step behavior.
 935			$( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
 936				var view              = e.data.view,
 937				    leftDragBoundary  = view.$el.offset().left,
 938				    sliderOffset      = leftDragBoundary,
 939				    sliderRightEdge   = leftDragBoundary + view.$el.width(),
 940				    rightDragBoundary = sliderRightEdge,
 941				    leftDragReset     = '0',
 942				    rightDragReset    = '100%',
 943				    handle            = $( ui.handle );
 944
 945				// In two handle mode, ensure handles can't be dragged past each other.
 946				// Adjust left/right boundaries and reset points.
 947				if ( view.model.get('compareTwoMode') ) {
 948					var handles = handle.parent().find('.ui-slider-handle');
 949					if ( handle.is( handles.first() ) ) { // We're the left handle
 950						rightDragBoundary = handles.last().offset().left;
 951						rightDragReset    = rightDragBoundary - sliderOffset;
 952					} else { // We're the right handle
 953						leftDragBoundary = handles.first().offset().left + handles.first().width();
 954						leftDragReset    = leftDragBoundary - sliderOffset;
 955					}
 956				}
 957
 958				// Follow mouse movements, as long as handle remains inside slider.
 959				if ( e.pageX < leftDragBoundary ) {
 960					handle.css( 'left', leftDragReset ); // Mouse to left of slider.
 961				} else if ( e.pageX > rightDragBoundary ) {
 962					handle.css( 'left', rightDragReset ); // Mouse to right of slider.
 963				} else {
 964					handle.css( 'left', e.pageX - sliderOffset ); // Mouse in slider.
 965				}
 966			} );
 967		},
 968
 969		getPosition: function( position ) {
 970			return isRtl ? this.model.revisions.length - position - 1: position;
 971		},
 972
 973		// Responds to slide events
 974		slide: function( event, ui ) {
 975			var attributes, movedRevision;
 976			// Compare two revisions mode
 977			if ( this.model.get('compareTwoMode') ) {
 978				// Prevent sliders from occupying same spot
 979				if ( ui.values[1] === ui.values[0] )
 980					return false;
 981				if ( isRtl )
 982					ui.values.reverse();
 983				attributes = {
 984					from: this.model.revisions.at( this.getPosition( ui.values[0] ) ),
 985					to: this.model.revisions.at( this.getPosition( ui.values[1] ) )
 986				};
 987			} else {
 988				attributes = {
 989					to: this.model.revisions.at( this.getPosition( ui.value ) )
 990				};
 991				// If we're at the first revision, unset 'from'.
 992				if ( this.getPosition( ui.value ) > 0 )
 993					attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 );
 994				else
 995					attributes.from = undefined;
 996			}
 997			movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
 998
 999			// If we are scrubbing, a scrub to a revision is considered a hover
1000			if ( this.model.get('scrubbing') )
1001				attributes.hoveredRevision = movedRevision;
1002
1003			this.model.set( attributes );
1004		},
1005
1006		stop: function( event, ui ) {
1007			$( window ).off('mousemove.wp.revisions');
1008			this.model.updateSliderSettings(); // To snap us back to a tick mark
1009			this.model.set({ scrubbing: false });
1010		}
1011	});
1012
1013	// The diff view.
1014	// This is the view for the current active diff.
1015	revisions.view.Diff = wp.Backbone.View.extend({
1016		className: 'revisions-diff',
1017		template: wp.template('revisions-diff'),
1018
1019		// Generate the options to be passed to the template.
1020		prepare: function() {
1021			return _.extend({ fields: this.model.fields.toJSON() }, this.options );
1022		}
1023	});
1024
1025	// The revisions router
1026	// takes URLs with #hash fragments and routes them
1027	revisions.Router = Backbone.Router.extend({
1028		initialize: function( options ) {
1029			this.model = options.model;
1030			this.routes = _.object([
1031				[ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ],
1032				[ this.baseUrl( '?from=:from&to=:to' ), 'handleRoute' ]
1033			]);
1034			// Maintain state and history when navigating
1035			this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
1036			this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
1037		},
1038
1039		baseUrl: function( url ) {
1040			return this.model.get('baseUrl') + url;
1041		},
1042
1043		updateUrl: function() {
1044			var from = this.model.has('from') ? this.model.get('from').id : 0;
1045			var to = this.model.get('to').id;
1046			if ( this.model.get('compareTwoMode' ) )
1047				this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ) );
1048			else
1049				this.navigate( this.baseUrl( '?revision=' + to ) );
1050		},
1051
1052		handleRoute: function( a, b ) {
1053			var from, to, compareTwo = _.isUndefined( b );
1054
1055			if ( ! compareTwo ) {
1056				b = this.model.revisions.get( a );
1057				a = this.model.revisions.prev( b );
1058				b = b ? b.id : 0;
1059				a = a ? a.id : 0;
1060			}
1061
1062			this.model.set({
1063				from: this.model.revisions.get( parseInt( a, 10 ) ),
1064				to: this.model.revisions.get( parseInt( a, 10 ) ),
1065				compareTwoMode: compareTwo
1066			});
1067		}
1068	});
1069
1070	// Initialize the revisions UI.
1071	revisions.init = function() {
1072		revisions.view.frame = new revisions.view.Frame({
1073			model: new revisions.model.FrameState({}, {
1074				revisions: new revisions.model.Revisions( revisions.settings.revisionData )
1075			})
1076		}).render();
1077	};
1078
1079	$( revisions.init );
1080}(jQuery));