home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress2 / wp-admin / js / revisions.js < prev    next >
Encoding:
JavaScript  |  2017-02-17  |  33.0 KB  |  1,170 lines

  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.                 if ( Backbone.History.started ) {
  415.                     Backbone.history.stop();
  416.                 }
  417.                 Backbone.history.start({ pushState: true });
  418.             }
  419.         },
  420.  
  421.         updateLoadingStatus: function() {
  422.             this.set( 'error', false );
  423.             this.set( 'loading', ! this.diff() );
  424.         },
  425.  
  426.         changeMode: function( model, value ) {
  427.             var toIndex = this.revisions.indexOf( this.get( 'to' ) );
  428.  
  429.             // If we were on the first revision before switching to two-handled mode,
  430.             // bump the 'to' position over one
  431.             if ( value && 0 === toIndex ) {
  432.                 this.set({
  433.                     from: this.revisions.at( toIndex ),
  434.                     to:   this.revisions.at( toIndex + 1 )
  435.                 });
  436.             }
  437.  
  438.             // When switching back to single-handled mode, reset 'from' model to
  439.             // one position before the 'to' model
  440.             if ( ! value && 0 !== toIndex ) { // '! value' means switching to single-handled mode
  441.                 this.set({
  442.                     from: this.revisions.at( toIndex - 1 ),
  443.                     to:   this.revisions.at( toIndex )
  444.                 });
  445.             }
  446.         },
  447.  
  448.         updatedRevisions: function( from, to ) {
  449.             if ( this.get( 'compareTwoMode' ) ) {
  450.                 // TODO: compare-two loading strategy
  451.             } else {
  452.                 this.diffs.loadAll( this.revisions.pluck('id'), to.id, 40 );
  453.             }
  454.         },
  455.  
  456.         // Fetch the currently loaded diff.
  457.         diff: function() {
  458.             return this.diffs.get( this._diffId );
  459.         },
  460.  
  461.         // So long as `from` and `to` are changed at the same time, the diff
  462.         // will only be updated once. This is because Backbone updates all of
  463.         // the changed attributes in `set`, and then fires the `change` events.
  464.         updateDiff: function( options ) {
  465.             var from, to, diffId, diff;
  466.  
  467.             options = options || {};
  468.             from = this.get('from');
  469.             to = this.get('to');
  470.             diffId = ( from ? from.id : 0 ) + ':' + to.id;
  471.  
  472.             // Check if we're actually changing the diff id.
  473.             if ( this._diffId === diffId ) {
  474.                 return $.Deferred().reject().promise();
  475.             }
  476.  
  477.             this._diffId = diffId;
  478.             this.trigger( 'update:revisions', from, to );
  479.  
  480.             diff = this.diffs.get( diffId );
  481.  
  482.             // If we already have the diff, then immediately trigger the update.
  483.             if ( diff ) {
  484.                 this.receiveDiff( diff );
  485.                 return $.Deferred().resolve().promise();
  486.             // Otherwise, fetch the diff.
  487.             } else {
  488.                 if ( options.immediate ) {
  489.                     return this._ensureDiff();
  490.                 } else {
  491.                     this._debouncedEnsureDiff();
  492.                     return $.Deferred().reject().promise();
  493.                 }
  494.             }
  495.         },
  496.  
  497.         // A simple wrapper around `updateDiff` to prevent the change event's
  498.         // parameters from being passed through.
  499.         changeRevisionHandler: function() {
  500.             this.updateDiff();
  501.         },
  502.  
  503.         receiveDiff: function( diff ) {
  504.             // Did we actually get a diff?
  505.             if ( _.isUndefined( diff ) || _.isUndefined( diff.id ) ) {
  506.                 this.set({
  507.                     loading: false,
  508.                     error: true
  509.                 });
  510.             } else if ( this._diffId === diff.id ) { // Make sure the current diff didn't change
  511.                 this.trigger( 'update:diff', diff );
  512.             }
  513.         },
  514.  
  515.         _ensureDiff: function() {
  516.             return this.diffs.ensure( this._diffId, this ).always( this.receiveDiff );
  517.         }
  518.     });
  519.  
  520.  
  521.     /**
  522.      * ========================================================================
  523.      * VIEWS
  524.      * ========================================================================
  525.      */
  526.  
  527.     /**
  528.      * wp.revisions.view.Frame
  529.      *
  530.      * Top level frame that orchestrates the revisions experience.
  531.      *
  532.      * @param {object}                     options       The options hash for the view.
  533.      * @param {revisions.model.FrameState} options.model The frame state model.
  534.      */
  535.     revisions.view.Frame = wp.Backbone.View.extend({
  536.         className: 'revisions',
  537.         template: wp.template('revisions-frame'),
  538.  
  539.         initialize: function() {
  540.             this.listenTo( this.model, 'update:diff', this.renderDiff );
  541.             this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
  542.             this.listenTo( this.model, 'change:loading', this.updateLoadingStatus );
  543.             this.listenTo( this.model, 'change:error', this.updateErrorStatus );
  544.  
  545.             this.views.set( '.revisions-control-frame', new revisions.view.Controls({
  546.                 model: this.model
  547.             }) );
  548.         },
  549.  
  550.         render: function() {
  551.             wp.Backbone.View.prototype.render.apply( this, arguments );
  552.  
  553.             $('html').css( 'overflow-y', 'scroll' );
  554.             $('#wpbody-content .wrap').append( this.el );
  555.             this.updateCompareTwoMode();
  556.             this.renderDiff( this.model.diff() );
  557.             this.views.ready();
  558.  
  559.             return this;
  560.         },
  561.  
  562.         renderDiff: function( diff ) {
  563.             this.views.set( '.revisions-diff-frame', new revisions.view.Diff({
  564.                 model: diff
  565.             }) );
  566.         },
  567.  
  568.         updateLoadingStatus: function() {
  569.             this.$el.toggleClass( 'loading', this.model.get('loading') );
  570.         },
  571.  
  572.         updateErrorStatus: function() {
  573.             this.$el.toggleClass( 'diff-error', this.model.get('error') );
  574.         },
  575.  
  576.         updateCompareTwoMode: function() {
  577.             this.$el.toggleClass( 'comparing-two-revisions', this.model.get('compareTwoMode') );
  578.         }
  579.     });
  580.  
  581.     /**
  582.      * wp.revisions.view.Controls
  583.      *
  584.      * The controls view.
  585.      *
  586.      * Contains the revision slider, previous/next buttons, the meta info and the compare checkbox.
  587.      */
  588.     revisions.view.Controls = wp.Backbone.View.extend({
  589.         className: 'revisions-controls',
  590.  
  591.         initialize: function() {
  592.             _.bindAll( this, 'setWidth' );
  593.  
  594.             // Add the button view
  595.             this.views.add( new revisions.view.Buttons({
  596.                 model: this.model
  597.             }) );
  598.  
  599.             // Add the checkbox view
  600.             this.views.add( new revisions.view.Checkbox({
  601.                 model: this.model
  602.             }) );
  603.  
  604.             // Prep the slider model
  605.             var slider = new revisions.model.Slider({
  606.                 frame: this.model,
  607.                 revisions: this.model.revisions
  608.             }),
  609.  
  610.             // Prep the tooltip model
  611.             tooltip = new revisions.model.Tooltip({
  612.                 frame: this.model,
  613.                 revisions: this.model.revisions,
  614.                 slider: slider
  615.             });
  616.  
  617.             // Add the tooltip view
  618.             this.views.add( new revisions.view.Tooltip({
  619.                 model: tooltip
  620.             }) );
  621.  
  622.             // Add the tickmarks view
  623.             this.views.add( new revisions.view.Tickmarks({
  624.                 model: tooltip
  625.             }) );
  626.  
  627.             // Add the slider view
  628.             this.views.add( new revisions.view.Slider({
  629.                 model: slider
  630.             }) );
  631.  
  632.             // Add the Metabox view
  633.             this.views.add( new revisions.view.Metabox({
  634.                 model: this.model
  635.             }) );
  636.         },
  637.  
  638.         ready: function() {
  639.             this.top = this.$el.offset().top;
  640.             this.window = $(window);
  641.             this.window.on( 'scroll.wp.revisions', {controls: this}, function(e) {
  642.                 var controls  = e.data.controls,
  643.                     container = controls.$el.parent(),
  644.                     scrolled  = controls.window.scrollTop(),
  645.                     frame     = controls.views.parent;
  646.  
  647.                 if ( scrolled >= controls.top ) {
  648.                     if ( ! frame.$el.hasClass('pinned') ) {
  649.                         controls.setWidth();
  650.                         container.css('height', container.height() + 'px' );
  651.                         controls.window.on('resize.wp.revisions.pinning click.wp.revisions.pinning', {controls: controls}, function(e) {
  652.                             e.data.controls.setWidth();
  653.                         });
  654.                     }
  655.                     frame.$el.addClass('pinned');
  656.                 } else if ( frame.$el.hasClass('pinned') ) {
  657.                     controls.window.off('.wp.revisions.pinning');
  658.                     controls.$el.css('width', 'auto');
  659.                     frame.$el.removeClass('pinned');
  660.                     container.css('height', 'auto');
  661.                     controls.top = controls.$el.offset().top;
  662.                 } else {
  663.                     controls.top = controls.$el.offset().top;
  664.                 }
  665.             });
  666.         },
  667.  
  668.         setWidth: function() {
  669.             this.$el.css('width', this.$el.parent().width() + 'px');
  670.         }
  671.     });
  672.  
  673.     // The tickmarks view
  674.     revisions.view.Tickmarks = wp.Backbone.View.extend({
  675.         className: 'revisions-tickmarks',
  676.         direction: isRtl ? 'right' : 'left',
  677.  
  678.         initialize: function() {
  679.             this.listenTo( this.model, 'change:revision', this.reportTickPosition );
  680.         },
  681.  
  682.         reportTickPosition: function( model, revision ) {
  683.             var offset, thisOffset, parentOffset, tick, index = this.model.revisions.indexOf( revision );
  684.             thisOffset = this.$el.allOffsets();
  685.             parentOffset = this.$el.parent().allOffsets();
  686.             if ( index === this.model.revisions.length - 1 ) {
  687.                 // Last one
  688.                 offset = {
  689.                     rightPlusWidth: thisOffset.left - parentOffset.left + 1,
  690.                     leftPlusWidth: thisOffset.right - parentOffset.right + 1
  691.                 };
  692.             } else {
  693.                 // Normal tick
  694.                 tick = this.$('div:nth-of-type(' + (index + 1) + ')');
  695.                 offset = tick.allPositions();
  696.                 _.extend( offset, {
  697.                     left: offset.left + thisOffset.left - parentOffset.left,
  698.                     right: offset.right + thisOffset.right - parentOffset.right
  699.                 });
  700.                 _.extend( offset, {
  701.                     leftPlusWidth: offset.left + tick.outerWidth(),
  702.                     rightPlusWidth: offset.right + tick.outerWidth()
  703.                 });
  704.             }
  705.             this.model.set({ offset: offset });
  706.         },
  707.  
  708.         ready: function() {
  709.             var tickCount, tickWidth;
  710.             tickCount = this.model.revisions.length - 1;
  711.             tickWidth = 1 / tickCount;
  712.             this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px');
  713.  
  714.             _(tickCount).times( function( index ){
  715.                 this.$el.append( '<div style="' + this.direction + ': ' + ( 100 * tickWidth * index ) + '%"></div>' );
  716.             }, this );
  717.         }
  718.     });
  719.  
  720.     // The metabox view
  721.     revisions.view.Metabox = wp.Backbone.View.extend({
  722.         className: 'revisions-meta',
  723.  
  724.         initialize: function() {
  725.             // Add the 'from' view
  726.             this.views.add( new revisions.view.MetaFrom({
  727.                 model: this.model,
  728.                 className: 'diff-meta diff-meta-from'
  729.             }) );
  730.  
  731.             // Add the 'to' view
  732.             this.views.add( new revisions.view.MetaTo({
  733.                 model: this.model
  734.             }) );
  735.         }
  736.     });
  737.  
  738.     // The revision meta view (to be extended)
  739.     revisions.view.Meta = wp.Backbone.View.extend({
  740.         template: wp.template('revisions-meta'),
  741.  
  742.         events: {
  743.             'click .restore-revision': 'restoreRevision'
  744.         },
  745.  
  746.         initialize: function() {
  747.             this.listenTo( this.model, 'update:revisions', this.render );
  748.         },
  749.  
  750.         prepare: function() {
  751.             return _.extend( this.model.toJSON()[this.type] || {}, {
  752.                 type: this.type
  753.             });
  754.         },
  755.  
  756.         restoreRevision: function() {
  757.             document.location = this.model.get('to').attributes.restoreUrl;
  758.         }
  759.     });
  760.  
  761.     // The revision meta 'from' view
  762.     revisions.view.MetaFrom = revisions.view.Meta.extend({
  763.         className: 'diff-meta diff-meta-from',
  764.         type: 'from'
  765.     });
  766.  
  767.     // The revision meta 'to' view
  768.     revisions.view.MetaTo = revisions.view.Meta.extend({
  769.         className: 'diff-meta diff-meta-to',
  770.         type: 'to'
  771.     });
  772.  
  773.     // The checkbox view.
  774.     revisions.view.Checkbox = wp.Backbone.View.extend({
  775.         className: 'revisions-checkbox',
  776.         template: wp.template('revisions-checkbox'),
  777.  
  778.         events: {
  779.             'click .compare-two-revisions': 'compareTwoToggle'
  780.         },
  781.  
  782.         initialize: function() {
  783.             this.listenTo( this.model, 'change:compareTwoMode', this.updateCompareTwoMode );
  784.         },
  785.  
  786.         ready: function() {
  787.             if ( this.model.revisions.length < 3 ) {
  788.                 $('.revision-toggle-compare-mode').hide();
  789.             }
  790.         },
  791.  
  792.         updateCompareTwoMode: function() {
  793.             this.$('.compare-two-revisions').prop( 'checked', this.model.get('compareTwoMode') );
  794.         },
  795.  
  796.         // Toggle the compare two mode feature when the compare two checkbox is checked.
  797.         compareTwoToggle: function() {
  798.             // Activate compare two mode?
  799.             this.model.set({ compareTwoMode: $('.compare-two-revisions').prop('checked') });
  800.         }
  801.     });
  802.  
  803.     // The tooltip view.
  804.     // Encapsulates the tooltip.
  805.     revisions.view.Tooltip = wp.Backbone.View.extend({
  806.         className: 'revisions-tooltip',
  807.         template: wp.template('revisions-meta'),
  808.  
  809.         initialize: function() {
  810.             this.listenTo( this.model, 'change:offset', this.render );
  811.             this.listenTo( this.model, 'change:hovering', this.toggleVisibility );
  812.             this.listenTo( this.model, 'change:scrubbing', this.toggleVisibility );
  813.         },
  814.  
  815.         prepare: function() {
  816.             if ( _.isNull( this.model.get('revision') ) ) {
  817.                 return;
  818.             } else {
  819.                 return _.extend( { type: 'tooltip' }, {
  820.                     attributes: this.model.get('revision').toJSON()
  821.                 });
  822.             }
  823.         },
  824.  
  825.         render: function() {
  826.             var otherDirection,
  827.                 direction,
  828.                 directionVal,
  829.                 flipped,
  830.                 css      = {},
  831.                 position = this.model.revisions.indexOf( this.model.get('revision') ) + 1;
  832.  
  833.             flipped = ( position / this.model.revisions.length ) > 0.5;
  834.             if ( isRtl ) {
  835.                 direction = flipped ? 'left' : 'right';
  836.                 directionVal = flipped ? 'leftPlusWidth' : direction;
  837.             } else {
  838.                 direction = flipped ? 'right' : 'left';
  839.                 directionVal = flipped ? 'rightPlusWidth' : direction;
  840.             }
  841.             otherDirection = 'right' === direction ? 'left': 'right';
  842.             wp.Backbone.View.prototype.render.apply( this, arguments );
  843.             css[direction] = this.model.get('offset')[directionVal] + 'px';
  844.             css[otherDirection] = '';
  845.             this.$el.toggleClass( 'flipped', flipped ).css( css );
  846.         },
  847.  
  848.         visible: function() {
  849.             return this.model.get( 'scrubbing' ) || this.model.get( 'hovering' );
  850.         },
  851.  
  852.         toggleVisibility: function() {
  853.             if ( this.visible() ) {
  854.                 this.$el.stop().show().fadeTo( 100 - this.el.style.opacity * 100, 1 );
  855.             } else {
  856.                 this.$el.stop().fadeTo( this.el.style.opacity * 300, 0, function(){ $(this).hide(); } );
  857.             }
  858.             return;
  859.         }
  860.     });
  861.  
  862.     // The buttons view.
  863.     // Encapsulates all of the configuration for the previous/next buttons.
  864.     revisions.view.Buttons = wp.Backbone.View.extend({
  865.         className: 'revisions-buttons',
  866.         template: wp.template('revisions-buttons'),
  867.  
  868.         events: {
  869.             'click .revisions-next .button': 'nextRevision',
  870.             'click .revisions-previous .button': 'previousRevision'
  871.         },
  872.  
  873.         initialize: function() {
  874.             this.listenTo( this.model, 'update:revisions', this.disabledButtonCheck );
  875.         },
  876.  
  877.         ready: function() {
  878.             this.disabledButtonCheck();
  879.         },
  880.  
  881.         // Go to a specific model index
  882.         gotoModel: function( toIndex ) {
  883.             var attributes = {
  884.                 to: this.model.revisions.at( toIndex )
  885.             };
  886.             // If we're at the first revision, unset 'from'.
  887.             if ( toIndex ) {
  888.                 attributes.from = this.model.revisions.at( toIndex - 1 );
  889.             } else {
  890.                 this.model.unset('from', { silent: true });
  891.             }
  892.  
  893.             this.model.set( attributes );
  894.         },
  895.  
  896.         // Go to the 'next' revision
  897.         nextRevision: function() {
  898.             var toIndex = this.model.revisions.indexOf( this.model.get('to') ) + 1;
  899.             this.gotoModel( toIndex );
  900.         },
  901.  
  902.         // Go to the 'previous' revision
  903.         previousRevision: function() {
  904.             var toIndex = this.model.revisions.indexOf( this.model.get('to') ) - 1;
  905.             this.gotoModel( toIndex );
  906.         },
  907.  
  908.         // Check to see if the Previous or Next buttons need to be disabled or enabled.
  909.         disabledButtonCheck: function() {
  910.             var maxVal   = this.model.revisions.length - 1,
  911.                 minVal   = 0,
  912.                 next     = $('.revisions-next .button'),
  913.                 previous = $('.revisions-previous .button'),
  914.                 val      = this.model.revisions.indexOf( this.model.get('to') );
  915.  
  916.             // Disable "Next" button if you're on the last node.
  917.             next.prop( 'disabled', ( maxVal === val ) );
  918.  
  919.             // Disable "Previous" button if you're on the first node.
  920.             previous.prop( 'disabled', ( minVal === val ) );
  921.         }
  922.     });
  923.  
  924.  
  925.     // The slider view.
  926.     revisions.view.Slider = wp.Backbone.View.extend({
  927.         className: 'wp-slider',
  928.         direction: isRtl ? 'right' : 'left',
  929.  
  930.         events: {
  931.             'mousemove' : 'mouseMove'
  932.         },
  933.  
  934.         initialize: function() {
  935.             _.bindAll( this, 'start', 'slide', 'stop', 'mouseMove', 'mouseEnter', 'mouseLeave' );
  936.             this.listenTo( this.model, 'update:slider', this.applySliderSettings );
  937.         },
  938.  
  939.         ready: function() {
  940.             this.$el.css('width', ( this.model.revisions.length * 50 ) + 'px');
  941.             this.$el.slider( _.extend( this.model.toJSON(), {
  942.                 start: this.start,
  943.                 slide: this.slide,
  944.                 stop:  this.stop
  945.             }) );
  946.  
  947.             this.$el.hoverIntent({
  948.                 over: this.mouseEnter,
  949.                 out: this.mouseLeave,
  950.                 timeout: 800
  951.             });
  952.  
  953.             this.applySliderSettings();
  954.         },
  955.  
  956.         mouseMove: function( e ) {
  957.             var zoneCount         = this.model.revisions.length - 1, // One fewer zone than models
  958.                 sliderFrom        = this.$el.allOffsets()[this.direction], // "From" edge of slider
  959.                 sliderWidth       = this.$el.width(), // Width of slider
  960.                 tickWidth         = sliderWidth / zoneCount, // Calculated width of zone
  961.                 actualX           = ( isRtl ? $(window).width() - e.pageX : e.pageX ) - sliderFrom, // Flipped for RTL - sliderFrom;
  962.                 currentModelIndex = Math.floor( ( actualX  + ( tickWidth / 2 )  ) / tickWidth ); // Calculate the model index
  963.  
  964.             // Ensure sane value for currentModelIndex.
  965.             if ( currentModelIndex < 0 ) {
  966.                 currentModelIndex = 0;
  967.             } else if ( currentModelIndex >= this.model.revisions.length ) {
  968.                 currentModelIndex = this.model.revisions.length - 1;
  969.             }
  970.  
  971.             // Update the tooltip mode
  972.             this.model.set({ hoveredRevision: this.model.revisions.at( currentModelIndex ) });
  973.         },
  974.  
  975.         mouseLeave: function() {
  976.             this.model.set({ hovering: false });
  977.         },
  978.  
  979.         mouseEnter: function() {
  980.             this.model.set({ hovering: true });
  981.         },
  982.  
  983.         applySliderSettings: function() {
  984.             this.$el.slider( _.pick( this.model.toJSON(), 'value', 'values', 'range' ) );
  985.             var handles = this.$('a.ui-slider-handle');
  986.  
  987.             if ( this.model.get('compareTwoMode') ) {
  988.                 // in RTL mode the 'left handle' is the second in the slider, 'right' is first
  989.                 handles.first()
  990.                     .toggleClass( 'to-handle', !! isRtl )
  991.                     .toggleClass( 'from-handle', ! isRtl );
  992.                 handles.last()
  993.                     .toggleClass( 'from-handle', !! isRtl )
  994.                     .toggleClass( 'to-handle', ! isRtl );
  995.             } else {
  996.                 handles.removeClass('from-handle to-handle');
  997.             }
  998.         },
  999.  
  1000.         start: function( event, ui ) {
  1001.             this.model.set({ scrubbing: true });
  1002.  
  1003.             // Track the mouse position to enable smooth dragging,
  1004.             // overrides default jQuery UI step behavior.
  1005.             $( window ).on( 'mousemove.wp.revisions', { view: this }, function( e ) {
  1006.                 var handles,
  1007.                     view              = e.data.view,
  1008.                     leftDragBoundary  = view.$el.offset().left,
  1009.                     sliderOffset      = leftDragBoundary,
  1010.                     sliderRightEdge   = leftDragBoundary + view.$el.width(),
  1011.                     rightDragBoundary = sliderRightEdge,
  1012.                     leftDragReset     = '0',
  1013.                     rightDragReset    = '100%',
  1014.                     handle            = $( ui.handle );
  1015.  
  1016.                 // In two handle mode, ensure handles can't be dragged past each other.
  1017.                 // Adjust left/right boundaries and reset points.
  1018.                 if ( view.model.get('compareTwoMode') ) {
  1019.                     handles = handle.parent().find('.ui-slider-handle');
  1020.                     if ( handle.is( handles.first() ) ) { // We're the left handle
  1021.                         rightDragBoundary = handles.last().offset().left;
  1022.                         rightDragReset    = rightDragBoundary - sliderOffset;
  1023.                     } else { // We're the right handle
  1024.                         leftDragBoundary = handles.first().offset().left + handles.first().width();
  1025.                         leftDragReset    = leftDragBoundary - sliderOffset;
  1026.                     }
  1027.                 }
  1028.  
  1029.                 // Follow mouse movements, as long as handle remains inside slider.
  1030.                 if ( e.pageX < leftDragBoundary ) {
  1031.                     handle.css( 'left', leftDragReset ); // Mouse to left of slider.
  1032.                 } else if ( e.pageX > rightDragBoundary ) {
  1033.                     handle.css( 'left', rightDragReset ); // Mouse to right of slider.
  1034.                 } else {
  1035.                     handle.css( 'left', e.pageX - sliderOffset ); // Mouse in slider.
  1036.                 }
  1037.             } );
  1038.         },
  1039.  
  1040.         getPosition: function( position ) {
  1041.             return isRtl ? this.model.revisions.length - position - 1: position;
  1042.         },
  1043.  
  1044.         // Responds to slide events
  1045.         slide: function( event, ui ) {
  1046.             var attributes, movedRevision;
  1047.             // Compare two revisions mode
  1048.             if ( this.model.get('compareTwoMode') ) {
  1049.                 // Prevent sliders from occupying same spot
  1050.                 if ( ui.values[1] === ui.values[0] ) {
  1051.                     return false;
  1052.                 }
  1053.                 if ( isRtl ) {
  1054.                     ui.values.reverse();
  1055.                 }
  1056.                 attributes = {
  1057.                     from: this.model.revisions.at( this.getPosition( ui.values[0] ) ),
  1058.                     to: this.model.revisions.at( this.getPosition( ui.values[1] ) )
  1059.                 };
  1060.             } else {
  1061.                 attributes = {
  1062.                     to: this.model.revisions.at( this.getPosition( ui.value ) )
  1063.                 };
  1064.                 // If we're at the first revision, unset 'from'.
  1065.                 if ( this.getPosition( ui.value ) > 0 ) {
  1066.                     attributes.from = this.model.revisions.at( this.getPosition( ui.value ) - 1 );
  1067.                 } else {
  1068.                     attributes.from = undefined;
  1069.                 }
  1070.             }
  1071.             movedRevision = this.model.revisions.at( this.getPosition( ui.value ) );
  1072.  
  1073.             // If we are scrubbing, a scrub to a revision is considered a hover
  1074.             if ( this.model.get('scrubbing') ) {
  1075.                 attributes.hoveredRevision = movedRevision;
  1076.             }
  1077.  
  1078.             this.model.set( attributes );
  1079.         },
  1080.  
  1081.         stop: function() {
  1082.             $( window ).off('mousemove.wp.revisions');
  1083.             this.model.updateSliderSettings(); // To snap us back to a tick mark
  1084.             this.model.set({ scrubbing: false });
  1085.         }
  1086.     });
  1087.  
  1088.     // The diff view.
  1089.     // This is the view for the current active diff.
  1090.     revisions.view.Diff = wp.Backbone.View.extend({
  1091.         className: 'revisions-diff',
  1092.         template:  wp.template('revisions-diff'),
  1093.  
  1094.         // Generate the options to be passed to the template.
  1095.         prepare: function() {
  1096.             return _.extend({ fields: this.model.fields.toJSON() }, this.options );
  1097.         }
  1098.     });
  1099.  
  1100.     // The revisions router.
  1101.     // Maintains the URL routes so browser URL matches state.
  1102.     revisions.Router = Backbone.Router.extend({
  1103.         initialize: function( options ) {
  1104.             this.model = options.model;
  1105.  
  1106.             // Maintain state and history when navigating
  1107.             this.listenTo( this.model, 'update:diff', _.debounce( this.updateUrl, 250 ) );
  1108.             this.listenTo( this.model, 'change:compareTwoMode', this.updateUrl );
  1109.         },
  1110.  
  1111.         baseUrl: function( url ) {
  1112.             return this.model.get('baseUrl') + url;
  1113.         },
  1114.  
  1115.         updateUrl: function() {
  1116.             var from = this.model.has('from') ? this.model.get('from').id : 0,
  1117.                 to   = this.model.get('to').id;
  1118.             if ( this.model.get('compareTwoMode' ) ) {
  1119.                 this.navigate( this.baseUrl( '?from=' + from + '&to=' + to ), { replace: true } );
  1120.             } else {
  1121.                 this.navigate( this.baseUrl( '?revision=' + to ), { replace: true } );
  1122.             }
  1123.         },
  1124.  
  1125.         handleRoute: function( a, b ) {
  1126.             var compareTwo = _.isUndefined( b );
  1127.  
  1128.             if ( ! compareTwo ) {
  1129.                 b = this.model.revisions.get( a );
  1130.                 a = this.model.revisions.prev( b );
  1131.                 b = b ? b.id : 0;
  1132.                 a = a ? a.id : 0;
  1133.             }
  1134.         }
  1135.     });
  1136.  
  1137.     /**
  1138.      * Initialize the revisions UI for revision.php.
  1139.      */
  1140.     revisions.init = function() {
  1141.         var state;
  1142.  
  1143.         // Bail if the current page is not revision.php.
  1144.         if ( ! window.adminpage || 'revision-php' !== window.adminpage ) {
  1145.             return;
  1146.         }
  1147.  
  1148.         state = new revisions.model.FrameState({
  1149.             initialDiffState: {
  1150.                 // wp_localize_script doesn't stringifies ints, so cast them.
  1151.                 to: parseInt( revisions.settings.to, 10 ),
  1152.                 from: parseInt( revisions.settings.from, 10 ),
  1153.                 // wp_localize_script does not allow for top-level booleans so do a comparator here.
  1154.                 compareTwoMode: ( revisions.settings.compareTwoMode === '1' )
  1155.             },
  1156.             diffData: revisions.settings.diffData,
  1157.             baseUrl: revisions.settings.baseUrl,
  1158.             postId: parseInt( revisions.settings.postId, 10 )
  1159.         }, {
  1160.             revisions: new revisions.model.Revisions( revisions.settings.revisionData )
  1161.         });
  1162.  
  1163.         revisions.view.frame = new revisions.view.Frame({
  1164.             model: state
  1165.         }).render();
  1166.     };
  1167.  
  1168.     $( revisions.init );
  1169. }(jQuery));
  1170.