PageRenderTime 69ms CodeModel.GetById 27ms app.highlight 33ms RepoModel.GetById 0ms app.codeStats 1ms

/wordpress/wp-admin/js/revisions.js

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