57

I want to check with JavaScript if the user has already opened my website in another tab in their browser.

It seems I cannot do that with pagevisibility...

The only way I see is to use WebSocket based on a session cookie, and check if the client has more than one socket. But by this way, from current tab, I have to ask my server if this user has a tab opened right next to their current browser tab. It is a little far-fetched!

Maybe with localstorage?

Mona Lisa
  • 193
  • 1
  • 12
ManUtopiK
  • 4,495
  • 3
  • 38
  • 52
  • What do you want to do if they have another tab open? Display a message saying its not the main window? – cgatian May 16 '14 at 00:12
  • What if the user has your site open in another browser instance rather than in another tab of the current browser instance? – Remy Lebeau May 16 '14 at 00:35
  • localStorage or cookies should work, assuming it's the same browser. What do you need to do exactly? – Hamza Kubba May 16 '14 at 01:29
  • 1
    Two reasons: - Open many tabs with facebook, and when you receive a chat message, you hear mutiple times the beep sound. I want to prevent this. - I want to transfert some datas from one tab to other tabs. – ManUtopiK May 17 '14 at 12:11
  • just as a comment, if you use websockets you could bypass all those browser-stuff and identify multiple tabs "easily" – john Smith Jan 08 '19 at 09:44
  • Does this answer your question? [Stop people having my website loaded on multiple tabs](https://stackoverflow.com/questions/11008177/stop-people-having-my-website-loaded-on-multiple-tabs) – Pere Joan Martorell Feb 02 '22 at 15:37

4 Answers4

69

The shorter version with localStorage and Storage listener

<script type="text/javascript">
        // Broadcast that you're opening a page.
        localStorage.openpages = Date.now();
        var onLocalStorageEvent = function(e){
            if(e.key == "openpages"){
                // Listen if anybody else is opening the same page!
                localStorage.page_available = Date.now();
            }
            if(e.key == "page_available"){
                alert("One more page already open");
            }
        };
        window.addEventListener('storage', onLocalStorageEvent, false);
</script>

Update:

  • Works on page crash as well.
  • Stimulate page crash in chrome: chrome://inducebrowsercrashforrealz

Live demo

Ondrej Slinták
  • 31,386
  • 20
  • 94
  • 126
Sasi Varunan
  • 2,806
  • 1
  • 23
  • 34
  • 4
    Does this work if the browser crashes or something else unexpected happens? – hugo der hungrige Feb 24 '18 at 18:36
  • @hugoderhungrige Browser crash doesn't remove any localstorage object, So it should still work – Sasi Varunan Feb 26 '18 at 09:50
  • 5
    That's what I mean. If openpages is set, page crashes and you reload the browser you'll wrongly get the alert or am I mistaken? – hugo der hungrige Feb 26 '18 at 12:35
  • @SasiVarunan On IOS Safari 13.7 I also get an alert on the first page load. It appears that the event listener fires on the first localStorage.openpages = Date.now() even though it has passed. Putting the event listener in a setTimeout(function()=>{}, 0) makes it wait long enough to work. I do think that could give it a possible, but unlikely, chance of missing the event if 2 tabs are opened at the exact same time. – Jim Lowrey Mar 09 '21 at 00:02
  • @hugoderhungrige Yes. Without a timeout on these locks there is always edge-cases where the thing will fail in a critical manner. – MrYellow Jan 18 '23 at 00:38
67

Using local storage I created a simple demo that should accomplish what your looking to do. Basically, it simply maintains a count of currently opened windows. When the window is closed the unload events fire and remove it from the total window count.

When you first look at it, you may think there's more going on than there really is. Most of it was a shotty attempt to add logic into who was the "main" window, and who should take over as the "main" window as you closed children. (Hence the setTimeout calls to recheck if it should be promoted to a main window) After some head scratching, I decided it would take too much time to implement and was outside the scope of this question. However, if you have two windows open (Main, and Child) and you close the Main, the child will be promoted to a main.

For the most part you should be able to get the general idea of whats going on and use it for your own implementation.

See it all in action here: http://jsbin.com/mipanuro/1/edit

Oh yeah, to actually see it in action... Open the link in multiple windows. :)

Update:

I've made the necessary changes to have the the local storage maintain the "main" window. As you close tabs child windows can then become promoted to a main window. There are two ways to control the "main" window state through a parameter passed to the constructor of WindowStateManager. This implementation is much nicer than my previous attempt.

JavaScript:

// noprotect

var statusWindow = document.getElementById('status');

