wp/wp-admin/js/revisions.js
changeset 0 d970ebf37754
child 5 5e2f62d02dcd
equal deleted inserted replaced
-1:000000000000 0:d970ebf37754
       
     1 window.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));