Possible to detect if a user has multiple tabs of your site open?
Asked Answered
M

9

41

I'm just thinking about the whole site registration process.

A user goes to your site, signs up, and then you tell him you've sent him an email and he needs to verify his email address. So he hits Ctrl+T, pops open a new tab, hits his Gmail fav button, doesn't read a word of your lengthy welcome email, but clicks the first link he sees. Gmail opens your site in yet another tab...

He doesn't need nor want two tabs for your site open, he just wants to view that darn page you've disallowed him access to until he registers.

So what do we do? I saw one site (but I forget what it was) that did a really good job, and it actually refreshed the first tab I had open without me having to press anything.

I'm thinking, it might be nice if we can detect if the user already has a tab to your site open, we could either close the new verification-tab automatically, or tell him he can close it can go back to his other tab (which we've now refreshed and logged him in).

Or, maybe when he got your annoying "please check your email" message, he went directly to his email, replacing your site with his email knowing full well that the email will link him back to the site again. In that case, we don't want to close the tab, but maybe could have saved his location from before, and redirect him there again?

Anyway, that's just the use case... the question still stands. Can we detect if a user already has a tab to your site open?


This question is not about how to detect when a user has completed the sign-up process. Ajax polling or comet can solve that issue. I specifically want to know if the user already has a tab open to your site or not.

Mohandis answered 27/10, 2010 at 23:47 Comment(7)
I'm not cogent enough to write out a full answer - but basically, you need to track it with a cookie, or local storage, or server-side - really, any form of storing data outside of a single window.Dray
@Ed: I care. I notice the little things. That site made a good impression on me!Mohandis
What is your target browser? Could you use something like localStorage?Peirce
@ItzWarty: I don't know... I guess this is more of an enhancement than a necessary feature, so it's not the end of the world if not all browsers support it.Mohandis
Have your page store date.getTime() to a variable. set localStorage["whatever"] to the stored value. Every few seconds, check to see if localStorage["whatever"] is equal to what you set it to before. If not, you definitely have two or more instances of your page up.Peirce
Perhaps this answer might help: https://mcmap.net/q/331489/-stop-people-having-my-website-loaded-on-multiple-tabsFan
@Fan Neat! First I've heard of the Broadcast API. Thanks for sharing!Mohandis
O
51

I'm fairly late to the party here (over a year), but I couldn't help but notice that you'd missed an incredibly easy and elegant solution (and probably what that website you saw used).

Using JavaScript you can change the name of the window you currently have open through:

window.name = "myWindow";

Then when you send out your confirmation email simply do (assuming you're sending a HTML email):

<a href="verificationlink.php" target="myWindow">Verify</a>

Which should result in the verificationLink opening up inside the window your website was already loaded into, if it's already been closed it'll open up a new tab with the window name specified.

Orthopteran answered 22/1, 2012 at 19:14 Comment(3)
Won't work if page is opened in two different browsers. But of course that was not mentioned in the question so WTH am I saying? :DAflcio
This doesn't really answer the question: "Can we detect if a user already has a tab to your site open?" Another example could be that you want to warn the user if they have two windows open on your site.Facing
@Facing it doesn't, but the asker had an X-Y problem, and this resolves X.Razid
H
9

You can stop the page functionality when user opened another tab or another window or even another browser

$(window).blur(function(){
    // code to stop functioning or close the page  
});
Hanlon answered 22/6, 2013 at 13:10 Comment(1)
doesn't handle the case when you open the tab/window from another PC for eg.Deadradeadweight
H
6

You can send an AJAX request every X seconds from the original tab that asks the server if it received a request from the email.

You cannot close the second tab automatically, but you could have it ask the server after 3X seconds whether it heard from the first tab.

Howsoever answered 27/10, 2010 at 23:53 Comment(0)
U
6

What I have here is a little bit different use case to you but it detects if the site is being accessed in another tab. In this case I wanted to limit people using some call center pages to only one tab. It works well and is purely client-side.

// 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);
}
Unbelief answered 29/4, 2017 at 4:12 Comment(2)
Can you summarize what you're doing here? Looks like you're giving each window/tab a random name and saving that into a cookie. Then you check the cookie every 10 seconds and if it doesn't match, you alert them? When they back down to 1 tab, will it reset the name so that the first tab is in a good state?Mohandis
The first tab doesn't get an error message; only subsequent tabs do. Thank you, @anthony-vipond -- This is exactly what I was looking for.Trauner
C
5

