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