Skip to content

Commit fdd7725

Browse files
author
Rusty Geldmacher
committed
Support for syncing timers across tabs
If a user had two tabs open, and left one in the background while working in the other, the background tab could time out and log the user out, which would cause a logout in the active tab as well. This commit adds an optional syncronization mechanism for a timer, which will use localStorage to send and receive activity events. When a tab is active, it'll set a key in localStorage, causing all other tabs to receive an event and reset theier activity timeouts. Bumped version to 1.1.0
1 parent cc32754 commit fdd7725

File tree

10 files changed

+444
-57
lines changed

10 files changed

+444
-57
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
####VERSION: 1.0.0
1+
####VERSION: 1.1.0
22
####RELEASED: 03/10/2014
33
####AUTHOR: TODD HORST
44
-------
@@ -26,6 +26,7 @@ Added:
2626
* `wheel` event. wheel is to replace DOMMouseScroll & mousewheel. We could remove DOMMouseScroll now as its technically not needed to support firefox n-1. mousewheel will stick around until we drop ie8.
2727
* `MSPointerDown` `MSPointerMove` events #22
2828
* ability to send only settings parameter
29+
* Ability to sync timer state across browser tabs
2930

3031
Update:
3132
* changed $().data() to jQuery.data() for speed
@@ -88,4 +89,4 @@ using the old version.
8889

