Make WordPress Core

Changeset 23683


Ignore:
Timestamp:
03/13/2013 10:08:16 AM (12 years ago)
Author:
azaozz
Message:

Autosave to the browser's sessionStorage, compare this autosave to the post content on page load and let the user restore it when the data is not the same. First run, see #23220

Location:
trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/wp-admin/edit-form-advanced.php

    r23631 r23683  
    302302?></h2>
    303303<?php if ( $notice ) : ?>
    304 <div id="notice" class="error"><p><?php echo $notice ?></p></div>
     304<div id="notice" class="error"><p id="has-newer-autosave"><?php echo $notice ?></p></div>
    305305<?php endif; ?>
    306306<?php if ( $message ) : ?>
  • trunk/wp-admin/includes/ajax-actions.php

    r23663 r23683  
    10511051    $id = $revision_id = 0;
    10521052
    1053     $post_ID = (int) $_POST['post_ID'];
    1054     $_POST['ID'] = $post_ID;
    1055     $post = get_post($post_ID);
     1053    $post_id = (int) $_POST['post_id'];
     1054    $_POST['ID'] = $_POST['post_ID'] = $post_id;
     1055    $post = get_post($post_id);
    10561056    if ( 'auto-draft' == $post->post_status )
    10571057        $_POST['post_status'] = 'draft';
     
    10691069
    10701070    if ( 'page' == $post->post_type ) {
    1071         if ( !current_user_can('edit_page', $post_ID) )
     1071        if ( !current_user_can('edit_page', $post->ID) )
    10721072            wp_die( __( 'You are not allowed to edit this page.' ) );
    10731073    } else {
    1074         if ( !current_user_can('edit_post', $post_ID) )
     1074        if ( !current_user_can('edit_post', $post->ID) )
    10751075            wp_die( __( 'You are not allowed to edit this post.' ) );
    10761076    }
  • trunk/wp-admin/includes/misc.php

    r23681 r23683  
    633633}
    634634add_filter( 'heartbeat_received', 'wp_refresh_post_lock', 10, 3 );
     635
     636/**
     637 * Output the HTML for restoring the post data from DOM storage
     638 *
     639 * @since 3.6
     640 * @access private
     641 */
     642function _local_storage_notice() {
     643    $screen = get_current_screen();
     644    if ( ! $screen || 'post' != $screen->id )
     645        return;
     646
     647    ?>
     648    <div id="local-storage-notice" class="hidden">
     649    <p class="local-restore">
     650        <?php _e('The backup of this post in your browser is different from the version below.'); ?>
     651        <a class="restore-backup" href="#"><?php _e('Restore the backup.'); ?></a>
     652    </p>
     653    <p class="undo-restore hidden">
     654        <?php _e('Post restored successfully.'); ?>
     655        <a class="undo-restore-backup" href="#"><?php _e('Undo.'); ?></a>
     656    </p>
     657    </div>
     658    <?php
     659}
     660add_action( 'admin_footer', '_local_storage_notice' );
  • trunk/wp-includes/js/admin-bar.js

    r23518 r23683  
    133133            }
    134134        });
     135
     136        // Empty sessionStorage on logging out
     137        if ( 'sessionStorage' in window ) {
     138            $('#wp-admin-bar-logout a').click( function() {
     139                try {
     140                    for ( var key in sessionStorage ) {
     141                        if ( key.indexOf('wp-autosave-') != -1 )
     142                            sessionStorage.removeItem(key);
     143                    }
     144                } catch(e) {}
     145            });
     146        }
    135147    });
    136148} else {
     
    311323                    scrollToTop( e.target || e.srcElement );
    312324                });
     325
     326                addEvent( document.getElementById('wp-admin-bar-logout'), 'click', function() {
     327                    if ( 'sessionStorage' in window ) {
     328                        try {
     329                            for ( var key in sessionStorage ) {
     330                                if ( key.indexOf('wp-autosave-') != -1 )
     331                                    sessionStorage.removeItem(key);
     332                            }
     333                        } catch(e) {}
     334                    }
     335                });
    313336            }
    314337
  • trunk/wp-includes/js/autosave.js

    r23518 r23683  
    188188    blockSave = false;
    189189    var res = autosave_parse_response(response), postID;
     190
    190191    if ( res && res.responses.length && !res.errors ) {
    191192        // An ID is sent only for real auto-saves, not for autosave=0 "keepalive" saves
     
    258259    autosave_disable_buttons();
    259260
    260     post_data = {
    261         action: "autosave",
    262         post_ID:  jQuery("#post_ID").val() || 0,
    263         autosavenonce: jQuery('#autosavenonce').val(),
    264         post_type: jQuery('#post_type').val() || "",
    265         autosave: 1
    266     };
    267 
    268     jQuery('.tags-input').each( function() {
    269         post_data[this.name] = this.value;
    270     } );
     261    post_data = wp.autosave.getPostData();
    271262
    272263    // We always send the ajax request in order to keep the post lock fresh.
    273264    // This (bool) tells whether or not to write the post to the DB during the ajax request.
    274     doAutoSave = true;
     265    doAutoSave = post_data.autosave;
    275266
    276267    // No autosave while thickbox is open (media buttons)
     
    278269        doAutoSave = false;
    279270
    280     /* Gotta do this up here so we can check the length when tinymce is in use */
    281     if ( rich && doAutoSave ) {
    282         ed = tinymce.activeEditor;
    283         // Don't run while the tinymce spellcheck is on. It resets all found words.
    284         if ( ed.plugins.spellchecker && ed.plugins.spellchecker.active ) {
    285             doAutoSave = false;
    286         } else {
    287             if ( 'mce_fullscreen' == ed.id || 'wp_mce_fullscreen' == ed.id )
    288                 tinymce.get('content').setContent(ed.getContent({format : 'raw'}), {format : 'raw'});
    289             tinymce.triggerSave();
    290         }
    291     }
    292 
    293     if ( fullscreen && fullscreen.settings.visible ) {
    294         post_data["post_title"] = jQuery('#wp-fullscreen-title').val() || '';
    295         post_data["content"] = jQuery("#wp_mce_fullscreen").val() || '';
    296     } else {
    297         post_data["post_title"] = jQuery("#title").val() || '';
    298         post_data["content"] = jQuery("#content").val() || '';
    299     }
    300 
    301     if ( jQuery('#post_name').val() )
    302         post_data["post_name"] = jQuery('#post_name').val();
    303 
    304271    // Nothing to save or no change.
    305272    if ( ( post_data["post_title"].length == 0 && post_data["content"].length == 0 ) || post_data["post_title"] + post_data["content"] == autosaveLast ) {
    306273        doAutoSave = false;
    307274    }
    308 
    309     origStatus = jQuery('#original_post_status').val();
    310 
    311     goodcats = ([]);
    312     jQuery("[name='post_category[]']:checked").each( function(i) {
    313         goodcats.push(this.value);
    314     } );
    315     post_data["catslist"] = goodcats.join(",");
    316 
    317     if ( jQuery("#comment_status").prop("checked") )
    318         post_data["comment_status"] = 'open';
    319     if ( jQuery("#ping_status").prop("checked") )
    320         post_data["ping_status"] = 'open';
    321     if ( jQuery("#excerpt").size() )
    322         post_data["excerpt"] = jQuery("#excerpt").val();
    323     if ( jQuery("#post_author").size() )
    324         post_data["post_author"] = jQuery("#post_author").val();
    325     if ( jQuery("#parent_id").val() )
    326         post_data["parent_id"] = jQuery("#parent_id").val();
    327     post_data["user_ID"] = jQuery("#user-id").val();
    328     if ( jQuery('#auto_draft').val() == '1' )
    329         post_data["auto_draft"] = '1';
    330275
    331276    if ( doAutoSave ) {
     
    351296    });
    352297}
     298
     299// Autosave in localStorage
     300// set as simple object/mixin for now
     301window.wp = window.wp || {};
     302wp.autosave = wp.autosave || {};
     303
     304(function($){
     305// Returns the data for saving in both localStorage and autosaves to the server
     306wp.autosave.getPostData = function() {
     307    var ed = typeof tinymce != 'undefined' ? tinymce.activeEditor : null, post_name, parent_id, cats = [],
     308        data = {
     309            action: 'autosave',
     310            autosave: true,
     311            post_id: $('#post_ID').val() || 0,
     312            autosavenonce: $('#autosavenonce').val() || '',
     313            post_type: $('#post_type').val() || '',
     314            post_author: $('#post_author').val() || '',
     315            excerpt: $('#excerpt').val() || ''
     316        };
     317
     318    if ( ed && !ed.isHidden() ) {
     319        // Don't run while the tinymce spellcheck is on. It resets all found words.
     320        if ( ed.plugins.spellchecker && ed.plugins.spellchecker.active ) {
     321            data.autosave = false;
     322            return data;
     323        } else {
     324            if ( 'mce_fullscreen' == ed.id )
     325                tinymce.get('content').setContent(ed.getContent({format : 'raw'}), {format : 'raw'});
     326
     327            tinymce.triggerSave();
     328        }
     329    }
     330
     331    if ( typeof fullscreen != 'undefined' && fullscreen.settings.visible ) {
     332        data['post_title'] = $('#wp-fullscreen-title').val() || '';
     333        data['content'] = $('#wp_mce_fullscreen').val() || '';
     334    } else {
     335        data['post_title'] = $('#title').val() || '';
     336        data['content'] = $('#content').val() || '';
     337    }
     338
     339    /*
     340    // We haven't been saving tags with autosave since 2.8... Start again?
     341    $('.the-tags').each( function() {
     342        data[this.name] = this.value;
     343    });
     344    */
     345
     346    $('input[id^="in-category-"]:checked').each( function() {
     347        cats.push(this.value);
     348    });
     349    data['catslist'] = cats.join(',');
     350
     351    if ( post_name = $('#post_name').val() )
     352        data['post_name'] = post_name;
     353
     354    if ( parent_id = $('#parent_id').val() )
     355        data['parent_id'] = parent_id;
     356
     357    if ( $('#comment_status').prop('checked') )
     358        data['comment_status'] = 'open';
     359
     360    if ( $('#ping_status').prop('checked') )
     361        data['ping_status'] = 'open';
     362
     363    if ( $('#auto_draft').val() == '1' )
     364        data['auto_draft'] = '1';
     365
     366    return data;
     367}
     368
     369wp.autosave.local = {
     370
     371    lastsaveddata: '',
     372    blog_id: 0,
     373    ajaxurl: window.ajaxurl || 'wp-admin/admin-ajax.php',
     374    hasStorage: false,
     375
     376    // Check if the browser supports sessionStorage and it's not disabled
     377    checkStorage: function() {
     378        var test = Math.random(), result = false;
     379
     380        try {
     381            sessionStorage.setItem('wp-test', test);
     382            result = sessionStorage.getItem('wp-test') == test;
     383            sessionStorage.removeItem('wp-test');
     384        } catch(e) {}
     385
     386        this.hasStorage = result;
     387        return result;
     388    },
     389
     390    /**
     391     * Initialize the local storage
     392     *
     393     * @return mixed False if no sessionStorage in the browser or an Object containing all post_data for this blog
     394     */
     395    getStorage: function() {
     396        var stored_obj = false;
     397        // Separate local storage containers for each blog_id
     398        if ( this.hasStorage && this.blog_id ) {
     399            stored_obj = sessionStorage.getItem( 'wp-autosave-' + this.blog_id );
     400
     401            if ( stored_obj )
     402                stored_obj = JSON.parse( stored_obj );
     403            else
     404                stored_obj = {};
     405        }
     406
     407        return stored_obj;
     408    },
     409
     410    /**
     411     * Set the storage for this blog
     412     *
     413     * Confirms that the data was saved successfully.
     414     *
     415     * @return bool
     416     */
     417    setStorage: function( stored_obj ) {
     418        var key;
     419
     420        if ( this.hasStorage && this.blog_id ) {
     421            key = 'wp-autosave-' + this.blog_id;
     422            sessionStorage.setItem( key, JSON.stringify( stored_obj ) );
     423            return sessionStorage.getItem( key ) !== null;
     424        }
     425
     426        return false;
     427    },
     428
     429    /**
     430     * Get the saved post data for the current post
     431     *
     432     * @return mixed False if no storage or no data or the post_data as an Object
     433     */
     434    getData: function() {
     435        var stored = this.getStorage(), post_id = $('#post_ID').val();
     436
     437        if ( !stored || !post_id )
     438            return false;
     439
     440        return stored[ 'post_' + post_id ] || false;
     441    },
     442
     443    /**
     444     * Set (save) post data in the storage
     445     *
     446     * @return bool
     447     */
     448    setData: function( stored_data ) {
     449        var stored = this.getStorage(), post_id = $('#post_ID').val();
     450
     451        if ( !stored || !post_id )
     452            return false;
     453
     454        stored[ 'post_' + post_id ] = stored_data;
     455
     456        return this.setStorage(stored);
     457    },
     458
     459    /**
     460     * Save post data for the current post
     461     *
     462     * Runs on a 15 sec. schedule, saves when there are differences in the post title or content.
     463     * When the optional data is provided, updates the last saved post data.
     464     *
     465     * $param data optional Object The post data for saving, minimum 'post_title' and 'content'
     466     * @return bool
     467     */
     468    save: function( data ) {
     469        var result = false;
     470
     471        if ( ! data ) {
     472            post_data = wp.autosave.getPostData();
     473        } else {
     474            post_data = this.getData() || {};
     475            $.extend( post_data, data );
     476        }
     477
     478        // If the content and title are empty or did not change since the last save, don't save again
     479        if ( post_data.post_title + ': ' + post_data.content == this.lastsaveddata )
     480            return false;
     481
     482        // Cannot get the post data at the moment
     483        if ( !post_data.autosave )
     484            return false;
     485
     486        post_data['save_time'] = (new Date()).getTime();
     487        post_data['status'] = $('#post_status').val() || '';
     488        result = this.setData( post_data );
     489
     490        if ( result )
     491            this.lastsaveddata = post_data.post_title + ': ' + post_data.content;
     492
     493        return result;
     494    },
     495
     496    // Initialize and run checkPost() on loading the script (before TinyMCE init)
     497    init: function( settings ) {
     498        var self = this;
     499
     500        // Run only on the Add/Edit Post screens and in browsers that have sessionStorage
     501        if ( 'post' != window.pagenow || ! this.checkStorage() )
     502            return;
     503        // editor.js has to be loaded before autosave.js
     504        if ( typeof switchEditors == 'undefined' )
     505            return;
     506
     507        if ( settings )
     508            $.extend( this, settings );
     509
     510        if ( !this.blog_id )
     511            this.blog_id = typeof window.autosaveL10n != 'undefined' ? window.autosaveL10n.blog_id : 0;
     512
     513        this.checkPost();
     514        $(document).ready( self.run );
     515    },
     516
     517    // Run on DOM ready
     518    run: function() {
     519        var self = this, post_data;
     520
     521        // Set the comparison string
     522        if ( !this.lastsaveddata ) {
     523            post_data = wp.autosave.getPostData();
     524
     525            if ( post_data.content && $('#wp-content-wrap').hasClass('tmce-active') )
     526                this.lastsaveddata = post_data.post_title + ': ' + switchEditors.pre_wpautop( post_data.content );
     527            else
     528                this.lastsaveddata = post_data.post_title + ': ' + post_data.content;
     529        }
     530
     531        // Set the schedule
     532        this.schedule = $.schedule({
     533            time: 15 * 1000,
     534            func: function() { wp.autosave.local.save(); },
     535            repeat: true,
     536            protect: true
     537        });
     538
     539        $('form#post').on('submit.autosave-local', function() {
     540            var editor = typeof tinymce != 'undefined' && tinymce.get('content');
     541
     542            if ( editor && ! editor.isHidden() ) {
     543                // Last onSubmit event in the editor, needs to run after the content has been moved to the textarea.
     544                editor.onSubmit.add( function() {
     545                    wp.autosave.local.save({
     546                        post_title: $('#title').val() || '',
     547                        content: $('#content').val() || '',
     548                        excerpt: $('#excerpt').val() || ''
     549                    });
     550                });
     551            } else {
     552                self.save({
     553                    post_title: $('#title').val() || '',
     554                    content: $('#content').val() || '',
     555                    excerpt: $('#excerpt').val() || ''
     556                });
     557            }
     558        });
     559    },
     560
     561    // Strip whitespace and compare two strings
     562    compare: function( str1, str2, strip_tags ) {
     563        function remove( string, strip_tags ) {
     564            string = string.toString();
     565
     566            if ( strip_tags )
     567                string = string.replace(/<[^<>]+>/g, '');
     568
     569            return string.replace(/[\x20\t\r\n\f]+/g, '');
     570        }
     571
     572        return ( remove( str1 || '', strip_tags ) == remove( str2 || '', strip_tags ) );
     573    },
     574
     575    /**
     576     * Check if the saved data for the current post (if any) is different than the loaded post data on the screen
     577     *
     578     * Shows a standard message letting the user restore the post data if different.
     579     *
     580     * @return void
     581     */
     582    checkPost: function() {
     583        var self = this, post_data = this.getData(), content, check_data, strip_tags = false, notice;
     584
     585        if ( ! post_data )
     586            return;
     587
     588        // There is a newer autosave. Don't show two "restore" notices at the same time.
     589        if ( $('#has-newer-autosave').length )
     590            return;
     591
     592        content = $('#content').val();
     593        check_data = $.extend( {}, post_data );
     594
     595        if ( $('#wp-content-wrap').hasClass('tmce-active') )
     596            content = switchEditors.pre_wpautop( content );
     597
     598        // The post has just been published, only compare text
     599        if ( $('#post_status').val() == 'publish' && check_data.status != 'publish' )
     600            strip_tags = true;
     601
     602        if ( this.compare( content, check_data.content, strip_tags ) && this.compare( $('#title').val(), check_data.post_title, strip_tags ) && this.compare( $('#excerpt').val(), check_data.excerpt, strip_tags ) )
     603            return;
     604
     605        // We have three choices here:
     606        // - Do an autosave and then show the standard notice "There is an autosave newer than...".
     607        // - Offer to load/restore the backed up post data.
     608        // - Restore the post_data without asking, then show a notice with an Undo link/button.
     609        // Doing an autosave will take few seconds and may take up to 30 and fail if network connectivity is bad
     610        // Restoring the post will leave the user with the proper content, but it won't be saved to the server until the next autosave.
     611
     612        this.restore_post_data = post_data;
     613        this.undo_post_data = wp.autosave.getPostData();
     614
     615        /*
     616        if ( $('#post_status').val() == 'publish' ) {
     617            // Different message when a post is published?
     618            // Comparing the current and saved post data may fail (false positive) when the post is published
     619            // as in some cases there are changes to post_content on publishing and updating before saving to the DB.
     620        }
     621        */
     622
     623        notice = $('#local-storage-notice');
     624        $('form#post').before( notice.addClass('updated').show() );
     625
     626        notice.on( 'click', function(e) {
     627            var target = $( e.target );
     628
     629            if ( target.hasClass('restore-backup') ) {
     630                self.restorePost( self.restore_post_data );
     631                target.parent().hide();
     632                $(this).find('p.undo-restore').show();
     633            } else if ( target.hasClass('undo-restore-backup') ) {
     634                self.restorePost( self.undo_post_data );
     635                target.parent().hide();
     636                $(this).find('p.local-restore').show();
     637            }
     638
     639            e.preventDefault();
     640        });
     641    },
     642
     643    // Restore the current title, content and excerpt from post_data.
     644    restorePost: function( post_data ) {
     645        var editor;
     646
     647        if ( post_data ) {
     648            // Set the last saved data
     649            this.lastsaveddata = post_data.post_title + ': ' + post_data.content;
     650
     651            if ( $('#title').val() != post_data.post_title )
     652                $('#title').focus().val( post_data.post_title || '' );
     653
     654            $('#excerpt').val( post_data.excerpt || '' );
     655            editor = typeof tinymce != 'undefined' && tinymce.get('content');
     656
     657            if ( editor && ! editor.isHidden() ) {
     658                // Make sure there's an undo level in the editor
     659                editor.undoManager.add();
     660                editor.setContent( post_data.content ? switchEditors.wpautop( post_data.content ) : '' );
     661            } else {
     662                // Make sure the Text editor is selected
     663                $('#content-html').click();
     664                $('#content').val( post_data.content );
     665            }
     666
     667            return true;
     668        }
     669
     670        return false;
     671    }
     672}
     673
     674wp.autosave.local.init();
     675
     676}(jQuery));
  • trunk/wp-includes/script-loader.php

    r23681 r23683  
    107107    ) );
    108108
    109     $scripts->add( 'autosave', "/wp-includes/js/autosave$suffix.js", array('schedule', 'wp-ajax-response'), false, 1 );
     109    $scripts->add( 'autosave', "/wp-includes/js/autosave$suffix.js", array('schedule', 'wp-ajax-response', 'editor'), false, 1 );
    110110
    111111    $scripts->add( 'heartbeat', "/wp-includes/js/heartbeat$suffix.js", array('jquery'), false, 1 );
     
    586586        'autosaveInterval' => AUTOSAVE_INTERVAL,
    587587        'savingText' => __('Saving Draft&#8230;'),
    588         'saveAlert' => __('The changes you made will be lost if you navigate away from this page.')
     588        'saveAlert' => __('The changes you made will be lost if you navigate away from this page.'),
     589        'blog_id' => get_current_blog_id(),
    589590    ) );
    590591
  • trunk/wp-login.php

    r23625 r23683  
    6767    if ( wp_is_mobile() ) { ?>
    6868        <meta name="viewport" content="width=320; initial-scale=0.9; maximum-scale=1.0; user-scalable=0;" /><?php
     69    }
     70
     71    // Remove all stored post data on logging out.
     72    // This could be added by add_action('login_head'...) like wp_shake_js()
     73    // but maybe better if it's not removable by plugins
     74    if ( 'loggedout' == $wp_error->get_error_code() ) {
     75        ?>
     76        <script>if("sessionStorage" in window){try{for(var key in sessionStorage){if(key.indexOf("wp-autosave-")!=-1){sessionStorage.removeItem(key)}}}catch(e){}};</script>
     77        <?php
    6978    }
    7079
Note: See TracChangeset for help on using the changeset viewer.