To flesh out John's answer, here is a working solution that uses plain JS and localStorage and updates the DOM with the count of the currently open tabs. Note that this solution detects the number of open tabs/windows for a given domain within one browser, but does not maintain the count across different browsers.

It uses the storage event to keep the count synchronized across all open tabs/windows without any need for refreshing the page.

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title></title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
(function() {
    var stor = window.localStorage;
    window.addEventListener("load", function(e) {
        var openTabs = stor.getItem("openTabs");
        if (openTabs) {
            openTabs++;
            stor.setItem("openTabs", openTabs)
        } else {
            stor.setItem("openTabs", 1)
        }
        render();
    })
    window.addEventListener("unload", function(e) {
        e.preventDefault();
        var openTabs = stor.getItem("openTabs");
        if (openTabs) {
            openTabs--;
            stor.setItem("openTabs", openTabs)
        }
        e.returnValue = '';
    });
    window.addEventListener('storage', function(e) {
        render();
    })

    function render() {
        var openTabs = stor.getItem("openTabs");
        var tabnum = document.getElementById("tabnum");
        var dname = document.getElementById("dname");
        tabnum.textContent = openTabs;
        dname.textContent = window.location.host
    }
}());
</script>
</head>
<body>
<div style="width:100%;height:100%;text-align:center;">
    <h1 >You Have<h1>
        <h1 id="tabnum">0</h1>
    <h1>Tab(s) of <span id="dname"></span> Open</h1>
</div>
</body>
</html>
Cholla answered 1/12, 2018 at 6:41 Comment(1)
One caveat I found when testing is that when the browser/system crashes and the user restores tabs , the "unload" handler doesn't fire, so a single tab restored believes it's a second tab.Razid
T
4

To add to other answers: You can also use localStorage. Have an entry like 'openedTabs'. When your page is opened, increase this number. When user leaves the page, decrease it.

Two answered 19/7, 2017 at 15:8 Comment(0)
J
2

The user will still have a session at the server. Why not store the user's location prior to registration, and when they confirm their registration, read the location back out of the session and redirect back to that page. No tab magic required. It's certainly not what I'd expect from a signup process.

Jasisa answered 27/10, 2010 at 23:54 Comment(5)
Then you'd still have the first tab.Howsoever
Yes. That's what happens in a signup process. We're well used to it.Jasisa
Then both tabs have the same page open... we don't need two with the exact same page. I'm asking how I can close one of them.Mohandis
They wouldn't though. You'd have one tab with the final page of your signup, the other with your destination. What do you intend to do when you've discovered the other tab? Try to close it? That's unexpected.Jasisa
Then inform them that they can close it because the other tab contains the info they want. And it wouldn't contain the "final page of the signup" because we've just redirected them, remember? My suggestion was (a) if the user has two tabs open, close one, or inform him that it can be closed, and refresh the other, or (b) if the user already closed the original tab, then redirect the new oneMohandis
Y
2

It is possible to track number of tabs of your site opened by saving data in localstorage of each tab and counting the same, I created a github repository which can track number of tabs of your website a user has opened.

To use it Include tab-counter.js in your page and it will start tracking number of opened tabs.

console.log(tabCount.tabsCount());
Yanez answered 30/12, 2018 at 10:11 Comment(0)
S
0

Here's a system that uses broadcast channels for cross tab comms. It also assigns a unique ID per tab and manages the discovery of already opened tabs, for new tabs. Finally, using the ID as a stable index, it allows the user to rename their tabs. Tab closing events are handled via polling as well (unload events are unreliable).

This plugs into redux via the callbacks in the constructor. These are onNewTab, onDestroyTab, onRenameTab in this example.

