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