Ticket #23216: 23216-heartbeat-cleanup.diff
File 23216-heartbeat-cleanup.diff, 38.6 KB (added by , 11 years ago) |
---|
-
wp-includes/js/heartbeat.js
1 /** 2 * Heartbeat API 3 * 4 * Heartbeat is a simple server polling API that sends XHR requests to 5 * the server every 15 seconds and triggers events (or callbacks) upon 6 * receiving data. Currently these 'ticks' handle transports for post locking, 7 * login-expiration warnings, and related tasks while a user is logged in. 8 * 9 * Available filters in ajax-actions.php: 10 * - heartbeat_received 11 * - heartbeat_send 12 * - heartbeat_tick 13 * - heartbeat_nopriv_received 14 * - heartbeat_nopriv_send 15 * - heartbeat_nopriv_tick 16 * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat() 17 * 18 * @since 3.6.0 19 */ 1 ( function( window, $, undefined ) { 20 2 21 // Ensure the global `wp` object exists. 22 window.wp = window.wp || {};3 // pull the correct document into scope 4 var document = window.document; 23 5 24 (function($){ 25 var Heartbeat = function() { 26 var self = this, 27 running, 28 beat, 29 screenId = typeof pagenow != 'undefined' ? pagenow : '', 30 url = typeof ajaxurl != 'undefined' ? ajaxurl : '', 31 settings, 32 tick = 0, 33 queue = {}, 34 interval, 35 connecting, 36 countdown = 0, 37 errorcount = 0, 38 tempInterval, 39 hasFocus = true, 40 isUserActive, 41 userActiveEvents, 42 winBlurTimeout, 43 frameBlurTimeout = -1; 6 /** 7 * Heartbeat API 8 * 9 * Heartbeat is a simple server polling API that sends XHR requests to the server every 15 seconds and triggers events 10 * (or callbacks) upon receiving data. Currently these 'ticks' handle transports for post locking, login-expiration 11 * warnings, and related tasks while a user is logged in. 12 * 13 * todo: if this code is run multiple times in different iframes, there could be a problem with iframes becoming 14 * inactive but activity occurring in the main window which is really weird... To fix this, we need to check if the 15 * current window is the top window just like ads do. This could either be a major bug OR something that's intended 16 * but either way should be confirmed ASAP. 17 * 18 * Available filters in ajax-actions.php: 19 * heartbeat_received 20 * heartbeat_send 21 * heartbeat_tick 22 * heartbeat_nopriv_received 23 * heartbeat_nopriv_send 24 * heartbeat_nopriv_tick 25 * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat() 26 * 27 * @since 3.6.0 28 * @type {*|{}} 29 */ 30 function Heartbeat() { 44 31 45 this.autostart = true; 46 this.connectionLost = false; 32 /** 33 * Container for all of the cached objects that we'll need references to later 34 * 35 * @type {{$document: null, $window: null, windowBlurTimer: null, frameBlurTimer: null, heartbeatTimer: null, ajaxRequest: null}} 36 * @private 37 */ 38 var _Cache = { 39 $document : null, 40 $window : null, 41 windowBlurTimer : null, 42 frameBlurTimer : null, 43 heartbeatTimer : null, 44 ajaxRequest : null 45 }; 47 46 48 if ( typeof( window.heartbeatSettings ) == 'object' ) {49 settings = $.extend( {}, window.heartbeatSettings );50 47 51 // Add private vars 52 url = settings.ajaxurl || url; 53 delete settings.ajaxurl; 54 delete settings.nonce; 48 /** 49 * Container for all of the settings that this module needs to track 50 * 51 * @type {{shouldAutoStart: boolean, hasActiveConnection: boolean, errorCountSinceConnected: number, ajaxURL: string, nonce: string, heartbeatInterval: number, temporaryHeartbeatInterval: number, screenId: string, hasFocus: boolean, userIsActive: boolean, lastUserActivityTimestamp: number, isConnecting: boolean, isRunning: boolean, userActivityCheckInterval: number, lastHeartbeatTick: number, countDown: number, ajaxRequestTimeout: number}} 52 * @private 53 */ 54 var _Settings = { 55 shouldAutoStart : true, 56 hasActiveConnection : false, 57 errorCountSinceConnected : 0, 58 ajaxURL : "", 59 nonce : "", 60 heartbeatInterval : 15, 61 temporaryHeartbeatInterval : 0, 62 screenId : "", 63 hasFocus : false, 64 userIsActive : false, 65 lastUserActivityTimestamp : _getUnixTimestamp(), 66 isConnecting : false, 67 isRunning : false, 68 userActivityCheckInterval : 30000, 69 lastHeartbeatTick : 0, 70 countDown : 0, 71 ajaxRequestTimeout : 30000 72 }; 55 73 56 interval = settings.interval || 15; // default interval57 delete settings.interval;58 // The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec.59 if ( interval < 15 )60 interval = 15;61 else if ( interval > 60 )62 interval = 60;74 /** 75 * Container for any AJAX data that will sent through the next heartbeat pulse 76 * 77 * @type {{}} 78 * @private 79 */ 80 var _QueuedAjaxData = {}; 63 81 64 interval = interval * 1000; 82 /** 83 * Handles setting up this object so that things are properly setup before it's used anywhere 84 * 85 * @private 86 */ 87 function _initialize() { 88 _buildSettings(); 89 _cacheObjects(); 90 _startCheckingIfTheUserIsActive(); 91 _bindWindowEvents(); 65 92 66 // 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set 67 screenId = screenId || settings.screenId || 'front'; 68 delete settings.screenId; 93 // check to see if we need to automatically start this module 94 if( _Settings.shouldAutoStart === true ) { 95 _Cache.$document.ready( _startAutomaticTick ); 96 } 97 } 69 98 70 // Add or overwrite public vars 71 $.extend( this, settings ); 99 /** 100 * Starts the automatic tick if the application should automatically start. Should only be called from `_initialize()` 101 * 102 * @private 103 */ 104 function _startAutomaticTick() { 105 _Settings.isRunning = true; 106 _Settings.lastHeartbeatTick = _getUnixTimestamp(); 107 _nextTick(); 72 108 } 73 109 74 function time(s) { 75 if ( s ) 76 return parseInt( (new Date()).getTime() / 1000 ); 77 78 return (new Date()).getTime(); 110 /** 111 * Starts the interval timer for checking if the user is currently active or not 112 * 113 * @private 114 */ 115 function _startCheckingIfTheUserIsActive() { 116 setInterval( _checkIfUserIsStillActive, _Settings.userActivityCheckInterval ); 79 117 } 80 118 81 function isLocalFrame( frame ) { 82 var origin, src = frame.src; 119 /** 120 * Handles standardizing our internal settings based on settings that might have been localized from the server. 121 * We didn't extend the global window `heartbeatSettings` object because we don't want to make use of `delete` 122 * and have to manage everything we don't care about. 123 * 124 * @private 125 */ 126 function _buildSettings() { 127 if( typeof window.heartbeatSettings !== "object" ) { 128 return; 129 } 83 130 84 if ( src && /^https?:\/\//.test( src ) ) {85 origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host;131 // temporarily cache the window heartbeatSettings for easier access 132 var tempSettings = window.heartbeatSettings; 86 133 87 if ( src.indexOf( origin ) !== 0 ) 88 return false; 134 // setup the ajax URL 135 _Settings.ajaxURL = window.ajaxurl || ""; 136 _Settings.ajaxURL = tempSettings.ajaxurl || _Settings.ajaxURL; 137 138 // setup the nonce 139 _Settings.nonce = tempSettings.nonce || ""; 140 141 // setup the heartbeat interval - force it to an integer. If the value of tempSettings.interval is incorrect 142 // and cannot be parsed, we still default to 10 143 _Settings.heartbeatInterval = parseInt( tempSettings.interval, 10 ) || _Settings.heartbeatInterval; 144 145 // keep the heartbeat interval within bounds 146 if( _Settings.heartbeatInterval < 15 ) { 147 _Settings.heartbeatInterval = 15; 89 148 } 149 else if( _Settings.heartbeatInterval > 60 ) { 150 _Settings.heartbeatInterval = 60; 151 } 90 152 91 try { 92 if ( frame.contentWindow.document ) 93 return true; 94 } catch(e) {} 153 // make sure the interval is in milliseconds now 154 _Settings.heartbeatInterval *= 1000; 95 155 96 return false; 156 // setup the screenID now 157 // screenId can be added from settings on the front-end where the JS global `pagenow` is not set 158 _Settings.screenId = window.pagenow || ( settings.screenId || "" ); 97 159 } 98 160 99 // Set error state and fire an event on XHR errors or timeout 100 function errorstate( error ) { 101 var trigger; 161 /** 162 * Caches any objects we might be using during the lifetime of this module 163 * 164 * @private 165 */ 166 function _cacheObjects() { 167 _Cache.$document = $( document ); 168 _Cache.$window = $( window ); 169 } 102 170 103 if ( error ) { 104 switch ( error ) { 105 case 'abort': 106 // do nothing 107 break; 108 case 'timeout': 109 // no response for 30 sec. 110 trigger = true; 111 break; 112 case 'parsererror': 113 case 'error': 114 case 'empty': 115 case 'unknown': 116 errorcount++; 171 /** 172 * Gets the current unix timestamp 173 * 174 * @returns {number} 175 */ 176 function _getUnixTimestamp() { 177 return ( new Date() ).getTime(); 178 } 117 179 118 if ( errorcount > 2 ) 119 trigger = true; 180 /** 181 * Handles enqueuing data to be sent along with the next heartbeat pulse. We require a key to be set. Note that 182 * a value is not required in the event that a user might want to dequeue a key from being passed along. 183 * 184 * As the data is sent later, this function doesn't return the XHR response. 185 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: 186 * $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) { 187 * // code 188 * }); 189 * If the same `key` is used more than once, the data is not overwritten when the third argument is `true`. 190 * Use wp.heartbeat.isQueued( 'key' ) to see if any data is already queued for that key. 191 * 192 * @param key Unique string for the data. The handle is used in PHP to receive the data. 193 * @param value The data to send. 194 * @param overwriteExistingData Whether to overwrite existing data in the queue. 195 * @returns {boolean} Whether the data was queued or not. 196 * @private 197 */ 198 function _enqueueData( key, value, overwriteExistingData ) { 199 overwriteExistingData = ( typeof overwriteExistingData === "boolean" ) ? overwriteExistingData : false; 120 200 121 break; 201 if( key !== undefined ) { 202 if( _QueuedAjaxData.hasOwnProperty( key ) === true && overwriteExistingData === false ) { 203 return false; 122 204 } 123 205 124 if ( trigger && ! self.connectionLost ) { 125 self.connectionLost = true; 126 $(document).trigger( 'heartbeat-connection-lost', [error] ); 127 } 128 } else if ( self.connectionLost ) { 129 errorcount = 0; 130 self.connectionLost = false; 131 $(document).trigger( 'heartbeat-connection-restored' ); 206 _QueuedAjaxData[ key ] = value; 207 return true; 132 208 } 209 return false; 133 210 } 134 211 135 function connect() { 136 var send = {}, data, i, empty = true, 137 nonce = typeof window.heartbeatSettings == 'object' ? window.heartbeatSettings.nonce : ''; 138 tick = time(); 212 /** 213 * Check if data with a particular handle is queued. 214 * 215 * @param key The handle for the data 216 * @returns {*} The data queued with that handle or null 217 * @private 218 */ 219 function _dataIsQueued( key ) { 220 return _QueuedAjaxData[ key ]; 221 } 139 222 140 data = $.extend( {}, queue ); 141 // Clear the data queue, anything added after this point will be send on the next tick 142 queue = {}; 223 /** 224 * If a value is specified, this function will set the internal value of the `shouldAutoStart` setting. Always 225 * returns the value of `shouldAutoStart` 226 * 227 * @param value 228 * @returns boolean 229 * @private 230 */ 231 function _shouldAutoStart( value ) { 232 if( typeof value === "boolean" ) { 233 return _Settings.shouldAutoStart = value; 234 } 143 235 144 $(document).trigger( 'heartbeat-send', [data] ); 236 return _Settings.shouldAutoStart; 237 } 145 238 146 for ( i in data ) { 147 if ( data.hasOwnProperty( i ) ) { 148 empty = false; 149 break; 150 } 239 /** 240 * Determines if the iframe passed to this function is indeed an iframe and whether or not the src of that iframe 241 * is local to us. The iframe will be classified as `local` if and only if it has either: 242 * 1. the same domain name and protocol as the currently open window 243 * 2. the document object can be accessed on the frame which means that our browser recognizes the iframe src 244 * as one of our own. 245 * 246 * @param iframe 247 * @returns {boolean} 248 * @private 249 */ 250 function _isLocalFrame( iframe ) { 251 if( iframe.nodeName !== "IFRAME" ) { 252 return false; 151 253 } 152 254 153 // If nothing to send (nothing is expecting a response), 154 // schedule the next tick and bail 155 if ( empty && ! self.connectionLost ) { 156 connecting = false; 157 next(); 158 return; 255 var origin = window.location.origin ? window.location.origin : window.location.protocol + "//" + window.location.host; 256 var src = iframe.getAttribute( "src" ); 257 258 if( /^https?:\/\//.test( src ) === true && src.indexOf( origin ) !== 0 ) { 259 return false; 159 260 } 160 261 161 send.data = data; 162 send.interval = interval / 1000; 163 send._nonce = nonce; 164 send.action = 'heartbeat'; 165 send.screen_id = screenId; 166 send.has_focus = hasFocus; 262 if( iframe.contentWindow !== undefined && iframe.contentWindow.document !== undefined ) { 263 return true; 264 } 167 265 168 connecting = true; 169 self.xhr = $.ajax({ 170 url: url, 171 type: 'post', 172 timeout: 30000, // throw an error if not completed after 30 sec. 173 data: send, 174 dataType: 'json' 175 }).done( function( response, textStatus, jqXHR ) { 176 var new_interval; 266 return false; 267 } 177 268 178 if ( ! response ) 179 return errorstate( 'empty' ); 269 /** 270 * If a value is specified, this function will set the internal value of the `hasActiveConnection` setting. 271 * Always returns the value of `hasActiveConnection` 272 * 273 * @param value (optional) 274 * @returns boolean 275 * @private 276 */ 277 function _hasActiveConnection( value ) { 278 if( typeof value === "boolean" ) { 279 _Settings.hasActiveConnection = value; 280 return value; 281 } 180 282 181 // Clear error state 182 if ( self.connectionLost ) 183 errorstate(); 283 return _Settings.hasActiveConnection; 284 } 184 285 185 if ( response.nonces_expired ) { 186 $(document).trigger( 'heartbeat-nonces-expired' ); 187 return; 188 } 286 /** 287 * Checks the error passed to this function and takes the appropriate action necessary. This function mainly 288 * catches the transitions from an active connection to that of a non-active connection. 289 * 290 * @param error 291 * @private 292 */ 293 function _triggerError( error ) { 294 var trigger = false; 295 error = error || false; 189 296 190 // Change the interval from PHP 191 if ( response.heartbeat_interval ) { 192 new_interval = response.heartbeat_interval; 193 delete response.heartbeat_interval; 297 // if the error was aborted, don't do anything 298 if( error === "abort" ) { 299 return; 300 } 301 else if( error === "timeout" ) { 302 trigger = true; 303 } 304 else if( error === "unknown" || error === "parsererror" || error === "error" || error === "empty" ) { 305 _Settings.errorCountSinceConnected++; 306 if( _Settings.errorCountSinceConnected > 2 ) { 307 trigger = true; 194 308 } 309 } 195 310 196 self.tick( response, textStatus, jqXHR ); 311 // take action if we need to trigger things 312 if( trigger === true && _hasActiveConnection() === true ) { 313 _hasActiveConnection( false ); 314 _Cache.$document.trigger( "heartbeat-connection-lost", [ error ] ); 315 } 316 else if( _hasActiveConnection() === false ) { 317 _Settings.errorCountSinceConnected = 0; 318 _hasActiveConnection( true ); 319 _Cache.$document.trigger( "heartbeat-connection-restored" ); 320 } 321 } 197 322 198 // do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast' 199 if ( new_interval ) 200 self.interval.call( self, new_interval ); 201 }).always( function() { 202 connecting = false; 203 next(); 204 }).fail( function( jqXHR, textStatus, error ) { 205 errorstate( textStatus || 'unknown' ); 206 self.error( jqXHR, textStatus, error ); 207 }); 208 }; 323 /** 324 * Handles connecting to the server-side heartbeat API and sending any queued data along. 325 * 326 * @private 327 */ 328 function _connect() { 329 // get the data we need to send 330 var dataToSend = $.extend( {}, _QueuedAjaxData ); 331 _clearAjaxDataQueue(); 209 332 210 function next() {211 var delta = time() - tick, t = interval;333 // trigger an event with the data we're sending 334 _Cache.$document.trigger( "heartbeat-send", [ dataToSend ] ); 212 335 213 if ( ! running ) 336 // make sure there is data to send and that we have an active connection. If this criteria is met, bail from 337 // this function 338 if( _objectIsEmpty( dataToSend ) === true && _hasActiveConnection() === true ) { 339 _Settings.isConnecting = false; 340 _nextTick(); 214 341 return; 342 } 215 343 216 if ( ! hasFocus ) { 217 t = 120000; // 2 min 218 } else if ( countdown > 0 && tempInterval ) { 219 t = tempInterval; 220 countdown--; 344 // build the ajax data we need to send now 345 var ajaxData = _buildAjaxData( dataToSend ); 346 347 // keep track of when we're connecting for this ajax call 348 _Settings.isConnecting = true; 349 350 // initiate a new ajax request 351 _Cache.ajaxRequest = $.ajax( { 352 url : _Settings.ajaxURL, 353 type : "post", 354 timeout : _Settings.ajaxRequestTimeout, 355 data : ajaxData, 356 dataType : "json" 357 } ); 358 359 // setup our promises 360 _Cache.ajaxRequest.done( _onAjaxDone ); 361 _Cache.ajaxRequest.always( _onAjaxAlwaysPromise ); 362 _Cache.ajaxRequest.fail( _onAjaxFailed ); 363 } 364 365 /** 366 * Handles taking action when the heartbeat pulse AJAX call is finished, whether fail or succeed 367 * 368 * @param response 369 * @param textStatus 370 * @param jqXHR 371 * @private 372 */ 373 function _onAjaxDone( response, textStatus, jqXHR ) { 374 var cachedHeartbeatInterval = null; 375 376 // make sure we got a response 377 if( ! response ) { 378 _triggerError( "empty" ); 379 return; 221 380 } 222 381 223 window.clearTimeout(beat); 382 // clear the error state if the connection was lost already 383 if( _Settings.hasActiveConnection() === false ) { 384 _triggerError(); 385 } 224 386 225 if ( delta < t ) { 226 beat = window.setTimeout( 227 function(){ 228 if ( running ) 229 connect(); 230 }, 231 t - delta 232 ); 233 } else { 234 connect(); 387 // check to see if the nonce has expired 388 // todo: this could use a strict check 389 if( response.nonces_expired ) { 390 _Cache.$document.trigger( "heartbeat-nonces-expired" ); 235 391 } 392 393 // check to see if a new interval needs to be sent 394 // todo: this could use a strict check 395 if( response.heartbeat_interval ) { 396 cachedHeartbeatInterval = response.heartbeat_interval; 397 398 // todo: what's the purpose of deleting this? 399 delete response.heartbeat_interval; 400 } 401 402 // emit an event with the data we need 403 _Cache.$document.trigger( "heartbeat-tick", [ response, textStatus, jqXHR ] ); 404 405 // check to see if we set a value 406 // do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast' 407 if( cachedHeartbeatInterval !== null ) { 408 _interval( cachedHeartbeatInterval ); 409 } 236 410 } 237 411 238 function blurred() { 239 window.clearTimeout(winBlurTimeout); 240 window.clearTimeout(frameBlurTimeout); 241 winBlurTimeout = frameBlurTimeout = 0; 412 /** 413 * Handles taking action when the heartbeat pulse is completed and has failed 414 * 415 * @param jqXHR 416 * @param textStatus 417 * @param error 418 * @private 419 */ 420 function _onAjaxFailed( jqXHR, textStatus, error ) { 421 _triggerError( textStatus || "unknown" ); 422 _Cache.$document.trigger( "heartbeat-error", [ jqXHR, textStatus, error ] ); 423 } 242 424 243 hasFocus = false; 425 /** 426 * Handles any actions to take for the always part of the Ajax promise 427 * 428 * @private 429 */ 430 function _onAjaxAlwaysPromise() { 431 _Settings.isConnecting = false; 432 _nextTick(); 244 433 } 245 434 246 function focused() { 247 window.clearTimeout(winBlurTimeout); 248 window.clearTimeout(frameBlurTimeout); 249 winBlurTimeout = frameBlurTimeout = 0; 435 /** 436 * Builds the data object literal that we'll use for initiating an AJAX request for each heartbeat pulse 437 * 438 * @param queuedData 439 * @returns {{}} 440 * @private 441 */ 442 function _buildAjaxData( queuedData ) { 443 return { 444 data : queuedData, 445 interval : ( _Settings.heartbeatInterval / 1000 ), 446 _nonce : _Settings.nonce, 447 action : "heartbeat", 448 screen_id : _Settings.screenId, 449 has_focus : _Settings.hasFocus 450 }; 451 } 250 452 251 isUserActive = time(); 453 /** 454 * Checks to see if the object literal passed to this function is currently empty or not. 455 * 456 * @param object 457 * @returns {boolean} 458 * @private 459 */ 460 function _objectIsEmpty( object ) { 461 for( var key in object ) { 462 if( object.hasOwnProperty( key ) ) { 463 return false; 464 } 465 } 252 466 253 if ( hasFocus )254 return;467 return true; 468 } 255 469 256 hasFocus = true; 257 window.clearTimeout(beat); 470 /** 471 * Handles clearing the current AJAX data queue. This was moved to a function for clarity moving forward along 472 * with the idea that we could possibly trigger events here or do additional things. 473 * 474 * @private 475 */ 476 function _clearAjaxDataQueue() { 477 _QueuedAjaxData = {}; 478 } 258 479 259 if ( ! connecting ) 260 next(); 480 /** 481 * Handles binding any events 482 * @private 483 */ 484 function _bindWindowEvents() { 485 _Cache.$window.on( "blur.wp-heartbeat-focus", _onWindowBlur ); 486 _Cache.$window.on( "focus.wp-heartbeat-focus", _onWindowFocus ); 261 487 } 262 488 263 function setFrameEvents() { 264 $('iframe').each( function( i, frame ){ 265 if ( ! isLocalFrame( frame ) ) 489 /** 490 * Handles performing anything we expect to happen when the window is blurred 491 * 492 * @param event 493 * @private 494 */ 495 function _onWindowBlur( event ) { 496 // bind our iframe events 497 _bindIframeEvents(); 498 499 // reset our window blur timer 500 _clearWindowBlurTimer(); 501 502 // todo: not really sure why this was 500 in the first place, seems hacky? (there wasn't any commenting as to why) 503 _Cache.windowBlurTimer = setTimeout( _userHasBecomeInactive, 500 ); 504 } 505 506 /** 507 * Handles performing anything we expect to happen when the window is focused 508 * 509 * @param event 510 * @private 511 */ 512 function _onWindowFocus( event ) { 513 _unbindIframeEvents(); 514 _userHasBecomeActive(); 515 } 516 517 /** 518 * Handles binding any iframe events that are needed for this API to work effectively 519 * 520 * @private 521 */ 522 function _bindIframeEvents() { 523 $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 524 if( _isLocalFrame( iframe ) === false ) { 266 525 return; 526 } 267 527 268 if ( $.data( frame, 'wp-heartbeat-focus' ) ) 528 // if we already set data for this iframe, skip it 529 if( $.data( iframe, "wp-heartbeat-focus" ) ) { 269 530 return; 531 } 270 532 271 $.data( frame, 'wp-heartbeat-focus', 1 ); 533 // set data for this frame now so we know not to set it again next time this is called 534 $.data( iframe, "wp-heartbeat-focus", true ); 272 535 273 $( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function(e) { 274 focused(); 275 }).on('blur.wp-heartbeat-focus', function(e) { 276 setFrameEvents(); 277 frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); 278 }); 279 }); 536 // cache a reference to the jquery object for this iframe window object 537 var $iframeWindow = $( iframe.contentWindow ); 538 539 // now setup a focus event for this iframe 540 $iframeWindow.on( "focus.wp-heartbeat-focus", _userHasBecomeActive ); 541 $iframeWindow.on( "blur.wp-heartbeat-focus", _onIframeBlur ); 542 543 } ); 280 544 } 281 545 282 $(window).on( 'blur.wp-heartbeat-focus', function(e) { 283 setFrameEvents(); 284 winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); 285 }).on( 'focus.wp-heartbeat-focus', function() { 286 $('iframe').each( function( i, frame ) { 287 if ( !isLocalFrame( frame ) ) 546 /** 547 * Callback for when an iframe becomes blurred on the page 548 * 549 * @param event 550 * @private 551 */ 552 function _onIframeBlur( event ) { 553 _bindIframeEvents(); 554 555 //clear the existing frame blur timer 556 _clearFrameBlurTimer(); 557 558 // todo: not really sure why this was 500 in the first place, seems hacky? (there wasn't any commenting as to why) 559 _Cache.frameBlurTimer = setTimeout( _userHasBecomeInactive, 500 ); 560 } 561 562 /** 563 * Unbinds any previously bound heartbeat focus events from all iframes on the page (if they belong to us) 564 * 565 * @private 566 */ 567 function _unbindIframeEvents() { 568 $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 569 if( _isLocalFrame( iframe ) === false ) { 288 570 return; 571 } 289 572 290 $.removeData( frame, 'wp-heartbeat-focus' ); 291 $( frame.contentWindow ).off( '.wp-heartbeat-focus' ); 292 }); 573 $.removeData( iframe, "wp-heartbeat-focus" ); 574 $( iframe.contentWindow ).off( ".wp-heartbeat-focus" ); 575 } ); 576 } 293 577 294 focused(); 295 }); 578 /** 579 * Performs the next tick of the timer for our heartbeat. Verifies that we're supposed to continue before trying 580 * to start the next tick. 581 * 582 * @private 583 */ 584 function _nextTick() { 585 var timeSinceLastTick = _getUnixTimestamp() - _Settings.lastHeartbeatTick; 586 var timeLimit = _Settings.heartbeatInterval; 296 587 297 function userIsActive() { 298 userActiveEvents = false; 299 $(document).off( '.wp-heartbeat-active' ); 300 $('iframe').each( function( i, frame ) { 301 if ( ! isLocalFrame( frame ) ) 588 // make sure we're running before doing anything 589 if( _Settings.isRunning === false ) { 590 return; 591 } 592 593 // normalize the timeLimit 594 if( _hasFocus() === false ) { 595 // set the time limit to 2 minutes because we don't have focus 596 timeLimit = 120000; 597 } 598 else if( _Settings.countDown > 0 && _Settings.temporaryHeartbeatInterval > 0 ) { 599 timeLimit = _Settings.temporaryHeartbeatInterval; 600 } 601 602 // clear the existing heartbeat timer 603 _clearHeartbeatTimer(); 604 605 // check to see if we need to connect now or later and then set things up to do just that 606 if( timeSinceLastTick < timeLimit ) { 607 _Cache.heartbeatTimer = setTimeout( _connect, timeLimit - timeSinceLastTick ); 608 } 609 else { 610 _connect(); 611 } 612 } 613 614 /** 615 * Handles unbinding any events that were previously bound through `_bindUserActivityEvents()` 616 * 617 * @private 618 */ 619 function _unbindUserActivityEvents() { 620 // remove the mouseover event for the document 621 _Cache.$document.off( ".wp-heartbeat-active" ); 622 623 // loop through all iframes on the page and if they are local iframes, remove the previously attached events 624 $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 625 626 // make sure this frame is one of ours before trying to access it's contentWindow 627 if( _isLocalFrame( iframe ) === false ) { 302 628 return; 629 } 303 630 304 $( frame.contentWindow ).off( '.wp-heartbeat-active' );305 });631 // bind a mouseover event to this iframe's window object 632 $( iframe.contentWindow ).off( ".wp-heartbeat-active" ); 306 633 307 focused();634 } ); 308 635 } 309 636 310 // Set 'hasFocus = true' if user is active and the window is in the background. 311 // Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus. 312 function checkUserActive() { 313 var lastActive = isUserActive ? time() - isUserActive : 0; 637 /** 638 * Handles binding any events necessary to mark the user as active again 639 * 640 * @private 641 */ 642 function _bindUserActivityEvents() { 643 // when the user moves their mouse, mark the the user as becoming active again 644 _Cache.$document.on( "mouseover.wp-heartbeat-active keyup.wp-heartbeat-active", _userHasBecomeActive ); 314 645 315 // Throttle down when no mouse or keyboard activity for 5 min 316 if ( lastActive > 300000 && hasFocus ) 317 blurred(); 646 // loop through all iframes on the page and if they are local iframes, 647 $( document.querySelectorAll( "iframe" ) ).each( function( i, iframe ) { 318 648 319 if ( ! userActiveEvents ) { 320 $(document).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); 649 // make sure this frame is one of ours before trying to access it's contentWindow 650 if( _isLocalFrame( iframe ) === false ) { 651 return; 652 } 321 653 322 $('iframe').each( function( i, frame ) { 323 if ( ! isLocalFrame( frame ) ) 324 return; 654 // bind a mouseover event to this iframe's window object 655 $( iframe.contentWindow ).on( "mouseover.wp-heartbeat-active keyup.wp-heartbeat-active", _userHasBecomeActive ); 325 656 326 $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive();} );327 });657 } ); 658 } 328 659 329 userActiveEvents = true; 660 /** 661 * Periodically called to ensure that the user is still active. If the user becomes inactive, we take action. 662 * 663 * @private 664 */ 665 function _checkIfUserIsStillActive() { 666 var timeSinceUserWasLastActive = 0; 667 if( _Settings.userIsActive === true ) { 668 timeSinceUserWasLastActive = _getUnixTimestamp() - _Settings.lastUserActivityTimestamp; 330 669 } 670 671 // check to see if the user has become inactive or not ( no activity in 5 minutes ) 672 if( timeSinceUserWasLastActive > 300000 && _hasFocus() === true ) { 673 _userHasBecomeInactive(); 674 _bindUserActivityEvents(); 675 } 331 676 } 332 677 333 // Check for user activity every 30 seconds. 334 window.setInterval( function(){ checkUserActive(); }, 30000 ); 678 /** 679 * Starts the heartbeat pulse if it isn't already running 680 * todo: maybe add a trigger() here for other applications that need to be informed of when heartbeat starts 681 * 682 * @returns {boolean} 683 * @private 684 */ 685 function _start() { 686 if( _Settings.isRunning === true ) { 687 return false; 688 } 335 689 336 if ( this.autostart ) { 337 $(document).ready( function() { 338 // Start one tick (15 sec) after DOM ready 339 running = true; 340 tick = time(); 341 next(); 342 }); 690 _connect(); 691 692 _Settings.isRunning = true; 693 return true; 343 694 } 344 695 345 this.hasFocus = function() { 346 return hasFocus; 696 /** 697 * Stops the heartbeat pulse if isn't already running 698 * todo: should this be clearing timers as well? ( _clearWindowBlurTimer(), _clearFrameBlurTimer() and _clearHeartbeatTimer() ) 699 * 700 * @returns {boolean} 701 * @private 702 */ 703 function _stop() { 704 if( _Cache.ajaxRequest !== null && _Cache.ajaxRequest.readyState !== 4 ) { 705 _Cache.ajaxRequest.abort(); 706 _Cache.ajaxRequest = null; 707 } 708 709 // fire an error 710 // todo: this wasn't passing anything previously. I feel like it should be passing 'abort' 711 _triggerError(); 712 713 // update our internal value for whether or not we are currently running heartbeat 714 _Settings.isRunning = false; 715 716 return true; 347 717 } 348 718 349 719 /** 350 * Get/Set the interval 720 * Gets or sets the current heartbeat interval based on speed and ticks passed to it. If no speed is passed, the 721 * function simply returns the current interval. 351 722 * 352 723 * When setting to 'fast', the interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec). 353 724 * If the window doesn't have focus, the interval slows down to 2 min. 354 725 * 355 * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec) 356 * @param string ticks Used with speed = 'fast', how many ticks before the speed reverts back 357 * @return int Current interval in seconds 726 * @param speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec) 727 * @param ticks Used with speed = 'fast', how many ticks before the speed reverts back 728 * @returns {number} interval in seconds 729 * @private 358 730 */ 359 this.interval = function( speed, ticks ) { 360 var reset, seconds; 361 ticks = parseInt( ticks, 10 ) || 30; 362 ticks = ticks < 1 || ticks > 30 ? 30 : ticks; 731 function _interval( speed, ticks ) { 732 var reset, seconds = 15; 363 733 364 if ( speed ) { 365 switch ( speed ) { 366 case 'fast': 367 seconds = 5; 368 countdown = ticks; 369 break; 370 case 'slow': 371 seconds = 60; 372 countdown = 0; 373 break; 374 case 'long-polling': 375 // Allow long polling, (experimental) 376 interval = 0; 377 return 0; 378 break; 379 default: 380 seconds = 15; 381 countdown = 0; 734 if( speed === undefined ) { 735 if( _hasFocus() === false ) { 736 return 120; 382 737 } 383 738 384 // Reset when the new interval value is lower than the current one 385 reset = seconds * 1000 < interval; 739 // return the existing values 740 return _Settings.temporaryHeartbeatInterval ? ( _Settings.temporaryHeartbeatInterval / 1000 ) : ( _Settings.heartbeatInterval * 1000 ); 741 } 386 742 387 if ( countdown > 0 ) { 388 tempInterval = seconds * 1000; 389 } else { 390 interval = seconds * 1000; 391 tempInterval = 0; 392 } 743 // normalize ticks before continuing 744 ticks = parseInt( ticks, 10 ) || 30; 393 745 394 if ( reset ) 395 next(); 746 // make sure `ticks` is within bounds 747 if( ticks < 1 || ticks > 30 ) { 748 ticks = 30; 396 749 } 397 750 398 if ( ! hasFocus ) 751 // convert speed from a string if necessary 752 if( speed === "fast" ) { 753 seconds = 5; 754 _Settings.countDown = ticks; 755 } 756 else if( speed === "slow" ) { 757 seconds = 60; 758 _Settings.countDown = 0; 759 } 760 // long-polling is currently experimental 761 else if( speed === "long-polling" ) { 762 return _Settings.heartbeatInterval = 0; 763 } 764 765 // determine whether or not we should reset based on whether the new interval value is lower than the current 766 // one or not 767 reset = seconds * 1000 < _Settings.heartbeatInterval; 768 769 if( _Settings.countDown > 0 ) { 770 _Settings.temporaryHeartbeatInterval = seconds * 1000; 771 } 772 else { 773 _Settings.heartbeatInterval = seconds * 1000; 774 _Settings.temporaryHeartbeatInterval = 0; 775 } 776 777 // check if we need to reset or not 778 if( reset === true ) { 779 _nextTick(); 780 } 781 782 // check to see if we have focus or not 783 if( _hasFocus() === false ) { 399 784 return 120; 785 } 400 786 401 return tempInterval ? tempInterval / 1000 : interval / 1000; 402 }; 787 // return the new values 788 return _Settings.temporaryHeartbeatInterval ? ( _Settings.temporaryHeartbeatInterval / 1000 ) : ( _Settings.heartbeatInterval * 1000 ); 789 } 403 790 404 // Start. Has no effect if heartbeat is already running 405 this.start = function() { 406 if ( running ) 407 return false; 791 /** 792 * Returns a boolean that indicates whether or not heartbeat has focus at the moment 793 * 794 * @returns boolean 795 * @private 796 */ 797 function _hasFocus() { 798 return _Settings.hasFocus; 799 } 408 800 409 running = true; 410 connect(); 411 return true; 412 }; 801 /** 802 * Handles the actions to take when the user has become inactive 803 * todo: possibly add a $document.trigger() here to notify other applications? 804 * 805 * @private 806 */ 807 function _userHasBecomeInactive() { 808 _clearWindowBlurTimer(); 809 _clearFrameBlurTimer(); 413 810 414 // Stop. If a XHR is in progress, abort it 415 this.stop = function() { 416 if ( self.xhr && self.xhr.readyState != 4 ) 417 self.xhr.abort(); 811 // set the value of hasFocus to false 812 _Settings.hasFocus = false; 813 } 418 814 419 // Reset the error state 420 errorstate(); 421 running = false; 422 return true; 815 /** 816 * Handles the actions to take when the heartbeat window becomes focused. 817 * todo: possibly add a $document.trigger() here to notify other applications? 818 * 819 * @private 820 */ 821 function _userHasBecomeActive() { 822 _clearWindowBlurTimer(); 823 _clearFrameBlurTimer(); 824 _unbindUserActivityEvents(); 825 _updateUsersLastActivityTime(); 826 827 if( _hasFocus() === true ) { 828 return; 829 } 830 831 // set the the focus to true and clear the heartbeat pulse 832 _Settings.hasFocus = true; 833 _clearHeartbeatTimer(); 834 835 if( _Settings.isConnecting === false ) { 836 _nextTick(); 837 } 423 838 } 424 839 425 840 /** 426 * Enqueue data to send with the next XHR 841 * Updates the user's last activity timestamp 842 * todo: perhaps trigger an event to notify other applications? 427 843 * 428 * As the data is sent later, this function doesn't return the XHR response. 429 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: 430 * $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) { 431 * // code 432 * }); 433 * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'. 434 * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle. 844 * @private 845 */ 846 function _updateUsersLastActivityTime() { 847 _Settings.lastUserActivityTimestamp = _getUnixTimestamp(); 848 } 849 850 /** 851 * Clears the window blur timer if it exists 435 852 * 436 * $param string handle Unique handle for the data. The handle is used in PHP to receive the data. 437 * $param mixed data The data to send. 438 * $param bool dont_overwrite Whether to overwrite existing data in the queue. 439 * $return bool Whether the data was queued or not. 853 * @private 440 854 */ 441 this.enqueue = function( handle, data, dont_overwrite ) { 442 if ( handle ) { 443 if ( queue.hasOwnProperty( handle ) && dont_overwrite ) 444 return false; 855 function _clearWindowBlurTimer() { 856 if( _Cache.windowBlurTimer !== null ) { 857 clearTimeout( _Cache.windowBlurTimer ); 858 _Cache.windowBlurTimer = null; 859 } 860 } 445 861 446 queue[handle] = data; 447 return true; 862 /** 863 * Clears the frame blur timer if it exists 864 * 865 * @private 866 */ 867 function _clearFrameBlurTimer() { 868 if( _Cache.frameBlurTimer !== null ) { 869 clearTimeout( _Cache.frameBlurTimer ); 870 _Cache.frameBlurTimer = null; 448 871 } 449 return false;450 872 } 451 873 452 874 /** 453 * C heck if data with a particular handle is queued875 * Clears the heartbeat timer 454 876 * 455 * $param string handle The handle for the data 456 * $return mixed The data queued with that handle or null 877 * @private 457 878 */ 458 this.isQueued = function( handle ) { 459 return queue[handle]; 879 function _clearHeartbeatTimer() { 880 if( _Cache.heartbeatTimer !== null ) { 881 clearTimeout( _Cache.heartbeatTimer ); 882 _Cache.heartbeatTimer = null; 883 } 460 884 } 885 886 /** 887 * Call our object initializer to make sure things get setup properly before this object is used 888 */ 889 _initialize(); 890 891 /** 892 * Explicitly expose any methods we want to be available to the public scope 893 */ 894 return { 895 shouldAutoStart : _shouldAutoStart, 896 hasActiveConnection : _hasActiveConnection, 897 isLocalFrame : _isLocalFrame, 898 enqueueData : _enqueueData, 899 dataIsQueued : _dataIsQueued, 900 start : _start, 901 stop : _stop, 902 interval : _interval, 903 hasFocus : _hasFocus 904 }; 461 905 } 462 906 463 $.extend( Heartbeat.prototype, { 464 tick: function( data, textStatus, jqXHR ) { 465 $(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] ); 466 }, 467 error: function( jqXHR, textStatus, error ) { 468 $(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] ); 469 } 470 }); 907 // ensure our global `wp` object exists 908 window.wp = window.wp || {}; 471 909 472 wp.heartbeat = new Heartbeat(); 910 // create our new heartbeat object and expose it to the public 911 window.wp.heartbeat = new Heartbeat(); 473 912 474 }(jQuery)); 913 } )( window, jQuery ); 914 No newline at end of file