(function (win)
{
    //Private variables
    var _LOCALSTORAGE_KEY = 'WINDOW_VALIDATION';
    var RECHECK_WINDOW_DELAY_MS = 100;
    var _initialized = false;
    var _isMainWindow = false;
    var _unloaded = false;
    var _windowArray;
    var _windowId;
    var _isNewWindowPromotedToMain = false;
    var _onWindowUpdated;

    
    function WindowStateManager(isNewWindowPromotedToMain, onWindowUpdated)
    {
        //this.resetWindows();
        _onWindowUpdated = onWindowUpdated;
        _isNewWindowPromotedToMain = isNewWindowPromotedToMain;
        _windowId = Date.now().toString();

        bindUnload();

        determineWindowState.call(this);

        _initialized = true;

        _onWindowUpdated.call(this);
    }

    //Determine the state of the window 
    //If its a main or child window
    function determineWindowState()
    {
        var self = this;
        var _previousState = _isMainWindow;

        _windowArray = localStorage.getItem(_LOCALSTORAGE_KEY);

        if (_windowArray === null || _windowArray === "NaN")
        {
            _windowArray = [];
        }
        else
        {
            _windowArray = JSON.parse(_windowArray);
        }

        if (_initialized)
        {
            //Determine if this window should be promoted
            if (_windowArray.length <= 1 ||
               (_isNewWindowPromotedToMain ? _windowArray[_windowArray.length - 1] : _windowArray[0]) === _windowId)
            {
                _isMainWindow = true;
            }
            else
            {
                _isMainWindow = false;
            }
        }
        else
        {
            if (_windowArray.length === 0)
            {
                _isMainWindow = true;
                _windowArray[0] = _windowId;
                localStorage.setItem(_LOCALSTORAGE_KEY, JSON.stringify(_windowArray));
            }
            else
            {
                _isMainWindow = false;
                _windowArray.push(_windowId);
                localStorage.setItem(_LOCALSTORAGE_KEY, JSON.stringify(_windowArray));
            }
        }

        //If the window state has been updated invoke callback
        if (_previousState !== _isMainWindow)
        {
            _onWindowUpdated.call(this);
        }

        //Perform a recheck of the window on a delay
        setTimeout(function()
                   {
                     determineWindowState.call(self);
                   }, RECHECK_WINDOW_DELAY_MS);
    }

    //Remove the window from the global count
    function removeWindow()
    {
        var __windowArray = JSON.parse(localStorage.getItem(_LOCALSTORAGE_KEY));
        for (var i = 0, length = __windowArray.length; i < length; i++)
        {
            if (__windowArray[i] === _windowId)
            {
                __windowArray.splice(i, 1);
                break;
            }
        }
        //Update the local storage with the new array
        localStorage.setItem(_LOCALSTORAGE_KEY, JSON.stringify(__windowArray));
    }

    //Bind unloading events  
    function bindUnload()
    {
        win.addEventListener('beforeunload', function ()
        {
            if (!_unloaded)
            {
                removeWindow();
            }
        });
        win.addEventListener('unload', function ()
        {
            if (!_unloaded)
            {
                removeWindow();
            }
        });
    }

    WindowStateManager.prototype.isMainWindow = function ()
    {
        return _isMainWindow;
    };

    WindowStateManager.prototype.resetWindows = function ()
    {
        localStorage.removeItem(_LOCALSTORAGE_KEY);
    };

    win.WindowStateManager = WindowStateManager;
})(window);

var WindowStateManager = new WindowStateManager(false, windowUpdated);

function windowUpdated()
{
    //"this" is a reference to the WindowStateManager
    statusWindow.className = (this.isMainWindow() ? 'main' : 'child');
}
//Resets the count in case something goes wrong in code
//WindowStateManager.resetWindows()

HTML:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <div id='status'> 
    <span class='mainWindow'>Main Window</span>
    <span class='childWindow'>Child Window</span>
  </div>
</body>
</html>

CSS:

#status
{
  display:table;
  width:100%;
  height:500px;
  border:1px solid black;
}
span
{
  vertical-align:middle;
  text-align:center; 
  margin:0 auto;
  font-size:50px;
  font-family:arial;
  color:#ba3fa3;  
  display:none;
}

#status.main .mainWindow,
#status.child .childWindow
{
  display:table-cell;
}

.mainWindow
{
  background-color:#22d86e;
}
.childWindow
{
  background-color:#70aeff;
}
Community
  • 1
  • 1
cgatian
  • 22,047
  • 9
  • 56
  • 76
  • Why do you need to be aware of which tab is the "Main" tab and which ones are "Child" tabs? – Derek Hubbard Dec 29 '21 at 16:57
  • @DerekHubbard sorry, it's been almost 8 years. I think it was just how I approached it, all windows could just be called Child. – cgatian Jan 05 '22 at 16:41
