WordPress.org

Make WordPress Core

Ticket #24425: 24425.draft.5.diff

File 24425.draft.5.diff, 55.2 KB (added by markjaquith, 9 years ago)

Some chunked loading.

  • wp-admin/admin-ajax.php

    do_action( 'admin_init' ); 
    4242
    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
    4848$core_actions_post = array(
    $core_actions_post = array( 
    5656        'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post',
    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
    6262// Register core Ajax calls.
  • wp-admin/css/wp-admin.css

    td.plugin-title p { 
    34813481/*------------------------------------------------------------------------------
    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: 20px;
     3500        padding: 40px 0 20px;
     3501        border-bottom: 1px solid #dfdfdf;
     3502        margin-bottom: 10px;
     3503}
     3504
     3505.revision-toggle-compare-mode {
     3506        position: absolute;
     3507        top: 0;
     3508        right: 0;
     3509}
     3510
     3511.revisions-previous {
     3512        float: left;
     3513}
     3514
     3515.revisions-next {
     3516        float: right;
     3517}
     3518
     3519.revisions-slider {
     3520        width: 70%;
     3521        margin: 6px auto 0;
     3522}
    34843523
    34853524/* Revision meta box */
    34863525.post-revisions li img,
    table.diff .diff-addedline ins { 
    35273566        position: relative;
    35283567}
    35293568
    3530 #toggle-revision-compare-mode {
    3531         position: absolute;
    3532         top: 0;
    3533         right: 0;
    3534         padding: 9px 9px 0 0;
    3535 }
    3536 
    35373569#loading-status {
    35383570        display: none;
    35393571        position: absolute;
    table.diff .diff-addedline ins { 
    35513583        padding: 20px 0;
    35523584}
    35533585
    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;
    3570 }
    3571 
    35723586.comparetwo #diff-slider {
    35733587        width: 95%;
    35743588}
  • wp-admin/includes/ajax-actions.php

    function wp_ajax_heartbeat() { 
    20822082        wp_send_json($response);
    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 );
     2085function wp_ajax_get_revision_diffs() {
     2086        require ABSPATH . 'wp-admin/includes/revision.php';
    21402087
    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
    2158                 );
     2088        // check_ajax_referer( 'revisions-ajax-nonce', 'nonce' );
    21592089
    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;
     2090        if ( ! $post = get_post( (int) $_REQUEST['post_id'] ) )
     2091                wp_send_json_error();
    22392092
    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 );
     2093        if ( ! current_user_can( 'read_post', $post->ID ) )
     2094                wp_send_json_error();
    22442095
    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                 }
     2096        // Really just pre-loading the cache here.
     2097        if ( ! $revisions = wp_get_post_revisions( $post->ID ) )
     2098                wp_send_json_error();
    22512099
    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 ) );
     2100        $return = array();
     2101        @set_time_limit( count( $_REQUEST['compare'] ) );
    22552102
    2256                 $revision_date_author_short = sprintf(
    2257                         '%s <strong>%s</strong><br />%s',
    2258                         $gravatar,
    2259                         $author,
    2260                         $date_short
    2261                 );
     2103        foreach ( $_REQUEST['compare'] as $compare_key ) {
     2104                list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to
    22622105
    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}"
     2106                $return[] = array(
     2107                        'id' => $compare_key,
     2108                        'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ),
    22702109                );
     2110        }
    22712111
    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();
     2112        wp_send_json_success( $return );
    23002113}
  • wp-admin/js/revisions.js

    new file mode 100644
    ---wp-admin/includes/revision.php	(revision 0)n+++wp-admin/includes/revision.php	(revision 0)
    @@ -0,0 +1,92 @@
    +<?php
    +
    +function wp_get_revision_ui_diff( $post, $compare_from, $compare_to ) {
    +	if ( ! $post = get_post( $post ) )
    +		return false;
    +
    +	if ( $compare_from ) {
    +		if ( ! $compare_from = get_post( $compare_from ) )
    +			return false;
    +	} else {
    +		// If we're dealing with the first revision...
    +		$compare_from = false;
    +	}
    +
    +	if ( ! $compare_to = get_post( $compare_to ) )
    +		return false;
    +
    +	// If comparing revisions, make sure we're dealing with the right post parent.
    +	if ( $compare_from && $compare_from->post_parent !== $post->ID )
    +		return false;
    +	if ( $compare_to->post_parent !== $post->ID )
    +		return false;
    +
    +	if ( $compare_from && strtotime( $compare_from->post_date_gmt ) > strtotime( $compare_to->post_date_gmt ) ) {
    +		$temp = $compare_from;
    +		$compare_from = $compare_to;
    +		$compare_to = $temp;
    +	}
    +
    +	$return = array();
    +
    +	foreach ( _wp_post_revision_fields() as $field => $name ) {
    +		$content_from = $compare_from ? apply_filters( "_wp_post_revision_field_$field", $compare_from->$field, $field, $compare_from, 'left' ) : '';
    +		$content_to = apply_filters( "_wp_post_revision_field_$field", $compare_to->$field, $field, $compare_to, 'right' );
    +
    +		$diff = wp_text_diff( $content_from, $content_to, array( 'show_split_view' => true ) );
    +
    +		if ( ! $diff && 'post_title' === $field ) {
    +			// It's a better user experience to still show the Title, even if it didn't change.
    +			// No, you didn't see this.
    +			$diff = "<table class='diff'><col class='ltype' /><col class='content' /><col class='ltype' /><col class='content' /><tbody><tr>";
    +			$diff .= '<td>' . esc_html( $compare_from->post_title ) . '</td><td></td><td>' . esc_html( $compare_to->post_title ) . '</td>';
    +			$diff .= '</tr></tbody>';
    +			$diff .= '</table>';
    +		}
    +
    +		if ( $diff ) {
    +			$return[] = array(
    +				'id' => $field,
    +				'name' => $name,
    +				'diff' => $diff,
    +			);
    +		}
    +	}
    +	return $return;
    +}
    +
    +function wp_prepare_revisions_for_js( $post ) {
    +	$post = get_post( $post );
    +	$revisions = array();
    +	$current = current_time( 'timestamp' );
    +
    +	$revisions = wp_get_post_revisions( $post->ID );
    +
    +	cache_users( wp_list_pluck( $revisions, 'post_author' ) );
    +
    +	foreach ( $revisions as $revision ) {
    +		$modified_gmt = strtotime( $revision->post_modified_gmt );
    +		$revisions[ $revision->ID ] = array(
    +			'id'           => $revision->ID,
    +			'title'        => get_the_title( $post->ID ),
    +			'author' => array(
    +				'id'     => (int) $revision->post_author,
    +				'avatar' => get_avatar( $revision->post_author, 24 ),
    +				'name'   => get_the_author_meta( 'display_name', $revision->post_author ),
    +			),
    +			'date'         => date_i18n( __( 'M j, Y @ G:i' ), $modified_gmt ),
    +			'dateShort'    => date_i18n( _x( 'j M @ G:i', 'revision date short format' ), $modified_gmt ),
    +			'timeAgo'      => human_time_diff( $modified_gmt, $current ),
    +			'autosave'     => wp_is_post_autosave( $revision ),
    +			'current'      => $revision->post_modified_gmt === $post->post_modified_gmt,
    +			'restoreNonce' => wp_create_nonce( 'restore-post_' . $revision->ID ),
    +		);
    +	}
    +
    +	return array(
    +		'postId'       => $post->ID,
    +		'nonce'        => wp_create_nonce( 'revisions-ajax-nonce' ),
    +		'restoreUrl'   => admin_url( 'revision.php?action=restore' ), // &revision=$ID&_wpnonce=$restoreNonce
    +		'revisionData' => array_values( $revisions ),
    +	);
    +}
     
    11window.wp = window.wp || {};
    22
    33(function($) {
    4         var Revision, Revisions, Diff, revisions;
     4        var revisions;
    55
    6         revisions = wp.revisions = function() {
    7                 Diff = revisions.Diff = new Diff();
    8         };
    9 
    10         _.extend( revisions, { model: {}, view: {}, controller: {} } );
     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         */
     17        revisions.model.Slider = Backbone.Model.extend({
     18                defaults: {
     19                        value: 0,
     20                        min: 0,
     21                        max: 1,
     22                        step: 1
     23                }
     24        });
    2125
    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();
    54                                         }
    55                                 });
    56                         }
    57                 },
    58 
    59                 loadDiffs: function( models ) {
    60                         var self = this,
    61                                 revisionsToLoad = models.where( { completed: false } ),
    62                                 delay = 0,
    63                                 totalChanges;
     26        revisions.model.Revision = Backbone.Model.extend({});
    6427
    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;
    69                         });
     28        revisions.model.Revisions = Backbone.Collection.extend({
     29                model: revisions.model.Revision,
    7030
    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                         );
     31                comparator: function( revision ) {
     32                        return revision.id;
    11133                },
    11234
    113                 startLeftModelLoading: function() {
    114                         this.leftModelLoading = true;
    115                         $('#revision-diff-container').addClass('left-model-loading');
     35                previousId: function( id ) {
     36                        var prev = this.at( this.indexOf( this.get( id ) ) - 1 );
     37                        return prev ? prev.id : false;
    11638                },
    11739
    118                 stopLeftModelLoading: function() {
    119                         this.leftModelLoading = false;
    120                 },
     40                nextId: function( id ) {
     41                        var next = this.at( this.indexOf( this.get( id ) ) + 1 );
     42                        return next ? next.id : false;
     43                }
     44        });
    12145
    122                 startRightModelLoading: function() {
    123                         this.rightModelLoading = true;
    124                         $('#revision-diff-container').addClass('right-model-loading');
    125                 },
     46        revisions.model.Field = Backbone.Model.extend({});
    12647
    127                 stopRightModelLoading: function() {
    128                         this.rightModelLoading = false;
    129                 },
     48        revisions.model.Fields = Backbone.Collection.extend({
     49                model: revisions.model.Field
     50        });
    13051
    131                 stopModelLoadingSpinner: function() {
    132                         $('#revision-diff-container').removeClass('right-model-loading');
    133                         $('#revision-diff-container').removeClass('left-model-loading');
    134                 },
     52        revisions.model.Diff = Backbone.Model.extend({
     53                initialize: function(attributes, options) {
     54                        var fields = this.get('fields');
     55                        this.unset('fields');
    13556
    136                 reloadModel: function() {
    137                         if ( this.singleRevision ) {
    138                                 this.reloadModelSingle();
    139                         } else {
    140                                 this.reloadLeftRight();
    141                         }
    142                 },
     57                        this.fields = new revisions.model.Fields( fields );
     58                }
     59        });
    14360
    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                         });
     61        revisions.model.Diffs = Backbone.Collection.extend({
     62                initialize: function(models, options) {
     63                        this.revisions = options.revisions;
     64                        this.requests = {};
    17365                },
    17466
    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                         });
     67                model: revisions.model.Diff,
    18568
    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                                         },
     69                ensure: function( id, context ) {
     70                        var diff = this.get( id );
     71                        var request = this.requests[ id ];
     72                        var deferred = $.Deferred();
     73                        var ids = {};
    19874
    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();
     75                        if ( diff ) {
     76                                deferred.resolveWith( context, [ diff ] );
     77                        } else {
     78                                this.trigger( 'ensure:load', ids );
     79                                _.each( ids, _.bind( function(id) {
     80                                        // Remove anything that has an ongoing request
     81                                        if ( this.requests[ id ] )
     82                                                delete ids[ id ];
     83                                }, this ) );
     84                                if ( ! request ) {
     85                                        // Always include the ID that started this ensure
     86                                        ids[ id ] = true;
     87                                        request = this.load( _.keys( ids ) );
    22888                                }
    229                         });
    23089
    231                 },
     90                                request.done( _.bind( function() {
     91                                        deferred.resolveWith( context, [ this.get( id ) ] );
     92                                }, this ) );
     93                        }
    23294
    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();
     95                        return deferred.promise();
    24196                },
    24297
    243                 disabledButtonCheck: function( val ) {
    244                         var maxVal = this.revisions.length - 1,
    245                                 next = ! isRtl ? $( '#next' ) : $( '#previous' ),
    246                                 prev = ! isRtl ? $( '#previous' ) : $( '#next' );
    247 
    248                         // Disable "Next" button if you're on the last node
    249                         if ( maxVal === val )
    250                                 next.prop( 'disabled', true );
    251                         else
    252                                 next.prop( 'disabled', false );
    253 
    254                         // Disable "Previous" button if you're on the 0 node
    255                         if ( 0 === val )
    256                                 prev.prop( 'disabled', true );
    257                         else
    258                                 prev.prop( 'disabled', false );
     98                loadNew: function( comparisons ) {
     99                        comparisons = _.object( comparisons, comparisons );
     100                        _.each( comparisons, _.bind( function( id ) {
     101                                // Exists
     102                                if ( this.get( id ) )
     103                                        delete comparisons[ id ];
     104                        }, this ) );
     105                        comparisons = _.toArray( comparisons );
     106                        console.log( 'Loading', comparisons );
     107                        return this.load( comparisons );
    259108                },
    260109
    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
    267                         });
    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                         });
     110                load: function( comparisons ) {
     111                        // Our collection should only ever grow, never shrink, so remove: false
     112                        return this.fetch({ data: { compare: comparisons }, remove: false });
    307113                },
    308114
    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                  */
    314                 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();
     115/**/
     116                loadLast: function( num ) {
     117                        num = num || 1;
     118                        var ids = this.getProximalDiffIds();
     119                        ids = _.last( ids, num );
     120                        if ( ids.length ) {
     121                                console.log( 'Last ' + num, ids );
     122                                return this.loadNew( ids );
    338123                        }
    339124                },
    340125
    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
     126                loadLastUnloaded: function( num ) {
     127                        num = num || 1;
     128                        var ids = this.getUnloadedProximalDiffIds();
     129                        ids = _.last( ids, num );
     130                        if ( ids.length ) {
     131                                console.log( 'Loading last ' + num );
     132                                return this.loadNew( ids );
    381133                        }
    382134                },
    383135
    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                  */
    389                 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();
     136                getProximalDiffIds: function() {
     137                        var previous = 0, ids = [];
     138                        this.revisions.each( _.bind( function(revision) {
     139                                ids.push( previous + ':' + revision.id );
     140                                previous = revision.id;
     141                        }, this ) );
     142                        return ids;
     143                },
     144
     145                getUnloadedProximalDiffIds: function() {
     146                        var comparisons = this.getProximalDiffIds();
     147                        comparisons = _.object( comparisons, comparisons );
     148                        _.each( comparisons, _.bind( function( id ) {
     149                                // Exists
     150                                if ( this.get( id ) )
     151                                        delete comparisons[ id ];
     152                        }, this ) );
     153                        return _.toArray( comparisons );
     154                },
     155
     156                loadAllBy: function( chunkSize ) {
     157                        chunkSize = chunkSize || 20;
     158                        var unloaded = this.getUnloadedProximalDiffIds();
     159                        if ( unloaded.length ) {
     160                                return this.loadLastUnloaded( chunkSize ).always( _.bind( function() {
     161                                        this.loadAllBy( chunkSize );
     162                                }, this ) );
    404163                        }
    405164                },
    406165
    407                 addTooltip: function( handle, message ) {
    408                         handle.find( '.ui-slider-tooltip' ).html( message );
    409                 },
    410 
    411                 width: function() {
    412                         return $( '#diff-slider' ).width();
    413                 },
     166/**/
    414167
    415                 setWidth: function( width ) {
    416                         $( '#diff-slider' ).width( width );
    417                 },
     168                sync: function( method, model, options ) {
     169                        if ( 'read' === method ) {
     170                                options = options || {};
     171                                options.context = this;
     172                                options.data = _.extend( options.data || {}, {
     173                                        action: 'get-revision-diffs',
     174                                        post_id: revisions.settings.postId
     175                                });
    418176
    419                 refresh: function( options, slide ) {
    420                         $( '#diff-slider' ).slider( 'option', options );
     177                                var deferred = wp.xhr.send( options );
     178                                var requests = this.requests;
    421179
    422                         // Triggers the slide event
    423                         if ( slide )
    424                                 $( '#diff-slider' ).trigger( 'slide' );
     180                                // Record that we're requesting each diff.
     181                                if ( options.data.compare ) {
     182                                        _.each( options.data.compare, function( id ) {
     183                                                requests[ id ] = deferred;
     184                                        });
     185                                }
    425186
    426                         Diff.disabledButtonCheck( options.value );
    427                 },
     187                                // When the request completes, clear the stored request.
     188                                deferred.always( function() {
     189                                        if ( options.data.compare ) {
     190                                                _.each( options.data.compare, function( id ) {
     191                                                        delete requests[ id ];
     192                                                });
     193                                        }
     194                                });
    428195
    429                 option: function( key ) {
    430                         return $( '#diff-slider' ).slider( 'option', key );
    431                 },
     196                                return deferred;
    432197
    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 );
     198                        // Otherwise, fall back to `Backbone.sync()`.
     199                        } else {
     200                                return Backbone.Model.prototype.sync.apply( this, arguments );
     201                        }
    444202                }
    445203        });
    446204
    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();
    476205
    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                         }
     206        revisions.model.FrameState = Backbone.Model.extend({
     207                initialize: function( attributes, options ) {
     208                        this.revisions = options.revisions;
     209                        this.diffs = new revisions.model.Diffs( [], {revisions: this.revisions} );
    482210
    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                         } );
     211                        this.listenTo( this, 'change:from change:to', this.updateDiffId );
    540212                },
    541213
    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;
     214                updateDiffId: function() {
     215                        var from = this.get('from');
     216                        var to = this.get('to');
     217                        this.set( 'diffId', (from ? from.id : '0') + ':' + to.id );
    556218                }
    557         } );
     219        });
     220
    558221
    559222        /**
    560          * wp.revisions.view.Interact
    561          *
    562          * Next/Prev buttons and the slider
     223         * ========================================================================
     224         * VIEWS
     225         * ========================================================================
    563226         */
    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                 },
    573227
    574                 render: function() {
    575                         var modelcount;
    576                         this.$el.html( this.template );
     228        // The frame view. This contains the entire page.
     229        revisions.view.Frame = wp.Backbone.View.extend({
     230                tagName: 'div',
     231                className: 'revisions',
     232                template: wp.template('revisions-frame'),
    577233
    578                         modelcount = Diff.revisions.length;
     234                initialize: function() {
     235                        this.model = new revisions.model.FrameState({}, {
     236                                revisions: this.collection
     237                        });
    579238
    580                         Diff.slider.singleRevision = Diff.singleRevision;
    581                         Diff.slider.render();
     239                        this.listenTo( this.model, 'change:diffId', this.updateDiff );
    582240
    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                                 });
     241                        this.views.set( '.revisions-control-frame', new revisions.view.Controls({
     242                                model: this.model
     243                        }) );
    589244
    590                                 $( '#revision-diff-container' ).removeClass( 'comparing-two-revisions' );
     245                        if ( this.model.revisions.length ) {
     246                                var last = this.model.revisions.last(2);
     247                                var attributes = { to: last.pop() };
    591248
    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                                 });
     249                                if ( last.length )
     250                                        attributes.from = last.pop();
    600251
    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' );
     252                                this.model.set( attributes );
    605253
     254                                // Load the rest: first 10, then the rest by 50
     255                                this.model.diffs.loadLastUnloaded( 10 ).always( _.bind( function() {
     256                                        console.log( 'Loading all by 50' );
     257                                        this.model.diffs.loadAllBy( 50 );
     258                                }, this ) );
    606259                        }
    607 
    608                         return this;
    609260                },
    610261
    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 ;
     262                render: function() {
     263                        wp.Backbone.View.prototype.render.apply( this, arguments );
    615264
    616                         Diff.revisionView.render();
     265                        $('#wpbody-content .wrap').append( this.el );
     266                        this.views.ready();
    617267
    618                         Diff.slider.refresh({
    619                                 value: Diff.rightDiff - 1
    620                         }, true );
     268                        return this;
    621269                },
    622270
    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 );
     271                updateDiff: function() {
     272                        this.model.diffs.ensure( this.model.get('diffId'), this ).done( function( diff ) {
     273                                if ( this.model.get('diffId') !== diff.id )
     274                                        return;
     275                                this.views.set( '.revisions-diff-frame', new revisions.view.Diff({
     276                                        model: diff
     277                                }) );
     278                        });
    633279                }
    634280        });
    635281
    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' ) );
     282        // The control view.
     283        // This contains the revision slider, previous/next buttons, and the compare checkbox.
     284        revisions.view.Controls = wp.Backbone.View.extend({
     285                tagName: 'div',
     286                className: 'revisions-controls',
     287                template: wp.template('revisions-controls'),
     288
     289                initialize: function() {
     290                        this.views.set( new revisions.view.Slider({
     291                                model: this.model
     292                        }) );
     293                }
     294        });
    685295
    686                         return this;
     296        // The slider view.
     297        // Encapsulates all of the configuration for the jQuery UI slider into a view.
     298        revisions.view.Slider = wp.Backbone.View.extend({
     299                tagName: 'div',
     300                className: 'wp-slider',
     301
     302                initialize: function() {
     303                        _.bindAll( this, 'start', 'slide', 'stop' );
     304
     305                        // Create the slider model from the provided collection data.
     306                        // TODO: This should actually pull from the model's `to` key.
     307                        var latestRevisionIndex = this.model.revisions.length - 1;
     308
     309                        this.settings = new revisions.model.Slider({
     310                                max: latestRevisionIndex,
     311                                value: latestRevisionIndex,
     312                                start: this.start,
     313                                slide: this.slide,
     314                                stop: this.stop
     315                        });
    687316                },
    688317
    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 );
     318                ready: function() {
     319                        this.$el.slider( this.settings.toJSON() );
     320                        this.settings.on( 'change', function( model, options ) {
     321                                // Apply changes to slider settings here.
     322                        }, this );
    695323                },
    696324
    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
     325                start: function() {
    705326
    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();
    726327                },
    727328
    728                 restore: function() {
    729                         document.location = $( '#restore-revision' ).data( 'restoreLink' );
    730                 }
    731         });
    732 
    733 
    734         /**
    735          * ========================================================================
    736          * MODELS
    737          * ========================================================================
    738          */
     329                slide: function( event, ui ) {
     330                        var attributes = {
     331                                to: this.model.revisions.at( ui.value )
     332                        };
    739333
    740         /**
    741          * wp.revisions.Revision
    742          */
    743         Revision = revisions.model.Revision = Backbone.Model.extend({
    744                 idAttribute: 'ID',
     334                        // If we're at the first revision, unset 'from'.
     335                        if ( ui.value )
     336                                attributes.from = this.model.revisions.at( ui.value - 1 );
     337                        else
     338                                this.model.unset('from', { silent: true });
    745339
    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
     340                        this.model.set( attributes );
    759341                },
    760342
    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                         }
     343                stop: function() {
    774344
    775345                }
    776346        });
    777347
    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                 },
     348        // The diff view.
     349        // This is the view for the current active diff.
     350        revisions.view.Diff = wp.Backbone.View.extend({
     351                tagName: 'div',
     352                className: 'revisions-diff',
     353                template: wp.template('revisions-diff'),
    807354
    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                         });
     355                // Generate the options to be passed to the template.
     356                prepare: function() {
     357                        return _.extend({ fields: this.model.fields.toJSON() }, this.options );
    815358                }
     359        });
    816360
    817         } );
    818 
    819         $( wp.revisions );
     361        // Initialize the revisions UI.
     362        revisions.init = function() {
     363                revisions.view.frame = new revisions.view.Frame({
     364                        collection: new revisions.model.Revisions( revisions.settings.revisionData )
     365                }).render();
     366        };
    820367
     368        $( revisions.init );
    821369}(jQuery));
  • wp-admin/revision.php

     
    88
    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
    1319$revision_id = absint( $revision );
    else 
    7783        $parent_file = $submenu_file = 'edit.php';
    7884
    7985wp_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 );
     86wp_localize_script( 'revisions', '_wpRevisionsSettings', wp_prepare_revisions_for_js( $post ) );
    8987
    9088/* Revisions Help Tab */
    9189
    require_once( './admin-header.php' ); 
    114112
    115113<div class="wrap">
    116114        <?php screen_icon(); ?>
    117         <div id="revision-diff-container" class="current-version right-model-loading">
    118                 <h2 class="long-header"><?php echo $h2; ?></h2>
     115        <h2 class="long-header"><?php echo $h2; ?></h2>
     116</div>
    119117
    120                 <div id="loading-status" class="updated message">
    121                         <p><span class="spinner" ></span></p>
    122                 </div>
     118<script id="tmpl-revisions-frame" type="text/html">
     119        <span class="spinner"></span>
     120        <div class="revisions-control-frame"></div>
     121        <div class="revisions-diff-frame"></div>
     122</script>
    123123
    124                 <div class="diff-slider-ticks-wrapper">
    125                         <div id="diff-slider-ticks"></div>
    126                 </div>
     124<script id="tmpl-revisions-controls" type="text/html">
     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>
    127131
    128                 <div id="revision-interact"></div>
     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>
    129135
    130                 <div id="revisions-diff"></div>
     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' ); ?>" />
    131138        </div>
    132 </div>
     139
     140        <div class="revisions-slider"></div>
     141</script>
    133142
    134143<script id="tmpl-revisions-diff" type="text/html">
     144        <# _.each( data.fields, function( field ) { #>
     145                <h3>{{{ field.name }}}</h3>
     146                {{{ field.diff }}}
     147        <# }); #>
     148</script>
     149
     150<script id="tmpl-revisions-diff-old" type="text/html">
    135151        <div id="toggle-revision-compare-mode">
    136152                <label>
    137153                        <input type="checkbox" id="compare-two-revisions" />
    require_once( './admin-header.php' ); 
    157173                </div>
    158174        </div>
    159175
    160         </div>
    161 
    162176        <div id="diff-table">{{{ data.diff }}}</div>
    163177</script>
    164178
  • wp-includes/revision.php

    function _wp_upgrade_revisions_of_post( $post, $revisions ) { 
    597597
    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 }