PageRenderTime 178ms CodeModel.GetById 16ms app.highlight 144ms RepoModel.GetById 1ms app.codeStats 1ms

/wp-admin/js/revisions.js

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