22

(2021) You can use BroadcastChannel to communicate between tabs of the same origin.

For example, put the following at the top level of your js code, then test by opening 2 tabs:

const bc = new BroadcastChannel("my-awesome-site");

bc.onmessage = (event) => {
  if (event.data === `Am I the first?`) {
    bc.postMessage(`No you're not.`);
    alert(`Another tab of this site just got opened`);
  }
  if (event.data === `No you're not.`) {
    alert(`An instance of this site is already running`);
  }
};

bc.postMessage(`Am I the first?`);

ZYinMD
  • 3,602
  • 1
  • 23
  • 32
  • Easy, only, you need to say what your code means. – Carlos May 15 '22 at 03:37
  • @Carlos here is a great tutorial on the `BroadcastChannel` and its usage: [link](https://www.digitalocean.com/community/tutorials/js-broadcastchannel-api ) – Sherzad Aug 02 '22 at 14:23
  • @Carlos note that it is not supported on Safari and IE! – Sherzad Aug 02 '22 at 14:26
  • 1
    Safari and IE aren't real browsers anyway. Docs say it works. – MrYellow Jan 26 '23 at 22:22
  • That's a ridiculous statement. Safari has a nearly 20% market share and in any case it works on all version of safari since 15.4, which came out three months before @Sherzard claimed it didn't work. https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API – John Lord Apr 25 '23 at 16:53
8

I know it is late, but maybe help someone

This snippet of code, will detect how many tabs are open and how many are active (visible) and if none of tabs is active, it will choose last opened tab, as active one.

This code will handle windows/tab crash too and it will refresh the count at crash.

Because localStorage is not supported on Stack Overflow currently, please test here.

<html>
<body>
Open in several tabs or windows
<div id="holder_element"></div>

<script type="text/javascript">
    //localStorage.clear();
    manage_crash();
    
    //Create a windows ID for each windows that is oppened
    var current_window_id = Date.now() + "";//convert to string
    var time_period = 3000;//ms
    
    //Check to see if PageVisibility API is supported or not
    var PV_API = page_visibility_API_check();
    
    /************************
    ** PAGE VISIBILITY API **
    *************************/
    function page_visibility_API_check ()
    {
        var page_visibility_API = false;
        var visibility_change_handler = false;
        if ('hidden' in document)
        {
            page_visibility_API = 'hidden';
            visibility_change_handler = 'visibilitychange';
        }
        else
        {
            var prefixes = ['webkit','moz','ms','o'];
            //loop over all the known prefixes
            for (var i = 0; i < prefixes.length; i++){
                if ((prefixes[i] + 'Hidden') in document)
                {
                    page_visibility_API = prefixes[i] + 'Hidden';
                    visibility_change_handler = prefixes[i] + 'visibilitychange';
                }
            }
        }
        
        if (!page_visibility_API)
        {
            //PageVisibility API is not supported in this device
            return page_visibility_API;
        }
        
        return {"hidden": page_visibility_API, "handler": visibility_change_handler};
    }
    
    if (PV_API)
    {
        document.addEventListener(PV_API.handler, function(){
            //console.log("current_window_id", current_window_id, "document[PV_API.hidden]", document[PV_API.hidden]);
            if (document[PV_API.hidden])
            {
                //windows is hidden now
                remove_from_active_windows(current_window_id);
                //skip_once = true;
            }
            else
            {
                //windows is visible now
                //add_to_active_windows(current_window_id);
                //skip_once = false;
                check_current_window_status ();
            }
        }, false);
    }
    
    /********************************************
    ** ADD CURRENT WINDOW TO main_windows LIST **
    *********************************************/
    add_to_main_windows_list(current_window_id);
    //update active_window to current window
    localStorage.active_window = current_window_id;
    
    /**************************************************************************
    ** REMOVE CURRENT WINDOWS FROM THE main_windows LIST ON CLOSE OR REFRESH **
    ***************************************************************************/
    window.addEventListener('beforeunload', function ()
    {
        remove_from_main_windows_list(current_window_id);
    });
    
    /*****************************
    ** ADD TO main_windows LIST **
    ******************************/
    function add_to_main_windows_list(window_id)
    {
        var temp_main_windows_list = get_main_windows_list();
        var index = temp_main_windows_list.indexOf(window_id);
        
        if (index < 0)
        {
            //this windows is not in the list currently
            temp_main_windows_list.push(window_id);
        }
        
        localStorage.main_windows = temp_main_windows_list.join(",");
        
        return temp_main_windows_list;
    }
    
    /**************************
    ** GET main_windows LIST **
    ***************************/
    function get_main_windows_list()
    {
        var temp_main_windows_list = [];
        if (localStorage.main_windows)
        {
            temp_main_windows_list = (localStorage.main_windows).split(",");
        }
        
        return temp_main_windows_list;
    }
    
    /**********************************************
    ** REMOVE WINDOWS FROM THE main_windows LIST **
    ***********************************************/
    function remove_from_main_windows_list(window_id)
    {
        var temp_main_windows_list = [];
        if (localStorage.main_windows)
        {
            temp_main_windows_list = (localStorage.main_windows).split(",");
        }
        
        var index = temp_main_windows_list.indexOf(window_id);
        if (index > -1) {
            temp_main_windows_list.splice(index, 1);
        }
        
        localStorage.main_windows = temp_main_windows_list.join(",");
        
        //remove from active windows too
        remove_from_active_windows(window_id);
        
        return temp_main_windows_list;
    }
    
    /**************************
    ** GET active_windows LIST **
    ***************************/
    function get_active_windows_list()
    {
        var temp_active_windows_list = [];
        if (localStorage.actived_windows)
        {
            temp_active_windows_list = (localStorage.actived_windows).split(",");
        }
        
        return temp_active_windows_list;
    }
    
    /*************************************
    ** REMOVE FROM actived_windows LIST **
    **************************************/
    function remove_from_active_windows(window_id)
    {
        var temp_active_windows_list = get_active_windows_list();
        
        var index = temp_active_windows_list.indexOf(window_id);
        if (index > -1) {
            temp_active_windows_list.splice(index, 1);
        }
        
        localStorage.actived_windows = temp_active_windows_list.join(",");
        
        return temp_active_windows_list;
    }
    
    /********************************
    ** ADD TO actived_windows LIST **
    *********************************/
    function add_to_active_windows(window_id)
    {
        var temp_active_windows_list = get_active_windows_list();
        
        var index = temp_active_windows_list.indexOf(window_id);
        
        if (index < 0)
        {
            //this windows is not in active list currently
            temp_active_windows_list.push(window_id);
        }

        localStorage.actived_windows = temp_active_windows_list.join(",");
        
        return temp_active_windows_list;
    }
    
    /*****************
    ** MANAGE CRASH **
    ******************/
    //If the last update didn't happened recently (more than time_period*2)
    //we will clear saved localStorage's data and reload the page
    function manage_crash()
    {
        if (localStorage.last_update)
        {
            if (parseInt(localStorage.last_update) + (time_period * 2) < Date.now())
            {
                //seems a crash came! who knows!?
                //localStorage.clear();
                localStorage.removeItem('main_windows');
                localStorage.removeItem('actived_windows');
                localStorage.removeItem('active_window');
                localStorage.removeItem('last_update');
                location.reload();
            }
        }
    }
    
    /********************************
    ** CHECK CURRENT WINDOW STATUS **
    *********************************/
    function check_current_window_status(test)
    {
        manage_crash();
        
        if (PV_API)
        {
            var active_status = "Inactive";
            var windows_list = get_main_windows_list();
            
            var active_windows_list = get_active_windows_list();
            
            if (windows_list.indexOf(localStorage.active_window) < 0)
            {
                //last actived windows is not alive anymore!
                //remove_from_main_windows_list(localStorage.active_window);
                
                //set the last added window, as active_window
                localStorage.active_window = windows_list[windows_list.length - 1];
            }
            
            if (! document[PV_API.hidden])
            {
                //Window's page is visible
                localStorage.active_window = current_window_id;
            }
            
            if (localStorage.active_window == current_window_id)
            {
                active_status = "Active";
            }
            
            if (active_status == "Active")
            {
                active_windows_list = add_to_active_windows(current_window_id);
            }
            else
            {
                active_windows_list = remove_from_active_windows(current_window_id);
            }
            
            console.log(test, active_windows_list);
            
            var element_holder = document.getElementById("holder_element");
            element_holder.insertAdjacentHTML("afterbegin", "<div>"+element_holder.childElementCount+") Current Windows is "+ active_status +"&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"+active_windows_list.length+" window(s) is visible and active of "+ windows_list.length +" windows</div>");
        }
        else
        {
            console.log("PageVisibility API is not supported :(");
            //our INACTIVE pages, will remain INACTIVE forever, you need to make some action in this case!
        }
        
        localStorage.last_update = Date.now();
    }
    
    //check storage continuously
    setInterval(function(){
        check_current_window_status ();
    }, time_period);
    
    //initial check
    check_current_window_status ();
</script>
</body>
</html>
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
Reza Amya
  • 1,494
  • 2
  • 25
  • 40