/** * Heartbeat API */ // Ensure the global `wp` object exists. window.wp = window.wp || {}; (function($){ var Heartbeat = function() { var self = this, running, beat, nonce, screenid = typeof pagenow != 'undefined' ? pagenow : '', url = typeof ajaxurl != 'undefined' ? ajaxurl : '', settings, tick = 0, queue = {}, interval, connecting, countdown = 0, errorcount = 0, tempInterval, hasFocus = true, isUserActive, userActiveEvents, winBlurTimeout, frameBlurTimeout = -1; this.autostart = true; this.connectionLost = false; if ( typeof( window.heartbeatSettings ) != 'undefined' ) { settings = window.heartbeatSettings; // Add private vars nonce = settings.nonce || ''; delete settings.nonce; url = settings.ajaxurl || url; delete settings.ajaxurl; interval = settings.interval || 15; // default interval delete settings.interval; // The interval can be from 5 to 60 sec. if ( interval < 5 ) interval = 5; else if ( interval > 60 ) interval = 60; interval = interval * 1000; // 'screenid' can be added from settings on the front-end where the JS global 'pagenow' is not set screenid = screenid || settings.screenid || 'site'; delete settings.screenid; // Add or overwrite public vars $.extend( this, settings ); } function time(s) { if ( s ) return parseInt( (new Date()).getTime() / 1000 ); return (new Date()).getTime(); } function isLocalFrame(frame) { try { if ( frame.contentWindow.document ) return true; } catch(e) {} return false; } // Set error state and fire an event if XHR errors or timeout function errorstate( error ) { var trigger; if ( error ) { switch ( error ) { case 'abort': // do nothing break; case 'timeout': // no response for 30 sec. trigger = true; break; case 'parsererror': case 'error': case 'empty': case 'unknown': errorcount++; if ( errorcount > 2 ) trigger = true; break; } if ( trigger && ! self.connectionLost ) { self.connectionLost = true; $(document).trigger( 'heartbeat-connection-lost' ); } } else if ( self.connectionLost ) { errorcount = 0; self.connectionLost = false; $(document).trigger( 'heartbeat-connection-restored' ); } } function connect() { var data = {}; tick = time(); data.data = $.extend( {}, queue ); // Clear the data queue, anything added after this point will be send on the next tick queue = {}; $(document).trigger( 'heartbeat-send', [data.data] ); data.interval = interval / 1000; data._nonce = nonce; data.action = 'heartbeat'; data.screenid = screenid; data.has_focus = hasFocus; connecting = true; self.xhr = $.ajax({ url: url, type: 'post', timeout: 30000, // throw an error of not completed after 30 sec. data: data, dataType: 'json' }).done( function( data, textStatus, jqXHR ) { var new_interval, timed; if ( ! data ) return errorstate( 'empty' ); // Clear error state if ( self.connectionLost ) errorstate(); // Change the interval from PHP new_interval = data.heartbeat_interval; delete data.heartbeat_interval; self.tick( data, textStatus, jqXHR ); // do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast' if ( new_interval ) self.interval.call( self, new_interval ); }).always( function() { connecting = false; next(); }).fail( function( jqXHR, textStatus, error ) { errorstate( textStatus || 'unknown' ); self.error( jqXHR, textStatus, error ); }); }; function next() { var delta = time() - tick, t = interval; if ( !running ) return; if ( !hasFocus ) { t = 120000; // 2 min } else if ( countdown > 0 && tempInterval ) { t = tempInterval; countdown--; } window.clearTimeout(beat); if ( delta < t ) { beat = window.setTimeout( function(){ if ( running ) connect(); }, t - delta ); } else { connect(); } } function blurred() { window.clearTimeout(winBlurTimeout); window.clearTimeout(frameBlurTimeout); winBlurTimeout = frameBlurTimeout = 0; hasFocus = false; // temp debug if ( self.debug ) console.log('### blurred(), slow down...') } function focused() { window.clearTimeout(winBlurTimeout); window.clearTimeout(frameBlurTimeout); winBlurTimeout = frameBlurTimeout = 0; isUserActive = time(); if ( hasFocus ) return; hasFocus = true; window.clearTimeout(beat); if ( !connecting ) next(); // temp debug if ( self.debug ) console.log('### focused(), speed up... ') } function setFrameEvents() { $('iframe').each( function(i, frame){ if ( !isLocalFrame(frame) ) return; if ( $.data(frame, 'wp-heartbeat-focus') ) return; $.data(frame, 'wp-heartbeat-focus', 1); $(frame.contentWindow).on('focus.wp-heartbeat-focus', function(e){ focused(); }).on('blur.wp-heartbeat-focus', function(e){ setFrameEvents(); frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); }); }); } $(window).on('blur.wp-heartbeat-focus', function(e) { setFrameEvents(); winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); }).on('focus.wp-heartbeat-focus', function() { $('iframe').each( function(i, frame){ if ( !isLocalFrame(frame) ) return; $.removeData(frame, 'wp-heartbeat-focus'); $(frame.contentWindow).off('.wp-heartbeat-focus'); }); focused(); }); function userIsActive() { userActiveEvents = false; $(document).off('.wp-heartbeat-active'); $('iframe').each( function(i, frame){ if ( !isLocalFrame(frame) ) return; $(frame.contentWindow).off('.wp-heartbeat-active'); }); focused(); // temp debug if ( self.debug ) console.log( 'userIsActive()' ); } // Set 'hasFocus = true' if user is active and the window is in the background. // Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus. function checkUserActive() { var lastActive = isUserActive ? time() - isUserActive : 0; // temp debug if ( self.debug ) console.log( 'checkUserActive(), lastActive = %s seconds ago', parseInt(lastActive / 1000) || 'null' ); // Throttle down when no mouse or keyboard activity for 5 min if ( lastActive > 300000 && hasFocus ) blurred(); if ( !userActiveEvents ) { $(document).on('mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); }); $('iframe').each( function(i, frame){ if ( !isLocalFrame(frame) ) return; $(frame.contentWindow).on('mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); }); }); userActiveEvents = true; } } // Check for user activity every 30 seconds. window.setInterval( function(){ checkUserActive(); }, 30000 ); if ( this.autostart ) { $(document).ready( function(){ // Start one tick (15 sec) after DOM ready running = true; tick = time(); next(); }); } this.hasFocus = function() { return hasFocus; } /** * Get/Set the interval * * When setting to 'fast', the interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec). * If the window doesn't have focus, the interval slows down to 2 min. * * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec) * @return int Current interval in seconds */ this.interval = function( speed ) { var reset, seconds; if ( speed ) { switch ( speed ) { case 'fast': seconds = 5; countdown = 30; break; case 'slow': seconds = 60; countdown = 0; break; case 'long-polling': // Allow long polling, (experimental) interval = 0; return 0; break; default: seconds = 15; countdown = 0; } // Reset when the new interval value is lower than the current one reset = seconds * 1000 < interval; if ( countdown > 0 ) { tempInterval = seconds * 1000; } else { interval = seconds * 1000; tempInterval = 0; } if ( reset ) next(); } if ( !hasFocus ) return 120; return tempInterval ? tempInterval / 1000 : interval / 1000; }; // Start. Has no effect if heartbeat is already running this.start = function() { if ( running ) return false; running = true; connect(); return true; }; // Stop. If a XHR is in progress, abort it this.stop = function() { if ( self.xhr && self.xhr.readyState != 4 ) self.xhr.abort(); // Reset the error state errorstate(); running = false; return true; } /** * Enqueue data to send with the next XHR * * As the data is sent later, this function doesn't return the XHR response. * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: * $(document).on('heartbeat-tick.myname', function(data, textStatus, jqXHR) { * // code * }); * If the same 'handle' is used more than once, the data is overwritten when the third argument is 'true'. * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle. * * $param string handle Unique handle for the data. The handle is used in PHP to receive the data. * $param mixed data The data to send. * $param bool dont_overwrite Whether to overwrite existing data in the queue. * $return bool Whether the data was queued or not. */ this.enqueue = function( handle, data, dont_overwrite ) { if ( handle ) { if ( queue.hasOwnProperty(handle) && dont_overwrite ) return false; queue[handle] = data; return true; } return false; } /** * Check if data with a particular handle is queued * * $param string handle The handle for the data * $return mixed The data queued with that handle or null */ this.isQueued = function( handle ) { return queue[handle]; } } $.extend( Heartbeat.prototype, { tick: function( data, textStatus, jqXHR ) { $(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] ); }, error: function( jqXHR, textStatus, error ) { $(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] ); } }); wp.heartbeat = new Heartbeat(); }(jQuery));