15
\$\begingroup\$

I had a requirement to restrict my web site's users to just one open tab in the browser. I am aware that this is not the best thing to do in terms of general user experience guidelines, but it's an internal application that doesn't play well with such a scenario so I had to do it.

The following is what I came up with:

function IsNewTab() {
  return $.cookie('TabOpen');
}

$(function() {
  if (!IsNewTab()) {
    $.cookie('TabOpen', "YES", {
      path: '/'
    });
    $(window).unload(function() {
      $.removeCookie('TabOpen', {
        path: '/'
      });
    });
  } else {
    alert('already some tab open')
      //OR
      //window.close()
  }
});

I'd like opinions on the above. Are there any loopholes (keeping in mind that the target audience isn't too technical and probably won't go hacking around with the browser console). Are there any possible improvements that can be made?

\$\endgroup\$
3
  • 3
    \$\begingroup\$ hmm interesting problem and solution. Welcome to CR! \$\endgroup\$
    – RubberDuck
    Nov 18, 2014 at 12:16
  • \$\begingroup\$ This functionality will not work on Safari Mobile, as the user can completely disable browser cookies. \$\endgroup\$ Feb 25, 2020 at 11:15
  • \$\begingroup\$ @Alexandru, it clearly won't work in any browser not running scripts, either. I guess that's a given with this approach. \$\endgroup\$ Feb 26, 2020 at 9:12

2 Answers 2

12
\$\begingroup\$

I can't think of a better approach than a session cookie. It feels safe in that you have three potential ways of leaving the page:

  • Navigate away
  • Close browser/tab
  • Browser crashes

So you're either going to have the option to remove the cookie on the unload event or the session cookie will expire anyway.

I would structure the code a little differently:

// Wrap in an IIFE accepting jQuery as a parameter.
(function ($) {
    var setCookie,
        removeCookie,
        // Create constants for things instead of having same string
        // in multiple places in code.
        COOKIE_NAME = 'TabOpen',
        SITE_WIDE_PATH = { path : '/' };

    setCookie = function () {
        $.cookie(COOKIE_NAME, '1', SITE_WIDE_PATH); 
    };

    removeCookie = function () {
        $.removeCookie(COOKIE_NAME, SITE_WIDE_PATH);
    };

    // We don't need to wait for DOM ready to check the cookie
    if ($.cookie(COOKIE_NAME) === undefined) {
        setCookie();
        $(window).unload(removeCookie);
    } else {
        // Replace the whole body with an error message when the DOM is ready.
        $(function () { 
            $('body').html('<div class="error">' + 
                '<h1>Sorry!</h1>' + 
                '<p>You can only have one instance of this web page open at a time.</p>' + 
                '</div>');
            });
    }
}(jQuery));

(code untested).

\$\endgroup\$
1
  • \$\begingroup\$ This did not work well in Safari, since it does not execute unload consitently. I added another line after the unload, as 'safety' and this then solved the issue for Safari. Line : $(window).on('beforeunload',removeCookie); \$\endgroup\$ Jul 19, 2018 at 20:25
4
\$\begingroup\$

There's certainly nothing wrong with using a cookie for this, but it is, alternatively, entirely possible to monitor open tabs on the client side, using localStorage.

The example below listens for page load and beforeunload events:

  • when a page is loaded, a new entry is added to localStorage
  • when a page is unloaded, the last entry is removed from localStorage

If there is ever more than one entry, then the browser can report not only that more than one tab is open from the same site but also how many tabs are open from the same site.

Example:

// registerOpenTab FUNCTION

const registerOpenTab = () => {

  let tabsOpen = 1;

  while (localStorage.getItem('openTab' + tabsOpen) !== null) {
  
    tabsOpen++;
  }

  localStorage.setItem('openTab' + tabsOpen, 'open');

  if (localStorage.getItem('openTab2') !== null) {

      window.alert('This application is already running in ' + (tabsOpen - 1) + ' other browser tab(s).')
  }
}


// unregisterOpenTab FUNCTION

const unregisterOpenTab = () => {

  let tabsOpen = 1;

  while (localStorage.getItem('openTab' + tabsOpen) !== null) {
  
    tabsOpen++;
  }

  localStorage.removeItem('openTab' + (tabsOpen - 1));
}


// EVENT LISTENERS
window.addEventListener('load', registerOpenTab);
window.addEventListener('beforeunload', unregisterOpenTab);

Alternative Example (without newlines after each statement):

// registerOpenTab FUNCTION
const registerOpenTab = () => {
  let tabsOpen = 1;
  while (localStorage.getItem('openTab' + tabsOpen) !== null) {
    tabsOpen++;
  }
  localStorage.setItem('openTab' + tabsOpen, 'open');
  if (localStorage.getItem('openTab2') !== null) {
      window.alert('This application is already running in ' + (tabsOpen - 1) + ' other browser tab(s).')
  }
}

// unregisterOpenTab FUNCTION
const unregisterOpenTab = () => {
  let tabsOpen = 1;
  while (localStorage.getItem('openTab' + tabsOpen) !== null) {
    tabsOpen++;
  }
  localStorage.removeItem('openTab' + (tabsOpen - 1));
}

// EVENT LISTENERS
window.addEventListener('load', registerOpenTab);
window.addEventListener('beforeunload', unregisterOpenTab);
\$\endgroup\$
2
  • \$\begingroup\$ It's annoying to see a newline after each statement. \$\endgroup\$
    – Tushar
    Feb 26, 2020 at 6:05
  • \$\begingroup\$ I've added an alternative example (without newlines) for you, @Tushar. \$\endgroup\$
    – Rounin
    Feb 26, 2020 at 8:17

Not the answer you're looking for? Browse other questions tagged or ask your own question.