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