Ticket #24425: 24425.draft.15.diff
File 24425.draft.15.diff, 61.1 KB (added by , 11 years ago) |
---|
-
wp-includes/revision.php
597 597 598 598 return true; 599 599 } 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 removed604 *605 * @since 3.6.0606 *607 * @see wp_parse_args() Used to change defaults to user defined settings.608 * @uses Text_Diff609 * @uses WP_Text_Diff_Renderer_Table610 *611 * @param string $left_string "old" (left) version of string612 * @param string $right_string "new" (right) version of string613 * @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 } -
wp-admin/admin-ajax.php
42 42 43 43 $core_actions_get = array( 44 44 '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', 46 46 ); 47 47 48 48 $core_actions_post = array( … … 56 56 'save-widget', 'set-post-thumbnail', 'date_format', 'time_format', 'wp-fullscreen-save-post', 57 57 'wp-remove-post-lock', 'dismiss-wp-pointer', 'upload-attachment', 'get-attachment', 58 58 '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', 60 60 ); 61 61 62 62 // Register core Ajax calls. -
wp-admin/includes/ajax-actions.php
2082 2082 wp_send_json($response); 2083 2083 } 2084 2084 2085 function wp_ajax_ revisions_data() {2086 check_ajax_referer( 'revisions-ajax-nonce', 'nonce' );2085 function wp_ajax_get_revision_diffs() { 2086 require ABSPATH . 'wp-admin/includes/revision.php'; 2087 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; 2088 // check_ajax_referer( 'revisions-ajax-nonce', 'nonce' ); 2096 2089 2097 $all_the_revisions = array(); 2098 if ( ! $post_id ) 2099 $post_id = $compare_to; 2090 if ( ! $post = get_post( (int) $_REQUEST['post_id'] ) ) 2091 wp_send_json_error(); 2100 2092 2101 if ( ! current_user_can( 'read_post', $post _id) )2102 continue;2093 if ( ! current_user_can( 'read_post', $post->ID ) ) 2094 wp_send_json_error(); 2103 2095 2104 if ( ! $revisions = wp_get_post_revisions( $post_id ) ) 2105 return; 2096 // Really just pre-loading the cache here. 2097 if ( ! $revisions = wp_get_post_revisions( $post->ID ) ) 2098 wp_send_json_error(); 2106 2099 2107 $left_revision = get_post( $compare_to ); 2100 $return = array(); 2101 @set_time_limit( count( $_REQUEST['compare'] ) ); 2108 2102 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 ); 2103 foreach ( $_REQUEST['compare'] as $compare_key ) { 2104 list( $compare_from, $compare_to ) = explode( ':', $compare_key ); // from:to 2113 2105 2114 if ( ! $compare_to ) 2115 $left_revision = get_post( $post_id ); 2116 2117 // make sure the right revision is the most recent, except on oldest revision 2118 if ( $compare_to && $right_revision->post_date < $left_revision->post_date ) { 2119 $temp = $left_revision; 2120 $left_revision = $right_revision; 2121 $right_revision = $temp; 2122 } 2123 2124 $lines_added = $lines_deleted = 0; 2125 $content = ''; 2126 // compare from left to right, passed from application 2127 foreach ( _wp_post_revision_fields() as $field => $field_value ) { 2128 $left_content = apply_filters( "_wp_post_revision_field_$field", $left_revision->$field, $field, $left_revision, 'left' ); 2129 $right_content = apply_filters( "_wp_post_revision_field_$field", $right_revision->$field, $field, $right_revision, 'right' ); 2130 2131 add_filter( "_wp_post_revision_field_$field", 'htmlspecialchars' ); 2132 2133 $args = array(); 2134 2135 if ( $show_split_view ) 2136 $args = array( 'show_split_view' => true ); 2137 2138 // compare_to == 0 means first revision, so compare to a blank field to show whats changed 2139 $diff = wp_text_diff_with_count( ( 0 == $compare_to ) ? '' : $left_content, $right_content, $args ); 2140 2141 if ( isset( $diff[ 'html' ] ) ) { 2142 $content .= sprintf( '<div class="diff-label">%s</div>', $field_value ); 2143 $content .= $diff[ 'html' ]; 2144 } 2145 2146 if ( isset( $diff[ 'lines_added' ] ) ) 2147 $lines_added = $lines_added + $diff[ 'lines_added' ]; 2148 2149 if ( isset( $diff[ 'lines_deleted' ] ) ) 2150 $lines_deleted = $lines_deleted + $diff[ 'lines_deleted' ]; 2151 } 2152 $content = '' == $content ? __( 'No difference' ) : $content; 2153 2154 $all_the_revisions = array ( 2155 'diff' => $content, 2156 'linesDeleted' => $lines_deleted, 2157 'linesAdded' => $lines_added 2106 $return[] = array( 2107 'id' => $compare_key, 2108 'fields' => wp_get_revision_ui_diff( $post, $compare_from, $compare_to ), 2158 2109 ); 2159 2160 echo json_encode( $all_the_revisions ); 2161 exit(); 2162 } // end single model fetch 2163 2164 $count = -1; 2165 2166 // reverse the list to start with oldest revision 2167 $revisions = array_reverse( $revisions ); 2168 2169 $previous_revision_id = 0; 2170 2171 /* translators: revision date format, see http://php.net/date */ 2172 $datef = _x( 'j F, Y @ G:i:s', 'revision date format'); 2173 2174 foreach ( $revisions as $revision ) : 2175 if ( ! $show_autosaves && wp_is_post_autosave( $revision ) ) 2176 continue; 2177 2178 $revision_from_date_author = ''; 2179 $is_current_revision = false; 2180 $count++; 2181 2182 /** 2183 * return blank data for diffs to the left of the left handle (for right handel model) 2184 * or to the right of the right handle (for left handel model) 2185 * and visa versa in RTL mode 2186 */ 2187 if( ! is_rtl() ) { 2188 if ( ( ( 0 != $left_handle_at && $count < $left_handle_at ) || 2189 ( 0 != $right_handle_at && $count > ( $right_handle_at - 2 ) ) ) ) { 2190 $all_the_revisions[] = array ( 2191 'ID' => $revision->ID, 2192 ); 2193 continue; 2194 } 2195 } else { // is_rtl 2196 if ( ( 0 != $left_handle_at && $count > ( $left_handle_at - 1 ) || 2197 ( 0 != $left_handle_at && $count < $right_handle_at ) ) ) { 2198 $all_the_revisions[] = array ( 2199 'ID' => $revision->ID, 2200 ); 2201 continue; 2202 } 2203 } 2204 2205 if ( $compare_two_mode ) { 2206 $compare_to_gravatar = get_avatar( $left_revision->post_author, 24 ); 2207 $compare_to_author = get_the_author_meta( 'display_name', $left_revision->post_author ); 2208 $compare_to_date = date_i18n( $datef, strtotime( $left_revision->post_modified ) ); 2209 2210 $revision_from_date_author = sprintf( 2211 /* translators: post revision title: 1: author avatar, 2: author name, 3: time ago, 4: date */ 2212 _x( '%1$s %2$s, %3$s ago (%4$s)', 'post revision title' ), 2213 $compare_to_gravatar, 2214 $compare_to_author, 2215 human_time_diff( strtotime( $left_revision->post_modified ), current_time( 'timestamp' ) ), 2216 $compare_to_date 2217 ); 2218 } 2219 2220 $gravatar = get_avatar( $revision->post_author, 24 ); 2221 $author = get_the_author_meta( 'display_name', $revision->post_author ); 2222 $date = date_i18n( $datef, strtotime( $revision->post_modified ) ); 2223 $revision_date_author = sprintf( 2224 /* translators: post revision title: 1: author avatar, 2: author name, 3: time ago, 4: date */ 2225 _x( '%1$s %2$s, %3$s ago (%4$s)', 'post revision title' ), 2226 $gravatar, 2227 $author, 2228 human_time_diff( strtotime( $revision->post_modified ), current_time( 'timestamp' ) ), 2229 $date 2230 ); 2231 2232 /* translators: 1: date */ 2233 $autosavef = _x( '%1$s [Autosave]', 'post revision title extra' ); 2234 /* translators: 1: date */ 2235 $currentf = _x( '%1$s [Current Revision]', 'post revision title extra' ); 2236 2237 if ( ! $post = get_post( $post_id ) ) 2238 continue; 2239 2240 if ( $left_revision->post_modified === $post->post_modified ) 2241 $revision_from_date_author = sprintf( $currentf, $revision_from_date_author ); 2242 elseif ( wp_is_post_autosave( $left_revision ) ) 2243 $revision_from_date_author = sprintf( $autosavef, $revision_from_date_author ); 2244 2245 if ( $revision->post_modified === $post->post_modified ) { 2246 $revision_date_author = sprintf( $currentf, $revision_date_author ); 2247 $is_current_revision = true; 2248 } elseif ( wp_is_post_autosave( $revision ) ) { 2249 $revision_date_author = sprintf( $autosavef, $revision_date_author ); 2250 } 2251 2252 /* translators: revision date short format, see http://php.net/date */ 2253 $date_short_format = _x( 'j M @ G:i', 'revision date short format'); 2254 $date_short = date_i18n( $date_short_format, strtotime( $revision->post_modified ) ); 2255 2256 $revision_date_author_short = sprintf( 2257 '%s <strong>%s</strong><br />%s', 2258 $gravatar, 2259 $author, 2260 $date_short 2261 ); 2262 2263 $restore_link = wp_nonce_url( 2264 add_query_arg( 2265 array( 'revision' => $revision->ID, 2266 'action' => 'restore' ), 2267 admin_url( 'revision.php' ) 2268 ), 2269 "restore-post_{$revision->ID}" 2270 ); 2271 2272 // if this is a left handled calculation swap data 2273 if ( 0 != $right_handle_at ) { 2274 $tmp = $revision_from_date_author; 2275 $revision_from_date_author = $revision_date_author; 2276 $revision_date_author = $tmp; 2277 } 2278 2279 if ( ( $compare_two_mode || -1 !== $previous_revision_id ) ) { 2280 $all_the_revisions[] = array ( 2281 'ID' => $revision->ID, 2282 'titleTo' => $revision_date_author, 2283 'titleFrom' => $revision_from_date_author, 2284 'titleTooltip' => $revision_date_author_short, 2285 'restoreLink' => urldecode( $restore_link ), 2286 'previousID' => $previous_revision_id, 2287 'isCurrent' => $is_current_revision, 2288 ); 2289 } 2290 $previous_revision_id = $revision->ID; 2291 2292 endforeach; 2293 2294 // in RTL + single handle mode, reverse the revision direction 2295 if ( is_rtl() && $compare_two_mode ) 2296 $all_the_revisions = array_reverse( $all_the_revisions ); 2297 2298 echo json_encode( $all_the_revisions ); 2299 exit(); 2110 } 2111 wp_send_json_success( $return ); 2300 2112 } -
wp-admin/includes/revision.php
1 <?php 2 3 function wp_get_revision_ui_diff( $post, $compare_from, $compare_to ) { 4 if ( ! $post = get_post( $post ) ) 5 return false; 6 7 if ( $compare_from ) { 8 if ( ! $compare_from = get_post( $compare_from ) ) 9 return false; 10 } else { 11 // If we're dealing with the first revision... 12 $compare_from = false; 13 } 14 15 if ( ! $compare_to = get_post( $compare_to ) ) 16 return false; 17 18 // If comparing revisions, make sure we're dealing with the right post parent. 19 if ( $compare_from && $compare_from->post_parent !== $post->ID ) 20 return false; 21 if ( $compare_to->post_parent !== $post->ID ) 22 return false; 23 24 if ( $compare_from && strtotime( $compare_from->post_date_gmt ) > strtotime( $compare_to->post_date_gmt ) ) { 25 $temp = $compare_from; 26 $compare_from = $compare_to; 27 $compare_to = $temp; 28 } 29 30 $return = array(); 31 32 foreach ( _wp_post_revision_fields() as $field => $name ) { 33 $content_from = $compare_from ? apply_filters( "_wp_post_revision_field_$field", $compare_from->$field, $field, $compare_from, 'left' ) : ''; 34 $content_to = apply_filters( "_wp_post_revision_field_$field", $compare_to->$field, $field, $compare_to, 'right' ); 35 36 $diff = wp_text_diff( $content_from, $content_to, array( 'show_split_view' => true ) ); 37 38 if ( ! $diff && 'post_title' === $field ) { 39 // It's a better user experience to still show the Title, even if it didn't change. 40 // No, you didn't see this. 41 $diff = "<table class='diff'><col class='ltype' /><col class='content' /><col class='ltype' /><col class='content' /><tbody><tr>"; 42 $diff .= '<td>' . esc_html( $compare_from->post_title ) . '</td><td></td><td>' . esc_html( $compare_to->post_title ) . '</td>'; 43 $diff .= '</tr></tbody>'; 44 $diff .= '</table>'; 45 } 46 47 if ( $diff ) { 48 $return[] = array( 49 'id' => $field, 50 'name' => $name, 51 'diff' => $diff, 52 ); 53 } 54 } 55 return $return; 56 } 57 58 function wp_prepare_revisions_for_js( $post, $selected_revision_id ) { 59 $post = get_post( $post ); 60 $revisions = array(); 61 $current = current_time( 'timestamp' ); 62 63 $revisions = wp_get_post_revisions( $post->ID ); 64 65 cache_users( wp_list_pluck( $revisions, 'post_author' ) ); 66 67 foreach ( $revisions as $revision ) { 68 $modified_gmt = strtotime( $revision->post_modified_gmt ); 69 $restore_link = wp_nonce_url( 70 add_query_arg( 71 array( 'revision' => $revision->ID, 72 'action' => 'restore' ), 73 admin_url( 'revision.php' ) 74 ), 75 "restore-post_{$revision->ID}" 76 ); 77 $revisions[ $revision->ID ] = array( 78 'id' => $revision->ID, 79 'title' => get_the_title( $post->ID ), 80 'author' => array( 81 'id' => (int) $revision->post_author, 82 'avatar' => get_avatar( $revision->post_author, 24 ), 83 'name' => get_the_author_meta( 'display_name', $revision->post_author ), 84 ), 85 'date' => date_i18n( __( 'M j, Y @ G:i' ), $modified_gmt ), 86 'dateShort' => date_i18n( _x( 'j M @ G:i', 'revision date short format' ), $modified_gmt ), 87 'timeAgo' => human_time_diff( $modified_gmt, $current ), 88 'autosave' => wp_is_post_autosave( $revision ), 89 'current' => $revision->post_modified_gmt === $post->post_modified_gmt, 90 'restoreUrl' => urldecode( $restore_link ), 91 ); 92 } 93 94 return array( 95 'postId' => $post->ID, 96 'nonce' => wp_create_nonce( 'revisions-ajax-nonce' ), 97 'revisionData' => array_values( $revisions ), 98 'selectedRevision' => $selected_revision_id, 99 ); 100 } 101 No newline at end of file -
wp-admin/js/revisions.js
1 1 window.wp = window.wp || {}; 2 2 3 3 (function($) { 4 var Revision, Revisions, Diff,revisions;4 var revisions; 5 5 6 revisions = wp.revisions = function() { 7 Diff = revisions.Diff = new Diff(); 8 }; 6 revisions = wp.revisions = { model: {}, view: {}, controller: {} }; 9 7 10 _.extend( revisions, { model: {}, view: {}, controller: {} } );11 12 8 // Link settings. 13 revisions. model.settings = typeof wpRevisionsSettings === 'undefined' ? {} :wpRevisionsSettings;9 revisions.settings = typeof _wpRevisionsSettings === 'undefined' ? {} : _wpRevisionsSettings; 14 10 15 11 16 12 /** 17 13 * ======================================================================== 18 * CONTROLLERS14 * MODELS 19 15 * ======================================================================== 20 16 */ 17 revisions.model.Slider = Backbone.Model.extend({ 18 defaults: { 19 value: 0, 20 min: 0, 21 max: 1, 22 step: 1 23 } 24 }); 21 25 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 26 revisions.model.Revision = Backbone.Model.extend({}); 41 27 42 constructor: function() { 43 var self = this; 44 this.slider = new revisions.view.Slider(); 28 revisions.model.Revisions = Backbone.Collection.extend({ 29 model: revisions.model.Revision, 45 30 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 } 31 comparator: function( revision ) { 32 return revision.id; 57 33 }, 34 }); 58 35 59 loadDiffs: function( models ) { 60 var self = this, 61 revisionsToLoad = models.where( { completed: false } ), 62 delay = 0, 63 totalChanges; 36 revisions.model.Field = Backbone.Model.extend({}); 64 37 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 }); 38 revisions.model.Fields = Backbone.Collection.extend({ 39 model: revisions.model.Field 40 }); 70 41 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 ); 42 revisions.model.Diff = Backbone.Model.extend({ 43 initialize: function(attributes, options) { 44 var fields = this.get('fields'); 45 this.unset('fields'); 79 46 80 // stop spinner when all models are loaded81 if ( 0 === models.where( { completed: false } ).length )82 self.stopModelLoadingSpinner();47 this.fields = new revisions.model.Fields( fields ); 48 } 49 }); 83 50 84 totalChanges = model.get( 'linesAdded' ) + model.get( 'linesDeleted' ), 85 scopeOfChanges = 'vsmall'; 51 revisions.model.Diffs = Backbone.Collection.extend({ 52 initialize: function(models, options) { 53 this.revisions = options.revisions; 54 this.requests = {}; 55 }, 86 56 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 57 model: revisions.model.Diff, 58 59 ensure: function( id, context ) { 60 var diff = this.get( id ); 61 var request = this.requests[ id ]; 62 var deferred = $.Deferred(); 63 var ids = {}; 64 65 if ( diff ) { 66 deferred.resolveWith( context, [ diff ] ); 67 } else { 68 this.trigger( 'ensure:load', ids ); 69 _.each( ids, _.bind( function(id) { 70 // Remove anything that has an ongoing request 71 if ( this.requests[ id ] ) 72 delete ids[ id ]; 73 }, this ) ); 74 if ( ! request ) { 75 // Always include the ID that started this ensure 76 ids[ id ] = true; 77 request = this.load( _.keys( ids ) ); 109 78 } 110 );111 },112 79 113 startLeftModelLoading:function() {114 this.leftModelLoading = true;115 $('#revision-diff-container').addClass('left-model-loading');116 },80 request.done( _.bind( function() { 81 deferred.resolveWith( context, [ this.get( id ) ] ); 82 }, this ) ); 83 } 117 84 118 stopLeftModelLoading: function() { 119 this.leftModelLoading = false; 85 return deferred.promise(); 120 86 }, 121 87 122 startRightModelLoading: function() { 123 this.rightModelLoading = true; 124 $('#revision-diff-container').addClass('right-model-loading'); 88 loadNew: function( comparisons ) { 89 comparisons = _.object( comparisons, comparisons ); 90 _.each( comparisons, _.bind( function( id ) { 91 // Exists 92 if ( this.get( id ) ) 93 delete comparisons[ id ]; 94 }, this ) ); 95 comparisons = _.toArray( comparisons ); 96 return this.load( comparisons ); 125 97 }, 126 98 127 stopRightModelLoading: function() { 128 this.rightModelLoading = false; 99 load: function( comparisons ) { 100 // Our collection should only ever grow, never shrink, so remove: false 101 return this.fetch({ data: { compare: comparisons }, remove: false }); 129 102 }, 130 103 131 stopModelLoadingSpinner: function() { 132 $('#revision-diff-container').removeClass('right-model-loading'); 133 $('#revision-diff-container').removeClass('left-model-loading'); 134 }, 104 /**/ 105 loadLast: function( num ) { 106 num = num || 1; 107 var ids = this.getProximalDiffIds(); 108 ids = _.last( ids, num ); 135 109 136 reloadModel: function() { 137 if ( this.singleRevision ) { 138 this.reloadModelSingle(); 139 } else { 140 this.reloadLeftRight(); 110 if ( ids.length ) { 111 return this.loadNew( ids ); 141 112 } 142 113 }, 143 114 144 // load the models for the single handle mode 145 reloadModelSingle: function() { 146 var self = this; 115 loadLastUnloaded: function( num ) { 116 num = num || 1; 117 var ids = this.getUnloadedProximalDiffIds(); 118 ids = _.last( ids, num ); 147 119 148 self.startRightModelLoading(); 120 if ( ids.length ) { 121 return this.loadNew( ids ); 122 } 123 }, 149 124 150 self.revisions.reload({ 151 options: { 152 'showAutosaves': self.autosaves, 153 'showSplitView': self.showSplitView 154 }, 125 getProximalDiffIds: function() { 126 var previous = 0, ids = []; 127 this.revisions.each( _.bind( function(revision) { 128 ids.push( previous + ':' + revision.id ); 129 previous = revision.id; 130 }, this ) ); 131 return ids; 132 }, 155 133 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 }); 134 getUnloadedProximalDiffIds: function() { 135 var comparisons = this.getProximalDiffIds(); 136 comparisons = _.object( comparisons, comparisons ); 137 _.each( comparisons, _.bind( function( id ) { 138 // Exists 139 if ( this.get( id ) ) 140 delete comparisons[ id ]; 141 }, this ) ); 142 return _.toArray( comparisons ); 173 143 }, 174 144 175 // load the models for the left handle (the right handler has moved) 176 reloadLeft: function() { 177 var self = this; 178 self.startLeftModelLoading(); 179 self.leftHandleRevisions = new Revisions( {}, { 180 'compareTo': self.revisions.at( self.rightDiff - 1 ).get( 'ID' ), // diff and model count off by 1 181 'showAutosaves': self.autosaves, 182 'showSplitView': self.showSplitView, 183 'rightHandleAt': self.rightDiff 184 }); 185 186 self.leftHandleRevisions.fetch({ 187 success: function(){ 188 self.stopLeftModelLoading(); 189 self.loadDiffs( self.leftHandleRevisions ); 190 self.tickmarkView.model = self.leftHandleRevisions; 191 self.slider.refresh({ 192 'max': self.revisions.length 193 }); 194 // ensure right handle not beyond length 195 if ( self.rightDiff > self.revisions.length ) 196 self.rightDiff = self.revisions.length; 197 }, 198 199 error: function() { 200 self.stopLeftModelLoading(); 201 } 202 }); 145 loadAllBy: function( chunkSize ) { 146 chunkSize = chunkSize || 20; 147 var unloaded = this.getUnloadedProximalDiffIds(); 148 if ( unloaded.length ) { 149 return this.loadLastUnloaded( chunkSize ).always( _.bind( function() { 150 this.loadAllBy( chunkSize ); 151 }, this ) ); 152 } 203 153 }, 204 154 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 }); 155 sync: function( method, model, options ) { 156 if ( 'read' === method ) { 157 options = options || {}; 158 options.context = this; 159 options.data = _.extend( options.data || {}, { 160 action: 'get-revision-diffs', 161 post_id: revisions.settings.postId 162 }); 215 163 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 }, 164 var deferred = wp.xhr.send( options ); 165 var requests = this.requests; 225 166 226 error: function( response ) { 227 self.stopRightModelLoading(); 167 // Record that we're requesting each diff. 168 if ( options.data.compare ) { 169 _.each( options.data.compare, function( id ) { 170 requests[ id ] = deferred; 171 }); 228 172 } 229 });230 173 231 }, 174 // When the request completes, clear the stored request. 175 deferred.always( function() { 176 if ( options.data.compare ) { 177 _.each( options.data.compare, function( id ) { 178 delete requests[ id ]; 179 }); 180 } 181 }); 232 182 233 /** 234 * reloadLeftRight reload models for both the left and right handles 235 */ 236 reloadLeftRight: function() { 237 this.startRightModelLoading(); 238 this.startLeftModelLoading(); 239 this.reloadLeft(); 240 this.reloadRight(); 241 }, 183 return deferred; 242 184 243 disabledButtonCheck: function( val ) { 244 var maxVal = this.revisions.length - 1, 245 next = ! isRtl ? $( '#next' ) : $( '#previous' ), 246 prev = ! isRtl ? $( '#previous' ) : $( '#next' ); 185 // Otherwise, fall back to `Backbone.sync()`. 186 } else { 187 return Backbone.Model.prototype.sync.apply( this, arguments ); 188 } 189 } 190 }); 247 191 248 // Disable "Next" button if you're on the last node249 if ( maxVal === val )250 next.prop( 'disabled', true );251 else252 next.prop( 'disabled', false );253 192 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 ); 193 revisions.model.FrameState = Backbone.Model.extend({ 194 initialize: function( attributes, options ) { 195 this.revisions = options.revisions; 196 this.diffs = new revisions.model.Diffs( [], {revisions: this.revisions} ); 197 198 this.listenTo( this, 'change:from change:to', this.updateDiffId ); 259 199 }, 260 200 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 201 updateDiffId: function() { 202 var from = this.get( 'from' ); 203 var to = this.get( 'to' ); 204 this.set( 'diffId', (from ? from.id : '0') + ':' + to.id ); 281 205 } 282 206 }); 283 207 … … 288 212 * ======================================================================== 289 213 */ 290 214 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, 215 // The frame view. This contains the entire page. 216 revisions.view.Frame = wp.Backbone.View.extend({ 217 tagName: 'div', 218 className: 'revisions', 219 template: wp.template('revisions-frame'), 299 220 300 initialize: function( options ) { 301 this.options = _.defaults( options || {}, { 302 value: 0, 303 min: 0, 304 max: 1, 305 step: 1 221 initialize: function() { 222 this.model = new revisions.model.FrameState({}, { 223 revisions: this.collection 306 224 }); 307 },308 225 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; 226 this.listenTo( this.model, 'change:diffId', this.updateDiff ); 322 227 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; 228 this.views.set( '.revisions-control-frame', new revisions.view.Controls({ 229 model: this.model 230 }) ); 327 231 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; 232 if ( this.model.revisions.length ) { 233 var last = this.model.revisions.last(2); 234 var attributes = { to: last.pop() }; 333 235 334 Diff.rightDiff = isRtl ? ui.values[0] : ui.values[1]; // handles are reversed in RTL mode335 }236 if ( last.length ) 237 attributes.from = last.pop(); 336 238 337 Diff.revisionView.render(); 239 this.model.set( attributes ); 240 241 // Load the rest: first 10, then the rest by 50 242 this.model.diffs.loadLastUnloaded( 10 ).always( _.bind( function() { 243 this.model.diffs.loadAllBy( 50 ); 244 }, this ) ); 338 245 } 339 246 }, 340 247 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; 248 render: function() { 249 wp.Backbone.View.prototype.render.apply( this, arguments ); 350 250 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; 251 $('#wpbody-content .wrap').append( this.el ); 252 this.views.ready(); 355 253 356 Diff.revisionView.draggingLeft = true; 254 return this; 255 }, 357 256 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 } 257 updateDiff: function() { 258 this.model.diffs.ensure( this.model.get('diffId'), this ).done( function( diff ) { 259 if ( this.model.get('diffId') !== diff.id ) 260 return; 261 this.views.set( '.revisions-diff-frame', new revisions.view.Diff({ 262 model: diff 263 })); 264 }); 265 } 266 }); 364 267 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 268 // The control view. 269 // This contains the revision slider, previous/next buttons, and the compare checkbox. 270 revisions.view.Controls = wp.Backbone.View.extend({ 271 tagName: 'div', 272 className: 'revisions-controls', 366 273 367 } else { 368 // Right handler 369 if ( Diff.rightModelLoading || 0 === Diff.rightHandleRevisions.length) // right model still loading, prevent sliding right handle 370 return false; 274 initialize: function() { 275 // Add the button view 276 this.views.add( new revisions.view.Buttons({ 277 model: this.model 278 })); 371 279 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 } 280 // Add the Slider view 281 this.views.add( new revisions.view.Slider({ 282 model: this.model 283 }) ); 378 284 379 Diff.revisionView.draggingLeft = false; 380 Diff.rightDiffStart = isRtl ? ui.values[0] : ui.values[1]; // in RTL mode the 'left handle' is the second in the slider, 'right' is first 381 } 382 }, 285 // Add the Meta view 286 this.views.add( new revisions.view.Meta({ 287 model: this.model 288 }) ); 289 } 290 }); 383 291 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; 292 // The meta view. 293 // This contains the revision meta, and the restore button. 294 revisions.view.Meta = wp.Backbone.View.extend({ 295 tagName: 'div', 296 className: 'revisions-meta', 297 template: wp.template('revisions-meta'), 393 298 394 // calculate and generate a diff for comparing to the left handle 395 // and the right handle, swap out when dragging 396 if ( $( ui.handle ).hasClass( 'left-handle' ) ) { 397 // Left handler 398 if ( Diff.leftDiffStart !== isRtl ? ui.values[1] : ui.values[0] ) // in RTL mode the 'left handle' is the second in the slider, 'right' is first 399 Diff.reloadRight(); 400 } else { 401 // Right handler 402 if ( Diff.rightDiffStart !== isRtl ? ui.values[0] : ui.values[1] ) // in RTL mode the 'left handle' is the second in the slider, 'right' is first 403 Diff.reloadLeft(); 404 } 299 initialize: function() { 300 this.listenTo( this.model, 'change:diffId', this.updateMeta ); 405 301 }, 406 302 407 addTooltip: function( handle, message ){408 handle.find( '.ui-slider-tooltip' ).html( message );303 events: { 304 'click #restore-revision': 'restoreRevision' 409 305 }, 410 306 411 width: function() { 412 return $( '#diff-slider' ).width(); 307 restoreRevision: function() { 308 var restoreUrl = this.model.get('to').attributes.restoreUrl.replace(/&/g, '&'); 309 document.location = restoreUrl; 413 310 }, 414 311 415 setWidth: function( width ) { 416 $( '#diff-slider' ).width( width ); 417 }, 418 419 refresh: function( options, slide ) { 420 $( '#diff-slider' ).slider( 'option', options ); 421 422 // Triggers the slide event 423 if ( slide ) 424 $( '#diff-slider' ).trigger( 'slide' ); 425 426 Diff.disabledButtonCheck( options.value ); 427 }, 428 429 option: function( key ) { 430 return $( '#diff-slider' ).slider( 'option', key ); 431 }, 432 433 render: function() { 434 var self = this; 435 // this.$el doesn't work, why? 436 $( '#diff-slider' ).slider( { 437 slide: $.proxy( self.slide, self ), 438 start: $.proxy( self.start, self ), 439 stop: $.proxy( self.stop, self ) 440 } ); 441 442 // Set options 443 this.refresh( this.options ); 312 updateMeta: function() { 313 this.$el.html( this.template( this.model.toJSON() ) ); 314 if( this.model.get( 'to' ).attributes.current ) { 315 $( '#restore-revision' ).prop( 'disabled', true); 316 } else { 317 $( '#restore-revision' ).prop( 'disabled', false) 318 } 444 319 } 445 320 }); 446 321 447 /**448 * wp.revisions.view.Tickmarks449 *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 322 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(); 323 // The buttons view. 324 // Encapsulates all of the configuration for the previous/next buttons, and the compare checkbox. 325 revisions.view.Buttons = wp.Backbone.View.extend({ 326 tagName: 'div', 327 className: 'revisions-buttons', 328 template: wp.template('revisions-controls'), 467 329 468 if ( tickWidth !== aTickWidth ) { // is the width already set correctly? 469 $( '.revision-tick' ).each( function() { 470 tickMargin = Math.floor( ( tickWidth - $( this ).width() ) / 2 ) + 1; 471 $( this ).css( 'border-left', tickMargin + 'px solid #f7f7f7'); // space the ticks out using margins 472 $( this ).css( 'border-right', ( tickWidth - tickMargin - $( this ).width() ) + 'px solid #f7f7f7'); // space the ticks out using margins 473 }); 474 firstTick = $( '.revision-tick' ).first(); //cache selectors for optimization 475 lastTick = $( '.revision-tick' ).last(); 476 477 sliderWidth = sliderWidth + Math.ceil( ( tickWidth - ( lastTick.outerWidth() - lastTick.innerWidth() ) ) / 2 ); // room for the last tick 478 sliderWidth = sliderWidth + Math.ceil( ( tickWidth - ( firstTick.outerWidth() - firstTick.innerWidth() ) ) / 2 ); // room for the first tick 479 firstTick.css( 'border-left', 'none' ); // first tick gets no left border 480 lastTick.css( 'border-right', 'none' ); // last tick gets no right border 481 } 482 483 /** 484 * reset the slider width 485 */ 486 Diff.slider.setWidth( sliderWidth ); 487 $( '.diff-slider-ticks-wrapper' ).width( sliderWidth ); 488 $( '#diff-slider-ticks' ).width( sliderWidth ); 489 490 /** 491 * go through all ticks, add hover and click interactions 492 */ 493 $( '.revision-tick' ).each( function() { 494 Diff.slider.addTooltip ( $( this ), Diff.revisions.at( tickCount++ ).get( 'titleTooltip' ) ); 495 $( this ).hover( 496 function() { 497 $( this ).find( '.ui-slider-tooltip' ).show().append('<div class="arrow"></div>'); 498 }, 499 function() { 500 $( this ).find( '.ui-slider-tooltip' ).hide().find( '.arrow' ).remove(); 501 } 502 ); 503 504 /** 505 * move the slider handle when the tick marks are clicked 506 */ 507 $( this ).on( 'click', 508 { tickCount: tickCount }, // pass the tick through so we know where to move the handle 509 function( event ) { 510 if ( Diff.slider.singleRevision ) { // single handle mode 511 Diff.rightDiff = event.data.tickCount; // reposition the right handle 512 Diff.slider.refresh({ 513 value: Diff.rightDiff - 1 514 } ); 515 } else { //compare two mode 516 if ( isRtl ) { 517 if ( event.data.tickCount < Diff.leftDiff ) { // click was on the 'left' side 518 Diff.rightDiff = event.data.tickCount; // set the 'right' handle location 519 Diff.reloadLeft(); // reload the left handle comparison models 520 } else { // middle or 'right' clicks 521 Diff.leftDiff = event.data.tickCount; // set the 'left' handle location 522 Diff.reloadRight(); // reload right handle models 523 } 524 } else { 525 if ( event.data.tickCount < Diff.leftDiff ) { // click was on the 'left' side 526 Diff.leftDiff = event.data.tickCount; // set the left handle location 527 Diff.reloadRight(); // reload the right handle comparison models 528 } else { // middle or 'right' clicks 529 Diff.rightDiff = event.data.tickCount; // set the right handle location 530 Diff.reloadLeft(); // reload left handle models 531 } 532 } 533 Diff.slider.refresh( { // set the slider handle positions 534 values: [ isRtl ? Diff.rightDiff : Diff.leftDiff, isRtl ? Diff.leftDiff : Diff.rightDiff ] 535 } ); 536 } 537 Diff.revisionView.render(); // render the main view 538 } ); 539 } ); 330 initialize: function() { 331 this.$el.html( this.template() ) 540 332 }, 541 333 542 // render the tick mark view543 render: function() {544 var self = this, addHtml;545 546 if ( null !== self.model ) {547 addHtml = "";548 _.each ( self.model.models, function( theModel ) {549 addHtml = addHtml + self.template ( theModel.toJSON() );550 });551 self.$el.html( addHtml );552 553 }554 self.resetTicks();555 return self;556 }557 } );558 559 /**560 * wp.revisions.view.Interact561 *562 * Next/Prev buttons and the slider563 */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 mode569 334 events: { 570 'click #next': ! isRtl ? 'nextRevision' : 'previousRevision',571 'click #previous': ! isRtl ? 'previousRevision' : 'nextRevision'335 'click #next': 'nextRevision', 336 'click #previous': 'previousRevision' 572 337 }, 338 339 nextRevision: function() { 340 var toIndex = this.model.revisions.indexOf( this.model.get( 'to' ) ); 341 toIndex = isRtl ? toIndex - 1 : toIndex + 1; 342 this.model.set( { 'to': this.model.revisions.at( toIndex ) } ); 343 }, 344 345 previousRevision: function() { 346 var toIndex = this.model.revisions.indexOf( this.model.get('to') ); 347 toIndex = isRtl ? toIndex + 1 : toIndex - 1; 348 this.model.set( { 'to': this.model.revisions.at( toIndex ) } ); 349 }, 573 350 574 render: function() { 575 var modelcount; 576 this.$el.html( this.template ); 577 578 modelcount = Diff.revisions.length; 579 580 Diff.slider.singleRevision = Diff.singleRevision; 581 Diff.slider.render(); 582 583 if ( Diff.singleRevision ) { 584 Diff.slider.refresh({ 585 value: Diff.rightDiff - 1, // rightDiff value is off model index by 1 586 min: 0, 587 max: modelcount - 1 588 }); 589 590 $( '#revision-diff-container' ).removeClass( 'comparing-two-revisions' ); 591 592 } else { 593 Diff.slider.refresh({ 594 // in RTL mode the 'left handle' is the second in the slider, 'right' is first 595 values: [ isRtl ? Diff.rightDiff : Diff.leftDiff, isRtl ? Diff.leftDiff : Diff.rightDiff ], 596 min: 1, 597 max: modelcount + 1, 598 range: true 599 }); 600 601 $( '#revision-diff-container' ).addClass( 'comparing-two-revisions' ); 602 // in RTL mode the 'left handle' is the second in the slider, 'right' is first 603 $( '#diff-slider a.ui-slider-handle' ).first().addClass( isRtl ? 'right-handle' : 'left-handle' ); 604 $( '#diff-slider a.ui-slider-handle' ).last().addClass( isRtl ? 'left-handle' : 'right-handle' ); 605 606 } 607 608 return this; 351 ready: function() { 352 this.listenTo( this.model, 'change:diffId', this.disabledButtonCheck ); 609 353 }, 610 354 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 ; 355 // Check to see if the Previous or Next buttons need to be disabled or enabled 356 disabledButtonCheck: function() { 357 var maxVal = isRtl ? 0 : this.model.revisions.length - 1, 358 minVal = isRtl ? this.model.revisions.length - 1 : 0, 359 next = $( '.revisions-next .button' ), 360 previous = $( '.revisions-previous .button' ), 361 val = this.model.revisions.indexOf( this.model.get( 'to' ) ); 615 362 616 Diff.revisionView.render(); 363 // Disable "Next" button if you're on the last node 364 if ( maxVal === val ) 365 next.prop( 'disabled', true ); 366 else 367 next.prop( 'disabled', false ); 617 368 618 Diff.slider.refresh({ 619 value: Diff.rightDiff - 1 620 }, true ); 369 // Disable "Previous" button if you're on the first node 370 if ( minVal === val ) 371 previous.prop( 'disabled', true ); 372 else 373 previous.prop( 'disabled', false ); 621 374 }, 622 375 623 // go to the previous revision624 previousRevision: function() {625 if ( Diff.rightDiff > 1 ) // unless at left boundry626 Diff.rightDiff = Diff.rightDiff - 1 ;627 376 628 Diff.revisionView.render();629 630 Diff.slider.refresh({631 value: Diff.rightDiff - 1632 }, true );633 }634 377 }); 635 378 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, 379 // The slider view. 380 // Encapsulates all of the configuration for the jQuery UI slider into a view. 381 revisions.view.Slider = wp.Backbone.View.extend({ 382 tagName: 'div', 383 className: 'wp-slider', 645 384 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 }, 385 initialize: function() { 386 _.bindAll( this, 'start', 'slide', 'stop' ); 651 387 652 // render the revisions653 render: function() {654 var addHtml = '', thediff;388 // Create the slider model from the provided collection data. 389 // TODO: This should actually pull from the model's `to` key. 390 var latestRevisionIndex = this.model.revisions.length - 1; 655 391 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 ); 392 // Find the initially selected revision 393 var initiallySelectedRevisionIndex = 394 this.model.revisions.indexOf( 395 this.model.revisions.findWhere( { id: Number( revisions.settings.selectedRevision ) } ) ); 675 396 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 } 397 this.settings = new revisions.model.Slider({ 398 max: latestRevisionIndex, 399 value: initiallySelectedRevisionIndex, 400 start: this.start, 401 slide: this.slide, 402 stop: this.stop 403 }); 404 }, 680 405 681 this.toggleCompareTwoCheckbox(); 406 ready: function() { 407 this.$el.slider( this.settings.toJSON() ); 408 this.settings.on( 'change', function( model, options ) { 409 // Apply changes to slider settings here. 410 this.$el.slider( { value: this.model.revisions.indexOf( this.model.get( 'to' ) ) } ); // Set handle to current to model 411 }, this ); 412 // Reset to the initially selected revision 413 this.slide( '', this.settings.attributes ); 682 414 683 // hide the restore button when on the last sport/current post data684 $( '#restore-revision' ).toggle( ! Diff.revisions.at( Diff.rightDiff - 1 ).get( 'isCurrent' ));415 // Listen for changes in the diffId 416 this.listenTo( this.model, 'change:diffId', this.diffIdChanged ); 685 417 686 return this;687 418 }, 688 419 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 ); 420 diffIdChanged: function() { 421 // Reset the view settings when diffId is changed 422 this.settings.set( { 'value': this.model.revisions.indexOf( this.model.get( 'to' ) ) } ); 695 423 }, 696 424 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 ; 425 start: function( event, ui ) { 426 // Track the mouse position to enable smooth dragging, overrides default jquery ui step behaviour 427 $( window ).mousemove( function( e ) { 428 var sliderLeft = $( '.wp-slider' ).offset().left, 429 sliderRight = sliderLeft + $( '.wp-slider' ).width(); 701 430 702 // in RTL mode handles are swapped, so boundary checks are different; 703 if ( isRtl ){ 704 Diff.leftDiff = Diff.revisions.length; // put the left handle at the rightmost position, representing current revision 705 706 if ( Diff.revisions.length === Diff.rightDiff ) // make sure 'left' handle not in rightmost slot 707 Diff.rightDiff = Diff.rightDiff - 1; 431 // Follow mouse movements, as long as handle remains inside slider 432 if ( e.clientX < sliderLeft ) { 433 $( ui.handle ).css( 'left', 0 ); // Mouse to left of slider 434 } else if ( e.clientX > sliderRight ) { 435 $( ui.handle ).css( 'left', sliderRight - sliderLeft); // Mouse to right of slider 708 436 } else { 709 if ( 1 === Diff.rightDiff ) // make sure right handle not in leftmost slot 710 Diff.rightDiff = 2; 437 $( ui.handle ).css( 'left', e.clientX - sliderLeft ); // Mouse in slider 711 438 } 439 } ); // End mousemove 440 }, 712 441 713 Diff.revisionView.draggingLeft = false; 442 slide: function( event, ui ) { 443 var attributes = { 444 to: this.model.revisions.at( ui.value ) 445 }; 714 446 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; 447 // If we're at the first revision, unset 'from'. 448 if ( ui.value ) 449 attributes.from = this.model.revisions.at( ui.value - 1 ); 450 else 451 this.model.unset('from', { silent: true }); 718 452 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(); 453 this.model.set( attributes ); 726 454 }, 727 455 728 restore: function() { 729 document.location = $( '#restore-revision' ).data( 'restoreLink' ); 456 stop: function( event, ui ) { 457 $( window ).unbind( 'mousemove' ); // Stop tracking the mouse 458 // Reset settings pops handle back to the step position 459 this.settings.trigger( 'change' ); 730 460 } 731 461 }); 732 462 463 // The diff view. 464 // This is the view for the current active diff. 465 revisions.view.Diff = wp.Backbone.View.extend({ 466 tagName: 'div', 467 className: 'revisions-diff', 468 template: wp.template('revisions-diff'), 733 469 734 /** 735 * ======================================================================== 736 * MODELS 737 * ======================================================================== 738 */ 739 740 /** 741 * wp.revisions.Revision 742 */ 743 Revision = revisions.model.Revision = Backbone.Model.extend({ 744 idAttribute: 'ID', 745 746 defaults: { 747 ID: 0, 748 titleTo: '', 749 titleTooltip: '', 750 titleFrom: '', 751 diff: '<div class="diff-loading"><div class="spinner"></div></div>', 752 restoreLink: '', 753 completed: false, 754 linesAdded: 0, 755 linesDeleted: 0, 756 scopeOfChanges: 'none', 757 previousID: 0, 758 isCurrent: false 759 }, 760 761 url: function() { 762 if ( Diff.singleRevision ) { 763 return ajaxurl + 764 '?action=revisions-data' + 765 '&show_autosaves=true' + 766 '&show_split_view=true' + 767 '&nonce=' + revisions.model.settings.nonce + 768 '&single_revision_id=' + this.id + 769 '&compare_to=' + this.get( 'previousID' ) + 770 '&post_id=' + revisions.model.settings.post_id; 771 } else { 772 return this.collection.url() + '&single_revision_id=' + this.id; 773 } 774 470 // Generate the options to be passed to the template. 471 prepare: function() { 472 return _.extend({ fields: this.model.fields.toJSON() }, this.options ); 775 473 } 776 474 }); 777 475 778 /** 779 * wp.revisions.Revisions 780 */ 781 Revisions = revisions.Revisions = Backbone.Collection.extend({ 782 model: Revision, 476 // Initialize the revisions UI. 477 revisions.init = function() { 478 revisions.view.frame = new revisions.view.Frame({ 479 collection: new revisions.model.Revisions( revisions.settings.revisionData ) 480 }).render(); 481 }; 783 482 784 initialize: function( models, options ) { 785 this.options = _.defaults( options || {}, { 786 'compareTo': revisions.model.settings.post_id, 787 'post_id': revisions.model.settings.post_id, 788 'showAutosaves': true, 789 'showSplitView': true, 790 'rightHandleAt': 0, 791 'leftHandleAt': 0, 792 'nonce': revisions.model.settings.nonce 793 }); 794 }, 795 796 url: function() { 797 return ajaxurl + 798 '?action=revisions-data' + 799 '&compare_to=' + this.options.compareTo + // revision are we comparing to 800 '&post_id=' + this.options.post_id + // the post id 801 '&show_autosaves=' + this.options.showAutosaves + // show or hide autosaves 802 '&show_split_view=' + this.options.showSplitView + // show in split view or single column view 803 '&right_handle_at=' + this.options.rightHandleAt + // mark point for comparison list 804 '&left_handle_at=' + this.options.leftHandleAt + // mark point for comparison list 805 '&nonce=' + this.options.nonce; 806 }, 807 808 reload: function( options ) { 809 this.options = _.defaults( options.options || {}, this.options ); 810 811 this.fetch({ 812 success: options.success || null, 813 error: options.error || null 814 }); 815 } 816 817 } ); 818 819 $( wp.revisions ); 820 483 $( revisions.init ); 821 484 }(jQuery)); -
wp-admin/revision.php
8 8 9 9 /** WordPress Administration Bootstrap */ 10 10 require_once('./admin.php'); 11 12 require 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 11 17 wp_reset_vars( array( 'revision', 'action' ) ); 12 18 13 19 $revision_id = absint( $revision ); … … 21 27 if ( ! current_user_can( 'edit_post', $revision->post_parent ) ) 22 28 break; 23 29 24 25 30 if ( ! $post = get_post( $revision->post_parent ) ) 26 31 break; 27 32 … … 77 82 $parent_file = $submenu_file = 'edit.php'; 78 83 79 84 wp_enqueue_script( 'revisions' ); 85 wp_localize_script( 'revisions', '_wpRevisionsSettings', wp_prepare_revisions_for_js( $post, $revision_id ) ); 80 86 81 82 $settings = array(83 'post_id' => $post->ID,84 'nonce' => wp_create_nonce( 'revisions-ajax-nonce' ),85 'revision_id' => $revision_id86 );87 88 wp_localize_script( 'revisions', 'wpRevisionsSettings', $settings );89 90 87 /* Revisions Help Tab */ 91 88 92 89 $revisions_overview = '<p>' . __( 'This screen is used for managing your content revisions.' ) . '</p>'; … … 114 111 115 112 <div class="wrap"> 116 113 <?php screen_icon(); ?> 117 < div id="revision-diff-container" class="current-version right-model-loading">118 <h2 class="long-header"><?php echo $h2; ?></h2>114 <h2 class="long-header"><?php echo $h2; ?></h2> 115 </div> 119 116 120 <div id="loading-status" class="updated message"> 121 <p><span class="spinner" ></span></p> 117 <script id="tmpl-revisions-frame" type="text/html"> 118 <span class="spinner"></span> 119 <div class="revisions-control-frame"></div> 120 <div class="revisions-diff-frame"></div> 121 </script> 122 123 <script id="tmpl-revisions-controls" type="text/html"> 124 125 <div class="revision-toggle-compare-mode"> 126 <label> 127 <input type="checkbox" class="compare-two-revisions" /> 128 <?php esc_attr_e( 'Compare two revisions' ); ?> 129 </label> 130 </div> 131 132 <div class="revisions-previous"> 133 <input class="button" type="button" id="previous" value="<?php echo esc_attr_x( 'Previous', 'Button label for a previous revision' ); ?>" /> 134 </div> 135 136 <div class="revisions-next"> 137 <input class="button" type="button" id="next" value="<?php echo esc_attr_x( 'Next', 'Button label for a next revision' ); ?>" /> 138 </div> 139 </script> 140 141 142 <script id="tmpl-revisions-meta" type="text/html"> 143 <div id="diff-header"> 144 <div id="diff-header-from" class="diff-header"> 145 <div id="diff-title-from" class="diff-title"> 146 <strong> 147 <?php _ex( 'From:', 'Followed by post revision info' ); ?></strong> 148 <# if ( 'undefined' !== typeof data.from ) { #> 149 {{{ data.from.attributes.author.avatar }}} {{{ data.from.attributes.author.name }}}, 150 {{{ data.from.attributes.timeAgo }}} <?php _e( 'ago' ); ?> 151 ({{{ data.from.attributes.dateShort }}}) 152 <# } #> 153 154 </div> 155 <div class="clear"></div> 122 156 </div> 123 157 124 <div class="diff-slider-ticks-wrapper"> 125 <div id="diff-slider-ticks"></div> 158 <div id="diff-header-to" class="diff-header"> 159 <div id="diff-title-to" class="diff-title"> 160 <strong><?php _ex( 'To:', 'Followed by post revision info' ); ?></strong> 161 <# if ( 'undefined' !== typeof data.to ) { #> 162 {{{ data.to.attributes.author.avatar }}} {{{ data.to.attributes.author.name }}}, 163 {{{ data.to.attributes.timeAgo }}} <?php _e( 'ago' ); ?> 164 ({{{ data.to.attributes.dateShort }}}) 165 <# } #> 126 166 </div> 127 167 128 <div id="revision-interact"></div> 129 130 <div id="revisions-diff"></div> 168 <input type="button" id="restore-revision" class="button button-primary" data-restore-link="{{{ data.restoreLink }}}" value="<?php esc_attr_e( 'Restore This Revision' )?>" /> 169 </div> 131 170 </div> 132 </ div>171 </script> 133 172 134 173 <script id="tmpl-revisions-diff" type="text/html"> 174 <# _.each( data.fields, function( field ) { #> 175 <h3>{{{ field.name }}}</h3> 176 {{{ field.diff }}} 177 <# }); #> 178 </script> 179 180 <script id="tmpl-revisions-diff-old" type="text/html"> 135 181 <div id="toggle-revision-compare-mode"> 136 182 <label> 137 183 <input type="checkbox" id="compare-two-revisions" /> … … 157 203 </div> 158 204 </div> 159 205 160 </div>161 162 206 <div id="diff-table">{{{ data.diff }}}</div> 163 207 </script> 164 208 165 <script id="tmpl-revision-interact " type="text/html">209 <script id="tmpl-revision-interact-old" type="text/html"> 166 210 <div id="diff-previous-revision"> 167 211 <input class="button" type="button" id="previous" value="<?php echo esc_attr_x( 'Previous', 'Button label for a previous revision' ); ?>" /> 168 212 </div> … … 171 215 <input class="button" type="button" id="next" value="<?php echo esc_attr_x( 'Next', 'Button label for a next revision' ); ?>" /> 172 216 </div> 173 217 174 <div id="diff-slider" class="wp-slider"></div>175 218 </script> 176 219 177 220 <script id="tmpl-revision-ticks" type="text/html"> -
wp-admin/css/wp-admin.css
3481 3481 /*------------------------------------------------------------------------------ 3482 3482 11.2 - Post Revisions 3483 3483 ------------------------------------------------------------------------------*/ 3484 .revisions .spinner { 3485 float: none; 3486 margin: 100px auto; 3487 } 3484 3488 3489 .revisions.loading .spinner { 3490 display: block; 3491 } 3492 3493 .revisions-control-frame, 3494 .revisions-diff-frame { 3495 position: relative; 3496 } 3497 3498 .revisions-controls { 3499 height: 60px; 3500 padding: 40px 0 20px; 3501 border-bottom: 1px solid #dfdfdf; 3502 margin-bottom: 10px; 3503 } 3504 3505 .revisions-meta { 3506 margin-top: 15px; 3507 } 3508 .revision-toggle-compare-mode { 3509 position: absolute; 3510 top: 0; 3511 right: 0; 3512 } 3513 3514 .revisions-previous { 3515 float: left; 3516 } 3517 3518 .revisions-next { 3519 float: right; 3520 } 3521 3522 .wp-slider { 3523 width: 70%; 3524 margin: 6px auto 0; 3525 } 3526 3485 3527 /* Revision meta box */ 3486 3528 .post-revisions li img, 3487 3529 #revisions-meta-restored img { … … 3527 3569 position: relative; 3528 3570 } 3529 3571 3530 #toggle-revision-compare-mode {3531 position: absolute;3532 top: 0;3533 right: 0;3534 padding: 9px 9px 0 0;3535 }3536 3537 3572 #loading-status { 3538 3573 display: none; 3539 3574 position: absolute; … … 3551 3586 padding: 20px 0; 3552 3587 } 3553 3588 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 3572 3589 .comparetwo #diff-slider { 3573 3590 width: 95%; 3574 3591 } … … 3588 3605 } 3589 3606 3590 3607 #diff-title-to strong { 3591 display: none;3608 display: inline; 3592 3609 } 3593 3610 3594 3611 .comparing-two-revisions #diff-title-to strong { … … 3605 3622 -webkit-border-radius: 3px; 3606 3623 border-radius: 3px; 3607 3624 padding: 5px 200px 5px 5px; 3625 clear: both; 3608 3626 } 3609 3627 3610 3628 .diff-header {