I have JavaScript that is doing activity periodically. When the user is not looking at the site (i.e., the window or tab does not have focus), it\'d be nice to not run.
There are 3 typical methods used to determine if the user can see the HTML page, however none of them work perfectly:
The W3C Page Visibility API is supposed to do this (supported since: Firefox 10, MSIE 10, Chrome 13). However, this API only raises events when the browser tab is fully overriden (e.g. when the user changes from one tab to another one). The API does not raise events when the visibility cannot be determined with 100% accuracy (e.g. Alt+Tab to switch to another application).
Using focus/blur based methods gives you a lot of false positive. For example, if the user displays a smaller window on top of the browser window, the browser window will lose the focus (onblur
raised) but the user is still able to see it (so it still need to be refreshed). See also http://javascript.info/tutorial/focus
In order to improve the imperfect behaviors described above, I use a combination of the 3 methods: W3C Visibility API, then focus/blur and user activity methods in order to reduce the false positive rate. This allows to manage the following events:
This is how it works: when the document lose the focus, the user activity (such as mouse move) on the document is monitored in order to determine if the window is visible or not. The page visibility probability is inversely proportional to the time of the last user activity on the page: if the user makes no activity on the document for a long time, the page is most probably not visible. The code below mimics the W3C Page Visibility API: it behaves the same way but has a small false positive rate. It has the advantage to be multibrowser (tested on Firefox 5, Firefox 10, MSIE 9, MSIE 7, Safari 5, Chrome 9).
<div id="x"></div> <script> /** Registers the handler to the event for the given object. @param obj the object which will raise the event @param evType the event type: click, keypress, mouseover, ... @param fn the event handler function @param isCapturing set the event mode (true = capturing event, false = bubbling event) @return true if the event handler has been attached correctly */ function addEvent(obj, evType, fn, isCapturing){ if (isCapturing==null) isCapturing=false; if (obj.addEventListener){ // Firefox obj.addEventListener(evType, fn, isCapturing); return true; } else if (obj.attachEvent){ // MSIE var r = obj.attachEvent('on'+evType, fn); return r; } else { return false; } } // register to the potential page visibility change addEvent(document, "potentialvisilitychange", function(event) { document.getElementById("x").innerHTML+="potentialVisilityChange: potentialHidden="+document.potentialHidden+", document.potentiallyHiddenSince="+document.potentiallyHiddenSince+" s<br>"; }); // register to the W3C Page Visibility API var hidden=null; var visibilityChange=null; if (typeof document.mozHidden !== "undefined") { hidden="mozHidden"; visibilityChange="mozvisibilitychange"; } else if (typeof document.msHidden !== "undefined") { hidden="msHidden"; visibilityChange="msvisibilitychange"; } else if (typeof document.webkitHidden!=="undefined") { hidden="webkitHidden"; visibilityChange="webkitvisibilitychange"; } else if (typeof document.hidden !=="hidden") { hidden="hidden"; visibilityChange="visibilitychange"; } if (hidden!=null && visibilityChange!=null) { addEvent(document, visibilityChange, function(event) { document.getElementById("x").innerHTML+=visibilityChange+": "+hidden+"="+document[hidden]+"<br>"; }); } var potentialPageVisibility = { pageVisibilityChangeThreshold:3*3600, // in seconds init:function() { function setAsNotHidden() { var dispatchEventRequired=document.potentialHidden; document.potentialHidden=false; document.potentiallyHiddenSince=0; if (dispatchEventRequired) dispatchPageVisibilityChangeEvent(); } function initPotentiallyHiddenDetection() { if (!hasFocusLocal) { // the window does not has the focus => check for user activity in the window lastActionDate=new Date(); if (timeoutHandler!=null) { clearTimeout(timeoutHandler); } timeoutHandler = setTimeout(checkPageVisibility, potentialPageVisibility.pageVisibilityChangeThreshold*1000+100); // +100 ms to avoid rounding issues under Firefox } } function dispatchPageVisibilityChangeEvent() { unifiedVisilityChangeEventDispatchAllowed=false; var evt = document.createEvent("Event"); evt.initEvent("potentialvisilitychange", true, true); document.dispatchEvent(evt); } function checkPageVisibility() { var potentialHiddenDuration=(hasFocusLocal || lastActionDate==null?0:Math.floor((new Date().getTime()-lastActionDate.getTime())/1000)); document.potentiallyHiddenSince=potentialHiddenDuration; if (potentialHiddenDuration>=potentialPageVisibility.pageVisibilityChangeThreshold && !document.potentialHidden) { // page visibility change threshold raiched => raise the even document.potentialHidden=true; dispatchPageVisibilityChangeEvent(); } } var lastActionDate=null; var hasFocusLocal=true; var hasMouseOver=true; document.potentialHidden=false; document.potentiallyHiddenSince=0; var timeoutHandler = null; addEvent(document, "pageshow", function(event) { document.getElementById("x").innerHTML+="pageshow/doc:<br>"; }); addEvent(document, "pagehide", function(event) { document.getElementById("x").innerHTML+="pagehide/doc:<br>"; }); addEvent(window, "pageshow", function(event) { document.getElementById("x").innerHTML+="pageshow/win:<br>"; // raised when the page first shows }); addEvent(window, "pagehide", function(event) { document.getElementById("x").innerHTML+="pagehide/win:<br>"; // not raised }); addEvent(document, "mousemove", function(event) { lastActionDate=new Date(); }); addEvent(document, "mouseover", function(event) { hasMouseOver=true; setAsNotHidden(); }); addEvent(document, "mouseout", function(event) { hasMouseOver=false; initPotentiallyHiddenDetection(); }); addEvent(window, "blur", function(event) { hasFocusLocal=false; initPotentiallyHiddenDetection(); }); addEvent(window, "focus", function(event) { hasFocusLocal=true; setAsNotHidden(); }); setAsNotHidden(); } } potentialPageVisibility.pageVisibilityChangeThreshold=4; // 4 seconds for testing potentialPageVisibility.init(); </script>
Since there is currently no working cross-browser solution without false positive, you should better think twice about disabling periodical activity on your web site.
Using : Page Visibility API
document.addEventListener( 'visibilitychange' , function() {
if (document.hidden) {
console.log('bye');
} else {
console.log('well back');
}
}, false );
Can i use ? http://caniuse.com/#feat=pagevisibility
var visibilityChange = (function (window) {
var inView = false;
return function (fn) {
window.onfocus = window.onblur = window.onpageshow = window.onpagehide = function (e) {
if ({focus:1, pageshow:1}[e.type]) {
if (inView) return;
fn("visible");
inView = true;
} else if (inView) {
fn("hidden");
inView = false;
}
};
};
}(this));
visibilityChange(function (state) {
console.log(state);
});
http://jsfiddle.net/ARTsinn/JTxQY/
For angular.js, here is a directive (based on the accepted answer) that will allow your controller to react to a change in visibility:
myApp.directive('reactOnWindowFocus', function($parse) {
return {
restrict: "A",
link: function(scope, element, attrs) {
var hidden = "hidden";
var currentlyVisible = true;
var functionOrExpression = $parse(attrs.reactOnWindowFocus);
// Standards:
if (hidden in document)
document.addEventListener("visibilitychange", onchange);
else if ((hidden = "mozHidden") in document)
document.addEventListener("mozvisibilitychange", onchange);
else if ((hidden = "webkitHidden") in document)
document.addEventListener("webkitvisibilitychange", onchange);
else if ((hidden = "msHidden") in document)
document.addEventListener("msvisibilitychange", onchange);
else if ("onfocusin" in document) {
// IE 9 and lower:
document.onfocusin = onshow;
document.onfocusout = onhide;
} else {
// All others:
window.onpageshow = window.onfocus = onshow;
window.onpagehide = window.onblur = onhide;
}
function onchange (evt) {
//occurs both on leaving and on returning
currentlyVisible = !currentlyVisible;
doSomethingIfAppropriate();
}
function onshow(evt) {
//for older browsers
currentlyVisible = true;
doSomethingIfAppropriate();
}
function onhide(evt) {
//for older browsers
currentlyVisible = false;
doSomethingIfAppropriate();
}
function doSomethingIfAppropriate() {
if (currentlyVisible) {
//trigger angular digest cycle in this scope
scope.$apply(function() {
functionOrExpression(scope);
});
}
}
}
};
});
You can use it like this example: <div react-on-window-focus="refresh()">
, where refresh()
is a scope function in the scope of whatever Controller is in scope.
There is a neat library available on GitHub:
https://github.com/serkanyersen/ifvisible.js
Example:
// If page is visible right now
if( ifvisible.now() ){
// Display pop-up
openPopUp();
}
I've tested version 1.0.1 on all browsers I have and can confirm that it works with:
... and probably all newer versions.
Doesn't fully work with:
.now()
always returns true
for me)For a solution without jQuery check out Visibility.js which provides information about three page states
visible ... page is visible
hidden ... page is not visible
prerender ... page is being prerendered by the browser
and also convenience-wrappers for setInterval
/* Perform action every second if visible */
Visibility.every(1000, function () {
action();
});
/* Perform action every second if visible, every 60 sec if not visible */
Visibility.every(1000, 60*1000, function () {
action();
});
A fallback for older browsers (IE < 10; iOS < 7) is also available