8990
I thought about allowing users to send a callback function in, ala most jquery methods. However
9091
after researching and thinking the callbacks would be synchronous and events are async. So we
91-
will be sticking with the current method
92+
will be sticking with the current method

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ $(function() {
7676

7777
// activity is any one of these events
7878
events [default:'mousemove keydown wheel DOMMouseScroll mousewheel mousedown touchstart touchmove MSPointerDown MSPointerMove']
79+
80+
// If set, the use a localStorage key to sync activity across browser tabs/windows
81+
timerSyncId [default:null]
7982
});
8083
```
8184

demos/index.html

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ <h4 class="modal-title" id="myModalLabel">Modal title</h4>
235235

236236
//Start timeout, passing no options
237237
//Same as $.idleTimer(docTimeout, docOptions);
238-
$(document).idleTimer(docTimeout);
238+
$(document).idleTimer({
239+
timeout: docTimeout,
240+
timerSyncId: "document-timer-demo"
241+
});
239242

240243
//For demo purposes, style based on initial state
241244
if ($(document).idleTimer("isIdle")) {
@@ -327,7 +330,7 @@ <h4 class="modal-title" id="myModalLabel">Modal title</h4>
327330
}
328331
$(this).blur();
329332
return false;
330-
});
333+
});
331334
$("#btRemaining").click(function () {
332335
$('#elStatus')
333336
.val(function (i, v) {
@@ -345,7 +348,7 @@ <h4 class="modal-title" id="myModalLabel">Modal title</h4>
345348
.scrollToBottom();
346349
$(this).blur();
347350
return false;
348-
});
351+
});
349352
$("#btState").click(function () {
350353
$('#elStatus')
351354
.val(function (i, v) {
@@ -357,7 +360,10 @@ <h4 class="modal-title" id="myModalLabel">Modal title</h4>
357360
});
358361

359362
//Clear value if there was one cached & start time
360-
$('#elStatus').val('').idleTimer(taTimeout);
363+
$('#elStatus').val('').idleTimer({
364+
timeout: taTimeout,
365+
timerSyncId: "element-timer-demo"
366+
});
361367

362368
//For demo purposes, show initial state
363369
if ($("#elStatus").idleTimer("isIdle")) {

dist/idle-timer.1.1.0.js

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/*! Idle Timer - v1.1.0 - 2016-03-21
2+
* https://github.com/thorst/jquery-idletimer
3+
* Copyright (c) 2016 Paul Irish; Licensed MIT */
4+
/*
5+
mousewheel (deprecated) -> IE6.0, Chrome, Opera, Safari
6+
DOMMouseScroll (deprecated) -> Firefox 1.0
7+
wheel (standard) -> Chrome 31, Firefox 17, IE9, Firefox Mobile 17.0
8+
9+
//No need to use, use DOMMouseScroll
10+
MozMousePixelScroll -> Firefox 3.5, Firefox Mobile 1.0
11+
12+
//Events
13+
WheelEvent -> see wheel
14+
MouseWheelEvent -> see mousewheel
15+
MouseScrollEvent -> Firefox 3.5, Firefox Mobile 1.0
16+
*/
17+
(function ($) {
18+
19+
$.idleTimer = function (firstParam, elem) {
20+
var opts;
21+
if ( typeof firstParam === "object" ) {
22+
opts = firstParam;
23+
firstParam = null;
24+
} else if (typeof firstParam === "number") {
25+
opts = { timeout: firstParam };
26+
firstParam = null;
27+
}
28+
29+
// element to watch
30+
elem = elem || document;
31+
32+
// defaults that are to be stored as instance props on the elem
33+
opts = $.extend({
34+
idle: false, // indicates if the user is idle
35+
timeout: 30000, // the amount of time (ms) before the user is considered idle
36+
events: "mousemove keydown wheel DOMMouseScroll mousewheel mousedown touchstart touchmove MSPointerDown MSPointerMove" // define active events
37+
}, opts);
38+
39+
var jqElem = $(elem),
40+
obj = jqElem.data("idleTimerObj") || {},
41+
42+
/* (intentionally not documented)
43+
* Toggles the idle state and fires an appropriate event.
44+
* @return {void}
45+
*/
46+
toggleIdleState = function (e) {
47+
var obj = $.data(elem, "idleTimerObj") || {};
48+
49+
// toggle the state
50+
obj.idle = !obj.idle;
51+
52+
// store toggle state date time
53+
obj.olddate = +new Date();
54+
55+
// create a custom event, with state and name space
56+
var event = $.Event((obj.idle ? "idle" : "active") + ".idleTimer");
57+
58+
// trigger event on object with elem and copy of obj
59+
$(elem).trigger(event, [elem, $.extend({}, obj), e]);
60+
},
61+
/**
62+
* Handle event triggers
63+
* @return {void}
64+
* @method event
65+
* @static
66+
*/
67+
handleEvent = function (e) {
68+
var obj = $.data(elem, "idleTimerObj") || {};
69+
70+
if (e.type === "storage" && e.originalEvent.key !== obj.timerSyncId) {
71+
return;
72+
}
73+
74+
// this is already paused, ignore events for now
75+
if (obj.remaining != null) { return; }
76+
77+
/*
78+
mousemove is kinda buggy, it can be triggered when it should be idle.
79+
Typically is happening between 115 - 150 milliseconds after idle triggered.
80+
@psyafter & @kaellis report "always triggered if using modal (jQuery ui, with overlay)"
81+
@thorst has similar issues on ios7 "after $.scrollTop() on text area"
82+
*/
83+
if (e.type === "mousemove") {
84+
// if coord are same, it didn't move
85+
if (e.pageX === obj.pageX && e.pageY === obj.pageY) {
86+
return;
87+
}
88+
// if coord don't exist how could it move
89+
if (typeof e.pageX === "undefined" && typeof e.pageY === "undefined") {
90+
return;
91+
}
92+
// under 200 ms is hard to do, and you would have to stop, as continuous activity will bypass this
93+
var elapsed = (+new Date()) - obj.olddate;
94+
if (elapsed < 200) {
95+
return;
96+
}
97+
}
98+
99+
// clear any existing timeout
100+
clearTimeout(obj.tId);
101+
102+
// if the idle timer is enabled, flip
103+
if (obj.idle) {
104+
toggleIdleState(e);
105+
}
106+
107+
// store when user was last active
108+
obj.lastActive = +new Date();
109+
110+
// update mouse coord
111+
obj.pageX = e.pageX;
112+
obj.pageY = e.pageY;
113+
114+
// sync lastActive
115+
if (e.type !== "storage" && obj.timerSyncId) {
116+
if (typeof(localStorage) !== "undefined") {
117+
localStorage.setItem(obj.timerSyncId, obj.lastActive);
118+
}
119+
}
120+
121+
// set a new timeout
122+
obj.tId = setTimeout(toggleIdleState, obj.timeout);
123+
},
124+
/**
125+
* Restore initial settings and restart timer
126+
* @return {void}
127+
* @method reset
128+
* @static
129+
*/
130+
reset = function () {
131+
132+
var obj = $.data(elem, "idleTimerObj") || {};
133+
134+
// reset settings
135+
obj.idle = obj.idleBackup;
136+
obj.olddate = +new Date();
137+
obj.lastActive = obj.olddate;
138+
obj.remaining = null;
139+
140+
// reset Timers
141+
clearTimeout(obj.tId);
142+
if (!obj.idle) {
143+
obj.tId = setTimeout(toggleIdleState, obj.timeout);
144+
}
145+
146+
},
147+
/**
148+
* Store remaining time, stop timer
149+
* You can pause from an idle OR active state
150+
* @return {void}
151+
* @method pause
152+
* @static
153+
*/
154+
pause = function () {
155+
156+
var obj = $.data(elem, "idleTimerObj") || {};
157+
158+
// this is already paused
159+
if ( obj.remaining != null ) { return; }
160+
161+
// define how much is left on the timer
162+
obj.remaining = obj.timeout - ((+new Date()) - obj.olddate);
163+
164+
// clear any existing timeout
165+
clearTimeout(obj.tId);
166+
},
167+
/**
168+
* Start timer with remaining value
169+
* @return {void}
170+
* @method resume
171+
* @static
172+
*/
173+
resume = function () {
174+
175+
var obj = $.data(elem, "idleTimerObj") || {};
176+
177+
// this isn't paused yet
178+
if ( obj.remaining == null ) { return; }
179+
180+
// start timer
181+
if ( !obj.idle ) {
182+
obj.tId = setTimeout(toggleIdleState, obj.remaining);
183+
}
184+
185+
// clear remaining
186+
obj.remaining = null;
187+
},
188+
/**
189+
* Stops the idle timer. This removes appropriate event handlers
190+
* and cancels any pending timeouts.
191+
* @return {void}
192+
* @method destroy
193+
* @static
194+
*/
195+
destroy = function () {
196+
197+
var obj = $.data(elem, "idleTimerObj") || {};
198+
199+
//clear any pending timeouts
200+
clearTimeout(obj.tId);
201+
202+
//Remove data
203+
jqElem.removeData("idleTimerObj");
204+
205+
//detach the event handlers
206+
jqElem.off("._idleTimer");
207+
},
208+
/**
209+
* Returns the time until becoming idle
210+
* @return {number}
211+
* @method remainingtime
212+
* @static
213+
*/
214+
remainingtime = function () {
215+
216+
var obj = $.data(elem, "idleTimerObj") || {};
217+
218+
//If idle there is no time remaining
219+
if ( obj.idle ) { return 0; }
220+
221+
//If its paused just return that
222+
if ( obj.remaining != null ) { return obj.remaining; }
223+
224+
//Determine remaining, if negative idle didn't finish flipping, just return 0
225+
var remaining = obj.timeout - ((+new Date()) - obj.lastActive);
226+
if (remaining < 0) { remaining = 0; }
227+
228+
//If this is paused return that number, else return current remaining
229+
return remaining;
230+
};
231+
232+
233+
// determine which function to call
234+
if (firstParam === null && typeof obj.idle !== "undefined") {
235+
// they think they want to init, but it already is, just reset
236+
reset();
237+
return jqElem;
238+
} else if (firstParam === null) {
239+
// they want to init
240+
} else if (firstParam !== null && typeof obj.idle === "undefined") {
241+
// they want to do something, but it isnt init
242+
// not sure the best way to handle this
243+
return false;
244+
} else if (firstParam === "destroy") {
245+
destroy();
246+
return jqElem;
247+
} else if (firstParam === "pause") {
248+
pause();
249+
return jqElem;
250+
} else if (firstParam === "resume") {
251+
resume();
252+
return jqElem;
253+
} else if (firstParam === "reset") {
254+
reset();
255+
return jqElem;
256+
} else if (firstParam === "getRemainingTime") {
257+
return remainingtime();
258+
} else if (firstParam === "getElapsedTime") {
259+
return (+new Date()) - obj.olddate;
260+
} else if (firstParam === "getLastActiveTime") {
261+
return obj.lastActive;
262+
} else if (firstParam === "isIdle") {
263+
return obj.idle;
264+
}
265+
266+
/* (intentionally not documented)
267+
* Handles a user event indicating that the user isn't idle. namespaced with internal idleTimer
268+
* @param {Event} event A DOM2-normalized event object.
269+
* @return {void}
270+
*/
271+
jqElem.on($.trim((opts.events + " ").split(" ").join("._idleTimer ")), function (e) {
272+
handleEvent(e);
273+
});
274+
275+
if (opts.timerSyncId) {
276+
$(window).bind("storage", handleEvent);
277+
}
278+
279+
// Internal Object Properties, This isn't all necessary, but we
280+
// explicitly define all keys here so we know what we are working with
281+
obj = $.extend({}, {
282+
olddate : +new Date(), // the last time state changed
283+
lastActive: +new Date(), // the last time timer was active
284+
idle : opts.idle, // current state
285+
idleBackup : opts.idle, // backup of idle parameter since it gets modified
286+
timeout : opts.timeout, // the interval to change state
287+
remaining : null, // how long until state changes
288+
timerSyncId : opts.timerSyncId, // localStorage key to use for syncing this timer
289+
tId : null, // the idle timer setTimeout
290+
pageX : null, // used to store the mouse coord
291+
pageY : null
292+
});
293+
294+
// set a timeout to toggle state. May wish to omit this in some situations
295+
if (!obj.idle) {
296+
obj.tId = setTimeout(toggleIdleState, obj.timeout);
297+
}
298+
299+
// store our instance on the object
300+
$.data(elem, "idleTimerObj", obj);
301+
302+
return jqElem;
303+
};
304+
305+
// This allows binding to element
306+
$.fn.idleTimer = function (firstParam) {
307+
if (this[0]) {
308+
return $.idleTimer(firstParam, this[0]);
309+
}
310+
311+
return this;
312+
};
313+
314+
})(jQuery);

dist/idle-timer.1.1.0.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)