PageRenderTime 71ms CodeModel.GetById 33ms app.highlight 32ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-admin/js/revisions.js

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