import { setTabs } from './redux/commonSlice';
import { store } from './redux/store';

const promiseTimeout = (ms, promise) => {
    let id;
    let timeout = new Promise((resolve, reject) => {
        id = setTimeout(() => {
            reject('Timed out in ' + ms + 'ms.');
        }, ms)
    })

    return Promise.race([
        promise,
        timeout
    ]).then((result) => {
        clearTimeout(id);
        return result;
    })
};

// Promise that can be resolved/rejected outside of its constructor. Like a signal an async event has occured.
class DeferredPromise {
    constructor() {
        this._promise = new Promise((resolve, reject) => {
            // assign the resolve and reject functions to `this`
            // making them usable on the class instance
            this.resolve = resolve;
            this.reject = reject;
        });
        // bind `then` and `catch` to implement the same interface as Promise
        this.then = this._promise.then.bind(this._promise);
        this.catch = this._promise.catch.bind(this._promise);
        this.finally = this._promise.finally.bind(this._promise);
        this[Symbol.toStringTag] = 'Promise';
    }
}

class TabManager {
    tabCreateCallback = undefined;
    tabDestroyCallback = undefined;
    tabRenameCallback = undefined;

    constructor(onNewTab, onDestroyTab, onRenameTab) {
        this.tabCreateCallback = onNewTab.bind(this);
        this.tabDestroyCallback = onDestroyTab.bind(this);
        this.tabRenameCallback = onRenameTab.bind(this);

        // creation time gives us a total ordering of open tabs, also acts as a tab ID
        this.creationEpoch = Date.now();
        this.channel = new BroadcastChannel("TabManager");
        this.channel.onmessage = this.onMessage.bind(this);

        // our current tab (self) counts too
        this.tabs = [];
        this.tabNames = {};

        // start heartbeats. We check liveness like this as there is _no_ stable browser API for tab close.
        // onbeforeunload is not reliable in all situations.
        this.heartbeatPromises = {};
        this.heartbeatIntervalMs = 1000;
        setTimeout(this.doHeartbeat.bind(this), this.heartbeatIntervalMs);
    }

    doComputeNames() {
        for (let i = 0; i < this.tabs.length; i++) {
            const tab = this.tabs[i];
            const name = this.tabNames[tab];
            const defaultName = `Tab ${i + 1}`;
            if (!name) {
                this.tabNames[tab] = defaultName;

                if (this.tabRenameCallback) {
                    this.tabRenameCallback(tab, name);
                }
                // if it's a default pattern but wrong inde value, rename it
            } else if (name && this.isDefaultName(name) && name !== defaultName) {
                this.tabNames[tab] = defaultName;

                if (this.tabRenameCallback) {
                    this.tabRenameCallback(tab, name);
                }
            }
        }
    }

    doHeartbeat() {
        for (let tab of this.tabs) {
            if (tab === this.creationEpoch) {
                continue;
            }

            this.channel.postMessage({ type: "heartbeat_request", value: tab });

            const heartbeatReply = new DeferredPromise();
            heartbeatReply.catch(e => { });

            // use only a fraction of poll interval to ensure timeouts occur before poll. Prevents spiral of death.
            let heartbeatReplyWithTimeout = promiseTimeout(this.heartbeatIntervalMs / 3, heartbeatReply);

            // destroy tab if heartbeat times out
            heartbeatReplyWithTimeout.then(success => {
                delete this.heartbeatPromises[tab];
            }).catch(error => {
                delete this.heartbeatPromises[tab];

                this.tabs = this.tabs.filter(id => id !== tab);
                this.tabs.sort();

                this.doComputeNames();
                if (this.tabDestroyCallback) {
                    this.tabDestroyCallback(tab);
                }
            });

            this.heartbeatPromises[tab] = heartbeatReply;
        }

        // re-schedule to loop again
        setTimeout(this.doHeartbeat.bind(this), this.heartbeatIntervalMs);
    }

    doInitialize() {
        this.tabs = [this.creationEpoch];
        this.doComputeNames();
        if (this.tabCreateCallback) {
            this.tabCreateCallback(this.creationEpoch);
        }
        this.channel.postMessage({ type: "creation", value: this.creationEpoch });
    }

