21 /** |
21 /** |
22 * Auto saves the post. |
22 * Auto saves the post. |
23 * |
23 * |
24 * @since 3.9.0 |
24 * @since 3.9.0 |
25 * |
25 * |
26 * @returns {Object} |
26 * @return {Object} |
27 * {{ |
27 * {{ |
28 * getPostData: getPostData, |
28 * getPostData: getPostData, |
29 * getCompareString: getCompareString, |
29 * getCompareString: getCompareString, |
30 * disableButtons: disableButtons, |
30 * disableButtons: disableButtons, |
31 * enableButtons: enableButtons, |
31 * enableButtons: enableButtons, |
44 * |
44 * |
45 * @since 3.9.0 |
45 * @since 3.9.0 |
46 * |
46 * |
47 * @param {string} type The type of autosave either local or remote. |
47 * @param {string} type The type of autosave either local or remote. |
48 * |
48 * |
49 * @returns {Object} Object containing the post data. |
49 * @return {Object} Object containing the post data. |
50 */ |
50 */ |
51 function getPostData( type ) { |
51 function getPostData( type ) { |
52 var post_name, parent_id, data, |
52 var post_name, parent_id, data, |
53 time = ( new Date() ).getTime(), |
53 time = ( new Date() ).getTime(), |
54 cats = [], |
54 cats = [], |
108 * |
108 * |
109 * @since 3.9.0 |
109 * @since 3.9.0 |
110 * |
110 * |
111 * @param {Object} postData The object containing the post data. |
111 * @param {Object} postData The object containing the post data. |
112 * |
112 * |
113 * @returns {string} A concatenated string with title, content and excerpt. |
113 * @return {string} A concatenated string with title, content and excerpt. |
114 */ |
114 */ |
115 function getCompareString( postData ) { |
115 function getCompareString( postData ) { |
116 if ( typeof postData === 'object' ) { |
116 if ( typeof postData === 'object' ) { |
117 return ( postData.post_title || '' ) + '::' + ( postData.content || '' ) + '::' + ( postData.excerpt || '' ); |
117 return ( postData.post_title || '' ) + '::' + ( postData.content || '' ) + '::' + ( postData.excerpt || '' ); |
118 } |
118 } |
137 /** |
137 /** |
138 * Enables save buttons. |
138 * Enables save buttons. |
139 * |
139 * |
140 * @since 3.9.0 |
140 * @since 3.9.0 |
141 * |
141 * |
142 * @returns {void} |
142 * @return {void} |
143 */ |
143 */ |
144 function enableButtons() { |
144 function enableButtons() { |
145 $document.trigger( 'autosave-enable-buttons' ); |
145 $document.trigger( 'autosave-enable-buttons' ); |
146 } |
146 } |
147 |
147 |
148 /** |
148 /** |
149 * Gets the content editor. |
149 * Gets the content editor. |
150 * |
150 * |
151 * @since 4.6.0 |
151 * @since 4.6.0 |
152 * |
152 * |
153 * @returns {boolean|*} Returns either false if the editor is undefined, |
153 * @return {boolean|*} Returns either false if the editor is undefined, |
154 * or the instance of the content editor. |
154 * or the instance of the content editor. |
155 */ |
155 */ |
156 function getEditor() { |
156 function getEditor() { |
157 return typeof tinymce !== 'undefined' && tinymce.get('content'); |
157 return typeof tinymce !== 'undefined' && tinymce.get('content'); |
158 } |
158 } |
159 |
159 |
160 /** |
160 /** |
161 * Autosave in localStorage. |
161 * Autosave in localStorage. |
162 * |
162 * |
163 * @since 3.9.0 |
163 * @since 3.9.0 |
164 * |
164 * |
165 * @returns { |
165 * @return { |
166 * { |
166 * { |
167 * hasStorage: *, |
167 * hasStorage: *, |
168 * getSavedPostData: getSavedPostData, |
168 * getSavedPostData: getSavedPostData, |
169 * save: save, |
169 * save: save, |
170 * suspend: suspend, |
170 * suspend: suspend, |
202 /** |
202 /** |
203 * Initializes the local storage. |
203 * Initializes the local storage. |
204 * |
204 * |
205 * @since 3.9.0 |
205 * @since 3.9.0 |
206 * |
206 * |
207 * @returns {boolean|Object} False if no sessionStorage in the browser or an Object |
207 * @return {boolean|Object} False if no sessionStorage in the browser or an Object |
208 * containing all postData for this blog. |
208 * containing all postData for this blog. |
209 */ |
209 */ |
210 function getStorage() { |
210 function getStorage() { |
211 var stored_obj = false; |
211 var stored_obj = false; |
212 // Separate local storage containers for each blog_id |
212 // Separate local storage containers for each blog_id. |
213 if ( hasStorage && blog_id ) { |
213 if ( hasStorage && blog_id ) { |
214 stored_obj = sessionStorage.getItem( 'wp-autosave-' + blog_id ); |
214 stored_obj = sessionStorage.getItem( 'wp-autosave-' + blog_id ); |
215 |
215 |
216 if ( stored_obj ) { |
216 if ( stored_obj ) { |
217 stored_obj = JSON.parse( stored_obj ); |
217 stored_obj = JSON.parse( stored_obj ); |
227 * Sets the storage for this blog. Confirms that the data was saved |
227 * Sets the storage for this blog. Confirms that the data was saved |
228 * successfully. |
228 * successfully. |
229 * |
229 * |
230 * @since 3.9.0 |
230 * @since 3.9.0 |
231 * |
231 * |
232 * @returns {boolean} True if the data was saved successfully, false if it wasn't saved. |
232 * @return {boolean} True if the data was saved successfully, false if it wasn't saved. |
233 */ |
233 */ |
234 function setStorage( stored_obj ) { |
234 function setStorage( stored_obj ) { |
235 var key; |
235 var key; |
236 |
236 |
237 if ( hasStorage && blog_id ) { |
237 if ( hasStorage && blog_id ) { |
246 /** |
246 /** |
247 * Gets the saved post data for the current post. |
247 * Gets the saved post data for the current post. |
248 * |
248 * |
249 * @since 3.9.0 |
249 * @since 3.9.0 |
250 * |
250 * |
251 * @returns {boolean|Object} False if no storage or no data or the postData as an Object. |
251 * @return {boolean|Object} False if no storage or no data or the postData as an Object. |
252 */ |
252 */ |
253 function getSavedPostData() { |
253 function getSavedPostData() { |
254 var stored = getStorage(); |
254 var stored = getStorage(); |
255 |
255 |
256 if ( ! stored || ! post_id ) { |
256 if ( ! stored || ! post_id ) { |
267 * |
267 * |
268 * @since 3.9.0 |
268 * @since 3.9.0 |
269 * |
269 * |
270 * @param {Object|boolean|null} stored_data The post data to store or null/false/empty to delete the key. |
270 * @param {Object|boolean|null} stored_data The post data to store or null/false/empty to delete the key. |
271 * |
271 * |
272 * @returns {boolean} True if data is stored, false if data was removed. |
272 * @return {boolean} True if data is stored, false if data was removed. |
273 */ |
273 */ |
274 function setData( stored_data ) { |
274 function setData( stored_data ) { |
275 var stored = getStorage(); |
275 var stored = getStorage(); |
276 |
276 |
277 if ( ! stored || ! post_id ) { |
277 if ( ! stored || ! post_id ) { |
292 /** |
292 /** |
293 * Sets isSuspended to true. |
293 * Sets isSuspended to true. |
294 * |
294 * |
295 * @since 3.9.0 |
295 * @since 3.9.0 |
296 * |
296 * |
297 * @returns {void} |
297 * @return {void} |
298 */ |
298 */ |
299 function suspend() { |
299 function suspend() { |
300 isSuspended = true; |
300 isSuspended = true; |
301 } |
301 } |
302 |
302 |
303 /** |
303 /** |
304 * Sets isSuspended to false. |
304 * Sets isSuspended to false. |
305 * |
305 * |
306 * @since 3.9.0 |
306 * @since 3.9.0 |
307 * |
307 * |
308 * @returns {void} |
308 * @return {void} |
309 */ |
309 */ |
310 function resume() { |
310 function resume() { |
311 isSuspended = false; |
311 isSuspended = false; |
312 } |
312 } |
313 |
313 |
314 /** |
314 /** |
315 * Saves post data for the current post. |
315 * Saves post data for the current post. |
316 * |
316 * |
317 * Runs on a 15 sec. interval, saves when there are differences in the post title or content. |
317 * Runs on a 15 seconds interval, saves when there are differences in the post title or content. |
318 * When the optional data is provided, updates the last saved post data. |
318 * When the optional data is provided, updates the last saved post data. |
319 * |
319 * |
320 * @since 3.9.0 |
320 * @since 3.9.0 |
321 * |
321 * |
322 * @param {Object} data The post data for saving, minimum 'post_title' and 'content'. |
322 * @param {Object} data The post data for saving, minimum 'post_title' and 'content'. |
323 * |
323 * |
324 * @returns {boolean} Returns true when data has been saved, otherwise it returns false. |
324 * @return {boolean} Returns true when data has been saved, otherwise it returns false. |
325 */ |
325 */ |
326 function save( data ) { |
326 function save( data ) { |
327 var postData, compareString, |
327 var postData, compareString, |
328 result = false; |
328 result = false; |
329 |
329 |
368 * |
368 * |
369 * Runs on DOM ready. |
369 * Runs on DOM ready. |
370 * |
370 * |
371 * @since 3.9.0 |
371 * @since 3.9.0 |
372 * |
372 * |
373 * @returns {void} |
373 * @return {void} |
374 */ |
374 */ |
375 function run() { |
375 function run() { |
376 post_id = $('#post_ID').val() || 0; |
376 post_id = $('#post_ID').val() || 0; |
377 |
377 |
378 // Check if the local post data is different than the loaded post data. |
378 // Check if the local post data is different than the loaded post data. |
379 if ( $( '#wp-content-wrap' ).hasClass( 'tmce-active' ) ) { |
379 if ( $( '#wp-content-wrap' ).hasClass( 'tmce-active' ) ) { |
380 |
380 |
381 // If TinyMCE loads first, check the post 1.5 sec. after it is ready. |
381 /* |
382 // By this time the content has been loaded in the editor and 'saved' to the textarea. |
382 * If TinyMCE loads first, check the post 1.5 seconds after it is ready. |
383 // This prevents false positives. |
383 * By this time the content has been loaded in the editor and 'saved' to the textarea. |
|
384 * This prevents false positives. |
|
385 */ |
384 $document.on( 'tinymce-editor-init.autosave', function() { |
386 $document.on( 'tinymce-editor-init.autosave', function() { |
385 window.setTimeout( function() { |
387 window.setTimeout( function() { |
386 checkPost(); |
388 checkPost(); |
387 }, 1500 ); |
389 }, 1500 ); |
388 }); |
390 }); |
389 } else { |
391 } else { |
390 checkPost(); |
392 checkPost(); |
391 } |
393 } |
392 |
394 |
393 // Save every 15 sec. |
395 // Save every 15 seconds. |
394 intervalTimer = window.setInterval( save, 15000 ); |
396 intervalTimer = window.setInterval( save, 15000 ); |
395 |
397 |
396 $( 'form#post' ).on( 'submit.autosave-local', function() { |
398 $( 'form#post' ).on( 'submit.autosave-local', function() { |
397 var editor = getEditor(), |
399 var editor = getEditor(), |
398 post_id = $('#post_ID').val() || 0; |
400 post_id = $('#post_ID').val() || 0; |
425 * |
427 * |
426 * @since 3.9.0 |
428 * @since 3.9.0 |
427 * |
429 * |
428 * @param {string} str1 The first string. |
430 * @param {string} str1 The first string. |
429 * @param {string} str2 The second string. |
431 * @param {string} str2 The second string. |
430 * @returns {boolean} True if the strings are the same. |
432 * @return {boolean} True if the strings are the same. |
431 */ |
433 */ |
432 function compare( str1, str2 ) { |
434 function compare( str1, str2 ) { |
433 function removeSpaces( string ) { |
435 function removeSpaces( string ) { |
434 return string.toString().replace(/[\x20\t\r\n\f]+/g, ''); |
436 return string.toString().replace(/[\x20\t\r\n\f]+/g, ''); |
435 } |
437 } |
443 * |
445 * |
444 * Shows a standard message letting the user restore the post data if different. |
446 * Shows a standard message letting the user restore the post data if different. |
445 * |
447 * |
446 * @since 3.9.0 |
448 * @since 3.9.0 |
447 * |
449 * |
448 * @returns {void} |
450 * @return {void} |
449 */ |
451 */ |
450 function checkPost() { |
452 function checkPost() { |
451 var content, post_title, excerpt, $notice, |
453 var content, post_title, excerpt, $notice, |
452 postData = getSavedPostData(), |
454 postData = getSavedPostData(), |
453 cookie = wpCookies.get( 'wp-saving-post' ), |
455 cookie = wpCookies.get( 'wp-saving-post' ), |
454 $newerAutosaveNotice = $( '#has-newer-autosave' ).parent( '.notice' ), |
456 $newerAutosaveNotice = $( '#has-newer-autosave' ).parent( '.notice' ), |
455 $headerEnd = $( '.wp-header-end' ); |
457 $headerEnd = $( '.wp-header-end' ); |
456 |
458 |
457 if ( cookie === post_id + '-saved' ) { |
459 if ( cookie === post_id + '-saved' ) { |
458 wpCookies.remove( 'wp-saving-post' ); |
460 wpCookies.remove( 'wp-saving-post' ); |
459 // The post was saved properly, remove old data and bail |
461 // The post was saved properly, remove old data and bail. |
460 setData( false ); |
462 setData( false ); |
461 return; |
463 return; |
462 } |
464 } |
463 |
465 |
464 if ( ! postData ) { |
466 if ( ! postData ) { |
511 * |
513 * |
512 * @since 3.9.0 |
514 * @since 3.9.0 |
513 * |
515 * |
514 * @param {Object} postData The object containing all post data. |
516 * @param {Object} postData The object containing all post data. |
515 * |
517 * |
516 * @returns {boolean} True if the post is restored. |
518 * @return {boolean} True if the post is restored. |
517 */ |
519 */ |
518 function restorePost( postData ) { |
520 function restorePost( postData ) { |
519 var editor; |
521 var editor; |
520 |
522 |
521 if ( postData ) { |
523 if ( postData ) { |
522 // Set the last saved data |
524 // Set the last saved data. |
523 lastCompareString = getCompareString( postData ); |
525 lastCompareString = getCompareString( postData ); |
524 |
526 |
525 if ( $( '#title' ).val() !== postData.post_title ) { |
527 if ( $( '#title' ).val() !== postData.post_title ) { |
526 $( '#title' ).focus().val( postData.post_title || '' ); |
528 $( '#title' ).focus().val( postData.post_title || '' ); |
527 } |
529 } |
532 if ( editor && ! editor.isHidden() && typeof switchEditors !== 'undefined' ) { |
534 if ( editor && ! editor.isHidden() && typeof switchEditors !== 'undefined' ) { |
533 if ( editor.settings.wpautop && postData.content ) { |
535 if ( editor.settings.wpautop && postData.content ) { |
534 postData.content = switchEditors.wpautop( postData.content ); |
536 postData.content = switchEditors.wpautop( postData.content ); |
535 } |
537 } |
536 |
538 |
537 // Make sure there's an undo level in the editor |
539 // Make sure there's an undo level in the editor. |
538 editor.undoManager.transact( function() { |
540 editor.undoManager.transact( function() { |
539 editor.setContent( postData.content || '' ); |
541 editor.setContent( postData.content || '' ); |
540 editor.nodeChanged(); |
542 editor.nodeChanged(); |
541 }); |
543 }); |
542 } else { |
544 } else { |
543 |
545 |
544 // Make sure the Text editor is selected |
546 // Make sure the Text editor is selected. |
545 $( '#content-html' ).click(); |
547 $( '#content-html' ).click(); |
546 $( '#content' ).focus(); |
548 $( '#content' ).focus(); |
547 |
549 |
548 // Using document.execCommand() will let the user undo. |
550 // Using document.execCommand() will let the user undo. |
549 document.execCommand( 'selectAll' ); |
551 document.execCommand( 'selectAll' ); |
556 return false; |
558 return false; |
557 } |
559 } |
558 |
560 |
559 blog_id = typeof window.autosaveL10n !== 'undefined' && window.autosaveL10n.blog_id; |
561 blog_id = typeof window.autosaveL10n !== 'undefined' && window.autosaveL10n.blog_id; |
560 |
562 |
561 // Check if the browser supports sessionStorage and it's not disabled, |
563 /* |
562 // then initialize and run checkPost(). |
564 * Check if the browser supports sessionStorage and it's not disabled, |
563 // Don't run if the post type supports neither 'editor' (textarea#content) nor 'excerpt'. |
565 * then initialize and run checkPost(). |
|
566 * Don't run if the post type supports neither 'editor' (textarea#content) nor 'excerpt'. |
|
567 */ |
564 if ( checkStorage() && blog_id && ( $('#content').length || $('#excerpt').length ) ) { |
568 if ( checkStorage() && blog_id && ( $('#content').length || $('#excerpt').length ) ) { |
565 $document.ready( run ); |
569 $document.ready( run ); |
566 } |
570 } |
567 |
571 |
568 return { |
572 return { |
577 /** |
581 /** |
578 * Auto saves the post on the server. |
582 * Auto saves the post on the server. |
579 * |
583 * |
580 * @since 3.9.0 |
584 * @since 3.9.0 |
581 * |
585 * |
582 * @returns {Object} { |
586 * @return {Object} { |
583 * { |
587 * { |
584 * tempBlockSave: tempBlockSave, |
588 * tempBlockSave: tempBlockSave, |
585 * triggerSave: triggerSave, |
589 * triggerSave: triggerSave, |
586 * postChanged: postChanged, |
590 * postChanged: postChanged, |
587 * suspend: suspend, |
591 * suspend: suspend, |
691 * If the post hasn't changed or it cannot be updated, |
695 * If the post hasn't changed or it cannot be updated, |
692 * because the autosave is blocked or suspended, the function returns false. |
696 * because the autosave is blocked or suspended, the function returns false. |
693 * |
697 * |
694 * @since 3.9.0 |
698 * @since 3.9.0 |
695 * |
699 * |
696 * @returns {Object} Returns the post data. |
700 * @return {Object} Returns the post data. |
697 */ |
701 */ |
698 function save() { |
702 function save() { |
699 var postData, compareString; |
703 var postData, compareString; |
700 |
704 |
701 // window.autosave() used for back-compat |
705 // window.autosave() used for back-compat. |
702 if ( isSuspended || _blockSave || ! window.autosave() ) { |
706 if ( isSuspended || _blockSave || ! window.autosave() ) { |
703 return false; |
707 return false; |
704 } |
708 } |
705 |
709 |
706 if ( ( new Date() ).getTime() < nextRun ) { |
710 if ( ( new Date() ).getTime() < nextRun ) { |
708 } |
712 } |
709 |
713 |
710 postData = getPostData(); |
714 postData = getPostData(); |
711 compareString = getCompareString( postData ); |
715 compareString = getCompareString( postData ); |
712 |
716 |
713 // First check |
717 // First check. |
714 if ( typeof lastCompareString === 'undefined' ) { |
718 if ( typeof lastCompareString === 'undefined' ) { |
715 lastCompareString = initialCompareString; |
719 lastCompareString = initialCompareString; |
716 } |
720 } |
717 |
721 |
718 // No change |
722 // No change. |
719 if ( compareString === lastCompareString ) { |
723 if ( compareString === lastCompareString ) { |
720 return false; |
724 return false; |
721 } |
725 } |
722 |
726 |
723 previousCompareString = compareString; |
727 previousCompareString = compareString; |
737 * |
741 * |
738 * @private |
742 * @private |
739 * |
743 * |
740 * @since 3.9.0 |
744 * @since 3.9.0 |
741 * |
745 * |
742 * @returns {void} |
746 * @return {void} |
743 */ |
747 */ |
744 function _schedule() { |
748 function _schedule() { |
745 nextRun = ( new Date() ).getTime() + ( autosaveL10n.autosaveInterval * 1000 ) || 60000; |
749 nextRun = ( new Date() ).getTime() + ( autosaveL10n.autosaveInterval * 1000 ) || 60000; |
746 } |
750 } |
747 |
751 |
748 /** |
752 /** |
749 * Sets the autosaveData on the autosave heartbeat. |
753 * Sets the autosaveData on the autosave heartbeat. |
750 * |
754 * |
751 * @since 3.9.0 |
755 * @since 3.9.0 |
752 * |
756 * |
753 * @returns {void} |
757 * @return {void} |
754 */ |
758 */ |
755 $document.on( 'heartbeat-send.autosave', function( event, data ) { |
759 $document.on( 'heartbeat-send.autosave', function( event, data ) { |
756 var autosaveData = save(); |
760 var autosaveData = save(); |
757 |
761 |
758 if ( autosaveData ) { |
762 if ( autosaveData ) { |
763 * Triggers the autosave of the post with the autosave data on the autosave |
767 * Triggers the autosave of the post with the autosave data on the autosave |
764 * heartbeat. |
768 * heartbeat. |
765 * |
769 * |
766 * @since 3.9.0 |
770 * @since 3.9.0 |
767 * |
771 * |
768 * @returns {void} |
772 * @return {void} |
769 */ |
773 */ |
770 }).on( 'heartbeat-tick.autosave', function( event, data ) { |
774 }).on( 'heartbeat-tick.autosave', function( event, data ) { |
771 if ( data.wp_autosave ) { |
775 if ( data.wp_autosave ) { |
772 response( data.wp_autosave ); |
776 response( data.wp_autosave ); |
773 } |
777 } |
774 /** |
778 /** |
775 * Disables buttons and throws a notice when the connection is lost. |
779 * Disables buttons and throws a notice when the connection is lost. |
776 * |
780 * |
777 * @since 3.9.0 |
781 * @since 3.9.0 |
778 * |
782 * |
779 * @returns {void} |
783 * @return {void} |
780 */ |
784 */ |
781 }).on( 'heartbeat-connection-lost.autosave', function( event, error, status ) { |
785 }).on( 'heartbeat-connection-lost.autosave', function( event, error, status ) { |
782 |
786 |
783 // When connection is lost, keep user from submitting changes. |
787 // When connection is lost, keep user from submitting changes. |
784 if ( 'timeout' === error || 603 === status ) { |
788 if ( 'timeout' === error || 603 === status ) { |
823 * This avoids any insignificant differences between the initial textarea content and the content |
827 * This avoids any insignificant differences between the initial textarea content and the content |
824 * extracted from the editor. |
828 * extracted from the editor. |
825 * |
829 * |
826 * @since 3.9.0 |
830 * @since 3.9.0 |
827 * |
831 * |
828 * @returns {void} |
832 * @return {void} |
829 */ |
833 */ |
830 $document.on( 'tinymce-editor-init.autosave', function( event, editor ) { |
834 $document.on( 'tinymce-editor-init.autosave', function( event, editor ) { |
831 if ( editor.id === 'content' ) { |
835 if ( editor.id === 'content' ) { |
832 window.setTimeout( function() { |
836 window.setTimeout( function() { |
833 editor.save(); |
837 editor.save(); |
834 initialCompareString = getCompareString(); |
838 initialCompareString = getCompareString(); |
835 }, 1000 ); |
839 }, 1000 ); |
836 } |
840 } |
837 }).ready( function() { |
841 }).ready( function() { |
838 |
842 |
839 // Set the initial compare string in case TinyMCE is not used or not loaded first |
843 // Set the initial compare string in case TinyMCE is not used or not loaded first. |
840 initialCompareString = getCompareString(); |
844 initialCompareString = getCompareString(); |
841 }); |
845 }); |
842 |
846 |
843 return { |
847 return { |
844 getPostData: getPostData, |
848 getPostData: getPostData, |