WordPress.org

Make WordPress Core

Changeset 24520


Ignore:
Timestamp:
06/26/2013 09:06:50 PM (6 years ago)
Author:
markjaquith
Message:

Cleanup of the revisions screen, both on the PHP API side, and the JS.

  • Much simpler PHP API
  • Cleaner and more Backbone-y JS API
  • Consequently, does batch queries; this now scales up to hundreds of revisions

Currently missing, but much easier considering the cleaned up base:

  • Compare two mode
  • RTL

props koopersmith, nacin, adamsilverstein, ocean90. see #24425

Location:
trunk
Files:
1 added
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/wp-admin/admin-ajax.php

    r24388 r24520  
    4343$core_actions_get = array(
    4444    'fetch-list', 'ajax-tag-search', 'wp-compression-test', 'imgedit-preview', 'oembed-cache',
    45     'autocomplete-user', 'dashboard-widgets', 'logged-in', 'revisions-data'
     45    'autocomplete-user', 'dashboard-widgets', 'logged-in',
    4646);
    4747
     
    5757    'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', 'get-attachment',
    5858    'query-attachments', 'save-attachment', 'save-attachment-compat', 'send-link-to-editor',
    59     'send-attachment-to-editor', 'save-attachment-order', 'heartbeat',
     59    'send-attachment-to-editor', 'save-attachment-order', 'heartbeat', 'get-revision-diffs',
    6060);
    6161
  • trunk/wp-admin/css/wp-admin.css

    r24429 r24520  
    34823482  11.2 - Post Revisions
    34833483------------------------------------------------------------------------------*/
     3484.revisions .spinner {
     3485    float: none;
     3486    margin: 100px auto;
     3487}
     3488
     3489.revisions.loading .spinner {
     3490    display: block;
     3491}
     3492
     3493.revisions-control-frame,
     3494.revisions-diff-frame {
     3495    position: relative;
     3496}
     3497
     3498.revisions-controls {
     3499    height: 60px;
     3500    padding: 40px 0 20px;
     3501    border-bottom: 1px solid #dfdfdf;
     3502    margin-bottom: 10px;
     3503}
     3504
     3505.revisions-meta {
     3506    margin-top: 15px;
     3507}
     3508.revision-toggle-compare-mode {
     3509    position: absolute;
     3510    top: 0;
     3511    right: 0;
     3512}
     3513
     3514.revisions-previous {
     3515    float: left;
     3516}
     3517
     3518.revisions-next {
     3519    float: right;
     3520}
     3521
     3522.wp-slider {
     3523    width: 70%;
     3524    margin: 6px auto 0;
     3525}
    34843526
    34853527/* Revision meta box */
     
    35263568#revisions-diff {
    35273569    position: relative;
    3528 }
    3529 
    3530 #toggle-revision-compare-mode {
    3531     position: absolute;
    3532     top: 0;
    3533     right: 0;
    3534     padding: 9px 9px 0 0;
    35353570}
    35363571
     
    35503585#revision-interact {
    35513586    padding: 20px 0;
    3552 }
    3553 
    3554 #diff-next-revision,
    3555 #diff-previous-revision {
    3556     margin-top: -.4em; /* Same line as the slider (height: .8em) */
    3557 }
    3558 
    3559 #diff-next-revision {
    3560     float: right;
    3561 }
    3562 
    3563 #diff-previous-revision {
    3564     float: left;
    3565 }
    3566 
    3567 #diff-slider {
    3568     width: 70%;
    3569     margin: 0 auto;
    35703587}
    35713588
     
    35893606
    35903607#diff-title-to strong {
    3591     display: none;
     3608    display: inline;
    35923609}
    35933610
     
    36063623    border-radius: 3px;
    36073624    padding: 5px 200px 5px 5px;
     3625    clear: both;
    36083626}
    36093627
  • trunk/wp-admin/includes/ajax-actions.php

    r24406 r24520  
    20832083}
    20842084
    2085 function wp_ajax_revisions_data() {
    2086     check_ajax_referer( 'revisions-ajax-nonce', 'nonce' );
    2087 
    2088     $compare_to = ! empty( $_GET['compare_to'] ) ? absint( $_GET['compare_to'] ) : 0;
    2089     $show_autosaves = ! empty( $_GET['show_autosaves'] );
    2090     $show_split_view = ! empty( $_GET['show_split_view'] );
    2091     $post_id = ! empty( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
    2092     $right_handle_at = ! empty( $_GET['right_handle_at'] ) ? (int) $_GET['right_handle_at'] : 0;
    2093     $left_handle_at = ! empty( $_GET['left_handle_at'] ) ? (int) $_GET['left_handle_at'] : 0;
    2094     $single_revision_id = ! empty( $_GET['single_revision_id'] ) ? absint( $_GET['single_revision_id'] ) : 0;
    2095     $compare_two_mode = (bool) $post_id;
    2096 
    2097     $all_the_revisions = array();
    2098     if ( ! $post_id )
    2099         $post_id = $compare_to;
    2100 
    2101     if ( ! current_user_can( 'read_post', $post_id ) )
    2102         continue;
    2103 
    2104     if ( ! $revisions = wp_get_post_revisions( $post_id ) )
    2105         return;
    2106 
    2107     $left_revision = get_post( $compare_to );
    2108 
    2109     // single model fetch mode
    2110     // return the diff of a single revision comparison
    2111     if ( $single_revision_id ) {
    2112         $right_revision = get_post( $single_revision_id );
    2113 
    2114         if ( ! $compare_to )
    2115             $left_revision = get_post( $post_id );
    2116 
    2117         // make sure the right revision is the most recent, except on oldest revision
    2118         if ( $compare_to && $right_revision->post_date < $left_revision->post_date ) {
    2119             $temp = $left_revision;
    2120             $left_revision = $right_revision;
    2121             $right_revision = $temp;
    2122         }
    2123 
    2124         $lines_added = $lines_deleted = 0;
    2125         $content = '';
    2126         // compare from left to right, passed from application
    2127         foreach ( _wp_post_revision_fields() as $field => $field_value ) {
    2128             $left_content = apply_filters( "_wp_post_revision_field_$field", $left_revision->$field, $field, $left_revision, 'left' );
    2129             $right_content = apply_filters( "_wp_post_revision_field_$field", $right_revision->$field, $field, $right_revision, 'right' );
    2130 
    2131             add_filter( "_wp_post_revision_field_$field", 'htmlspecialchars' );
    2132 
    2133             $args = array();
    2134 
    2135             if ( $show_split_view )
    2136                  $args = array( 'show_split_view' => true );
    2137 
    2138             // compare_to == 0 means first revision, so compare to a blank field to show whats changed
    2139             $diff = wp_text_diff_with_count( ( 0 == $compare_to ) ? '' : $left_content, $right_content, $args );
    2140 
    2141             if ( isset( $diff[ 'html' ] ) ) {
    2142                 $content .= sprintf( '<div class="diff-label">%s</div>', $field_value );
    2143                 $content .= $diff[ 'html' ];
    2144             }
    2145 
    2146             if ( isset( $diff[ 'lines_added' ] ) )
    2147                 $lines_added = $lines_added + $diff[ 'lines_added' ];
    2148 
    2149             if ( isset( $diff[ 'lines_deleted' ] ) )
    2150                 $lines_deleted = $lines_deleted + $diff[ 'lines_deleted' ];
    2151         }
    2152         $content = '' == $content ? __( 'No difference' ) : $content;
    2153 
    2154         $all_the_revisions = array (
    2155             'diff'         => $content,
    2156             'linesDeleted' => $lines_deleted,
    2157             'linesAdded'   => $lines_added
     2085function wp_ajax_get_revision_diffs() {
     2086    require ABSPATH . 'wp-admin/includes/revision.php';
     2087
     2088    // check_ajax_referer( 'revisions-ajax-nonce', 'nonce' );
     2089
     2090    if ( ! $post = get_post( (int) $_REQUEST['post_id'] ) )
     2091        wp_send_json_error();
     2092
     2093    if ( ! current_user_can( 'read_post', $post->ID ) )
     2094        wp_send_json_error();
     2095
     2096    // Really just pre-loading the cache here.
     2097    if ( ! $revisions = wp_get_post_revisions( $post->ID ) )
     2098        wp_send_json_error();
     2099
     2100    $return = array();
     2101    @set_time_limit( count( $_REQUEST['compare'] ) );
     2102
     2103    foreach ( $_REQUEST['compare'] as $compare_key ) {
     2104        list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to
     2105
     2106        $return[] = array(
     2107            'id' => $compare_key,
     2108            'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ),
    21582109        );
    2159 
    2160         echo json_encode( $all_the_revisions );
    2161         exit();
    2162     } // end single model fetch
    2163 
    2164     $count = -1;
    2165 
    2166     // reverse the list to start with oldest revision
    2167     $revisions = array_reverse( $revisions );
    2168 
    2169     $previous_revision_id = 0;
    2170 
    2171     /* translators: revision date format, see http://php.net/date */
    2172     $datef = _x( 'j F, Y @ G:i:s', 'revision date format');
    2173 
    2174     foreach ( $revisions as $revision ) :
    2175         if ( ! $show_autosaves && wp_is_post_autosave( $revision ) )
    2176             continue;
    2177 
    2178         $revision_from_date_author = '';
    2179         $is_current_revision = false;
    2180         $count++;
    2181 
    2182         /**
    2183         * return blank data for diffs to the left of the left handle (for right handel model)
    2184         * or to the right of the right handle (for left handel model)
    2185         * and visa versa in RTL mode
    2186         */
    2187         if( ! is_rtl() ) {
    2188             if ( ( ( 0 != $left_handle_at && $count < $left_handle_at ) ||
    2189                  ( 0 != $right_handle_at && $count > ( $right_handle_at - 2 ) ) ) ) {
    2190                 $all_the_revisions[] = array (
    2191                     'ID' => $revision->ID,
    2192                 );
    2193                 continue;
    2194             }
    2195         } else { // is_rtl
    2196             if ( ( 0 != $left_handle_at && $count > ( $left_handle_at - 1 ) ||
    2197                  ( 0 != $left_handle_at && $count < $right_handle_at ) ) ) {
    2198                 $all_the_revisions[] = array (
    2199                     'ID' => $revision->ID,
    2200                 );
    2201                 continue;
    2202             }
    2203         }
    2204 
    2205         if ( $compare_two_mode ) {
    2206             $compare_to_gravatar = get_avatar( $left_revision->post_author, 24 );
    2207             $compare_to_author = get_the_author_meta( 'display_name', $left_revision->post_author );
    2208             $compare_to_date = date_i18n( $datef, strtotime( $left_revision->post_modified ) );
    2209 
    2210             $revision_from_date_author = sprintf(
    2211                 /* translators: post revision title: 1: author avatar, 2: author name, 3: time ago, 4: date */
    2212                 _x( '%1$s %2$s, %3$s ago (%4$s)', 'post revision title' ),
    2213                 $compare_to_gravatar,
    2214                 $compare_to_author,
    2215                 human_time_diff( strtotime( $left_revision->post_modified ), current_time( 'timestamp' ) ),
    2216                 $compare_to_date
    2217             );
    2218         }
    2219 
    2220         $gravatar = get_avatar( $revision->post_author, 24 );
    2221         $author = get_the_author_meta( 'display_name', $revision->post_author );
    2222         $date = date_i18n( $datef, strtotime( $revision->post_modified ) );
    2223         $revision_date_author = sprintf(
    2224             /* translators: post revision title: 1: author avatar, 2: author name, 3: time ago, 4: date */
    2225             _x( '%1$s %2$s, %3$s ago (%4$s)', 'post revision title' ),
    2226             $gravatar,
    2227             $author,
    2228             human_time_diff( strtotime( $revision->post_modified ), current_time( 'timestamp' ) ),
    2229             $date
    2230         );
    2231 
    2232         /* translators: 1: date */
    2233         $autosavef = _x( '%1$s [Autosave]', 'post revision title extra' );
    2234         /* translators: 1: date */
    2235         $currentf  = _x( '%1$s [Current Revision]', 'post revision title extra' );
    2236 
    2237         if ( ! $post = get_post( $post_id ) )
    2238             continue;
    2239 
    2240         if ( $left_revision->post_modified === $post->post_modified )
    2241             $revision_from_date_author = sprintf( $currentf, $revision_from_date_author );
    2242         elseif ( wp_is_post_autosave( $left_revision ) )
    2243             $revision_from_date_author = sprintf( $autosavef, $revision_from_date_author );
    2244 
    2245         if ( $revision->post_modified === $post->post_modified ) {
    2246             $revision_date_author = sprintf( $currentf, $revision_date_author );
    2247             $is_current_revision = true;
    2248         } elseif ( wp_is_post_autosave( $revision ) ) {
    2249             $revision_date_author = sprintf( $autosavef, $revision_date_author );
    2250         }
    2251 
    2252         /* translators: revision date short format, see http://php.net/date */
    2253         $date_short_format = _x( 'j M @ G:i', 'revision date short format');
    2254         $date_short = date_i18n( $date_short_format, strtotime( $revision->post_modified ) );
    2255 
    2256         $revision_date_author_short = sprintf(
    2257             '%s <strong>%s</strong><br />%s',
    2258             $gravatar,
    2259             $author,
    2260             $date_short
    2261         );
    2262 
    2263         $restore_link = wp_nonce_url(
    2264             add_query_arg(
    2265                 array( 'revision' => $revision->ID,
    2266                     'action' => 'restore' ),
    2267                     admin_url( 'revision.php' )
    2268             ),
    2269             "restore-post_{$revision->ID}"
    2270         );
    2271 
    2272         // if this is a left handled calculation swap data
    2273         if ( 0 != $right_handle_at ) {
    2274             $tmp = $revision_from_date_author;
    2275             $revision_from_date_author = $revision_date_author;
    2276             $revision_date_author = $tmp;
    2277         }
    2278 
    2279         if ( ( $compare_two_mode || -1 !== $previous_revision_id ) ) {
    2280             $all_the_revisions[] = array (
    2281                 'ID'           => $revision->ID,
    2282                 'titleTo'      => $revision_date_author,
    2283                 'titleFrom'    => $revision_from_date_author,
    2284                 'titleTooltip' => $revision_date_author_short,
    2285                 'restoreLink'  => urldecode( $restore_link ),
    2286                 'previousID'   => $previous_revision_id,
    2287                 'isCurrent'    => $is_current_revision,
    2288             );
    2289         }
    2290         $previous_revision_id = $revision->ID;
    2291 
    2292     endforeach;
    2293 
    2294     // in RTL + single handle mode, reverse the revision direction
    2295     if ( is_rtl() && $compare_two_mode )
    2296         $all_the_revisions = array_reverse( $all_the_revisions );
    2297 
    2298     echo json_encode( $all_the_revisions );
    2299     exit();
    2300 }
     2110    }
     2111    wp_send_json_success( $return );
     2112}
  • trunk/wp-admin/js/revisions.js

    r24254 r24520  
    22
    33(function($) {
    4     var Revision, Revisions, Diff, revisions;
    5 
    6     revisions = wp.revisions = function() {
    7         Diff = revisions.Diff = new Diff();
    8     };
    9 
    10     _.extend( revisions, { model: {}, view: {}, controller: {} } );
     4    var revisions;
     5
     6    revisions = wp.revisions = { model: {}, view: {}, controller: {} };
    117
    128    // Link settings.
    13     revisions.model.settings = typeof wpRevisionsSettings === 'undefined' ? {} : wpRevisionsSettings;
     9    revisions.settings = typeof _wpRevisionsSettings === 'undefined' ? {} : _wpRevisionsSettings;
    1410
    1511
    1612    /**
    1713     * ========================================================================
    18      * CONTROLLERS
     14     * MODELS
    1915     * ========================================================================
    2016     */
    21 
    22     /**
    23      * wp.revisions.controller.Diff
    24      *
    25      * Controlls the diff
    26      */
    27     Diff = revisions.controller.Diff = Backbone.Model.extend( {
    28         rightDiff: 1,
    29         leftDiff: 1,
    30         revisions: null,
    31         leftHandleRevisions: null,
    32         rightHandleRevisions: null,
    33         revisionsInteractions: null,
    34         autosaves: true,
    35         showSplitView: true,
    36         singleRevision: true,
    37         leftModelLoading: false,    // keep track of model loads
    38         rightModelLoading: false,   // disallow slider interaction, also repeat loads, while loading
    39         tickmarkView: null, // the slider tickmarks
    40         slider: null, // the slider instance
    41 
    42         constructor: function() {
    43             var self    = this;
    44             this.slider = new revisions.view.Slider();
    45 
    46             if ( null === this.revisions ) {
    47                 this.revisions = new Revisions(); // set up collection
    48                 this.startRightModelLoading();
    49 
    50                 this.revisions.fetch({ // load revision data
    51                     success: function() {
    52                         self.stopRightModelLoading();
    53                         self.completeApplicationSetup();
     17    revisions.model.Slider = Backbone.Model.extend({
     18        defaults: {
     19            value: 0,
     20            min: 0,
     21            max: 1,
     22            step: 1
     23        }
     24    });
     25
     26    revisions.model.Revision = Backbone.Model.extend({});
     27
     28    revisions.model.Revisions = Backbone.Collection.extend({
     29        model: revisions.model.Revision,
     30
     31        comparator: function( revision ) {
     32            return revision.id;
     33        },
     34    });
     35
     36    revisions.model.Field = Backbone.Model.extend({});
     37
     38    revisions.model.Fields = Backbone.Collection.extend({
     39        model: revisions.model.Field
     40    });
     41
     42    revisions.model.Diff = Backbone.Model.extend({
     43        initialize: function(attributes, options) {
     44            var fields = this.get('fields');
     45            this.unset('fields');
     46
     47            this.fields = new revisions.model.Fields( fields );
     48        }
     49    });
     50
     51    revisions.model.Diffs = Backbone.Collection.extend({
     52        initialize: function(models, options) {
     53            this.revisions = options.revisions;
     54            this.requests  = {};
     55        },
     56
     57        model: revisions.model.Diff,
     58
     59        ensure: function( id, context ) {
     60            var diff     = this.get( id );
     61            var request  = this.requests[ id ];
     62            var deferred = $.Deferred();
     63            var ids      = {};
     64
     65            if ( diff ) {
     66                deferred.resolveWith( context, [ diff ] );
     67            } else {
     68                this.trigger( 'ensure:load', ids );
     69                _.each( ids, _.bind( function(id) {
     70                    // Remove anything that has an ongoing request
     71                    if ( this.requests[ id ] )
     72                        delete ids[ id ];
     73                }, this ) );
     74                if ( ! request ) {
     75                    // Always include the ID that started this ensure
     76                    ids[ id ] = true;
     77                    request   = this.load( _.keys( ids ) );
     78                }
     79
     80                request.done( _.bind( function() {
     81                    deferred.resolveWith( context, [ this.get( id ) ] );
     82                }, this ) );
     83            }
     84
     85            return deferred.promise();
     86        },
     87
     88        loadNew: function( comparisons ) {
     89            comparisons = _.object( comparisons, comparisons );
     90            _.each( comparisons, _.bind( function( id ) {
     91                // Exists
     92                if ( this.get( id ) )
     93                    delete comparisons[ id ];
     94            }, this ) );
     95            comparisons = _.toArray( comparisons );
     96            return this.load( comparisons );
     97        },
     98
     99        load: function( comparisons ) {
     100            // Our collection should only ever grow, never shrink, so remove: false
     101            return this.fetch({ data: { compare: comparisons }, remove: false });
     102        },
     103
     104/**/
     105        loadLast: function( num ) {
     106            num     = num || 1;
     107            var ids = this.getProximalDiffIds();
     108            ids     = _.last( ids, num );
     109
     110            if ( ids.length ) {
     111                return this.loadNew( ids );
     112            }
     113        },
     114
     115        loadLastUnloaded: function( num ) {
     116            num     = num || 1;
     117            var ids = this.getUnloadedProximalDiffIds();
     118            ids     = _.last( ids, num );
     119
     120            if ( ids.length ) {
     121                return this.loadNew( ids );
     122            }
     123        },
     124
     125        getProximalDiffIds: function() {
     126            var previous = 0, ids = [];
     127            this.revisions.each( _.bind( function(revision) {
     128                ids.push( previous + ':' + revision.id );
     129                previous = revision.id;
     130            }, this ) );
     131            return ids;
     132        },
     133
     134        getUnloadedProximalDiffIds: function() {
     135            var comparisons = this.getProximalDiffIds();
     136            comparisons     = _.object( comparisons, comparisons );
     137            _.each( comparisons, _.bind( function( id ) {
     138                // Exists
     139                if ( this.get( id ) )
     140                    delete comparisons[ id ];
     141            }, this ) );
     142            return _.toArray( comparisons );
     143        },
     144
     145        loadAllBy: function( chunkSize ) {
     146            chunkSize    = chunkSize || 20;
     147            var unloaded = this.getUnloadedProximalDiffIds();
     148            if ( unloaded.length ) {
     149                return this.loadLastUnloaded( chunkSize ).always( _.bind( function() {
     150                    this.loadAllBy( chunkSize );
     151                }, this ) );
     152            }
     153        },
     154
     155        sync: function( method, model, options ) {
     156            if ( 'read' === method ) {
     157                options         = options || {};
     158                options.context = this;
     159                options.data    = _.extend( options.data || {}, {
     160                    action: 'get-revision-diffs',
     161                    post_id: revisions.settings.postId
     162                });
     163
     164                var deferred = wp.xhr.send( options );
     165                var requests = this.requests;
     166
     167                // Record that we're requesting each diff.
     168                if ( options.data.compare ) {
     169                    _.each( options.data.compare, function( id ) {
     170                        requests[ id ] = deferred;
     171                    });
     172                }
     173
     174                // When the request completes, clear the stored request.
     175                deferred.always( function() {
     176                    if ( options.data.compare ) {
     177                        _.each( options.data.compare, function( id ) {
     178                            delete requests[ id ];
     179                        });
    54180                    }
    55181                });
    56             }
    57         },
    58 
    59         loadDiffs: function( models ) {
    60             var self = this,
    61                 revisionsToLoad = models.where( { completed: false } ),
    62                 delay = 0,
    63                 totalChanges;
    64 
    65             // match slider to passed revision_id
    66             _.each( revisionsToLoad, function( revision ) {
    67                 if ( revision.get( 'ID' ) == revisions.model.settings.revision_id )
    68                     self.rightDiff = self.revisions.indexOf( revision ) + 1;
     182
     183                return deferred;
     184
     185            // Otherwise, fall back to `Backbone.sync()`.
     186            } else {
     187                return Backbone.Model.prototype.sync.apply( this, arguments );
     188            }
     189        }
     190    });
     191
     192
     193    revisions.model.FrameState = Backbone.Model.extend({
     194        initialize: function( attributes, options ) {
     195            this.revisions = options.revisions;
     196            this.diffs     = new revisions.model.Diffs( [], {revisions: this.revisions} );
     197
     198            this.listenTo( this, 'change:from change:to', this.updateDiffId );
     199        },
     200
     201        updateDiffId: function() {
     202            var from = this.get( 'from' );
     203            var to   = this.get( 'to' );
     204            this.set( 'diffId', (from ? from.id : '0') + ':' + to.id );
     205        }
     206    });
     207
     208
     209    /**
     210     * ========================================================================
     211     * VIEWS
     212     * ========================================================================
     213     */
     214
     215    // The frame view. This contains the entire page.
     216    revisions.view.Frame = wp.Backbone.View.extend({
     217        tagName: 'div',
     218        className: 'revisions',
     219        template: wp.template('revisions-frame'),
     220
     221        initialize: function() {
     222            this.model = new revisions.model.FrameState({}, {
     223                revisions: this.collection
    69224            });
    70225
    71             _.each( revisionsToLoad, function( revision ) {
    72                     _.delay( function() {
    73                         revision.fetch( {
    74                             update: true,
    75                             add: false,
    76                             remove: false,
    77                             success: function( model ) {
    78                                 model.set( 'completed', true );
    79 
    80                                 // stop spinner when all models are loaded
    81                                 if ( 0 === models.where( { completed: false } ).length )
    82                                     self.stopModelLoadingSpinner();
    83 
    84                                 totalChanges = model.get( 'linesAdded' ) + model.get( 'linesDeleted' ),
    85                                     scopeOfChanges = 'vsmall';
    86 
    87                                 // Note: hard coded scope of changes
    88                                 // TODO change to dynamic based on range of values
    89                                 if ( totalChanges > 1 && totalChanges <= 3 ) {
    90                                     scopeOfChanges = 'small';
    91                                 } else if ( totalChanges > 3 && totalChanges <= 5 ) {
    92                                     scopeOfChanges = 'med';
    93                                 } else if ( totalChanges > 5 && totalChanges <= 10 ) {
    94                                     scopeOfChanges = 'large';
    95                                 } else if ( totalChanges > 10 ) {
    96                                     scopeOfChanges = 'vlarge';
    97                                 }
    98                                 model.set( 'scopeOfChanges', scopeOfChanges );
    99                                 if ( 0 !== self.rightDiff &&
    100                                     model.get( 'ID' ) === self.revisions.at( self.rightDiff - 1 ).get( 'ID' ) ) {
    101                                     // reload if current model refreshed
    102                                     self.revisionView.render();
    103                                 }
    104                                 self.tickmarkView.render();
    105                             }
    106                     } );
    107                     }, delay ) ;
    108                     delay = delay + 150; // stagger model loads to avoid hammering server with requests
    109                 }
    110             );
    111         },
    112 
    113         startLeftModelLoading: function() {
    114             this.leftModelLoading = true;
    115             $('#revision-diff-container').addClass('left-model-loading');
    116         },
    117 
    118         stopLeftModelLoading: function() {
    119             this.leftModelLoading = false;
    120         },
    121 
    122         startRightModelLoading: function() {
    123             this.rightModelLoading = true;
    124             $('#revision-diff-container').addClass('right-model-loading');
    125         },
    126 
    127         stopRightModelLoading: function() {
    128             this.rightModelLoading = false;
    129         },
    130 
    131         stopModelLoadingSpinner: function() {
    132             $('#revision-diff-container').removeClass('right-model-loading');
    133             $('#revision-diff-container').removeClass('left-model-loading');
    134         },
    135 
    136         reloadModel: function() {
    137             if ( this.singleRevision ) {
    138                 this.reloadModelSingle();
     226            this.listenTo( this.model, 'change:diffId', this.updateDiff );
     227
     228            this.views.set( '.revisions-control-frame', new revisions.view.Controls({
     229                model: this.model
     230            }) );
     231
     232            if ( this.model.revisions.length ) {
     233                var last = this.model.revisions.last(2);
     234                var attributes = { to: last.pop() };
     235
     236                if ( last.length )
     237                    attributes.from = last.pop();
     238
     239                this.model.set( attributes );
     240
     241                // Load the rest: first 10, then the rest by 50
     242                this.model.diffs.loadLastUnloaded( 10 ).always( _.bind( function() {
     243                    this.model.diffs.loadAllBy( 50 );
     244                }, this ) );
     245            }
     246        },
     247
     248        render: function() {
     249            wp.Backbone.View.prototype.render.apply( this, arguments );
     250
     251            $('#wpbody-content .wrap').append( this.el );
     252            this.views.ready();
     253
     254            return this;
     255        },
     256
     257        updateDiff: function() {
     258            this.model.diffs.ensure( this.model.get('diffId'), this ).done( function( diff ) {
     259                if ( this.model.get('diffId') !== diff.id )
     260                    return;
     261                this.views.set( '.revisions-diff-frame', new revisions.view.Diff({
     262                    model: diff
     263                }));
     264            });
     265        }
     266    });
     267
     268    // The control view.
     269    // This contains the revision slider, previous/next buttons, and the compare checkbox.
     270    revisions.view.Controls = wp.Backbone.View.extend({
     271        tagName: 'div',
     272        className: 'revisions-controls',
     273
     274        initialize: function() {
     275            // Add the button view
     276            this.views.add( new revisions.view.Buttons({
     277                model: this.model
     278            }));
     279
     280            // Add the Slider view
     281            this.views.add( new revisions.view.Slider({
     282                model: this.model
     283            }) );
     284
     285            // Add the Meta view
     286            this.views.add( new revisions.view.Meta({
     287                model: this.model
     288            }) );
     289        }
     290    });
     291
     292    // The meta view.
     293    // This contains the revision meta, and the restore button.
     294    revisions.view.Meta = wp.Backbone.View.extend({
     295        tagName: 'div',
     296        className: 'revisions-meta',
     297        template: wp.template('revisions-meta'),
     298
     299        initialize: function() {
     300            this.listenTo( this.model, 'change:diffId', this.updateMeta );
     301        },
     302
     303        events: {
     304            'click #restore-revision': 'restoreRevision'
     305        },
     306
     307        restoreRevision: function() {
     308            var restoreUrl    = this.model.get('to').attributes.restoreUrl.replace(/&amp;/g, '&');
     309            document.location = restoreUrl;
     310        },
     311
     312        updateMeta: function() {
     313            this.$el.html( this.template( this.model.toJSON() ) );
     314            if( this.model.get( 'to' ).attributes.current ) {
     315                $( '#restore-revision' ).prop( 'disabled', true);
    139316            } else {
    140                 this.reloadLeftRight();
    141             }
    142         },
    143 
    144         // load the models for the single handle mode
    145         reloadModelSingle: function() {
    146             var self = this;
    147 
    148             self.startRightModelLoading();
    149 
    150             self.revisions.reload({
    151                 options: {
    152                 'showAutosaves': self.autosaves,
    153                 'showSplitView': self.showSplitView
    154                 },
    155 
    156                 success: function() {
    157                     var revisionCount = self.revisions.length;
    158                     self.revisionView.model = self.revisions;
    159                     self.revisionView.render();
    160                     self.loadDiffs( self.revisions );
    161                     self.tickmarkView.model = self.revisions;
    162                     self.tickmarkView.render();
    163                     self.slider.refresh({
    164                         'max': revisionCount - 1, // slider starts at 0 in single handle mode
    165                         'value': self.rightDiff - 1 // slider starts at 0 in single handle mode
    166                     }, true);
    167                 },
    168 
    169                 error: function() {
    170                     self.stopRightModelLoading();
    171                 }
    172             });
    173         },
    174 
    175         // load the models for the left handle (the right handler has moved)
    176         reloadLeft: function() {
    177             var self = this;
    178             self.startLeftModelLoading();
    179             self.leftHandleRevisions = new Revisions( {}, {
    180                 'compareTo': self.revisions.at( self.rightDiff - 1 ).get( 'ID' ), // diff and model count off by 1
    181                 'showAutosaves': self.autosaves,
    182                 'showSplitView': self.showSplitView,
    183                 'rightHandleAt': self.rightDiff
    184             });
    185 
    186             self.leftHandleRevisions.fetch({
    187                 success: function(){
    188                     self.stopLeftModelLoading();
    189                     self.loadDiffs( self.leftHandleRevisions );
    190                     self.tickmarkView.model = self.leftHandleRevisions;
    191                     self.slider.refresh({
    192                         'max': self.revisions.length
    193                     });
    194                     // ensure right handle not beyond length
    195                     if ( self.rightDiff > self.revisions.length )
    196                         self.rightDiff = self.revisions.length;
    197                     },
    198 
    199                 error: function() {
    200                     self.stopLeftModelLoading();
    201                 }
    202             });
    203         },
    204 
    205         // load the models for the right handle (the left handle has moved)
    206         reloadRight: function() {
    207             var self = this;
    208             self.startRightModelLoading();
    209             self.rightHandleRevisions = new Revisions( {}, {
    210                 'compareTo': self.revisions.at( self.leftDiff - 1 ).get( 'ID' ), // diff and model count off by 1
    211                 'showAutosaves': self.autosaves,
    212                 'showSplitView': self.showSplitView,
    213                 'leftHandleAt': self.leftDiff
    214             });
    215 
    216             self.rightHandleRevisions.fetch({
    217                 success: function(){
    218                     self.stopRightModelLoading();
    219                     self.loadDiffs( self.rightHandleRevisions );
    220                     self.tickmarkView.model = self.rightHandleRevisions;
    221                     self.slider.refresh({
    222                         'max': self.revisions.length
    223                     }, true);
    224                 },
    225 
    226                 error: function( response ) {
    227                     self.stopRightModelLoading();
    228                 }
    229             });
    230 
    231         },
    232 
    233         /**
    234          * reloadLeftRight reload models for both the left and right handles
    235          */
    236         reloadLeftRight: function() {
    237             this.startRightModelLoading();
    238             this.startLeftModelLoading();
    239             this.reloadLeft();
    240             this.reloadRight();
    241         },
    242 
    243         disabledButtonCheck: function( val ) {
    244             var maxVal = this.revisions.length - 1,
    245                 next = ! isRtl ? $( '#next' ) : $( '#previous' ),
    246                 prev = ! isRtl ? $( '#previous' ) : $( '#next' );
     317                $( '#restore-revision' ).prop( 'disabled', false)
     318            }
     319        }
     320    });
     321
     322
     323    // The buttons view.
     324    // Encapsulates all of the configuration for the previous/next buttons, and the compare checkbox.
     325    revisions.view.Buttons = wp.Backbone.View.extend({
     326        tagName: 'div',
     327        className: 'revisions-buttons',
     328        template: wp.template('revisions-controls'),
     329
     330        initialize: function() {
     331            this.$el.html( this.template() )
     332        },
     333
     334        events: {
     335            'click #next': 'nextRevision',
     336            'click #previous': 'previousRevision'
     337        },
     338       
     339        gotoModel: function( toIndex ) {
     340            var attributes = {
     341                to: this.model.revisions.at( isRtl ? this.model.revisions.length - toIndex - 1 : toIndex ) // Reverse directions for Rtl
     342            };
     343            // If we're at the first revision, unset 'from'.
     344            if ( isRtl ? this.model.revisions.length - toIndex - 1 : toIndex ) // Reverse directions for Rtl
     345                attributes.from = this.model.revisions.at( isRtl ? this.model.revisions.length - toIndex - 2 : toIndex - 1 );
     346            else
     347                this.model.unset('from', { silent: true });
     348
     349            this.model.set( attributes );
     350        },
     351
     352        nextRevision: function() {
     353            var toIndex = this.model.revisions.indexOf( this.model.get( 'to' ) );
     354            toIndex     = isRtl ? toIndex - 1 : toIndex + 1;
     355            this.gotoModel( toIndex );
     356        },
     357       
     358        previousRevision: function() {
     359            var toIndex = this.model.revisions.indexOf( this.model.get('to') );
     360            toIndex     = isRtl ? toIndex + 1 : toIndex - 1;
     361            this.gotoModel( toIndex );
     362        },
     363
     364        ready: function() {
     365            this.listenTo( this.model, 'change:diffId', this.disabledButtonCheck );
     366        },
     367
     368        // Check to see if the Previous or Next buttons need to be disabled or enabled
     369        disabledButtonCheck: function() {
     370            var maxVal   = isRtl ? 0 : this.model.revisions.length - 1,
     371                minVal   = isRtl ? this.model.revisions.length - 1 : 0,
     372                next     = $( '.revisions-next .button' ),
     373                previous = $( '.revisions-previous .button' ),
     374                val      = this.model.revisions.indexOf( this.model.get( 'to' ) );
    247375
    248376            // Disable "Next" button if you're on the last node
     
    252380                next.prop( 'disabled', false );
    253381
    254             // Disable "Previous" button if you're on the 0 node
    255             if ( 0 === val )
    256                 prev.prop( 'disabled', true );
     382            // Disable "Previous" button if you're on the first node
     383            if ( minVal === val )
     384                previous.prop( 'disabled', true );
    257385            else
    258                 prev.prop( 'disabled', false );
    259         },
    260 
    261         /**
    262          * completeApplicationSetup finishes loading all views once the initial model load is complete
    263          */
    264         completeApplicationSetup: function() {
    265             this.revisionView = new revisions.view.Diff({
    266                 model: this.revisions
     386                previous.prop( 'disabled', false );
     387        },
     388
     389
     390    });
     391
     392    // The slider view.
     393    // Encapsulates all of the configuration for the jQuery UI slider into a view.
     394    revisions.view.Slider = wp.Backbone.View.extend({
     395        tagName: 'div',
     396        className: 'wp-slider',
     397
     398        initialize: function() {
     399            _.bindAll( this, 'start', 'slide', 'stop' );
     400
     401            // Create the slider model from the provided collection data.
     402            // TODO: This should actually pull from the model's `to` key.
     403            var latestRevisionIndex = this.model.revisions.length - 1;
     404
     405            // Find the initially selected revision
     406            var initiallySelectedRevisionIndex =
     407                this.model.revisions.indexOf(
     408                    this.model.revisions.findWhere(  { id: Number( revisions.settings.selectedRevision ) } ) );
     409
     410            this.settings = new revisions.model.Slider({
     411                max:   latestRevisionIndex,
     412                value: initiallySelectedRevisionIndex,
     413                start: this.start,
     414                slide: this.slide,
     415                stop:  this.stop
    267416            });
    268             this.revisionView.render(); // render the revision view
    269 
    270             this.loadDiffs( this.revisions ); // get the actual revisions data
    271 
    272             this.revisionsInteractions = new revisions.view.Interact({
    273                 model: this.revisions
    274             });
    275             this.revisionsInteractions.render(); // render the interaction view
    276 
    277             this.tickmarkView = new revisions.view.Tickmarks({
    278                 model: this.revisions
    279             });
    280             this.tickmarkView.render(); // render the tickmark view
    281         }
    282     });
    283 
    284 
    285     /**
    286      * ========================================================================
    287      * VIEWS
    288      * ========================================================================
    289      */
    290 
    291     /**
    292      * wp.revisions.view.Slider
    293      *
    294      * The slider
    295      */
    296     revisions.view.Slider = Backbone.View.extend({
    297         el: $( '#diff-slider' ),
    298         singleRevision: true,
    299 
    300         initialize: function( options ) {
    301             this.options = _.defaults( options || {}, {
    302                 value: 0,
    303                 min: 0,
    304                 max: 1,
    305                 step: 1
    306             });
    307         },
    308 
    309         /**
    310          * respond to slider slide events
    311          * Note: in one handle mode, jQuery UI reports leftmost position as 0
    312          * in two handle mode, jQuery UI Slider reports leftmost position as 1
    313          */
     417        },
     418
     419        ready: function() {
     420            this.$el.slider( this.settings.toJSON() );
     421            this.settings.on( 'change', function( model, options ) {
     422                // Apply changes to slider settings here.
     423                this.$el.slider( { value: this.model.revisions.indexOf( this.model.get( 'to' ) ) } ); // Set handle to current to model
     424            }, this );
     425            // Reset to the initially selected revision
     426            this.slide( '', this.settings.attributes );
     427
     428            // Listen for changes in the diffId
     429            this.listenTo( this.model, 'change:diffId', this.diffIdChanged );
     430
     431        },
     432
     433        diffIdChanged: function() {
     434            // Reset the view settings when diffId is changed
     435            this.settings.set( { 'value': this.model.revisions.indexOf( this.model.get( 'to' ) ) } );
     436        },
     437
     438        start: function( event, ui ) {
     439            // Track the mouse position to enable smooth dragging, overrides default jquery ui step behaviour
     440            $( window ).mousemove( function( e ) {
     441                var sliderLeft  = $( '.wp-slider' ).offset().left,
     442                    sliderRight = sliderLeft + $( '.wp-slider' ).width();
     443
     444                // Follow mouse movements, as long as handle remains inside slider
     445                if ( e.clientX < sliderLeft ) {
     446                    $( ui.handle ).css( 'left', 0 ); // Mouse to left of slider
     447                } else if ( e.clientX > sliderRight ) {
     448                    $( ui.handle ).css( 'left', sliderRight - sliderLeft); // Mouse to right of slider
     449                } else {
     450                    $( ui.handle ).css( 'left', e.clientX - sliderLeft ); // Mouse in slider
     451                }
     452            } ); // End mousemove
     453        },
     454
    314455        slide: function( event, ui ) {
    315             if ( this.singleRevision ) {
    316                 Diff.rightDiff = ( ui.value + 1 );
    317                 Diff.revisionView.render();
    318                 Diff.disabledButtonCheck( ui.value );
    319             } else {
    320                 if ( ui.values[0] === ui.values[1] ) // prevent compare to self
    321                     return false;
    322 
    323                 if ( $( ui.handle ).hasClass( 'left-handle' ) ) {
    324                     // Left handler
    325                     if ( Diff.leftModelLoading ) // left model still loading, prevent sliding left handle
    326                         return false;
    327 
    328                     Diff.leftDiff = isRtl ? ui.values[1] : ui.values[0]; // handles are reversed in RTL mode
    329                 } else {
    330                     // Right handler
    331                     if ( Diff.rightModelLoading ) // right model still loading, prevent sliding right handle
    332                         return false;
    333 
    334                     Diff.rightDiff = isRtl ? ui.values[0] : ui.values[1]; // handles are reversed in RTL mode
    335                 }
    336 
    337                 Diff.revisionView.render();
    338             }
    339         },
    340 
    341         /**
    342          * responds to slider start sliding events
    343          * in two handle mode stores start position, so if unchanged at stop event no need to reload diffs
    344          * also swaps in the appropriate models - left handled or right handled
    345          */
    346         start: function( event, ui ) {
    347             // Not needed in one mode
    348             if ( this.singleRevision )
    349                 return;
    350 
    351             if ( $( ui.handle ).hasClass( 'left-handle' ) ) {
    352                 // Left handler
    353                 if ( Diff.leftModelLoading ) // left model still loading, prevent sliding left handle
    354                     return false;
    355 
    356                 Diff.revisionView.draggingLeft = true;
    357 
    358                 if ( Diff.revisionView.model !== Diff.leftHandleRevisions &&
    359                         null !== Diff.leftHandleRevisions ) {
    360                     Diff.revisionView.model = Diff.leftHandleRevisions; // use the left handle models
    361                     Diff.tickmarkView.model = Diff.leftHandleRevisions;
    362                     Diff.tickmarkView.render();
    363                 }
    364 
    365                 Diff.leftDiffStart = isRtl ? ui.values[1] : ui.values[0]; // in RTL mode the 'left handle' is the second in the slider, 'right' is first
    366 
    367             } else {
    368                 // Right handler
    369                 if ( Diff.rightModelLoading || 0 === Diff.rightHandleRevisions.length) // right model still loading, prevent sliding right handle
    370                     return false;
    371 
    372                 if ( Diff.revisionView.model !== Diff.rightHandleRevisions &&
    373                         null !== Diff.rightHandleRevisions ) {
    374                     Diff.revisionView.model = Diff.rightHandleRevisions; // use the right handle models
    375                     Diff.tickmarkView.model = Diff.rightHandleRevisions;
    376                     Diff.tickmarkView.render();
    377                 }
    378 
    379                 Diff.revisionView.draggingLeft = false;
    380                 Diff.rightDiffStart = isRtl ? ui.values[0] : ui.values[1]; // in RTL mode the 'left handle' is the second in the slider, 'right' is first
    381             }
    382         },
    383 
    384         /**
    385          * responds to slider stop events
    386          * in two handled mode, if the handle that stopped has moved, reload the diffs for the other handle
    387          * the other handle compares to this handle's position, so if it changes they need to be recalculated
    388          */
     456            var attributes = {
     457                to: this.model.revisions.at( isRtl ? this.model.revisions.length - ui.value - 1 : ui.value ) // Reverse directions for Rtl
     458            };
     459
     460            // If we're at the first revision, unset 'from'.
     461            if ( isRtl ? this.model.revisions.length - ui.value - 1 : ui.value ) // Reverse directions for Rtl
     462                attributes.from = this.model.revisions.at( isRtl ? this.model.revisions.length - ui.value - 2 : ui.value - 1 );
     463            else
     464                this.model.unset('from', { silent: true });
     465
     466            this.model.set( attributes );
     467        },
     468
    389469        stop: function( event, ui ) {
    390             // Not needed in one mode
    391             if ( this.singleRevision )
    392                 return;
    393 
    394             // calculate and generate a diff for comparing to the left handle
    395             // and the right handle, swap out when dragging
    396             if ( $( ui.handle ).hasClass( 'left-handle' ) ) {
    397                 // Left handler
    398                 if ( Diff.leftDiffStart !== isRtl ? ui.values[1] : ui.values[0] ) // in RTL mode the 'left handle' is the second in the slider, 'right' is first
    399                     Diff.reloadRight();
    400             } else {
    401                 // Right handler
    402                 if ( Diff.rightDiffStart !== isRtl ? ui.values[0] : ui.values[1] ) // in RTL mode the 'left handle' is the second in the slider, 'right' is first
    403                     Diff.reloadLeft();
    404             }
    405         },
    406 
    407         addTooltip: function( handle, message ) {
    408             handle.find( '.ui-slider-tooltip' ).html( message );
    409         },
    410 
    411         width: function() {
    412             return $( '#diff-slider' ).width();
    413         },
    414 
    415         setWidth: function( width ) {
    416             $( '#diff-slider' ).width( width );
    417         },
    418 
    419         refresh: function( options, slide ) {
    420             $( '#diff-slider' ).slider( 'option', options );
    421 
    422             // Triggers the slide event
    423             if ( slide )
    424                 $( '#diff-slider' ).trigger( 'slide' );
    425 
    426             Diff.disabledButtonCheck( options.value );
    427         },
    428 
    429         option: function( key ) {
    430             return $( '#diff-slider' ).slider( 'option', key );
    431         },
    432 
    433         render: function() {
    434             var self = this;
    435             // this.$el doesn't work, why?
    436             $( '#diff-slider' ).slider( {
    437                 slide: $.proxy( self.slide, self ),
    438                 start: $.proxy( self.start, self ),
    439                 stop:  $.proxy( self.stop, self )
    440             } );
    441 
    442             // Set options
    443             this.refresh( this.options );
    444         }
    445     });
    446 
    447     /**
    448      * wp.revisions.view.Tickmarks
    449      *
    450      * The slider tickmarks.
    451      */
    452     revisions.view.Tickmarks = Backbone.View.extend({
    453         el: $('#diff-slider-ticks'),
    454         template: wp.template('revision-ticks'),
    455         model: Revision,
    456 
    457         resetTicks: function() {
    458             var sliderMax, sliderWidth, adjustMax, tickWidth, tickCount = 0, aTickWidth, tickMargin, self = this, firstTick, lastTick;
    459             sliderMax   = Diff.slider.option( 'max' );
    460             sliderWidth = Diff.slider.width();
    461             adjustMax   = Diff.singleRevision ? 0 : 1;
    462             tickWidth   = Math.floor( sliderWidth / ( sliderMax - adjustMax ) );
    463             tickWidth   = ( tickWidth > 50 ) ? 50 : tickWidth; // set minimum and maximum widths for tick marks
    464             tickWidth   = ( tickWidth < 6 ) ? 6 : tickWidth;
    465             sliderWidth = tickWidth * ( sliderMax - adjustMax ); // calculate the slider width
    466             aTickWidth  = $( '.revision-tick' ).width();
    467 
    468             if ( tickWidth !== aTickWidth ) { // is the width already set correctly?
    469                 $( '.revision-tick' ).each( function() {
    470                     tickMargin = Math.floor( ( tickWidth - $( this ).width() ) / 2 ) + 1;
    471                     $( this ).css( 'border-left', tickMargin + 'px solid #f7f7f7'); // space the ticks out using margins
    472                     $( this ).css( 'border-right', ( tickWidth - tickMargin - $( this ).width() ) + 'px solid #f7f7f7'); // space the ticks out using margins
    473                 });
    474                 firstTick = $( '.revision-tick' ).first(); //cache selectors for optimization
    475                 lastTick = $( '.revision-tick' ).last();
    476 
    477                 sliderWidth = sliderWidth + Math.ceil( ( tickWidth - ( lastTick.outerWidth() - lastTick.innerWidth() ) ) / 2 ); // room for the last tick
    478                 sliderWidth = sliderWidth + Math.ceil( ( tickWidth - ( firstTick.outerWidth() - firstTick.innerWidth() ) ) / 2 ); // room for the first tick
    479                 firstTick.css( 'border-left', 'none' ); // first tick gets no left border
    480                 lastTick.css( 'border-right', 'none' ); // last tick gets no right border
    481             }
    482 
    483             /**
    484              * reset the slider width
    485              */
    486             Diff.slider.setWidth( sliderWidth );
    487             $( '.diff-slider-ticks-wrapper' ).width( sliderWidth );
    488             $( '#diff-slider-ticks' ).width( sliderWidth );
    489 
    490             /**
    491              * go through all ticks, add hover and click interactions
    492              */
    493             $( '.revision-tick' ).each( function() {
    494                 Diff.slider.addTooltip ( $( this ), Diff.revisions.at( tickCount++ ).get( 'titleTooltip' ) );
    495                 $( this ).hover(
    496                     function() {
    497                         $( this ).find( '.ui-slider-tooltip' ).show().append('<div class="arrow"></div>');
    498                     },
    499                     function() {
    500                         $( this ).find( '.ui-slider-tooltip' ).hide().find( '.arrow' ).remove();
    501                     }
    502                 );
    503 
    504                 /**
    505                  * move the slider handle when the tick marks are clicked
    506                  */
    507                 $( this ).on( 'click',
    508                     { tickCount: tickCount }, // pass the tick through so we know where to move the handle
    509                     function( event ) {
    510                         if ( Diff.slider.singleRevision ) { // single handle mode
    511                             Diff.rightDiff = event.data.tickCount; // reposition the right handle
    512                             Diff.slider.refresh({
    513                                 value: Diff.rightDiff - 1
    514                             } );
    515                         } else { //compare two mode
    516                             if ( isRtl ) {
    517                                 if ( event.data.tickCount < Diff.leftDiff ) { // click was on the 'left' side
    518                                         Diff.rightDiff = event.data.tickCount; // set the 'right' handle location
    519                                         Diff.reloadLeft(); // reload the left handle comparison models
    520                                 } else { // middle or 'right' clicks
    521                                     Diff.leftDiff = event.data.tickCount; // set the 'left' handle location
    522                                     Diff.reloadRight(); // reload right handle models
    523                                 }
    524                             } else {
    525                                 if ( event.data.tickCount < Diff.leftDiff ) { // click was on the 'left' side
    526                                         Diff.leftDiff = event.data.tickCount; // set the left handle location
    527                                         Diff.reloadRight(); // reload the right handle comparison models
    528                                 } else { // middle or 'right' clicks
    529                                     Diff.rightDiff = event.data.tickCount; // set the right handle location
    530                                     Diff.reloadLeft(); // reload left handle models
    531                                 }
    532                             }
    533                             Diff.slider.refresh( { // set the slider handle positions
    534                                 values: [ isRtl ? Diff.rightDiff : Diff.leftDiff, isRtl ? Diff.leftDiff : Diff.rightDiff ]
    535                             } );
    536                         }
    537                         Diff.revisionView.render(); // render the main view
    538                     } );
    539             } );
    540         },
    541 
    542         // render the tick mark view
    543         render: function() {
    544             var self = this, addHtml;
    545 
    546             if ( null !== self.model ) {
    547                 addHtml = "";
    548                 _.each ( self.model.models, function( theModel ) {
    549                     addHtml = addHtml + self.template ( theModel.toJSON() );
    550                 });
    551                 self.$el.html( addHtml );
    552 
    553             }
    554             self.resetTicks();
    555             return self;
    556         }
    557     } );
    558 
    559     /**
    560      * wp.revisions.view.Interact
    561      *
    562      * Next/Prev buttons and the slider
    563      */
    564     revisions.view.Interact = Backbone.View.extend({
    565         el: $( '#revision-interact' ),
    566         template: wp.template( 'revision-interact' ),
    567 
    568         // next and previous buttons, only available in compare one mode
    569         events: {
    570             'click #next':     ! isRtl ? 'nextRevision' : 'previousRevision',
    571             'click #previous': ! isRtl ? 'previousRevision' : 'nextRevision'
    572         },
    573 
    574         render: function() {
    575             var modelcount;
    576             this.$el.html( this.template );
    577 
    578             modelcount = Diff.revisions.length;
    579 
    580             Diff.slider.singleRevision = Diff.singleRevision;
    581             Diff.slider.render();
    582 
    583             if ( Diff.singleRevision ) {
    584                 Diff.slider.refresh({
    585                     value: Diff.rightDiff - 1, // rightDiff value is off model index by 1
    586                     min: 0,
    587                     max: modelcount - 1
    588                 });
    589 
    590                 $( '#revision-diff-container' ).removeClass( 'comparing-two-revisions' );
    591 
    592             } else {
    593                 Diff.slider.refresh({
    594                     // in RTL mode the 'left handle' is the second in the slider, 'right' is first
    595                     values: [ isRtl ? Diff.rightDiff : Diff.leftDiff, isRtl ? Diff.leftDiff : Diff.rightDiff ],
    596                     min: 1,
    597                     max: modelcount + 1,
    598                     range: true
    599                 });
    600 
    601                 $( '#revision-diff-container' ).addClass( 'comparing-two-revisions' );
    602                 // in RTL mode the 'left handle' is the second in the slider, 'right' is first
    603                 $( '#diff-slider a.ui-slider-handle' ).first().addClass( isRtl ? 'right-handle' : 'left-handle' );
    604                 $( '#diff-slider a.ui-slider-handle' ).last().addClass( isRtl ? 'left-handle' : 'right-handle' );
    605 
    606             }
    607 
    608             return this;
    609         },
    610 
    611         // go to the next revision
    612         nextRevision: function() {
    613             if ( Diff.rightDiff < this.model.length ) // unless at right boundry
    614                 Diff.rightDiff = Diff.rightDiff + 1 ;
    615 
    616             Diff.revisionView.render();
    617 
    618             Diff.slider.refresh({
    619                 value: Diff.rightDiff - 1
    620             }, true );
    621         },
    622 
    623         // go to the previous revision
    624         previousRevision: function() {
    625             if ( Diff.rightDiff > 1 ) // unless at left boundry
    626                 Diff.rightDiff = Diff.rightDiff - 1 ;
    627 
    628             Diff.revisionView.render();
    629 
    630             Diff.slider.refresh({
    631                 value: Diff.rightDiff - 1
    632             }, true );
    633         }
    634     });
    635 
    636     /**
    637      * wp.revisions.view.Diff
    638      *
    639      * Diff, compare two checkbox and restore button
    640      */
    641     revisions.view.Diff = Backbone.View.extend({
    642         el: $( '#revisions-diff' ),
    643         template: wp.template( 'revisions-diff' ),
    644         draggingLeft: false,
    645 
    646         // the compare two button is in this view, add the interaction here
    647         events: {
    648             'click #compare-two-revisions': 'compareTwo',
    649             'click #restore-revision':      'restore'
    650         },
    651 
    652         // render the revisions
    653         render: function() {
    654             var addHtml = '', thediff;
    655 
    656             // compare two revisions mode?
    657             if ( ! Diff.singleRevision ) {
    658                 if ( this.draggingLeft ) {
    659                     thediff = Diff.leftDiff - 1; //leftDiff value is off model index by 1
    660                     if ( this.model.at( thediff ) ) {
    661                         addHtml = this.template( this.model.at( thediff ).toJSON() );
    662                     }
    663                 } else { // dragging right handle
    664                     thediff = Diff.rightDiff - 1; // rightDiff value is off model index by 1
    665                     if ( this.model.at( thediff ) ) {
    666                         addHtml = this.template( this.model.at( thediff ).toJSON() );
    667                     }
    668                 }
    669             } else { // end compare two revisions mode, eg only one slider handle
    670                 if ( this.model.at( Diff.rightDiff - 1 ) ) { // rightDiff value is off model index by 1
    671                     addHtml = this.template( this.model.at( Diff.rightDiff - 1 ).toJSON() );
    672                 }
    673             }
    674             this.$el.html( addHtml );
    675 
    676             if ( this.model.length < 2 ) {
    677                 $( '#diff-slider' ).hide(); // don't allow compare two if fewer than three revisions
    678                 $( '.diff-slider-ticks-wrapper' ).hide();
    679             }
    680 
    681             this.toggleCompareTwoCheckbox();
    682 
    683             // hide the restore button when on the last sport/current post data
    684             $( '#restore-revision' ).toggle( ! Diff.revisions.at( Diff.rightDiff - 1 ).get( 'isCurrent' ) );
    685 
    686             return this;
    687         },
    688 
    689         toggleCompareTwoCheckbox: function() {
    690             // don't allow compare two if fewer than three revisions
    691             if ( this.model.length < 3 )
    692                 $( '#toggle-revision-compare-mode' ).hide();
    693 
    694             $( '#compare-two-revisions' ).prop( 'checked', ! Diff.singleRevision );
    695         },
    696 
    697         // turn on/off the compare two mode
    698         compareTwo: function() {
    699             if ( $( '#compare-two-revisions' ).is( ':checked' ) ) { // compare 2 mode
    700                 Diff.singleRevision = false ;
    701 
    702                 // in RTL mode handles are swapped, so boundary checks are different;
    703                 if ( isRtl ){
    704                     Diff.leftDiff = Diff.revisions.length; // put the left handle at the rightmost position, representing current revision
    705 
    706                     if ( Diff.revisions.length === Diff.rightDiff ) // make sure 'left' handle not in rightmost slot
    707                         Diff.rightDiff = Diff.rightDiff - 1;
    708                 } else {
    709                     if ( 1 === Diff.rightDiff ) // make sure right handle not in leftmost slot
    710                         Diff.rightDiff = 2;
    711                 }
    712 
    713                 Diff.revisionView.draggingLeft = false;
    714 
    715                 revisions.model.settings.revision_id = ''; // reset passed revision id so switching back to one handle mode doesn't re-select revision
    716                 Diff.reloadLeftRight(); // load diffs for left and right handles
    717                 Diff.revisionView.model = Diff.rightHandleRevisions;
    718 
    719             } else { // compare one mode
    720                 Diff.singleRevision = true;
    721                 Diff.revisionView.draggingLeft = false;
    722                 Diff.reloadModelSingle();
    723             }
    724             Diff.revisionsInteractions.render();
    725             Diff.tickmarkView.render();
    726         },
    727 
    728         restore: function() {
    729             document.location = $( '#restore-revision' ).data( 'restoreLink' );
    730         }
    731     });
    732 
    733 
    734     /**
    735      * ========================================================================
    736      * MODELS
    737      * ========================================================================
    738      */
    739 
    740     /**
    741      * wp.revisions.Revision
    742      */
    743     Revision = revisions.model.Revision = Backbone.Model.extend({
    744         idAttribute: 'ID',
    745 
    746         defaults: {
    747             ID: 0,
    748             titleTo: '',
    749             titleTooltip: '',
    750             titleFrom: '',
    751             diff: '<div class="diff-loading"><div class="spinner"></div></div>',
    752             restoreLink: '',
    753             completed: false,
    754             linesAdded: 0,
    755             linesDeleted: 0,
    756             scopeOfChanges: 'none',
    757             previousID: 0,
    758             isCurrent: false
    759         },
    760 
    761         url: function() {
    762             if ( Diff.singleRevision ) {
    763                 return ajaxurl +
    764                     '?action=revisions-data' +
    765                     '&show_autosaves=true' +
    766                     '&show_split_view=true' +
    767                     '&nonce=' + revisions.model.settings.nonce +
    768                     '&single_revision_id=' + this.id +
    769                     '&compare_to=' + this.get( 'previousID' ) +
    770                     '&post_id=' + revisions.model.settings.post_id;
    771             } else {
    772                 return this.collection.url() + '&single_revision_id=' + this.id;
    773             }
    774 
    775         }
    776     });
    777 
    778     /**
    779      * wp.revisions.Revisions
    780      */
    781     Revisions = revisions.Revisions = Backbone.Collection.extend({
    782         model: Revision,
    783 
    784         initialize: function( models, options ) {
    785             this.options = _.defaults( options || {}, {
    786                 'compareTo': revisions.model.settings.post_id,
    787                 'post_id': revisions.model.settings.post_id,
    788                 'showAutosaves': true,
    789                 'showSplitView': true,
    790                 'rightHandleAt': 0,
    791                 'leftHandleAt': 0,
    792                 'nonce': revisions.model.settings.nonce
    793             });
    794         },
    795 
    796         url: function() {
    797             return ajaxurl +
    798                 '?action=revisions-data' +
    799                 '&compare_to=' + this.options.compareTo + // revision are we comparing to
    800                 '&post_id=' + this.options.post_id + // the post id
    801                 '&show_autosaves=' + this.options.showAutosaves + // show or hide autosaves
    802                 '&show_split_view=' + this.options.showSplitView + // show in split view or single column view
    803                 '&right_handle_at=' + this.options.rightHandleAt + // mark point for comparison list
    804                 '&left_handle_at=' + this.options.leftHandleAt + // mark point for comparison list
    805                 '&nonce=' + this.options.nonce;
    806         },
    807 
    808         reload: function( options ) {
    809             this.options = _.defaults( options.options || {}, this.options );
    810 
    811             this.fetch({
    812                 success: options.success || null,
    813                 error: options.error || null
    814             });
    815         }
    816 
    817     } );
    818 
    819     $( wp.revisions );
    820 
     470            $( window ).unbind( 'mousemove' ); // Stop tracking the mouse
     471            // Reset settings pops handle back to the step position
     472            this.settings.trigger( 'change' );
     473        }
     474    });
     475
     476    // The diff view.
     477    // This is the view for the current active diff.
     478    revisions.view.Diff = wp.Backbone.View.extend({
     479        tagName: 'div',
     480        className: 'revisions-diff',
     481        template: wp.template('revisions-diff'),
     482
     483        // Generate the options to be passed to the template.
     484        prepare: function() {
     485            return _.extend({ fields: this.model.fields.toJSON() }, this.options );
     486        }
     487    });
     488
     489    // Initialize the revisions UI.
     490    revisions.init = function() {
     491        revisions.view.frame = new revisions.view.Frame({
     492            collection: new revisions.model.Revisions( revisions.settings.revisionData )
     493        }).render();
     494    };
     495
     496    $( revisions.init );
    821497}(jQuery));
  • trunk/wp-admin/revision.php

    r24425 r24520  
    99/** WordPress Administration Bootstrap */
    1010require_once('./admin.php');
     11
     12require ABSPATH . 'wp-admin/includes/revision.php';
     13
     14// wp_get_revision_ui_diff( $post, $compare_from, $compare_to )
     15// wp_prepare_revisions_for_js( $post )
     16
    1117wp_reset_vars( array( 'revision', 'action' ) );
    1218
     
    2127    if ( ! current_user_can( 'edit_post', $revision->post_parent ) )
    2228        break;
    23 
    2429
    2530    if ( ! $post = get_post( $revision->post_parent ) )
     
    7883
    7984wp_enqueue_script( 'revisions' );
    80 
    81 
    82 $settings = array(
    83     'post_id'     => $post->ID,
    84     'nonce'       => wp_create_nonce( 'revisions-ajax-nonce' ),
    85     'revision_id' => $revision_id
    86 );
    87 
    88 wp_localize_script( 'revisions', 'wpRevisionsSettings', $settings );
     85wp_localize_script( 'revisions', '_wpRevisionsSettings', wp_prepare_revisions_for_js( $post, $revision_id ) );
    8986
    9087/* Revisions Help Tab */
     
    115112<div class="wrap">
    116113    <?php screen_icon(); ?>
    117     <div id="revision-diff-container" class="current-version right-model-loading">
    118         <h2 class="long-header"><?php echo $h2; ?></h2>
    119 
    120         <div id="loading-status" class="updated message">
    121             <p><span class="spinner" ></span></p>
    122         </div>
    123 
    124         <div class="diff-slider-ticks-wrapper">
    125             <div id="diff-slider-ticks"></div>
    126         </div>
    127 
    128         <div id="revision-interact"></div>
    129 
    130         <div id="revisions-diff"></div>
    131     </div>
     114    <h2 class="long-header"><?php echo $h2; ?></h2>
    132115</div>
    133116
     117<script id="tmpl-revisions-frame" type="text/html">
     118    <span class="spinner"></span>
     119    <div class="revisions-control-frame"></div>
     120    <div class="revisions-diff-frame"></div>
     121</script>
     122
     123<script id="tmpl-revisions-controls" type="text/html">
     124
     125    <div class="revision-toggle-compare-mode">
     126        <label>
     127            <input type="checkbox" class="compare-two-revisions" />
     128            <?php esc_attr_e( 'Compare two revisions' ); ?>
     129        </label>
     130    </div>
     131
     132    <div class="revisions-previous">
     133        <input class="button" type="button" id="previous" value="<?php echo esc_attr_x( 'Previous', 'Button label for a previous revision' ); ?>" />
     134    </div>
     135
     136    <div class="revisions-next">
     137        <input class="button" type="button" id="next" value="<?php echo esc_attr_x( 'Next', 'Button label for a next revision' ); ?>" />
     138    </div>
     139</script>
     140
     141
     142<script id="tmpl-revisions-meta" type="text/html">
     143    <div id="diff-header">
     144        <div id="diff-header-from" class="diff-header">
     145            <div id="diff-title-from" class="diff-title">
     146                <strong>
     147                <?php _ex( 'From:', 'Followed by post revision info' ); ?></strong>
     148                    <# if ( 'undefined' !== typeof data.from ) { #>
     149                        {{{ data.from.attributes.author.avatar }}} {{{ data.from.attributes.author.name }}},
     150                        {{{ data.from.attributes.timeAgo }}} <?php _e( 'ago' ); ?>
     151                        ({{{ data.from.attributes.dateShort }}})
     152                    <# } #>
     153
     154            </div>
     155            <div class="clear"></div>
     156        </div>
     157
     158        <div id="diff-header-to" class="diff-header">
     159            <div id="diff-title-to" class="diff-title">
     160                <strong><?php _ex( 'To:', 'Followed by post revision info' ); ?></strong>
     161                    <# if ( 'undefined' !== typeof data.to ) { #>
     162                        {{{ data.to.attributes.author.avatar }}} {{{ data.to.attributes.author.name }}},
     163                        {{{ data.to.attributes.timeAgo }}} <?php _e( 'ago' ); ?>
     164                        ({{{ data.to.attributes.dateShort }}})
     165                    <# } #>
     166        </div>
     167
     168            <input type="button" id="restore-revision" class="button button-primary" data-restore-link="{{{ data.restoreLink }}}" value="<?php esc_attr_e( 'Restore This Revision' )?>" />
     169        </div>
     170    </div>
     171</script>
     172
    134173<script id="tmpl-revisions-diff" type="text/html">
     174    <# _.each( data.fields, function( field ) { #>
     175        <h3>{{{ field.name }}}</h3>
     176        {{{ field.diff }}}
     177    <# }); #>
     178</script>
     179
     180<script id="tmpl-revisions-diff-old" type="text/html">
    135181    <div id="toggle-revision-compare-mode">
    136182        <label>
     
    158204    </div>
    159205
    160     </div>
    161 
    162206    <div id="diff-table">{{{ data.diff }}}</div>
    163207</script>
    164208
    165 <script id="tmpl-revision-interact" type="text/html">
     209<script id="tmpl-revision-interact-old" type="text/html">
    166210    <div id="diff-previous-revision">
    167211        <input class="button" type="button" id="previous" value="<?php echo esc_attr_x( 'Previous', 'Button label for a previous revision' ); ?>" />
     
    172216    </div>
    173217
    174     <div id="diff-slider" class="wp-slider"></div>
    175218</script>
    176219
  • trunk/wp-includes/revision.php

    r24414 r24520  
    598598    return true;
    599599}
    600 
    601 /**
    602  * Displays a human readable HTML representation of the difference between two strings.
    603  * similar to wp_text_diff, but tracks and returns could of lines added and removed
    604  *
    605  * @since 3.6.0
    606  *
    607  * @see wp_parse_args() Used to change defaults to user defined settings.
    608  * @uses Text_Diff
    609  * @uses WP_Text_Diff_Renderer_Table
    610  *
    611  * @param string $left_string "old" (left) version of string
    612  * @param string $right_string "new" (right) version of string
    613  * @param string|array $args Optional. Change 'title', 'title_left', and 'title_right' defaults.
    614  * @return array contains html, linesadded & linesdeletd, empty string if strings are equivalent.
    615  */
    616 function wp_text_diff_with_count( $left_string, $right_string, $args = null ) {
    617     $defaults = array( 'title' => '', 'title_left' => '', 'title_right' => '' );
    618     $args = wp_parse_args( $args, $defaults );
    619 
    620     if ( ! class_exists( 'WP_Text_Diff_Renderer_Table' ) )
    621             require( ABSPATH . WPINC . '/wp-diff.php' );
    622 
    623     $left_string  = normalize_whitespace( $left_string );
    624     $right_string = normalize_whitespace( $right_string );
    625 
    626     $left_lines  = explode( "\n", $left_string );
    627     $right_lines = explode( "\n", $right_string) ;
    628 
    629     $text_diff = new Text_Diff($left_lines, $right_lines  );
    630     $lines_added = $text_diff->countAddedLines();
    631     $lines_deleted = $text_diff->countDeletedLines();
    632 
    633     $renderer  = new WP_Text_Diff_Renderer_Table();
    634     $diff = $renderer->render( $text_diff );
    635 
    636     if ( !$diff )
    637             return '';
    638 
    639         $r  = "<table class='diff'>\n";
    640 
    641     if ( ! empty( $args[ 'show_split_view' ] ) ) {
    642         $r .= "<col class='content diffsplit left' /><col class='content diffsplit middle' /><col class='content diffsplit right' />";
    643     } else {
    644         $r .= "<col class='content' />";
    645     }
    646 
    647     if ( $args['title'] || $args['title_left'] || $args['title_right'] )
    648         $r .= "<thead>";
    649     if ( $args['title'] )
    650         $r .= "<tr class='diff-title'><th colspan='4'>$args[title]</th></tr>\n";
    651     if ( $args['title_left'] || $args['title_right'] ) {
    652         $r .= "<tr class='diff-sub-title'>\n";
    653         $r .= "\t<td></td><th>$args[title_left]</th>\n";
    654         $r .= "\t<td></td><th>$args[title_right]</th>\n";
    655         $r .= "</tr>\n";
    656     }
    657     if ( $args['title'] || $args['title_left'] || $args['title_right'] )
    658         $r .= "</thead>\n";
    659 
    660     $r .= "<tbody>\n$diff\n</tbody>\n";
    661     $r .= "</table>";
    662 
    663     return array( 'html' => $r, 'lines_added' => $lines_added, 'lines_deleted' => $lines_deleted );
    664 }
Note: See TracChangeset for help on using the changeset viewer.