| | 1 | // Autosave in localStorage |
| | 2 | // set as simple object/mixin for now |
| | 3 | window.wp = window.wp || {}; |
| | 4 | wp.autosave = wp.autosave || {}; |
| | 5 | |
| | 6 | (function($){ |
| | 7 | // Returns only post ID, title, content and excerpt for local autosaves, the rest of the fields for remote |
| | 8 | wp.autosave.getPostData = function( local ) { |
| | 9 | var ed = typeof tinymce != 'undefined' ? tinymce.activeEditor : null, post_name, parent_id, cats = [], |
| | 10 | data = { |
| | 11 | autosave: true, |
| | 12 | post_id: $('#post_ID').val() || 0 |
| | 13 | }; |
| | 14 | |
| | 15 | if ( ed && !ed.isHidden() ) { |
| | 16 | // Don't run while the tinymce spellcheck is on. It resets all found words. |
| | 17 | if ( ed.plugins.spellchecker && ed.plugins.spellchecker.active ) { |
| | 18 | data.autosave = false; |
| | 19 | return data; |
| | 20 | } else { |
| | 21 | if ( 'mce_fullscreen' == ed.id ) |
| | 22 | tinymce.get('content').setContent(ed.getContent({format : 'raw'}), {format : 'raw'}); |
| | 23 | |
| | 24 | tinymce.triggerSave(); |
| | 25 | } |
| | 26 | } |
| | 27 | |
| | 28 | if ( typeof fullscreen != 'undefined' && fullscreen.settings.visible ) { |
| | 29 | data['post_title'] = $('#wp-fullscreen-title').val() || ''; |
| | 30 | data['content'] = $('#wp_mce_fullscreen').val() || ''; |
| | 31 | } else { |
| | 32 | data['post_title'] = $('#title').val() || ''; |
| | 33 | data['content'] = $('#content').val() || ''; |
| | 34 | } |
| | 35 | |
| | 36 | data['excerpt'] = $('#excerpt').val() || ''; |
| | 37 | |
| | 38 | if ( local ) |
| | 39 | return data; |
| | 40 | |
| | 41 | $.extend( data, { |
| | 42 | action: 'autosave', |
| | 43 | autosavenonce: $('#autosavenonce').val() || '', |
| | 44 | post_type: $('#post_type').val() || '', |
| | 45 | post_author: $('#post_author').val() || '' |
| | 46 | }); |
| | 47 | |
| | 48 | $('.tags-input').each( function() { |
| | 49 | data[this.name] = this.value; |
| | 50 | }); |
| | 51 | |
| | 52 | $('input[id^="in-category-"]:checked').each( function() { |
| | 53 | cats.push(this.value); |
| | 54 | }); |
| | 55 | data['catslist'] = cats.join(','); |
| | 56 | |
| | 57 | if ( post_name = $('#post_name').val() ) |
| | 58 | data['post_name'] = post_name; |
| | 59 | |
| | 60 | if ( parent_id = $('#parent_id').val() ) |
| | 61 | data['parent_id'] = parent_id; |
| | 62 | |
| | 63 | if ( $('#comment_status').prop('checked') ) |
| | 64 | data['comment_status'] = 'open'; |
| | 65 | |
| | 66 | if ( $('#ping_status').prop('checked') ) |
| | 67 | data['ping_status'] = 'open'; |
| | 68 | |
| | 69 | if ( $('#auto_draft').val() == '1' ) |
| | 70 | data['auto_draft'] = '1'; |
| | 71 | |
| | 72 | return data; |
| | 73 | } |
| | 74 | |
| | 75 | wp.autosave.local = { |
| | 76 | |
| | 77 | lastsaveddata: '', |
| | 78 | unexpired: {}, |
| | 79 | blog_id: typeof window.autosaveLocal != 'undefined' ? window.autosaveLocal.blog_id : 0, |
| | 80 | |
| | 81 | // Check if the browser supports localStorage and it's not disabled |
| | 82 | hasStorage: (function() { |
| | 83 | var test = Math.random(), result; |
| | 84 | |
| | 85 | try { |
| | 86 | localStorage.setItem('wp-test', test); |
| | 87 | result = localStorage.getItem('wp-test') == test; |
| | 88 | localStorage.removeItem('wp-test'); |
| | 89 | return result; |
| | 90 | } catch(e) { |
| | 91 | return false; |
| | 92 | } |
| | 93 | }()), |
| | 94 | |
| | 95 | /** |
| | 96 | * Initialize the local storage |
| | 97 | * @return mixed simple object or false if no localStorage in the browser |
| | 98 | */ |
| | 99 | getStorage: function() { |
| | 100 | var storage = false; |
| | 101 | |
| | 102 | // separate local storage containers for each blog_id |
| | 103 | if ( this.hasStorage && this.blog_id ) { |
| | 104 | storage = localStorage.getItem( 'wp_autosave_' + this.blog_id ); |
| | 105 | |
| | 106 | if ( storage ) |
| | 107 | storage = JSON.parse( storage ); |
| | 108 | else |
| | 109 | storage = {}; |
| | 110 | } |
| | 111 | |
| | 112 | return storage; |
| | 113 | }, |
| | 114 | |
| | 115 | setStorage: function( storage_obj ) { |
| | 116 | if ( this.hasStorage ) |
| | 117 | return localStorage.setItem( 'wp_autosave_' + this.blog_id, JSON.stringify( storage_obj ) ); |
| | 118 | }, |
| | 119 | |
| | 120 | getData: function( post_id ) { |
| | 121 | var storage = this.getStorage(); |
| | 122 | |
| | 123 | post_id = post_id || $('#post_ID').val(); |
| | 124 | |
| | 125 | if ( !storage || !post_id ) |
| | 126 | return false; |
| | 127 | |
| | 128 | return storage[ 'post_' + post_id ] || []; |
| | 129 | }, |
| | 130 | |
| | 131 | setData: function( stored_data ) { |
| | 132 | var storage = this.getStorage(), post_id = $('#post_ID').val(), result; |
| | 133 | |
| | 134 | if ( !storage || !post_id ) |
| | 135 | return false; |
| | 136 | |
| | 137 | storage[ 'post_' + post_id ] = stored_data; |
| | 138 | result = this.setStorage(storage); |
| | 139 | |
| | 140 | if ( result === false ) { |
| | 141 | // localStorage is full, try running the cleanup to free some space |
| | 142 | this.cleanup(); |
| | 143 | result = this.setStorage(storage); |
| | 144 | } |
| | 145 | |
| | 146 | if ( result === false ) { |
| | 147 | // localStorage is still full, show a warning? |
| | 148 | // A warning will need a good explanation how the user can empty the storage or give more space, different for each browser... |
| | 149 | return false; |
| | 150 | } |
| | 151 | |
| | 152 | return true; |
| | 153 | }, |
| | 154 | |
| | 155 | /** |
| | 156 | * Set post data for particular post id |
| | 157 | */ |
| | 158 | save: function() { |
| | 159 | var stored_data, post_data, result = false; |
| | 160 | |
| | 161 | // Prepares data for saving in local storage. |
| | 162 | post_data = wp.autosave.getPostData( true ); |
| | 163 | |
| | 164 | // If the content and title are empty or did not change since the last save, don't save again |
| | 165 | if ( ( ! post_data.post_title && ! post_data.content ) || post_data.post_title + post_data.content + post_data.excerpt == this.lastsaveddata ) |
| | 166 | return false; |
| | 167 | |
| | 168 | // Cannot get the post data at this moment |
| | 169 | if ( !post_data.autosave ) |
| | 170 | return false; |
| | 171 | |
| | 172 | $.extend( post_data, { |
| | 173 | save_time: (new Date()).getTime(), |
| | 174 | storage_status: 'fresh' |
| | 175 | }); |
| | 176 | |
| | 177 | stored_data = this.getData(); |
| | 178 | |
| | 179 | if ( stored_data ) { |
| | 180 | if ( stored_data.length > 4 ) { |
| | 181 | // Store only the 5 newest revisions |
| | 182 | stored_data.pop(); |
| | 183 | } |
| | 184 | |
| | 185 | stored_data.unshift( post_data ); |
| | 186 | result = this.setData( stored_data ); |
| | 187 | |
| | 188 | if ( result ) { |
| | 189 | this.lastsaveddata = post_data.post_title + post_data.content; |
| | 190 | // Update the display of local backups as they are added |
| | 191 | this.showItems(); |
| | 192 | } |
| | 193 | } |
| | 194 | |
| | 195 | return result; |
| | 196 | }, |
| | 197 | |
| | 198 | /** |
| | 199 | * Checks if all revisions have expired. Removes expired revisions from local storage after 24 hours. |
| | 200 | */ |
| | 201 | cleanup: function() { |
| | 202 | var self = this, storage = this.getStorage(), current_time = (new Date()).getTime(), deleted = false, has_unexpired = false; |
| | 203 | |
| | 204 | if ( !storage ) |
| | 205 | return; |
| | 206 | |
| | 207 | $.each( storage, function( key, value ) { |
| | 208 | var new_arr; |
| | 209 | |
| | 210 | if ( !value ) |
| | 211 | return; |
| | 212 | |
| | 213 | new_arr = $.grep( value, function( post_data ) { |
| | 214 | if ( post_data.storage_status ) { |
| | 215 | if ( !self.unexpired[key] ) |
| | 216 | self.unexpired[key] = post_data; |
| | 217 | |
| | 218 | has_unexpired = true; |
| | 219 | return true; |
| | 220 | } |
| | 221 | // keep if less than 24 hr |
| | 222 | if ( ( current_time - post_data.save_time ) < 86400000 ) |
| | 223 | return true; |
| | 224 | |
| | 225 | return false; |
| | 226 | }); |
| | 227 | |
| | 228 | if ( new_arr.length != value.length ) |
| | 229 | deleted = true; |
| | 230 | |
| | 231 | if ( new_arr.length ) |
| | 232 | storage[key] = new_arr; |
| | 233 | else |
| | 234 | delete storage[key]; |
| | 235 | }); |
| | 236 | |
| | 237 | if ( deleted ) |
| | 238 | this.setStorage( storage ); |
| | 239 | |
| | 240 | if ( has_unexpired && !this.warning_done ) { |
| | 241 | // show warning about unexpired post data |
| | 242 | this.showWarning(); |
| | 243 | this.warning_done = true; |
| | 244 | } |
| | 245 | }, |
| | 246 | |
| | 247 | /** |
| | 248 | * Marks post data as expired |
| | 249 | * |
| | 250 | * For use after remote autosave has completed |
| | 251 | */ |
| | 252 | expire: function( key ) { |
| | 253 | var stored_data = this.getData(); |
| | 254 | |
| | 255 | if ( ! isNaN( key ) ) { |
| | 256 | delete stored_data[key].storage_status; |
| | 257 | } else { |
| | 258 | $.each( stored_data, function( key, post_data ) { |
| | 259 | delete post_data.storage_status; |
| | 260 | }); |
| | 261 | } |
| | 262 | |
| | 263 | return this.setData( stored_data ); |
| | 264 | }, |
| | 265 | |
| | 266 | showItems: function() { |
| | 267 | var self = this, data; |
| | 268 | |
| | 269 | function formatTime( milisec ) { |
| | 270 | var sec = parseInt( milisec / 1000, 10 ) || 0, |
| | 271 | h = Math.floor( sec / 3600 ), |
| | 272 | m = Math.floor( (sec % 3600) / 60 ), |
| | 273 | s = sec - ( (h * 3600) + (m * 60) ); |
| | 274 | |
| | 275 | function zeroize(n) { |
| | 276 | if ( !n ) |
| | 277 | return '00'; |
| | 278 | |
| | 279 | if ( n < 10 ) |
| | 280 | return '0' + n.toString(); |
| | 281 | |
| | 282 | return n.toString(); |
| | 283 | } |
| | 284 | |
| | 285 | return { |
| | 286 | h: zeroize(h), |
| | 287 | m: zeroize(m), |
| | 288 | s: zeroize(s) |
| | 289 | }; |
| | 290 | } |
| | 291 | |
| | 292 | if ( window.pagenow && window.pagenow == 'post' ) { |
| | 293 | data = this.getData(), current_time = (new Date()).getTime(); |
| | 294 | |
| | 295 | if ( data && data.length ) { |
| | 296 | $.each( data, function( key, post_data ) { |
| | 297 | var timeSaved = formatTime( current_time - post_data.save_time ), |
| | 298 | element = $('#localsave-items #localsave-item-' + key); |
| | 299 | |
| | 300 | $('.localsave-no-revisions').remove(); |
| | 301 | |
| | 302 | // can do something here with post_data.storage_status to show non-expired |
| | 303 | // (not saved to the server) backups with different color, etc. |
| | 304 | |
| | 305 | if ( ! element.length ) { |
| | 306 | element = $('.localsave-item-main').clone().removeClass('localsave-item-main').attr( 'id', 'localsave-item-' + key ); |
| | 307 | |
| | 308 | $('.localsave-text', element).html( $('.localsave-text', element).text().replace( /%s/, function(){ |
| | 309 | return '<span class="localsave-h">' + timeSaved.h + '</span>:' + |
| | 310 | '<span class="localsave-m">' + timeSaved.m + '</span>:' + |
| | 311 | '<span class="localsave-s">' + timeSaved.s + '</span>'; |
| | 312 | } ) ); |
| | 313 | |
| | 314 | $('#localsave-items').append( element.data('key', key).show() ); |
| | 315 | } else { |
| | 316 | element.data('key', key); |
| | 317 | $('.localsave-h', element).text( timeSaved.h ); |
| | 318 | $('.localsave-m', element).text( timeSaved.m ); |
| | 319 | $('.localsave-s', element).text( timeSaved.s ); |
| | 320 | } |
| | 321 | }); |
| | 322 | } |
| | 323 | } |
| | 324 | }, |
| | 325 | |
| | 326 | preview: function( key ) { |
| | 327 | var data = this.getData(), content, element; |
| | 328 | |
| | 329 | if ( !data[key] ) |
| | 330 | return; // error message? |
| | 331 | |
| | 332 | this.expire( key ); |
| | 333 | content = data[key].content; |
| | 334 | |
| | 335 | if ( $('#wp-content-wrap').hasClass('tmce-active') && typeof switchEditors != 'undefined' ) { |
| | 336 | content = '<div>' + switchEditors.wpautop( content ) + '</div>'; |
| | 337 | } else if ( $('#wp-content-wrap').hasClass('html-active') ) |
| | 338 | content = '<textarea readonly>' + this.esc_html( content ) + '</textarea>'; |
| | 339 | |
| | 340 | element = $('.localsave-preview-wrap').show(); |
| | 341 | element.find('div.localsave-preview').html( content ); |
| | 342 | element.find('a.localsave-do-restore').data('key', key); |
| | 343 | $('#localsave-item-' + key).append( element ); |
| | 344 | }, |
| | 345 | |
| | 346 | closePreview: function() { |
| | 347 | var element = $('.localsave-preview-wrap').css('display', ''); |
| | 348 | |
| | 349 | element.find('div.localsave-preview').empty(); |
| | 350 | element.find('a.localsave-do-restore').removeData('key'); |
| | 351 | $('#localsave-items').append( element ); |
| | 352 | }, |
| | 353 | |
| | 354 | restore: function( key ) { |
| | 355 | var data = this.getData(), content, editor; |
| | 356 | |
| | 357 | if ( !data[key] ) |
| | 358 | return; // error message? |
| | 359 | |
| | 360 | content = data[key].content; |
| | 361 | |
| | 362 | if ( typeof tinymce != 'undefined' ) |
| | 363 | editor = tinymce.get('content'); |
| | 364 | |
| | 365 | if ( typeof switchEditors != 'undefined' && editor && ! editor.isHidden() ) { |
| | 366 | editor.setContent( switchEditors.wpautop( content ) ); |
| | 367 | } else if ( ( editor = $('textarea#content') ) && editor.length ) { |
| | 368 | editor.val( content ); |
| | 369 | } |
| | 370 | |
| | 371 | this.closePreview(); |
| | 372 | }, |
| | 373 | |
| | 374 | init: function() { |
| | 375 | var self = this, post_data; |
| | 376 | |
| | 377 | $('#localsave-items').on( 'click.localsave-items', function(e) { |
| | 378 | var target = $(e.target); |
| | 379 | |
| | 380 | if ( target.hasClass('localsave-text') ) { |
| | 381 | self.preview( target.parent().data('key') ); |
| | 382 | } else if ( target.hasClass('localsave-do-close') ) { |
| | 383 | self.closePreview(); |
| | 384 | } else if ( target.hasClass('localsave-do-restore') ) { |
| | 385 | self.restore( target.data('key') ); |
| | 386 | } |
| | 387 | // Stop all links, including links in the preview |
| | 388 | e.preventDefault(); |
| | 389 | }); |
| | 390 | |
| | 391 | post_data = wp.autosave.getPostData( true ); |
| | 392 | // Set the comparison string |
| | 393 | if ( $('#wp-content-wrap').hasClass('tmce-active') && typeof switchEditors != 'undefined' ) |
| | 394 | this.lastsaveddata = post_data.post_title + switchEditors.pre_wpautop(post_data.content); |
| | 395 | else |
| | 396 | this.lastsaveddata = post_data.post_title + post_data.content; |
| | 397 | |
| | 398 | // Set the schedule |
| | 399 | this.schedule = $.schedule({ |
| | 400 | time: 15 * 1000, |
| | 401 | func: function() { wp.autosave.local.save(); }, |
| | 402 | repeat: true, |
| | 403 | protect: true |
| | 404 | }); |
| | 405 | |
| | 406 | $('form#post').on( 'submit.localsave-submit', function() { |
| | 407 | self.expire(); |
| | 408 | $.cancel( self.schedule ); |
| | 409 | }); |
| | 410 | |
| | 411 | // Show the local revisions |
| | 412 | this.showItems(); |
| | 413 | }, |
| | 414 | |
| | 415 | /** |
| | 416 | * Escape html so we can show in div |
| | 417 | * @param string str |
| | 418 | * @return string |
| | 419 | */ |
| | 420 | esc_html: function( str ) { |
| | 421 | return $('<div/>').text(str).html(); |
| | 422 | }, |
| | 423 | |
| | 424 | /** |
| | 425 | * Show warning that one has historical saved data available for recovery in case one wants. |
| | 426 | */ |
| | 427 | showWarning: function() { |
| | 428 | var self = this, current_post_id = $('#post_ID').val() || '', html = '', same_screen = false; |
| | 429 | |
| | 430 | $.each( this.unexpired, function( key, post_data ) { |
| | 431 | var title = post_data.post_title || '#' + post_data.post_id; |
| | 432 | |
| | 433 | if ( window.pagenow && window.pagenow == 'post' && current_post_id == post_data.post_id ) { |
| | 434 | html += ' <span class="localsave-recover"> <a class="localsave-recover-local" href="#revisionsdiv">' + self.esc_html(title) + '</a> </span> '; |
| | 435 | same_screen = true; |
| | 436 | } else { |
| | 437 | html += ' <span class="localsave-recover"> <a href="post.php?post='+post_data.post_id+'&action=edit&localsave-recover=1#revisionsdiv">' + |
| | 438 | self.esc_html(title) + '</a> </span> '; |
| | 439 | } |
| | 440 | }); |
| | 441 | |
| | 442 | if ( html && window.autosaveLocal ) { |
| | 443 | $('#wpbody .wrap h2:first').after( |
| | 444 | '<div class="error" id="localsave-warning">' + |
| | 445 | '<p>' + window.autosaveLocal.warning + html + |
| | 446 | '</p></div>' |
| | 447 | ); |
| | 448 | |
| | 449 | if ( same_screen ) { |
| | 450 | $('#localsave-warning a.localsave-recover-local').click( function(){ |
| | 451 | // Make sure the Revisions postbox is open and not hidden |
| | 452 | $('#revisionsdiv').removeClass('closed').show(); |
| | 453 | // Hide the warning |
| | 454 | $('#localsave-warning').slideUp('fast', function(){ $(this).remove(); }); |
| | 455 | }); |
| | 456 | } |
| | 457 | } |
| | 458 | } |
| | 459 | } |
| | 460 | |
| | 461 | $(document).ready( function() { |
| | 462 | wp.autosave.local.cleanup(); |
| | 463 | if ( window.pagenow && 'post' == window.pagenow ) |
| | 464 | wp.autosave.local.init(); |
| | 465 | }); |
| | 466 | |
| | 467 | }(jQuery)); |