]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-admin/js/revisions.js
WordPress 3.7-scripts
[autoinstalls/wordpress.git] / wp-admin / js / revisions.js
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));