* @output wp-includes/js/autosave.js
/* global tinymce, wpCookies, autosaveL10n, switchEditors */
window.autosave = function() {
* Adds autosave to the window object on dom ready.
* @param {jQuery} $ jQuery object.
* @param {window} The window object.
( function( $, window ) {
* getPostData: getPostData,
* getCompareString: getCompareString,
* disableButtons: disableButtons,
* enableButtons: enableButtons,
* local: ({hasStorage, getSavedPostData, save, suspend, resume}|*),
* server: ({tempBlockSave, triggerSave, postChanged, suspend, resume}|*)
* The object with all functions for autosave.
var initialCompareString,
$document = $( document );
* Sets the initial compare data.
function setInitialCompare() {
post_title: $( '#title' ).val() || '',
content: $( '#content' ).val() || '',
excerpt: $( '#excerpt' ).val() || ''
initialCompareString = getCompareString( initialCompareData );
* Returns the data saved in both local and remote autosave.
* @param {string} type The type of autosave either local or remote.
* @return {Object} Object containing the post data.
function getPostData( type ) {
var post_name, parent_id, data,
time = ( new Date() ).getTime(),
// Don't run editor.save() more often than every 3 seconds.
// It is resource intensive and might slow down typing in long posts on slow devices.
if ( editor && editor.isDirty() && ! editor.isHidden() && time - 3000 > lastTriggerSave ) {
post_id: $( '#post_ID' ).val() || 0,
post_type: $( '#post_type' ).val() || '',
post_author: $( '#post_author' ).val() || '',
post_title: $( '#title' ).val() || '',
content: $( '#content' ).val() || '',
excerpt: $( '#excerpt' ).val() || ''
if ( type === 'local' ) {
$( 'input[id^="in-category-"]:checked' ).each( function() {
data.catslist = cats.join(',');
if ( post_name = $( '#post_name' ).val() ) {
data.post_name = post_name;
if ( parent_id = $( '#parent_id' ).val() ) {
data.parent_id = parent_id;
if ( $( '#comment_status' ).prop( 'checked' ) ) {
data.comment_status = 'open';
if ( $( '#ping_status' ).prop( 'checked' ) ) {
data.ping_status = 'open';
if ( $( '#auto_draft' ).val() === '1' ) {
* Concatenates the title, content and excerpt. This is used to track changes
* @param {Object} postData The object containing the post data.
* @return {string} A concatenated string with title, content and excerpt.
function getCompareString( postData ) {
if ( typeof postData === 'object' ) {
return ( postData.post_title || '' ) + '::' + ( postData.content || '' ) + '::' + ( postData.excerpt || '' );
return ( $('#title').val() || '' ) + '::' + ( $('#content').val() || '' ) + '::' + ( $('#excerpt').val() || '' );
function disableButtons() {
$document.trigger('autosave-disable-buttons');
// Re-enable 5 sec later. Just gives autosave a head start to avoid collisions.
setTimeout( enableButtons, 5000 );
function enableButtons() {
$document.trigger( 'autosave-enable-buttons' );
* Gets the content editor.
* @return {boolean|*} Returns either false if the editor is undefined,
* or the instance of the content editor.
return typeof tinymce !== 'undefined' && tinymce.get('content');
* Autosave in localStorage.
* getSavedPostData: getSavedPostData,
* The object with all functions for local storage autosave.
function autosaveLocal() {
var blog_id, post_id, hasStorage, intervalTimer,
* Checks if the browser supports sessionStorage and it's not disabled.
* @return {boolean} True if the sessionStorage is supported and enabled.
function checkStorage() {
var test = Math.random().toString(),
window.sessionStorage.setItem( 'wp-test', test );
result = window.sessionStorage.getItem( 'wp-test' ) === test;
window.sessionStorage.removeItem( 'wp-test' );
* Initializes the local storage.
* @return {boolean|Object} False if no sessionStorage in the browser or an Object
* containing all postData for this blog.
// Separate local storage containers for each blog_id.
if ( hasStorage && blog_id ) {
stored_obj = sessionStorage.getItem( 'wp-autosave-' + blog_id );
stored_obj = JSON.parse( stored_obj );
* Sets the storage for this blog. Confirms that the data was saved
* @return {boolean} True if the data was saved successfully, false if it wasn't saved.
function setStorage( stored_obj ) {
if ( hasStorage && blog_id ) {
key = 'wp-autosave-' + blog_id;
sessionStorage.setItem( key, JSON.stringify( stored_obj ) );
return sessionStorage.getItem( key ) !== null;
* Gets the saved post data for the current post.
* @return {boolean|Object} False if no storage or no data or the postData as an Object.
function getSavedPostData() {
var stored = getStorage();
if ( ! stored || ! post_id ) {
return stored[ 'post_' + post_id ] || false;
* Sets (save or delete) post data in the storage.
* If stored_data evaluates to 'false' the storage key for the current post will be removed.
* @param {Object|boolean|null} stored_data The post data to store or null/false/empty to delete the key.
* @return {boolean} True if data is stored, false if data was removed.
function setData( stored_data ) {
var stored = getStorage();
if ( ! stored || ! post_id ) {
stored[ 'post_' + post_id ] = stored_data;
} else if ( stored.hasOwnProperty( 'post_' + post_id ) ) {
delete stored[ 'post_' + post_id ];
return setStorage( stored );
* Sets isSuspended to true.
* Sets isSuspended to false.
* Saves post data for the current post.
* Runs on a 15 seconds interval, saves when there are differences in the post title or content.
* When the optional data is provided, updates the last saved post data.
* @param {Object} data The post data for saving, minimum 'post_title' and 'content'.
* @return {boolean} Returns true when data has been saved, otherwise it returns false.
var postData, compareString,
if ( isSuspended || ! hasStorage ) {
postData = getSavedPostData() || {};
$.extend( postData, data );
postData = getPostData('local');
compareString = getCompareString( postData );
if ( typeof lastCompareString === 'undefined' ) {
lastCompareString = initialCompareString;
// If the content, title and excerpt did not change since the last save, don't save again.
if ( compareString === lastCompareString ) {
postData.save_time = ( new Date() ).getTime();
postData.status = $( '#post_status' ).val() || '';
result = setData( postData );
lastCompareString = compareString;
* Initializes the auto save function.
* Checks whether the editor is active or not to use the editor events
* to autosave, or uses the values from the elements to autosave.
post_id = $('#post_ID').val() || 0;
// Check if the local post data is different than the loaded post data.
if ( $( '#wp-content-wrap' ).hasClass( 'tmce-active' ) ) {
* If TinyMCE loads first, check the post 1.5 seconds after it is ready.
* By this time the content has been loaded in the editor and 'saved' to the textarea.
* This prevents false positives.
$document.on( 'tinymce-editor-init.autosave', function() {
window.setTimeout( function() {
// Save every 15 seconds.
intervalTimer = window.setInterval( save, 15000 );
$( 'form#post' ).on( 'submit.autosave-local', function() {
var editor = getEditor(),
post_id = $('#post_ID').val() || 0;
if ( editor && ! editor.isHidden() ) {
// Last onSubmit event in the editor, needs to run after the content has been moved to the textarea.
editor.on( 'submit', function() {
post_title: $( '#title' ).val() || '',
content: $( '#content' ).val() || '',
excerpt: $( '#excerpt' ).val() || ''
post_title: $( '#title' ).val() || '',
content: $( '#content' ).val() || '',
excerpt: $( '#excerpt' ).val() || ''
var secure = ( 'https:' === window.location.protocol );
wpCookies.set( 'wp-saving-post', post_id + '-check', 24 * 60 * 60, false, false, secure );
* Compares 2 strings. Removes whitespaces in the strings before comparing them.
* @param {string} str1 The first string.
* @param {string} str2 The second string.
* @return {boolean} True if the strings are the same.
function compare( str1, str2 ) {
function removeSpaces( string ) {
return string.toString().replace(/[\x20\t\r\n\f]+/g, '');
return ( removeSpaces( str1 || '' ) === removeSpaces( str2 || '' ) );
* Checks if the saved data for the current post (if any) is different than the
* loaded post data on the screen.
* Shows a standard message letting the user restore the post data if different.
var content, post_title, excerpt, $notice,
postData = getSavedPostData(),
cookie = wpCookies.get( 'wp-saving-post' ),
$newerAutosaveNotice = $( '#has-newer-autosave' ).parent( '.notice' ),
$headerEnd = $( '.wp-header-end' );
if ( cookie === post_id + '-saved' ) {
wpCookies.remove( 'wp-saving-post' );
// The post was saved properly, remove old data and bail.
content = $( '#content' ).val() || '';
post_title = $( '#title' ).val() || '';
excerpt = $( '#excerpt' ).val() || '';
if ( compare( content, postData.content ) && compare( post_title, postData.post_title ) &&
compare( excerpt, postData.excerpt ) ) {
* If '.wp-header-end' is found, append the notices after it otherwise
* after the first h1 or h2 heading found within the main content.
if ( ! $headerEnd.length ) {