    onMessage(event) {
        if (event.data.type == "creation") {
            const newTabId = event.data.value;

            // add the new tab
            if (!this.tabs.includes(newTabId)) {
                this.tabs.push(newTabId);
                this.tabs.sort();
                this.doComputeNames();
                if (this.tabCreateCallback) {
                    this.tabCreateCallback(newTabId);
                }
            }

            // send all of the tabs we know about to it
            this.channel.postMessage({ type: "syncnew", value: this.tabs });

            // those tabs we just sent might already have custom names, lets send the older rename requests
            // which would have had to have occured. I.E. lets replay forward time and sync the states of ours to theirs.
            for (let tab of this.tabs) {
                const name = this.tabNames[tab];
                if (name && !this.isDefaultName(name)) {
                    this.notifyTabRename(tab, name);
                }
            }
        } else if (event.data.type == "syncnew") {
            let newTabs = [];

            // just got a list of new tabs add them if we down't know about them
            for (let id of event.data.value) {
                if (!this.tabs.includes(id)) {
                    newTabs.push(id);
                }
            }

            // merge the lists and notify of only newly discovered
            if (newTabs.length) {
                this.tabs = this.tabs.concat(newTabs);
                this.tabs.sort();
                this.doComputeNames();

                for (let id of newTabs) {
                    if (this.tabCreateCallback) {
                        this.tabCreateCallback(id);
                    }
                }
            }
        } else if (event.data.type == "heartbeat_request") {
            // it's for us, say hi back
            if (event.data.value === this.creationEpoch) {
                this.channel.postMessage({ type: "heartbeat_reply", value: this.creationEpoch });
            }
        } else if (event.data.type == "heartbeat_reply") {
            // got a reply, cool resolve the heartbeat
            if (this.heartbeatPromises[event.data.value]) {
                // try catch since this is racy, entry may have timed out after this check passed
                try {
                    this.heartbeatPromises[event.data.value].resolve();
                } catch {

                }
            }
        } else if (event.data.type == "rename") {
            // someone renamed themselves, lets update our record
            const { id, name } = event.data.value;
            if (this.tabs.includes(id)) {
                this.tabNames[id] = name;

                // first original (potentially illegal) rename callback first
                if (this.tabRenameCallback) {
                    this.tabRenameCallback(id, name);
                }

                // force tab numbers back to consistent
                this.doComputeNames();
            }
        }
    }

    setTabName(id, name) {
        if (this.tabs.includes(id)) {
            this.tabNames[id] = name;
            this.notifyTabRename(id, name);

            if (this.tabRenameCallback) {
                this.tabRenameCallback(id, name);
            }

            // force tab numbers back to consistent
            this.doComputeNames();
        }
    }

    notifyTabRename(id, name) {
        this.channel.postMessage({ type: "rename", value: { id, name } });
    }

    isDefaultName(name) {
        return name.match(/Tab [0-9]+/)
    }

    getMyTabId() {
        return this.creationEpoch;
    }

    getMyTabIndex() {
        return this.tabs.findIndex(tab => tab === this.creationEpoch);
    }

    isMyTab(id) {
        return id === this.creationEpoch;
    }

    getAllTabs() {
        return this.tabs.map((tab, idx) => {
            return { id: tab, index: idx, name: this.tabNames[tab] ?? "" };
        }, this);
    }
}

function onDestroyTab(id) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} destroyed`);
}

function onNewTab(id) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} created`);
}

function onRenameTab(id, name) {
    store.dispatch(setTabs(this.getAllTabs()));
    console.log(`Tab ${id} renamed to ${name}`);
}

const TabManager = new TabManager(onNewTab, onDestroyTab, onRenameTab);
export default TabManager;

Initialize it on page load

window.addEventListener("DOMContentLoaded", function (event) {
    TabManager.doInitialize();
});

Access any of the methods on the static object at any time. Note that you can get rename events out of order from create / destroy. This could be resolved, but it wasn't important for me.

Scotia answered 20/3, 2022 at 3:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.