UNPKG

active-timeout.js

Version:

JavaScript library that lets you measure time the user has spent viewing your page. Inactive time will not be counted.

156 lines (138 loc) 5.14 kB
window.ActiveTimeout = (function () { // Gets the Visibility API properties. If the browser doesn't support // that, this library would basically be useless, except for the fancy way // of handling time. var _visibility = (function () { var p, e; if ("hidden" in document) { p = "hidden"; e = "visibilitychange"; } else if ("msHidden" in document) { p = "msHidden"; e = "msvisibilitychange"; } else if ("webkitHidden" in document) { p = "webkitHidden"; e = "webkitvisibilitychange"; } else { return null; } return { hidden: p, event: e }; })(); function _isPageHidden() { return (_visibility && document[_visibility.hidden]); } /** * Invokes a function after a ~17ms interval. Uses `requestAnimationFrame` * if supported to ease the load on the browser. * @param {function} callback Function to execute. */ function _delay(callback) { if (typeof requestAnimationFrame !== "undefined") { requestAnimationFrame(callback); } else { setTimeout(callback, (1000 / 60)); } } var _listeners = []; if (_visibility) { document.addEventListener(_visibility.event, function () { if (!_isPageHidden()) { // The page is not hidden, but a change occured. That means // is *was* previously hidden, so the timers receive an event // that tells them to ignore the next tick since it would // contain the inactive time. for (var i = _listeners.length - 1; i >= 0; i--) { _listeners[i](); } } }); } /** * Measure ~60 intervals per second and ignore ones where the user was * inactive. Repeat until the predicate returns a falsy value. * @param {function} predicate Stops pulsing when it returns a falsy value. */ function pulse(predicate) { var ignoreTicks = 0; var listener = function () { ignoreTicks += 1; } _listeners.push(listener); (function measure(last) { var proceed = false, now = Date.now(); // Whenever the page loses focus, the next tick should be // ignored, otherwise the inactive time would be added and this // whole charade would be meaningless. if (ignoreTicks > 0) { ignoreTicks -= 1; proceed = true; } else { if (_isPageHidden()) { // Page is hidden, continue to pulse and wait for focus. proceed = true; } else { if (!last) { // First iteration of the recursion, continue and wait // for some time to pass. proceed = true; } else { // Finally, the predicate decides whether to continue. proceed = predicate(now - last); } } } if (proceed === true) { _delay(function () { measure(now); }); } else { _listeners.splice(_listeners.indexOf(listener), 1); } })(); } /** * Count *active* passed time until a predicate returns a falsy value. * @param {function} predicate Stops the counter when it returns a falsy value. */ function count(predicate) { var time = 0; pulse(function (tick) { time += tick; return predicate(time, tick); }); } /** * Invoke a function after the user has spent a set amount of *active* time * on the page. Optionally provide a callback for each tick. * @param {function} completeCallback Function to call when the timeout * completes. * @param {function} tickCallback Optional function to call upon each * timer tick. * @param {number} time How much milliseconds to wait. */ function timeout(completeCallback, tickCallback, time) { if (typeof tickCallback === "number") { time = tickCallback; tickCallback = null; } count(function (passed, tick) { if (typeof tickCallback === "function") { tickCallback(time - passed, tick); } if (passed >= time) { completeCallback(); return false; } else { return true; } }); } return { set: timeout, count: count, pulse: pulse }; })();