1 /** |
1 /** |
2 * Heartbeat API |
2 * Heartbeat API |
3 * |
3 * |
4 * Note: this API is "experimental" meaning it will likely change a lot |
4 * Heartbeat is a simple server polling API that sends XHR requests to |
5 * in the next few releases based on feedback from 3.6.0. If you intend |
5 * the server every 15 - 60 seconds and triggers events (or callbacks) upon |
6 * to use it, please follow the development closely. |
6 * receiving data. Currently these 'ticks' handle transports for post locking, |
|
7 * login-expiration warnings, autosave, and related tasks while a user is logged in. |
7 * |
8 * |
8 * Heartbeat is a simple server polling API that sends XHR requests to |
9 * Available PHP filters (in ajax-actions.php): |
9 * the server every 15 seconds and triggers events (or callbacks) upon |
|
10 * receiving data. Currently these 'ticks' handle transports for post locking, |
|
11 * login-expiration warnings, and related tasks while a user is logged in. |
|
12 * |
|
13 * Available filters in ajax-actions.php: |
|
14 * - heartbeat_received |
10 * - heartbeat_received |
15 * - heartbeat_send |
11 * - heartbeat_send |
16 * - heartbeat_tick |
12 * - heartbeat_tick |
17 * - heartbeat_nopriv_received |
13 * - heartbeat_nopriv_received |
18 * - heartbeat_nopriv_send |
14 * - heartbeat_nopriv_send |
19 * - heartbeat_nopriv_tick |
15 * - heartbeat_nopriv_tick |
20 * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat() |
16 * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat() |
21 * |
17 * |
|
18 * Custom jQuery events: |
|
19 * - heartbeat-send |
|
20 * - heartbeat-tick |
|
21 * - heartbeat-error |
|
22 * - heartbeat-connection-lost |
|
23 * - heartbeat-connection-restored |
|
24 * - heartbeat-nonces-expired |
|
25 * |
22 * @since 3.6.0 |
26 * @since 3.6.0 |
23 */ |
27 */ |
24 |
28 |
25 // Ensure the global `wp` object exists. |
29 ( function( $, window, undefined ) { |
26 window.wp = window.wp || {}; |
|
27 |
|
28 (function($){ |
|
29 var Heartbeat = function() { |
30 var Heartbeat = function() { |
30 var self = this, |
31 var $document = $(document), |
31 running, |
32 settings = { |
32 beat, |
33 // Suspend/resume |
33 screenId = typeof pagenow != 'undefined' ? pagenow : '', |
34 suspend: false, |
34 url = typeof ajaxurl != 'undefined' ? ajaxurl : '', |
35 |
35 settings, |
36 // Whether suspending is enabled |
36 tick = 0, |
37 suspendEnabled: true, |
37 queue = {}, |
38 |
38 interval, |
39 // Current screen id, defaults to the JS global 'pagenow' when present (in the admin) or 'front' |
39 connecting, |
40 screenId: '', |
40 countdown = 0, |
41 |
41 errorcount = 0, |
42 // XHR request URL, defaults to the JS global 'ajaxurl' when present |
42 tempInterval, |
43 url: '', |
43 hasFocus = true, |
44 |
44 isUserActive, |
45 // Timestamp, start of the last connection request |
45 userActiveEvents, |
46 lastTick: 0, |
46 winBlurTimeout, |
47 |
47 frameBlurTimeout = -1, |
48 // Container for the enqueued items |
48 hasConnectionError = null; |
49 queue: {}, |
49 |
50 |
50 /** |
51 // Connect interval (in seconds) |
51 * Returns a boolean that's indicative of whether or not there is a connection error |
52 mainInterval: 60, |
52 * |
53 |
53 * @returns boolean |
54 // Used when the interval is set to 5 sec. temporarily |
54 */ |
55 tempInterval: 0, |
55 this.hasConnectionError = function() { |
56 |
56 return !! hasConnectionError; |
57 // Used when the interval is reset |
57 }; |
58 originalInterval: 0, |
58 |
59 |
59 if ( typeof( window.heartbeatSettings ) == 'object' ) { |
60 // Used to limit the number of AJAX requests. |
60 settings = $.extend( {}, window.heartbeatSettings ); |
61 minimalInterval: 0, |
61 |
62 |
62 // Add private vars |
63 // Used together with tempInterval |
63 url = settings.ajaxurl || url; |
64 countdown: 0, |
64 delete settings.ajaxurl; |
65 |
65 delete settings.nonce; |
66 // Whether a connection is currently in progress |
66 |
67 connecting: false, |
67 interval = settings.interval || 15; // default interval |
68 |
68 delete settings.interval; |
69 // Whether a connection error occurred |
69 // The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec. |
70 connectionError: false, |
70 if ( interval < 15 ) |
71 |
71 interval = 15; |
72 // Used to track non-critical errors |
72 else if ( interval > 60 ) |
73 errorcount: 0, |
73 interval = 60; |
74 |
74 |
75 // Whether at least one connection has completed successfully |
75 interval = interval * 1000; |
76 hasConnected: false, |
76 |
77 |
77 // 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set |
78 // Whether the current browser window is in focus and the user is active |
78 screenId = screenId || settings.screenId || 'front'; |
79 hasFocus: true, |
79 delete settings.screenId; |
80 |
80 |
81 // Timestamp, last time the user was active. Checked every 30 sec. |
81 // Add or overwrite public vars |
82 userActivity: 0, |
82 $.extend( this, settings ); |
83 |
83 } |
84 // Flags whether events tracking user activity were set |
84 |
85 userActivityEvents: false, |
85 function time(s) { |
86 |
86 if ( s ) |
87 checkFocusTimer: 0, |
87 return parseInt( (new Date()).getTime() / 1000 ); |
88 beatTimer: 0 |
88 |
89 }; |
|
90 |
|
91 /** |
|
92 * Set local vars and events, then start |
|
93 * |
|
94 * @access private |
|
95 * |
|
96 * @return void |
|
97 */ |
|
98 function initialize() { |
|
99 var options, hidden, visibilityState, visibilitychange; |
|
100 |
|
101 if ( typeof window.pagenow === 'string' ) { |
|
102 settings.screenId = window.pagenow; |
|
103 } |
|
104 |
|
105 if ( typeof window.ajaxurl === 'string' ) { |
|
106 settings.url = window.ajaxurl; |
|
107 } |
|
108 |
|
109 // Pull in options passed from PHP |
|
110 if ( typeof window.heartbeatSettings === 'object' ) { |
|
111 options = window.heartbeatSettings; |
|
112 |
|
113 // The XHR URL can be passed as option when window.ajaxurl is not set |
|
114 if ( ! settings.url && options.ajaxurl ) { |
|
115 settings.url = options.ajaxurl; |
|
116 } |
|
117 |
|
118 // The interval can be from 15 to 120 sec. and can be set temporarily to 5 sec. |
|
119 // It can be set in the initial options or changed later from JS and/or from PHP. |
|
120 if ( options.interval ) { |
|
121 settings.mainInterval = options.interval; |
|
122 |
|
123 if ( settings.mainInterval < 15 ) { |
|
124 settings.mainInterval = 15; |
|
125 } else if ( settings.mainInterval > 120 ) { |
|
126 settings.mainInterval = 120; |
|
127 } |
|
128 } |
|
129 |
|
130 // Used to limit the number of AJAX requests. Overrides all other intervals if they are shorter. |
|
131 // Needed for some hosts that cannot handle frequent requests and the user may exceed the allocated server CPU time, etc. |
|
132 // The minimal interval can be up to 600 sec. however setting it to longer than 120 sec. will limit or disable |
|
133 // some of the functionality (like post locks). |
|
134 // Once set at initialization, minimalInterval cannot be changed/overriden. |
|
135 if ( options.minimalInterval ) { |
|
136 options.minimalInterval = parseInt( options.minimalInterval, 10 ); |
|
137 settings.minimalInterval = options.minimalInterval > 0 && options.minimalInterval <= 600 ? options.minimalInterval * 1000 : 0; |
|
138 } |
|
139 |
|
140 if ( settings.minimalInterval && settings.mainInterval < settings.minimalInterval ) { |
|
141 settings.mainInterval = settings.minimalInterval; |
|
142 } |
|
143 |
|
144 // 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set |
|
145 if ( ! settings.screenId ) { |
|
146 settings.screenId = options.screenId || 'front'; |
|
147 } |
|
148 |
|
149 if ( options.suspension === 'disable' ) { |
|
150 settings.suspendEnabled = false; |
|
151 } |
|
152 } |
|
153 |
|
154 // Convert to milliseconds |
|
155 settings.mainInterval = settings.mainInterval * 1000; |
|
156 settings.originalInterval = settings.mainInterval; |
|
157 |
|
158 // Switch the interval to 120 sec. by using the Page Visibility API. |
|
159 // If the browser doesn't support it (Safari < 7, Android < 4.4, IE < 10), the interval |
|
160 // will be increased to 120 sec. after 5 min. of mouse and keyboard inactivity. |
|
161 if ( typeof document.hidden !== 'undefined' ) { |
|
162 hidden = 'hidden'; |
|
163 visibilitychange = 'visibilitychange'; |
|
164 visibilityState = 'visibilityState'; |
|
165 } else if ( typeof document.msHidden !== 'undefined' ) { // IE10 |
|
166 hidden = 'msHidden'; |
|
167 visibilitychange = 'msvisibilitychange'; |
|
168 visibilityState = 'msVisibilityState'; |
|
169 } else if ( typeof document.webkitHidden !== 'undefined' ) { // Android |
|
170 hidden = 'webkitHidden'; |
|
171 visibilitychange = 'webkitvisibilitychange'; |
|
172 visibilityState = 'webkitVisibilityState'; |
|
173 } |
|
174 |
|
175 if ( hidden ) { |
|
176 if ( document[hidden] ) { |
|
177 settings.hasFocus = false; |
|
178 } |
|
179 |
|
180 $document.on( visibilitychange + '.wp-heartbeat', function() { |
|
181 if ( document[visibilityState] === 'hidden' ) { |
|
182 blurred(); |
|
183 window.clearInterval( settings.checkFocusTimer ); |
|
184 } else { |
|
185 focused(); |
|
186 if ( document.hasFocus ) { |
|
187 settings.checkFocusTimer = window.setInterval( checkFocus, 10000 ); |
|
188 } |
|
189 } |
|
190 }); |
|
191 } |
|
192 |
|
193 // Use document.hasFocus() if available. |
|
194 if ( document.hasFocus ) { |
|
195 settings.checkFocusTimer = window.setInterval( checkFocus, 10000 ); |
|
196 } |
|
197 |
|
198 $(window).on( 'unload.wp-heartbeat', function() { |
|
199 // Don't connect any more |
|
200 settings.suspend = true; |
|
201 |
|
202 // Abort the last request if not completed |
|
203 if ( settings.xhr && settings.xhr.readyState !== 4 ) { |
|
204 settings.xhr.abort(); |
|
205 } |
|
206 }); |
|
207 |
|
208 // Check for user activity every 30 seconds. |
|
209 window.setInterval( checkUserActivity, 30000 ); |
|
210 |
|
211 // Start one tick after DOM ready |
|
212 $document.ready( function() { |
|
213 settings.lastTick = time(); |
|
214 scheduleNextTick(); |
|
215 }); |
|
216 } |
|
217 |
|
218 /** |
|
219 * Return the current time according to the browser |
|
220 * |
|
221 * @access private |
|
222 * |
|
223 * @return int |
|
224 */ |
|
225 function time() { |
89 return (new Date()).getTime(); |
226 return (new Date()).getTime(); |
90 } |
227 } |
91 |
228 |
|
229 /** |
|
230 * Check if the iframe is from the same origin |
|
231 * |
|
232 * @access private |
|
233 * |
|
234 * @return bool |
|
235 */ |
92 function isLocalFrame( frame ) { |
236 function isLocalFrame( frame ) { |
93 var origin, src = frame.src; |
237 var origin, src = frame.src; |
94 |
238 |
|
239 // Need to compare strings as WebKit doesn't throw JS errors when iframes have different origin. |
|
240 // It throws uncatchable exceptions. |
95 if ( src && /^https?:\/\//.test( src ) ) { |
241 if ( src && /^https?:\/\//.test( src ) ) { |
96 origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host; |
242 origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host; |
97 |
243 |
98 if ( src.indexOf( origin ) !== 0 ) |
244 if ( src.indexOf( origin ) !== 0 ) { |
99 return false; |
245 return false; |
|
246 } |
100 } |
247 } |
101 |
248 |
102 try { |
249 try { |
103 if ( frame.contentWindow.document ) |
250 if ( frame.contentWindow.document ) { |
104 return true; |
251 return true; |
|
252 } |
105 } catch(e) {} |
253 } catch(e) {} |
106 |
254 |
107 return false; |
255 return false; |
108 } |
256 } |
109 |
257 |
110 // Set error state and fire an event on XHR errors or timeout |
258 /** |
111 function errorstate( error, status ) { |
259 * Check if the document's focus has changed |
|
260 * |
|
261 * @access private |
|
262 * |
|
263 * @return void |
|
264 */ |
|
265 function checkFocus() { |
|
266 if ( settings.hasFocus && ! document.hasFocus() ) { |
|
267 blurred(); |
|
268 } else if ( ! settings.hasFocus && document.hasFocus() ) { |
|
269 focused(); |
|
270 } |
|
271 } |
|
272 |
|
273 /** |
|
274 * Set error state and fire an event on XHR errors or timeout |
|
275 * |
|
276 * @access private |
|
277 * |
|
278 * @param string error The error type passed from the XHR |
|
279 * @param int status The HTTP status code passed from jqXHR (200, 404, 500, etc.) |
|
280 * @return void |
|
281 */ |
|
282 function setErrorState( error, status ) { |
112 var trigger; |
283 var trigger; |
113 |
284 |
114 if ( error ) { |
285 if ( error ) { |
115 switch ( error ) { |
286 switch ( error ) { |
116 case 'abort': |
287 case 'abort': |
118 break; |
289 break; |
119 case 'timeout': |
290 case 'timeout': |
120 // no response for 30 sec. |
291 // no response for 30 sec. |
121 trigger = true; |
292 trigger = true; |
122 break; |
293 break; |
|
294 case 'error': |
|
295 if ( 503 === status && settings.hasConnected ) { |
|
296 trigger = true; |
|
297 break; |
|
298 } |
|
299 /* falls through */ |
123 case 'parsererror': |
300 case 'parsererror': |
124 case 'error': |
|
125 case 'empty': |
301 case 'empty': |
126 case 'unknown': |
302 case 'unknown': |
127 errorcount++; |
303 settings.errorcount++; |
128 |
304 |
129 if ( errorcount > 2 ) |
305 if ( settings.errorcount > 2 && settings.hasConnected ) { |
130 trigger = true; |
306 trigger = true; |
|
307 } |
131 |
308 |
132 break; |
309 break; |
133 } |
310 } |
134 |
311 |
135 if ( 503 == status && false === hasConnectionError ) { |
312 if ( trigger && ! hasConnectionError() ) { |
136 trigger = true; |
313 settings.connectionError = true; |
137 } |
314 $document.trigger( 'heartbeat-connection-lost', [error, status] ); |
138 |
315 } |
139 if ( trigger && ! self.hasConnectionError() ) { |
316 } |
140 hasConnectionError = true; |
317 } |
141 $(document).trigger( 'heartbeat-connection-lost', [error, status] ); |
318 |
142 } |
319 /** |
143 } else if ( self.hasConnectionError() ) { |
320 * Clear the error state and fire an event |
144 errorcount = 0; |
321 * |
145 hasConnectionError = false; |
322 * @access private |
146 $(document).trigger( 'heartbeat-connection-restored' ); |
323 * |
147 } else if ( null === hasConnectionError ) { |
324 * @return void |
148 hasConnectionError = false; |
325 */ |
149 } |
326 function clearErrorState() { |
150 } |
327 // Has connected successfully |
151 |
328 settings.hasConnected = true; |
|
329 |
|
330 if ( hasConnectionError() ) { |
|
331 settings.errorcount = 0; |
|
332 settings.connectionError = false; |
|
333 $document.trigger( 'heartbeat-connection-restored' ); |
|
334 } |
|
335 } |
|
336 |
|
337 /** |
|
338 * Gather the data and connect to the server |
|
339 * |
|
340 * @access private |
|
341 * |
|
342 * @return void |
|
343 */ |
152 function connect() { |
344 function connect() { |
153 var send = {}, data, i, empty = true, |
345 var ajaxData, heartbeatData; |
154 nonce = typeof window.heartbeatSettings == 'object' ? window.heartbeatSettings.nonce : ''; |
346 |
155 tick = time(); |
347 // If the connection to the server is slower than the interval, |
156 |
348 // heartbeat connects as soon as the previous connection's response is received. |
157 data = $.extend( {}, queue ); |
349 if ( settings.connecting || settings.suspend ) { |
|
350 return; |
|
351 } |
|
352 |
|
353 settings.lastTick = time(); |
|
354 |
|
355 heartbeatData = $.extend( {}, settings.queue ); |
158 // Clear the data queue, anything added after this point will be send on the next tick |
356 // Clear the data queue, anything added after this point will be send on the next tick |
159 queue = {}; |
357 settings.queue = {}; |
160 |
358 |
161 $(document).trigger( 'heartbeat-send', [data] ); |
359 $document.trigger( 'heartbeat-send', [ heartbeatData ] ); |
162 |
360 |
163 for ( i in data ) { |
361 ajaxData = { |
164 if ( data.hasOwnProperty( i ) ) { |
362 data: heartbeatData, |
165 empty = false; |
363 interval: settings.tempInterval ? settings.tempInterval / 1000 : settings.mainInterval / 1000, |
166 break; |
364 _nonce: typeof window.heartbeatSettings === 'object' ? window.heartbeatSettings.nonce : '', |
167 } |
365 action: 'heartbeat', |
168 } |
366 screen_id: settings.screenId, |
169 |
367 has_focus: settings.hasFocus |
170 // If nothing to send (nothing is expecting a response), |
368 }; |
171 // schedule the next tick and bail |
369 |
172 if ( empty && ! self.hasConnectionError() ) { |
370 settings.connecting = true; |
173 connecting = false; |
371 settings.xhr = $.ajax({ |
174 next(); |
372 url: settings.url, |
175 return; |
|
176 } |
|
177 |
|
178 send.data = data; |
|
179 send.interval = interval / 1000; |
|
180 send._nonce = nonce; |
|
181 send.action = 'heartbeat'; |
|
182 send.screen_id = screenId; |
|
183 send.has_focus = hasFocus; |
|
184 |
|
185 connecting = true; |
|
186 self.xhr = $.ajax({ |
|
187 url: url, |
|
188 type: 'post', |
373 type: 'post', |
189 timeout: 30000, // throw an error if not completed after 30 sec. |
374 timeout: 30000, // throw an error if not completed after 30 sec. |
190 data: send, |
375 data: ajaxData, |
191 dataType: 'json' |
376 dataType: 'json' |
|
377 }).always( function() { |
|
378 settings.connecting = false; |
|
379 scheduleNextTick(); |
192 }).done( function( response, textStatus, jqXHR ) { |
380 }).done( function( response, textStatus, jqXHR ) { |
193 var new_interval; |
381 var newInterval; |
194 |
382 |
195 if ( ! response ) |
383 if ( ! response ) { |
196 return errorstate( 'empty' ); |
384 setErrorState( 'empty' ); |
197 |
385 return; |
198 // Clear error state |
386 } |
199 if ( self.hasConnectionError() ) |
387 |
200 errorstate(); |
388 clearErrorState(); |
201 |
389 |
202 if ( response.nonces_expired ) { |
390 if ( response.nonces_expired ) { |
203 $(document).trigger( 'heartbeat-nonces-expired' ); |
391 $document.trigger( 'heartbeat-nonces-expired' ); |
204 return; |
392 return; |
205 } |
393 } |
206 |
394 |
207 // Change the interval from PHP |
395 // Change the interval from PHP |
208 if ( response.heartbeat_interval ) { |
396 if ( response.heartbeat_interval ) { |
209 new_interval = response.heartbeat_interval; |
397 newInterval = response.heartbeat_interval; |
210 delete response.heartbeat_interval; |
398 delete response.heartbeat_interval; |
211 } |
399 } |
212 |
400 |
213 self.tick( response, textStatus, jqXHR ); |
401 $document.trigger( 'heartbeat-tick', [response, textStatus, jqXHR] ); |
214 |
402 |
215 // do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast' |
403 // Do this last, can trigger the next XHR if connection time > 5 sec. and newInterval == 'fast' |
216 if ( new_interval ) |
404 if ( newInterval ) { |
217 self.interval.call( self, new_interval ); |
405 interval( newInterval ); |
218 }).always( function() { |
406 } |
219 connecting = false; |
|
220 next(); |
|
221 }).fail( function( jqXHR, textStatus, error ) { |
407 }).fail( function( jqXHR, textStatus, error ) { |
222 errorstate( textStatus || 'unknown', jqXHR.status ); |
408 setErrorState( textStatus || 'unknown', jqXHR.status ); |
223 self.error( jqXHR, textStatus, error ); |
409 $document.trigger( 'heartbeat-error', [jqXHR, textStatus, error] ); |
224 }); |
410 }); |
225 } |
411 } |
226 |
412 |
227 function next() { |
413 /** |
228 var delta = time() - tick, t = interval; |
414 * Schedule the next connection |
229 |
415 * |
230 if ( ! running ) |
416 * Fires immediately if the connection time is longer than the interval. |
|
417 * |
|
418 * @access private |
|
419 * |
|
420 * @return void |
|
421 */ |
|
422 function scheduleNextTick() { |
|
423 var delta = time() - settings.lastTick, |
|
424 interval = settings.mainInterval; |
|
425 |
|
426 if ( settings.suspend ) { |
231 return; |
427 return; |
232 |
428 } |
233 if ( ! hasFocus ) { |
429 |
234 t = 100000; // 100 sec. Post locks expire after 120 sec. |
430 if ( ! settings.hasFocus ) { |
235 } else if ( countdown > 0 && tempInterval ) { |
431 interval = 120000; // 120 sec. Post locks expire after 150 sec. |
236 t = tempInterval; |
432 } else if ( settings.countdown > 0 && settings.tempInterval ) { |
237 countdown--; |
433 interval = settings.tempInterval; |
238 } |
434 settings.countdown--; |
239 |
435 |
240 window.clearTimeout(beat); |
436 if ( settings.countdown < 1 ) { |
241 |
437 settings.tempInterval = 0; |
242 if ( delta < t ) { |
438 } |
243 beat = window.setTimeout( |
439 } |
244 function(){ |
440 |
245 if ( running ) |
441 if ( settings.minimalInterval && interval < settings.minimalInterval ) { |
246 connect(); |
442 interval = settings.minimalInterval; |
|
443 } |
|
444 |
|
445 window.clearTimeout( settings.beatTimer ); |
|
446 |
|
447 if ( delta < interval ) { |
|
448 settings.beatTimer = window.setTimeout( |
|
449 function() { |
|
450 connect(); |
247 }, |
451 }, |
248 t - delta |
452 interval - delta |
249 ); |
453 ); |
250 } else { |
454 } else { |
251 connect(); |
455 connect(); |
252 } |
456 } |
253 } |
457 } |
254 |
458 |
|
459 /** |
|
460 * Set the internal state when the browser window becomes hidden or loses focus |
|
461 * |
|
462 * @access private |
|
463 * |
|
464 * @return void |
|
465 */ |
255 function blurred() { |
466 function blurred() { |
256 window.clearTimeout(winBlurTimeout); |
467 settings.hasFocus = false; |
257 window.clearTimeout(frameBlurTimeout); |
468 } |
258 winBlurTimeout = frameBlurTimeout = 0; |
469 |
259 |
470 /** |
260 hasFocus = false; |
471 * Set the internal state when the browser window becomes visible or is in focus |
261 } |
472 * |
262 |
473 * @access private |
|
474 * |
|
475 * @return void |
|
476 */ |
263 function focused() { |
477 function focused() { |
264 window.clearTimeout(winBlurTimeout); |
478 settings.userActivity = time(); |
265 window.clearTimeout(frameBlurTimeout); |
479 |
266 winBlurTimeout = frameBlurTimeout = 0; |
480 // Resume if suspended |
267 |
481 settings.suspend = false; |
268 isUserActive = time(); |
482 |
269 |
483 if ( ! settings.hasFocus ) { |
270 if ( hasFocus ) |
484 settings.hasFocus = true; |
271 return; |
485 scheduleNextTick(); |
272 |
486 } |
273 hasFocus = true; |
487 } |
274 window.clearTimeout(beat); |
488 |
275 |
489 /** |
276 if ( ! connecting ) |
490 * Runs when the user becomes active after a period of inactivity |
277 next(); |
491 * |
278 } |
492 * @access private |
279 |
493 * |
280 function setFrameEvents() { |
494 * @return void |
281 $('iframe').each( function( i, frame ){ |
495 */ |
282 if ( ! isLocalFrame( frame ) ) |
496 function userIsActive() { |
283 return; |
497 settings.userActivityEvents = false; |
284 |
498 $document.off( '.wp-heartbeat-active' ); |
285 if ( $.data( frame, 'wp-heartbeat-focus' ) ) |
499 |
286 return; |
500 $('iframe').each( function( i, frame ) { |
287 |
501 if ( isLocalFrame( frame ) ) { |
288 $.data( frame, 'wp-heartbeat-focus', 1 ); |
502 $( frame.contentWindow ).off( '.wp-heartbeat-active' ); |
289 |
503 } |
290 $( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function(e) { |
504 }); |
291 focused(); |
505 |
292 }).on('blur.wp-heartbeat-focus', function(e) { |
506 focused(); |
293 setFrameEvents(); |
507 } |
294 frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); |
508 |
|
509 /** |
|
510 * Check for user activity |
|
511 * |
|
512 * Runs every 30 sec. |
|
513 * Sets 'hasFocus = true' if user is active and the window is in the background. |
|
514 * Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) |
|
515 * for 5 min. even when the window has focus. |
|
516 * |
|
517 * @access private |
|
518 * |
|
519 * @return void |
|
520 */ |
|
521 function checkUserActivity() { |
|
522 var lastActive = settings.userActivity ? time() - settings.userActivity : 0; |
|
523 |
|
524 // Throttle down when no mouse or keyboard activity for 5 min. |
|
525 if ( lastActive > 300000 && settings.hasFocus ) { |
|
526 blurred(); |
|
527 } |
|
528 |
|
529 // Suspend after 10 min. of inactivity when suspending is enabled. |
|
530 // Always suspend after 60 min. of inactivity. This will release the post lock, etc. |
|
531 if ( ( settings.suspendEnabled && lastActive > 600000 ) || lastActive > 3600000 ) { |
|
532 settings.suspend = true; |
|
533 } |
|
534 |
|
535 if ( ! settings.userActivityEvents ) { |
|
536 $document.on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active', function() { |
|
537 userIsActive(); |
295 }); |
538 }); |
296 }); |
|
297 } |
|
298 |
|
299 $(window).on( 'blur.wp-heartbeat-focus', function(e) { |
|
300 setFrameEvents(); |
|
301 winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 ); |
|
302 }).on( 'focus.wp-heartbeat-focus', function() { |
|
303 $('iframe').each( function( i, frame ) { |
|
304 if ( !isLocalFrame( frame ) ) |
|
305 return; |
|
306 |
|
307 $.removeData( frame, 'wp-heartbeat-focus' ); |
|
308 $( frame.contentWindow ).off( '.wp-heartbeat-focus' ); |
|
309 }); |
|
310 |
|
311 focused(); |
|
312 }); |
|
313 |
|
314 function userIsActive() { |
|
315 userActiveEvents = false; |
|
316 $(document).off( '.wp-heartbeat-active' ); |
|
317 $('iframe').each( function( i, frame ) { |
|
318 if ( ! isLocalFrame( frame ) ) |
|
319 return; |
|
320 |
|
321 $( frame.contentWindow ).off( '.wp-heartbeat-active' ); |
|
322 }); |
|
323 |
|
324 focused(); |
|
325 } |
|
326 |
|
327 // Set 'hasFocus = true' if user is active and the window is in the background. |
|
328 // Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus. |
|
329 function checkUserActive() { |
|
330 var lastActive = isUserActive ? time() - isUserActive : 0; |
|
331 |
|
332 // Throttle down when no mouse or keyboard activity for 5 min |
|
333 if ( lastActive > 300000 && hasFocus ) |
|
334 blurred(); |
|
335 |
|
336 if ( ! userActiveEvents ) { |
|
337 $(document).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); |
|
338 |
539 |
339 $('iframe').each( function( i, frame ) { |
540 $('iframe').each( function( i, frame ) { |
340 if ( ! isLocalFrame( frame ) ) |
541 if ( isLocalFrame( frame ) ) { |
341 return; |
542 $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active', function() { |
342 |
543 userIsActive(); |
343 $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } ); |
544 }); |
|
545 } |
344 }); |
546 }); |
345 |
547 |
346 userActiveEvents = true; |
548 settings.userActivityEvents = true; |
347 } |
549 } |
348 } |
550 } |
349 |
551 |
350 // Check for user activity every 30 seconds. |
552 // Public methods |
351 window.setInterval( function(){ checkUserActive(); }, 30000 ); |
553 |
352 $(document).ready( function() { |
554 /** |
353 // Start one tick (15 sec) after DOM ready |
555 * Whether the window (or any local iframe in it) has focus, or the user is active |
354 running = true; |
556 * |
355 tick = time(); |
557 * @return bool |
356 next(); |
558 */ |
357 }); |
559 function hasFocus() { |
358 |
560 return settings.hasFocus; |
359 this.hasFocus = function() { |
561 } |
360 return hasFocus; |
562 |
361 }; |
563 /** |
|
564 * Whether there is a connection error |
|
565 * |
|
566 * @return bool |
|
567 */ |
|
568 function hasConnectionError() { |
|
569 return settings.connectionError; |
|
570 } |
|
571 |
|
572 /** |
|
573 * Connect asap regardless of 'hasFocus' |
|
574 * |
|
575 * Will not open two concurrent connections. If a connection is in progress, |
|
576 * will connect again immediately after the current connection completes. |
|
577 * |
|
578 * @return void |
|
579 */ |
|
580 function connectNow() { |
|
581 settings.lastTick = 0; |
|
582 scheduleNextTick(); |
|
583 } |
|
584 |
|
585 /** |
|
586 * Disable suspending |
|
587 * |
|
588 * Should be used only when Heartbeat is performing critical tasks like autosave, post-locking, etc. |
|
589 * Using this on many screens may overload the user's hosting account if several |
|
590 * browser windows/tabs are left open for a long time. |
|
591 * |
|
592 * @return void |
|
593 */ |
|
594 function disableSuspend() { |
|
595 settings.suspendEnabled = false; |
|
596 } |
362 |
597 |
363 /** |
598 /** |
364 * Get/Set the interval |
599 * Get/Set the interval |
365 * |
600 * |
366 * When setting to 'fast', the interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec). |
601 * When setting to 'fast' or 5, by default interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec). |
|
602 * In this case the number of 'ticks' can be passed as second argument. |
367 * If the window doesn't have focus, the interval slows down to 2 min. |
603 * If the window doesn't have focus, the interval slows down to 2 min. |
368 * |
604 * |
369 * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec) |
605 * @param mixed speed Interval: 'fast' or 5, 15, 30, 60, 120 |
370 * @param string ticks Used with speed = 'fast', how many ticks before the speed reverts back |
606 * @param string ticks Used with speed = 'fast' or 5, how many ticks before the interval reverts back |
371 * @return int Current interval in seconds |
607 * @return int Current interval in seconds |
372 */ |
608 */ |
373 this.interval = function( speed, ticks ) { |
609 function interval( speed, ticks ) { |
374 var reset, seconds; |
610 var newInterval, |
375 ticks = parseInt( ticks, 10 ) || 30; |
611 oldInterval = settings.tempInterval ? settings.tempInterval : settings.mainInterval; |
376 ticks = ticks < 1 || ticks > 30 ? 30 : ticks; |
|
377 |
612 |
378 if ( speed ) { |
613 if ( speed ) { |
379 switch ( speed ) { |
614 switch ( speed ) { |
380 case 'fast': |
615 case 'fast': |
381 seconds = 5; |
616 case 5: |
382 countdown = ticks; |
617 newInterval = 5000; |
383 break; |
618 break; |
384 case 'slow': |
619 case 15: |
385 seconds = 60; |
620 newInterval = 15000; |
386 countdown = 0; |
621 break; |
|
622 case 30: |
|
623 newInterval = 30000; |
|
624 break; |
|
625 case 60: |
|
626 newInterval = 60000; |
|
627 break; |
|
628 case 120: |
|
629 newInterval = 120000; |
387 break; |
630 break; |
388 case 'long-polling': |
631 case 'long-polling': |
389 // Allow long polling, (experimental) |
632 // Allow long polling, (experimental) |
390 interval = 0; |
633 settings.mainInterval = 0; |
391 return 0; |
634 return 0; |
392 break; |
|
393 default: |
635 default: |
394 seconds = 15; |
636 newInterval = settings.originalInterval; |
395 countdown = 0; |
637 } |
396 } |
638 |
397 |
639 if ( settings.minimalInterval && newInterval < settings.minimalInterval ) { |
398 // Reset when the new interval value is lower than the current one |
640 newInterval = settings.minimalInterval; |
399 reset = seconds * 1000 < interval; |
641 } |
400 |
642 |
401 if ( countdown > 0 ) { |
643 if ( 5000 === newInterval ) { |
402 tempInterval = seconds * 1000; |
644 ticks = parseInt( ticks, 10 ) || 30; |
|
645 ticks = ticks < 1 || ticks > 30 ? 30 : ticks; |
|
646 |
|
647 settings.countdown = ticks; |
|
648 settings.tempInterval = newInterval; |
403 } else { |
649 } else { |
404 interval = seconds * 1000; |
650 settings.countdown = 0; |
405 tempInterval = 0; |
651 settings.tempInterval = 0; |
406 } |
652 settings.mainInterval = newInterval; |
407 |
653 } |
408 if ( reset ) |
654 |
409 next(); |
655 // Change the next connection time if new interval has been set. |
410 } |
656 // Will connect immediately if the time since the last connection |
411 |
657 // is greater than the new interval. |
412 if ( ! hasFocus ) |
658 if ( newInterval !== oldInterval ) { |
413 return 120; |
659 scheduleNextTick(); |
414 |
660 } |
415 return tempInterval ? tempInterval / 1000 : interval / 1000; |
661 } |
416 }; |
662 |
|
663 return settings.tempInterval ? settings.tempInterval / 1000 : settings.mainInterval / 1000; |
|
664 } |
417 |
665 |
418 /** |
666 /** |
419 * Enqueue data to send with the next XHR |
667 * Enqueue data to send with the next XHR |
420 * |
668 * |
421 * As the data is sent later, this function doesn't return the XHR response. |
669 * As the data is send asynchronously, this function doesn't return the XHR response. |
422 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: |
670 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example: |
423 * $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) { |
671 * $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) { |
424 * // code |
672 * // code |
425 * }); |
673 * }); |
426 * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'. |
674 * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'. |
427 * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle. |
675 * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle. |
428 * |
676 * |
429 * $param string handle Unique handle for the data. The handle is used in PHP to receive the data. |
677 * $param string handle Unique handle for the data. The handle is used in PHP to receive the data. |
430 * $param mixed data The data to send. |
678 * $param mixed data The data to send. |
431 * $param bool dont_overwrite Whether to overwrite existing data in the queue. |
679 * $param bool noOverwrite Whether to overwrite existing data in the queue. |
432 * $return bool Whether the data was queued or not. |
680 * $return bool Whether the data was queued or not. |
433 */ |
681 */ |
434 this.enqueue = function( handle, data, dont_overwrite ) { |
682 function enqueue( handle, data, noOverwrite ) { |
435 if ( handle ) { |
683 if ( handle ) { |
436 if ( dont_overwrite && this.isQueued( handle ) ) |
684 if ( noOverwrite && this.isQueued( handle ) ) { |
437 return false; |
685 return false; |
438 |
686 } |
439 queue[handle] = data; |
687 |
|
688 settings.queue[handle] = data; |
440 return true; |
689 return true; |
441 } |
690 } |
442 return false; |
691 return false; |
443 }; |
692 } |
444 |
693 |
445 /** |
694 /** |
446 * Check if data with a particular handle is queued |
695 * Check if data with a particular handle is queued |
447 * |
696 * |
448 * $param string handle The handle for the data |
697 * $param string handle The handle for the data |
449 * $return bool Whether some data is queued with this handle |
698 * $return bool Whether some data is queued with this handle |
450 */ |
699 */ |
451 this.isQueued = function( handle ) { |
700 function isQueued( handle ) { |
452 if ( handle ) |
701 if ( handle ) { |
453 return queue.hasOwnProperty( handle ); |
702 return settings.queue.hasOwnProperty( handle ); |
454 }; |
703 } |
|
704 } |
455 |
705 |
456 /** |
706 /** |
457 * Remove data with a particular handle from the queue |
707 * Remove data with a particular handle from the queue |
458 * |
708 * |
459 * $param string handle The handle for the data |
709 * $param string handle The handle for the data |
460 * $return void |
710 * $return void |
461 */ |
711 */ |
462 this.dequeue = function( handle ) { |
712 function dequeue( handle ) { |
463 if ( handle ) |
713 if ( handle ) { |
464 delete queue[handle]; |
714 delete settings.queue[handle]; |
465 }; |
715 } |
|
716 } |
466 |
717 |
467 /** |
718 /** |
468 * Get data that was enqueued with a particular handle |
719 * Get data that was enqueued with a particular handle |
469 * |
720 * |
470 * $param string handle The handle for the data |
721 * $param string handle The handle for the data |
471 * $return mixed The data or undefined |
722 * $return mixed The data or undefined |
472 */ |
723 */ |
473 this.getQueuedItem = function( handle ) { |
724 function getQueuedItem( handle ) { |
474 if ( handle ) |
725 if ( handle ) { |
475 return this.isQueued( handle ) ? queue[handle] : undefined; |
726 return this.isQueued( handle ) ? settings.queue[handle] : undefined; |
|
727 } |
|
728 } |
|
729 |
|
730 initialize(); |
|
731 |
|
732 // Expose public methods |
|
733 return { |
|
734 hasFocus: hasFocus, |
|
735 connectNow: connectNow, |
|
736 disableSuspend: disableSuspend, |
|
737 interval: interval, |
|
738 hasConnectionError: hasConnectionError, |
|
739 enqueue: enqueue, |
|
740 dequeue: dequeue, |
|
741 isQueued: isQueued, |
|
742 getQueuedItem: getQueuedItem |
476 }; |
743 }; |
477 }; |
744 }; |
478 |
745 |
479 $.extend( Heartbeat.prototype, { |
746 // Ensure the global `wp` object exists. |
480 tick: function( data, textStatus, jqXHR ) { |
747 window.wp = window.wp || {}; |
481 $(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] ); |
748 window.wp.heartbeat = new Heartbeat(); |
482 }, |
749 |
483 error: function( jqXHR, textStatus, error ) { |
750 }( jQuery, window )); |
484 $(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] ); |
|
485 } |
|
486 }); |
|
487 |
|
488 wp.heartbeat = new Heartbeat(); |
|
489 |
|
490 }(jQuery)); |
|