How to close a progressive web app - javascript

Once a pwa is installed a mobile device, how do I close the application like a native app without having the user click the back button multiple times.
I understand it's a bad idea to window.close on a web page but this a pwa on a mobile device.
In Cordova you will use navigator.app.exitApp, this is of course not available on pwa.

This is a solution I created today.
When you click the back button a dialog will request you to just click the back button one more time to actually close the app, or Cancel to go back to the page.
The whole thing uses some manipulation of the history, and it works on Chrome. One could adjust things so that it would work for more browsers. It seems browsers often have slightly different philosophies when it comes to how the history should work in detail.
Therefore, in this example, I have a condition that it does its thing only for Android with Chrome, as you can see in the code.
There is also a condition of fullscreen (my PWA runs in fullscreen) so that this logic will not be used in normal browser (you can test in normal browser by setting testInBrowser = true).
closePWA.js
var testInBrowser = false; // set this to true to test in browser
if ( testInBrowser
||
/Android/i.test(navigator.userAgent)
&& /Chrome/i.test(navigator.userAgent)
&& window.matchMedia('(display-mode: fullscreen)').matches
) {
if (getCookie('closing') == "true") {
setCookie('closing', '', 1);
showCloseDialog();
returnToOriginalPageIfUserCancels();
window.stop();
} else {
history.pushState(null, null);
window.addEventListener('popstate', function () {
setCookie('closing', 'true', 10);
history.go(-(history.length - 2));
})
}
}
function showCloseDialog() {
document.body.style='background-color:#aaa;';
var s = document.createElement('SPAN');
s.style='border-radius:10px;padding:50px 30px 40px 30px;display:table;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);text-align:center;background-color:#fff;font-size:30px;font-family:arial;font-weight:bold;';
s.appendChild(document.createTextNode('Click back button again to close'));
s.appendChild(document.createElement('BR'));
s.appendChild(document.createElement('BR'));
s.appendChild(document.createTextNode('or'));
s.appendChild(document.createElement('BR'));
s.appendChild(document.createElement('BR'));
var b = document.createElement('BUTTON');
b.style='font-size:30px;font-family:arial;background:none!important;border:none;color:blue;font-weight:bold;';
b.innerHTML='Cancel'
b.addEventListener('click',function(){outsideResolve()});
s.appendChild(b);
s.appendChild(document.createElement('BR'));
s.appendChild(document.createElement('BR'));
s.appendChild(document.createTextNode('to go back'));
document.body.appendChild(s);
}
async function returnToOriginalPageIfUserCancels() {
await new Promise(function(resolve) {
outsideResolve = resolve;
});
var steps = history.length - 1;
if (steps==1) steps=0; // takes care of the case when user clicks back on first page
history.go(steps);
}
function setCookie(cname, cvalue, exseconds) {
var d = new Date();
d.setTime(d.getTime() + (exseconds * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
The script should be added right at the beginning of the body element, or very early in the body. This is important. It will not work if added in header, and it will not work properly if added later on in the body, or at the end of the body.
This should be done in every page of your PWA.
Like this:
<!DOCTYPE html>
<html>
<head>
<title>A page</title>
</head>
<body>
<script src="/script/closePWA.js"></script>
<h3>A page</h3>
Go to another page
</body>
</html>
You can test it here
2020-08-17: Update: It seems that Chrome in newer versions has changed the way the history works. So the code must be adjusted. That's of course typical when not using standard solutions and inventing something like this. It shouldn't be difficult, but it requires some investigation. Hopefully the history functionality in Chrome will stabilize after some time so that this script will not have to be adjusted all the time. Right now when I tested it, it actually works in the Firefox browser.
Demo PWA (You must open in a new window / tab and with Chrome Firefox [see update above] if you want to test in browser.)
This demo is configured with testInBrowser = true so that you can test it in your normal Chrome browser. It will of course not close the browser, but you can see how the history jumps.
If you want to test it in your normal (Chrome) browser it is important that you open it in a new window / tab, because the idea is that there must not be any earlier page in the history.
The real test is to try it in your Android device with Chrome. Open it in your Chrome browser, press the tripple dots menu, select "Add shortcut on homescreen".
When the Progressive Web App is installed, start it and navigate back and forth between page 1 and 2 for a while, and then test the whole thing by pressing the back button of your device.

Related

Browser refresh using OnBeforeUnload event

On the web page when the user clicks on the logout button, I have a JavaScript function which clears the user session which is stored on the MongoDB. It works fine.
Also I would like to clear the user session when the user closes the browser. Here is the issue I am facing : when the user clicks on the refresh icon the logout function is getting called. I only want this function to be called when the browser is closed.
Can I prevent this function from calling on the page refresh?
My logic here is to clean up the session when the browser is closed. I am deleting the session which is on MongoDB. Is there a better way to manage the session?
var isRefresh = false;
$(window).keydown(function (event) {
if (event.keyCode == 116) { // User presses F5 to refresh
refresh = true;
}
});
// Calls the logout function when you close the browser thus cleans the session data.
var windowsCloseEventListener = window.attachEvent || window.addEventListener;
var chkForBrowserCloseEvents = window.attachEvent ? 'onbeforeunload' : 'beforeunload'; /// make IE7, IE8 compitable
windowsCloseEventListener(chkForBrowserCloseEvents, function (e) { // For >=IE7, Chrome, Firefox
if (!isRefresh) {
$scope.logout();
}
});
So far as I know, you can't actually test for the browser being closed; you can only test for a window (or tab) being closed. That generally suffices, unless of course a page-refresh happens, since it counts as both closing and re-opening a web page inside a window or tab. What is needed is an event-handler for a click on the browser's page-refresh button, but I'm not sure such exists. Here is something:
How to show the "Are you sure you want to navigate away from this page?" when changes committed?
and also
Handling Browser close event and Page Refresh
One thing I've encountered in trying to find something about a page-refresh event-handler is the notion of using "local storage". You could, as part of the on-before-unload handler, put a small data item, time-stamped, into local storage. Activate a kind of timer on the Server, and wait for that time to expire before erasing the session. When your page is loaded, test local storage for that data item. If it exists and the time-stamp was very recent, you know the page was refreshed and can can do appropriate things based on knowing that --like sending an AJAX message to the Server telling it to not erase the session just yet.
To better manage a session and work around user pressing a browser refresh, you need to
Remove the logout functionality when the window is closed.
Implement InActivity monitor on the client and the server which will clear the session if the user is inactive for certain time. So if the user closes the browser, the session in MongoDB will persist for some time, till the server clears it.
Implement keepalive pings from the client to the server which will ping the server every set time interval if the user is active on the page. To reduce network traffic, you can reschedule keepalive, whenever the server is invoked either using AJAX or RESTful api.
Save the user session in the browser in local storage so the session can be re-read in case of browser refresh.
This InActivity monitor can be a good starting point. https://github.com/HackedByChinese/ng-idle/blob/develop/src/angular-idle.js. I have done all the above in my angular project. Hope that helps.
As Alex suggested, you can use session Cookies to check if your website has been seen for the current session (IE7+ support).
Example
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ') c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
}
return null;
}
var isRefresh = true;
var sessionAlive = readCookie("sessionAlive");
if (sessionAlive === null) {
isRefresh = false;
sessionStorage.setItem("sessionAlive", true);
}
var windowsCloseEventListener = window.attachEvent || window.addEventListener;
var chkForBrowserCloseEvents = window.attachEvent ? 'onbeforeunload' : 'beforeunload';
windowsCloseEventListener(chkForBrowserCloseEvents, function (e) {
if (!isRefresh) {
$scope.logout();
}
});
You can use sessionStorage, which is more of the HTML5 way, to see if the browser has been closed (new session) (IE8+ support).
Example:
var isRefresh = true;
var sessionAlive = sessionStorage.getItem("sessionAlive");
if (sessionAlive === null) {
isRefresh = false;
sessionStorage.setItem("sessionAlive", true);
}
var windowsCloseEventListener = window.attachEvent || window.addEventListener;
var chkForBrowserCloseEvents = window.attachEvent ? 'onbeforeunload' : 'beforeunload';
windowsCloseEventListener(chkForBrowserCloseEvents, function (e) {
if (!isRefresh) {
$scope.logout();
}
});
readCookie taken from: http://www.quirksmode.org/js/cookies.html
Use cookie with session expire should work.
WindowEventHandlers.onbeforeunload
window.onbeforeunload = function(e) {
return 'Dialog text here.';
};
this example shows that the addEventListener and attachEvent methods cannot be used for the onbeforeunload event (except in Google Chrome and Safari):
if (window.addEventListener) { // all browsers except IE before version 9
window.addEventListener ("beforeunload", OnBeforeUnLoad, false);
}
else {
if (window.attachEvent) { // IE before version 9
window.attachEvent ("onbeforeunload", OnBeforeUnLoad);
}
}
// the OnBeforeUnLoad method will only be called in Google Chrome and Safari
function OnBeforeUnLoad () {
return "All data that you have entered will be lost!";
}
you can use "onbeforeunload " event to handle this situation.
$wnd.onbeforeunload = function(e) {
var logoutClicked = callSomeFunctionToGetIfLogoutButtonIsClicked();
if (!logoutClicked) {
return "Some message";
}
return;
}

Prevent page reload on back button (use cache). Or scroll user back down. (Issue on, at least, iPhone)

Is is possible to prevent a page from being reloaded when a user clicks the browser back button? Or at least make sure that the page is scrolled down like it was before?
My webpage shows a list of items. The user scrolls down and then clicks on an item to go to another page. When they then click the back button, I want them to be back on the original page, scrolled down.
This works fine on my desktop (Windows, Chrome), but on my iPhone (iOS7, Safari and Chrome) it reloads the page, which, first of all, takes time and is annoying. Chrome then scrolls back down, but Safari does not, which means that the user is back at the top. The behavior, actually, doesn't seem to be quite consistent.
Is this just a decision made by the browser? It also reloads the page after the screen has been locked and then unlocked. (Again, I'm trying on the iPhone.) Or is it something about my html/header/js that could be doing it?
I'm thinking that I can use HTML caching and then, on the page itself, keep track of how far the user has scrolled. Then when the page is reloaded from cache, I can make it scroll down to where it was before. Is that really the way to go?
It looks like www.amazon.com has got it working, so there must be some solution.
You can use javascript to avoid "reload", but several other reasons can make the page reload on mobiles.
I recommend that instead of trying to avoid the reload, you save user data using cookies, or better localStorage.
For example you want to save the last page position (position of the scroll bar):
(function () {
var scrollFired = false;
function saveScroll () {
if (window.localStorage) {
window.localStorage.setItem("scrollLeftTop", window.pageXOffset + "," + window.pageYOffset);
}
}
function autoScroll() {
scrollFired = true;
if (window.localStorage) {
var xy = window.localStorage.getItem("scrollLeftTop");
if (xy) {
var a = xy.split(",");
var x = a[0];
var y = a[1];
window.scroll(x, y);
}
}
}
function onLoadAutoScroll() {
if(scrollFired === false) {//Prevent scroll if run DOMContentLoaded
autoScroll();
}
}
window.addEventListener("DOMContentLoaded", autoScroll, false);
window.addEventListener("load", onLoadAutoScroll, false);//If DOMContentLoaded not "run"
window.addEventListener("scroll", saveScroll, false);
})();
Compatibility
Note: If needs localStorage in older browsers, try (cookies like localStorage):
if (!window.localStorage) {
window.localStorage = {
getItem: function (sKey) {
if (!sKey || !this.hasOwnProperty(sKey)) { return null; }
return unescape(document.cookie.replace(new RegExp("(?:^|.*;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*"), "$1"));
},
key: function (nKeyId) {
return unescape(document.cookie.replace(/\s*\=(?:.(?!;))*$/, "").split(/\s*\=(?:[^;](?!;))*[^;]?;\s*/)[nKeyId]);
},
setItem: function (sKey, sValue) {
if(!sKey) { return; }
document.cookie = escape(sKey) + "=" + escape(sValue) + "; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/";
this.length = document.cookie.match(/\=/g).length;
},
length: 0,
removeItem: function (sKey) {
if (!sKey || !this.hasOwnProperty(sKey)) { return; }
document.cookie = escape(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
this.length--;
},
hasOwnProperty: function (sKey) {
return (new RegExp("(?:^|;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
}
};
window.localStorage.length = (document.cookie.match(/\=/g) || window.localStorage).length;
}
I don't believe you can "prevent a page from being reloaded when a user clicks the browser back button" (as your question begins). The user should have final say in where their browser navigates.
You could bind a function to the beforeunload event, and in that function you could put an alert that would delay the user's press of back or reload with a chance for them to read a custom message and choose to proceed or cancel.
Here that is in jQuery:
$(window).bind('beforeunload', function() {
return 'Are you sure?';
});
https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
If your concern is more simply reloading a cached-and-scrolled version of the previous page, rather than executing a completely fresh reload…that sounds more like either a setting on your content server and/or a setting on the user's browser.
I know with my registrar/host I can log in and access a GUI cache control, or I could upload custom cache instructions in my .htaccess file. These would control whether the user is allowed to cache files from your server, and for how long, as I understand. The user's web browser, too, should have settings for whether to cache files or to load-fresh every time.

Can you control which display a window created by Javascript is displayed on?

My PC has more than one display attached. I'm working on a web app that displays data in a table in the browser. When the user clicks on a row of the table, a window with details about the row selected is displayed. If the user clicks on a different row, a new window opens.
I have two requirements I need to make work:
The windows should not stack up one on top of the other, they should cascade. I've got code that works for Firefox, but IE and Chrome just open each window in the same place so they stack up. If I keep track of the number of windows opened, I can finagle a cascading mechanism, but is there a better way?
If the machine has multiple displays, it'd be nice if the window with the table were on one screen and the details windows opened on the other. I haven't been able to find anything in any of the searches I've done. Is there some code somewhere that does this?
Edit:
For #Neal:
Function newWin( mypage, myname, cascade ) {
if (!cascade) cascade = false;
if (!myname)
myname = 'DETAILS';
var w = 820;
var h = 685;
var settings = 'height=' + h + ',';
settings += 'width=' + w + ',';
if (!cascade) {
if (screen.width) {
var winl = (screen.width - w) / 2;
var wint = (screen.height - h) / 2;
}
else {
winl = 0;
wint = 0;
}
if (winl < 0) winl = 0;
if (wint < 0) wint = 0;
settings += 'top=' + wint + ',';
settings += 'left=' + winl + ',';
}
settings += 'scrollbars=yes,resizable=yes,menubar=yes,location=yes';
gPopupWindow = window.open(mypage, myname, settings);
// fix for IE focus
// when from ajax loaded list
// see convoy dropdownlist item click
//gPopupWindow.focus();
setTimeout(function () { gPopupWindow.focus() }, 0);
}
By passing true for cascade, Firefox lets the OS open the window where it wants. But IE & Chrome always put the window in the same place, though where it goes on the screen is different for each browser. I was hoping that IE & Chrome would act like Firefox does, but browser differences is a fact of life.
As I said, I can keep track of how many windows have been opened by calling this function and compute new left & top coordinates based on that number. I'd rather not, though.
The fact is that there's no way to control on which screen a new window will be displayed on a multi-screen system from JavaScript. Not without writing your own DLL and incorporating it into your application. There are ways to call functions in DLLs, but it's not worth it, as the DLL would be Windows specific and you'd need to do something else on the Mac & Linux.
Further, different browsers complicate this problem, as the method to add custom code differs from browser to browser, as well.
For that matter, if you don't specify the left & top coordinates when displaying a new window, each browser displays the new window in a different place on whatever screen it decides to display the window on. The behavior is completely inconsistent browser to browser, let alone OS to OS.
Don't do it.

javascript - read a seperate tab to check if it's open

I'm looking for a way to somehow read / check if another browser tab is open before opening the requested tab.
For example:
This is for my traffic exchange site, they just open mysite.com/surf.php and leave it viewing user's submitted sites in a frame. They earn points just for leaving that running.
Now lets say USER A has SURF PAGE A running fine and then opens SURF PAGE B then he has 2 mysite.com/surf.php running and earning double the points everybody else will earn.
What I want to happen is:
USER A has SURF PAGE A running fine and then tries to open SURF PAGE B which will check if another mysite.com/surf.php is already open and if it is to redirect the request for the 2nd surf page to another mysite.com/surf-error.php
So they can only ever have 1 mysite.com/surf.php running at any given time.
How would I go about doing this?
Browser windows on the same domain in the same browser can exchange some information via:
Cookies
Local Storage
Communication with a common server
You can use 1) or 2) to store some information about an active page and refuse to let other pages be active if one is already active.
But, the most reliable way to enforce policies like you are asking about is to use the actual server to enforce it. If users have a login, then code the server to only allow a logged in user to accumulate points for one site at a time.
Other than these options, if you want to enforce it all client-side, you would probably need a browser-plugin that could monitor all open browser windows (which I assume is not practical). You cannot do monitoring of multiple windows opened by the user from plain javascript in a web page.
when you start tracking time for someone set a session variable
session.trackingtime = true
when you check again to start tracking time make sure that value is set to false. When you stop tracking time set the variables to false.
I have done something very similar today. Just update the else if part to do a redirect in your case.
// helper function to set cookies
function setCookie(cname, cvalue, seconds) {
var d = new Date();
d.setTime(d.getTime() + (seconds * 1000));
var expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
// helper function to get a cookie
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
// Do not allow multiple call center tabs
if (~window.location.hash.indexOf('#admin/callcenter')) {
$(window).on('beforeunload onbeforeunload', function(){
document.cookie = 'ic_window_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
});
function validateCallCenterTab() {
var win_id_cookie_duration = 10; // in seconds
if (!window.name) {
window.name = Math.random().toString();
}
if (!getCookie('ic_window_id') || window.name === getCookie('ic_window_id')) {
// This means they are using just one tab. Set/clobber the cookie to prolong the tab's validity.
setCookie('ic_window_id', window.name, win_id_cookie_duration);
} else if (getCookie('ic_window_id') !== window.name) {
// this means another browser tab is open, alert them to close the tabs until there is only one remaining
var message = 'You cannot have this website open in multiple tabs. ' +
'Please close them until there is only one remaining. Thanks!';
$('html').html(message);
clearInterval(callCenterInterval);
throw 'Multiple call center tabs error. Program terminating.';
}
}
callCenterInterval = setInterval(validateCallCenterTab, 3000);
}

Stop people having my website loaded on multiple tabs

I want users to browse my site from only one tab in their browser. How can this be done? Would I use javascript and cookies?
For example, I have a website: www.example.com - and I want my clients to only be able to visit the site from one single tab in one browser. If they open another tab and load the site (or a subpage of the site) - I want an alert "Can't open multiple instances", and then redirect them to an error page.
Once thing to note - if the user changes the address from www.example.com/action/door/mine.aspx to www.example.com - that should work fine, because the user is in the same (original) tab.
Any help will be appreciated. Thanks in advance.
I've created a simple solution for this. The master page layout creates a tab GUID and stores it in sessionStorage area of the tab. The using an event listener on the storage area I write the tab GUID to the sites localStorage area. The listener then compares the tabs GUID to the one written to site storage and if they differ then it knows more than one tab is open.
So if I have three tabs A,B,C then click something in tab C, tab A and B detect another tab is open and warn user of this. I haven't yet got to fixing it so the last tab used get's notification, work in progress.
Here's the JS I have in master page, plus in the login page I have a localStorage.Clear to clear last tab from previous session.
// multi tab detection
function register_tab_GUID() {
// detect local storage available
if (typeof (Storage) !== "undefined") {
// get (set if not) tab GUID and store in tab session
if (sessionStorage["tabGUID"] == null) sessionStorage["tabGUID"] = tab_GUID();
var guid = sessionStorage["tabGUID"];
// add eventlistener to local storage
window.addEventListener("storage", storage_Handler, false);
// set tab GUID in local storage
localStorage["tabGUID"] = guid;
}
}
function storage_Handler(e) {
// if tabGUID does not match then more than one tab and GUID
if (e.key == 'tabGUID') {
if (e.oldValue != e.newValue) tab_Warning();
}
}
function tab_GUID() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function tab_Warning() {
alert("Another tab is open!");
}
Note: It's IE9+
Hope this helps.
UPDATE - 2020
Client side implementation:
We can make use of Broadcast Channel API which allows communication across browsing contexts (windows, tabs, frames or iframes) provided both contexts are from same origin.
A simple implementation to detect 2nd tab loading the website from the 1st tab:
//in entry point of your app (index.js)
const channel = new BroadcastChannel('tab');
channel.postMessage('another-tab');
// note that listener is added after posting the message
channel.addEventListener('message', (msg) => {
if (msg.data === 'another-tab') {
// message received from 2nd tab
alert('Cannot open multiple instances');
}
});
This doesn't use localStorage or cookies and it even works if 1st tab is offline and 2nd tab is being loaded.
Note: This is not supported in Safari & IE11 yet :(
UPDATE - 2022
From March 2022, it is now officially supported on Safari đŸ¥³
Take a note on its browser compatibility.
However, there's a polyfill available that does the job.
EDIT2:
It's the exact thing which is mentioned at this answer, You need 2 IDs:
One random one
One consistent one (this will be our SSID actually, since you limit tabs of a single browser, it's better to get generated form browser's unique parameters)
You can generate consistent one from browser's user-agent or get it from server-side. store both of them server-side.
Store the random one in window.name property which is tab-specific.
Send a heartbeat every 1~2 seconds to your server containing both consistent ID and random one. if server fails to receive the heartbeat, it cleans up database and de-register dead clients.
on every browser's request, check window.name for the value. if it were missing, check with the server-side whether if the previous tab is closed or not (cleaned from database).
If yes, generate a new pair for client if no, reject them.
Two suggestions on top of my mind:
Server-side (better): provide all your clients, a user name and password. request them on their first visit of your site to enter with their credentials. then on every other request, check for whether user with said credentials is already logged in or not.
Client *
|
|
Server ---> Check whether
Already logged
or not?
______________
| |
yes no
| |
permit reject
them them
Client-side: If you really need a strong check of this, use evercookie to store an already-logged-in cookie on client's machine.
Side-note: Do know that every attempt in client side is not secure at all! client-side should help server-side, it shouldn't be used as the one and only source of security. even evercookies can be deleted so, give my first suggestion a go.
**EDIT:**
Evercookie is really doing a good job at storing most secure zombie cookies ever but since the library itself is a little bit heavy for browsers (storing a cookie takes more than 100ms each time) it's not really recommended for using in real-world web app.
use these instead if you went with server-side solution:
Way around ASP.NET session being shared across multiple tab windows
Kiranvj's answer
Extending rehman_00001's answer to handle the case where you want the alert on the new tabs instead.
const channel = new BroadcastChannel('tab');
let isOriginal = true;
channel.postMessage('another-tab');
// note that listener is added after posting the message
channel.addEventListener('message', (msg) => {
if (msg.data === 'another-tab' && isOriginal) {
// message received from 2nd tab
// reply to all new tabs that the website is already open
channel.postMessage('already-open');
}
if (msg.data === 'already-open') {
isOriginal = false;
// message received from original tab
// replace this with whatever logic you need
alert('Cannot open multiple instances');
}
});
I know this post is pretty old, but in case it helps anybody, I recently looked into basically doing the same thing using localStorage and sessionStorage.
Similar Anthony's answer, it sets an interval to make sure the originating tab keeps the entry fresh, so that if the browser crashes or somehow closes without calling the unload event (included in the comments but not part of the code for testing purposes), then there would just be a short delay before the application would run properly in a new browser window.
Obviously, you would change the "tab is good", "tab is bad" conditions to do whatever logic you want.
Oh, and also, the createGUID method is just a utility to make the session identifier unique... it is from this answer to a previous question (wanted to make sure I wasn't taking credit for that).
https://jsfiddle.net/yex8k2ts/30/
let localStorageTimeout = 15 * 1000; // 15,000 milliseconds = 15 seconds.
let localStorageResetInterval = 10 * 1000; // 10,000 milliseconds = 10 seconds.
let localStorageTabKey = 'test-application-browser-tab';
let sessionStorageGuidKey = 'browser-tab-guid';
function createGUID() {
let guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
/*eslint-disable*/
let r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
/*eslint-enable*/
return v.toString(16);
});
return guid;
}
/**
* Compare our tab identifier associated with this session (particular tab)
* with that of one that is in localStorage (the active one for this browser).
* This browser tab is good if any of the following are true:
* 1. There is no localStorage Guid yet (first browser tab).
* 2. The localStorage Guid matches the session Guid. Same tab, refreshed.
* 3. The localStorage timeout period has ended.
*
* If our current session is the correct active one, an interval will continue
* to re-insert the localStorage value with an updated timestamp.
*
* Another thing, that should be done (so you can open a tab within 15 seconds of closing it) would be to do the following (or hook onto an existing onunload method):
* window.onunload = () => {
localStorage.removeItem(localStorageTabKey);
};
*/
function testTab() {
let sessionGuid = sessionStorage.getItem(sessionStorageGuidKey) || createGUID();
let tabObj = JSON.parse(localStorage.getItem(localStorageTabKey)) || null;
sessionStorage.setItem(sessionStorageGuidKey, sessionGuid);
// If no or stale tab object, our session is the winner. If the guid matches, ours is still the winner
if (tabObj === null || (tabObj.timestamp < new Date().getTime() - localStorageTimeout) || tabObj.guid === sessionGuid) {
function setTabObj() {
let newTabObj = {
guid: sessionGuid,
timestamp: new Date().getTime()
};
localStorage.setItem(localStorageTabKey, JSON.stringify(newTabObj));
}
setTabObj();
setInterval(setTabObj, localStorageResetInterval);
return true;
} else {
// An active tab is already open that does not match our session guid.
return false;
}
}
if (testTab()) {
document.getElementById('result').innerHTML = 'tab is good';
} else {
document.getElementById('result').innerHTML = 'tab is bad';
}
window.addEventListener('load', function () {
if (localStorage.getItem('web_browser') == null) {
// new tab
localStorage.setItem('web_browser', 'true');
window.addEventListener('unload', function() {
localStorage.removeItem('web_browser');
})
} else {
// duplicate tab
return;
}
})
Put this script at the beginning of html pages, where you don't want users to duplicate current page or tab.
The same problem (and solution) : https://sites.google.com/site/sarittechworld/track-client-windows
Similar :
http://www.codeproject.com/Articles/35859/Detect-and-prevent-multiple-windows-or-tab-usage-i
The best way to solve this is to have one-time session IDs.
Eg, each page contain a session ID, that is valid for one visit, is unique, and random.
When clicking any one link, it will use & invalidate the session ID, and the new page will have a new session ID.
This will force the user to always browse in the newest window or tab, and also prevents session stealing over the wire.
Any attempt to reuse a old session ID should immediately kill also the active session IDs for that user.
Its also important to store, in the session management system, which pages is accessible from page X. So if page X (with session ID abc) contains links to page 1, 2 and 3, any attempt to visit page 4 with session ID abc, will fail and also kill the session.
This will force the user to always have one single session track, and always follow the logic on the site. Any attempt to go forward, back, using history or log entires, or opening multiple windows or tabs, will fail and logout the user in all windows, tabs and devices.
All this can be completely implemented on server-side, without any client-side logic.
Why do you want to do this?
Could try to do some ugly hacking, but the result would be: There is no way you could completely suppress this behaviour.
This could not be solved by JavaScript, because there is always the possibility that the user has disabled JavaScript in his browser, or allows only a certain subset.
The user could open a new browser, use a different computer, etc. to visit multiple pages at once.
But more important:
Also, your site would be the only site that has this behaviour and for this reason this will confuse everybody which uses your site, because it doesn't work like a web site should work. Everybody who tries to open a second tab will think: "This is odd. This website sucks because it different then websites should be. I will not come again!" ;-)
I wrote this to stop a call center page from being accessed in multiple tabs. It works well and is purely client-side. Just update the else if part to do what you want if it detects a new tab.
// helper function to set cookies
function setCookie(cname, cvalue, seconds) {
var d = new Date();
d.setTime(d.getTime() + (seconds * 1000));
var expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
// helper function to get a cookie
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
// Do not allow multiple call center tabs
if (~window.location.hash.indexOf('#admin/callcenter')) {
$(window).on('beforeunload onbeforeunload', function(){
document.cookie = 'ic_window_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
});
function validateCallCenterTab() {
var win_id_cookie_duration = 10; // in seconds
if (!window.name) {
window.name = Math.random().toString();
}
if (!getCookie('ic_window_id') || window.name === getCookie('ic_window_id')) {
// This means they are using just one tab. Set/clobber the cookie to prolong the tab's validity.
setCookie('ic_window_id', window.name, win_id_cookie_duration);
} else if (getCookie('ic_window_id') !== window.name) {
// this means another browser tab is open, alert them to close the tabs until there is only one remaining
var message = 'You cannot have this website open in multiple tabs. ' +
'Please close them until there is only one remaining. Thanks!';
$('html').html(message);
clearInterval(callCenterInterval);
throw 'Multiple call center tabs error. Program terminating.';
}
}
callCenterInterval = setInterval(validateCallCenterTab, 3000);
}

Categories

Resources