jQuery mobile pageshow event not firing on first $.mobile.changePage() - javascript

I'm writing a phonegap/jquery mobile app and have an issue I cant seem to solve.
When the app loads (device ready and jqm_mobile_init) fire and the app creates/opens a database and checks if a user is signed in (just a flag in the db). If so the app calls $.mobile.changePage("#home", {transition:"none"}); to redirect them to the "home" page.
Then on the "home" page pageshow event I grab a load of info from the db and append it to a listview within the home page.
However, the first time this runs (with the $.mobile.changePage event) the pageshow event isn't trigged (so none of my data gets appended to the listview). If I navigate around the app and then visit the page the data shows fine. This only happens when using $.mobile.changePage to change to the home page.
How can I make pageshow() fire on $.mobile.changePage? or is there another way to do it?
Heres my code:
/************************************************
Try to create/open the DB, if not catch the error
***********************************************/
try {
if (!window.openDatabase) {
alert('not supported');
} else {
var shortName = 'test';
var version = '1.0';
var displayName = 'test Database';
var maxSize = 200000; // in bytes
// database instance in db.
var db = openDatabase(shortName, version, displayName, maxSize);
// Create tables
createTables(db);
// Check if there is a signedin user
isUserSignedInQuery(db);
}
} catch(e) {
// Error handling code goes here.
if (e == 2) {
// Version number mismatch.
alert("Invalid database version.");
} else {
alert("Unknown error "+e+".");
}
return;
}
// Universal null/blank data handler
function nullDataHandler(transaction, results) { }
// Universal error callback
function errorHandler(error) {
//alert("Error processing SQL: " +error.message+ " Error Code: " +error.code);
}
// Create tables if dont already exist
function createTables(db) {
db.transaction(
function (transaction) {
// create tables
}
);
}
/**********************************************************************************************
Check if there is a signed in user, if so redirect to listings page, if not display login page
**********************************************************************************************/
function isUserSignedInQuery(db) {
db.transaction(
function (transaction) {
transaction.executeSql("SELECT * FROM USERS WHERE signedIn=1;",
[], // array of values for the ? placeholders
isUserSignedInDataHandler, errorHandler);
}
);
}
function isUserSignedInDataHandler(transaction, results) {
// Handle the results
if (results.rows.length > 0) {
//console.log("someones logged in!");
// Assign signed in user to global var
console.log("logged in user = " + results.rows.item(0).id);
window.currentSignedInUserId = results.rows.item(0).id;
$.mobile.changePage( "#home", { transition: "none"} );
} else {
$.mobile.changePage( "#login", { transition: "none"} );
}
}
/**********************************************************************************************
Sign in page:
**********************************************************************************************/
function doesSigningInUserAlreadyExistQuery(db) {
db.transaction(
function (transaction) {
transaction.executeSql("SELECT * FROM USERS WHERE username='"+usernameValue+"' ORDER BY id LIMIT 0,1;",
[], // array of values for the ? placeholders
doesSigningInUserAlreadyExistDataHandler, errorHandler);
}
);
}
function doesSigningInUserAlreadyExistDataHandler(transaction, results) {
// User exists, sign them in.
if (results.rows.length > 0) {
//console.log("user exists");
// Find number of rows
var len = results.rows.length;
//console.log(len);
for (var i=0; i<len; i++){
//console.log(results.rows.item(i));
db.transaction(
function (transaction) {
transaction.executeSql('UPDATE USERS SET signedIn = 1 WHERE username="'+usernameValue+'"');
}
);
// Assign signed in user to global var
window.currentSignedInUserId = results.rows.item(0).id;
// Redirect to home/listings page
$.mobile.changePage( "#home", { transition: "slidefade"} );
}
// User is new, create them and sign them in
} else {
db.transaction(
function (transaction) {
transaction.executeSql('INSERT INTO USERS (username, password, userId, defaultHandler, autoSync, updateCaseTypes'
+', updateHistorical, updateFavorite, signedIn) '
+'VALUES ("'+usernameValue+'", "eclipse", "userid321", "Another User", 1, 1, 1, 1, 1);', [],
function (transaction, resultSet) {
if (!resultSet.rowsAffected) {
// Previous insert failed.
alert('No rows affected!');
return false;
}
alert('insert ID was '+resultSet.insertId);
//Assign signed in user to global var
window.currentSignedInUserId = resultSet.insertId;
});
}
);
// Redirect to home/listings page
$.mobile.changePage( "#home", {
reloadPage: true,
transition: "slidefade"} );
}
}
$('#login').live('pageshow', function(event) {
console.log(window.currentSignedInUserId); // This is empty - global var not working
// Should this be tap??????? Find out. -----------
$('a#signIn').click(function() {
// Get values of all fields & buld vars
var username = $('#login-username');
var password = $('#login-password');
// Check if fields are empty
if( !username.val() ) {
username.addClass('empty');
$('label.login-username').addClass('blank');
}
if( !password.val() ) {
password.addClass('empty');
$('label.login-password').addClass('blank');
}
// If both not empty, check if user exists, if so sql update if not sql insert
if (username.val() && password.val()) {
// Get username
usernameValue = username.val();
// Run function
doesSigningInUserAlreadyExistQuery(db);
}
});
});
$('#home').live('pageshow', function(event) {
console.log("Page show fired on recordings page");
db.transaction(getRecordingsQuery, getRecordingsDataHandler, errorHandler);
// get stuff, loop through it and append
// Refresh the list to add JQM styles etc
$('#recordings-list').listview('refresh');
}
});

I've managed to resolve it, its not really a proper fix but it works at the expense of a screen flicker whilst the screen refreshes.
If it helps anyone, I added allowSamePageTransitions: true which solved the issue (at the expense of a flicker).

You should be using on() instead of live(). live() has been deprecated.
Have you tried putting it in the beforepageshow instead of pageshow? It seems like a better place to put data gathering/dynamic page element generation.

Related

service-worker.js catch updatefound before running other custom functions

Is there a way to check if the service worker found an update before loading custom functions?
i have this function which is working, but it runs the custom functions twice, and seems very untidy..
I'm looking for a way to only run the custom functions once, and not when an update was found and installed. When an update is found, the user || the page will reload automatically and then the custom functions can run normally..
I added the reg.events in this function to determine where to place my custom functions. I hope this question is understandable..
function installApp(path, scope) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(path, {
scope: scope
}).then((reg) => {
// event listener to catch the prompt if any and store in
// an instance for later use with add to homescreen() function.
getPrompt();
// this is a custom alert type notification
makeProgress('System','is ok');
/* THIS IS THE UPDATE FOUND FUNCTION */
reg.onupdatefound = function() {
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// the _clear() function removes items from the locaforage db to
// force the app to not auto login, but let the user
// login again to refresh any data when the page reloads
_clear('uuid');
_clear('user');
_clear('token');
makeProgress('new version','reload app');
} else {
// removes any custom notifications
clearProgress();
//just go into the app because everything is loaded.
//We dont need to reinstall the
//homescreen or listen for the homescreen because this
//is an update and the homescreen should already be installed?
enterApp();
}
break;
case 'redundant':
// removes any custom notifications cause
//the install is complete
clearProgress();
enterApp();
console.log('The installing service worker became redundant.');
break;
}
};
return;
};
/** Here is the events that fire during the install
// process and where i am currently stuck **/
if (reg.installing) {
makeProgress('updating','files');
/* THE SERVICE WORKER IS DOWNLOADING THE CACHE FROM THE SERVER */
} else if (reg.waiting) {
/* what message here ?*/
/* as far as i can tell, THE SERVICE WORKER IS WAITING FOR
*//*PREVIOUS SERVICE WORKER TO BEREFRESHED SO A RELOAD */
/*UI SHOULD COME HERE??*/
} else if (reg.active) {
/* what message here ?*/
/* IS THIS THE BEST PLACE TO RUN THE BELOW CUSTOM
*//*FUNCTIONS?? WILL //THEY ALWAYS FIRE */
}
/** AT WHICH OF THE EVENTS ABOVE WILL I ADD THE FUNCTIONS FROM HERE **/
requestWakeLock();
const browserFeatures = detectFeatures(reg);
setCompatibilityArray(browserFeatures);
localforage.ready().then(function() {
localforage.getItem('homescreen').then(function (value) {
if(value != 1){
if (platform == 'iPhone' || platform == 'iPad') {
installHome();
} else {
makeProgress('waiting', 'prompt');
waitPrompt();
}
return;
} else {
enterApp();
return;
}
}).catch(function (err) {
alertIt('something went wrong. Please refresh the page to try again. If the problem persists, try another browser.</br>', 'warning', 0);
return;
});
}).catch(function (err) {
alertIt('Something went wrong.<br>Please refresh the page to restart the installation process.<br>'+err, 'danger', 0);
return;
});
/** TO HERE, WITHOUT RUNNING THESE FUNCTION DURING*/
/*THE ONUPDATEFOUND EVENT AS THEN THEY WILL RUN TWICE**/
}, (err) => {
alertIt('Something went wrong.<br>Please refresh the page to restart the installation process.<br>', 'danger', 0);
})
} else {
alertIt('This browser is not compatible with this app.<br>Please try to use a different browser to install this application.<br>', 'danger', 0);
return;
}
}
I initialize this script like so:
window.addEventListener("load", () => {
makeProgress('Checking','system');
installApp(appsPath, appScope);
})
basically they must not be invoked if a new update is found..
I discovered that the onupdate function runs when old service worker is active..
If the onupdate function fires it changes a variable to a true value
I then used a time out function in the active event to see if a variable had changed... if it did change then i return false, and let the onupdate functions continue their course.. otherwise i continue to load my custom functions...Its working, but it doesn't seem like the best way.
Do you have a better method?
so like this:
function installApp(path, scope) {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(path, {
scope: scope
}).then((reg) => {
getPrompt();
makeProgress('refreshing','files');
var entApp = true;
reg.onupdatefound = function() {
entApp = false;
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
_clear('uuid');
_clear('user');
_clear('token');
makeProgress('new version','reloading app');
setTimeout(function(){
location.reload();
}, 2500);
return;
} else {
/*NOT SURE WHAT IS SUPPOSED TO GO HERE, SO I JUST RELOADED THE PAGE*/
makeProgress('new version','reloading app');
setTimeout(function(){
location.reload();
}, 2500);
return;
}
break;
case 'redundant':
/*NOT SURE WHAT IS SUPPOSED TO GO HERE, SO I JUST RELOADED THE PAGE*/
makeProgress('new version','reloading app');
setTimeout(function(){
location.reload();
}, 2500);
return;
break;
}
};
return;
};
if (reg.active) {
/** RIGHT HERE IS WHERE THE ONUPDATE FIRES. I GAVE IT A
2.5 SECONDS TO DO ITS THING, THEN CHECKED TO SEE IF THERE WAS
AN UPDATE, IF NO UPDATE THEN I RUN MY CUSTOM FUNCTIONS, OTHERWISE
THE ONUPDATE FUNCTION RELOADS THE PAGE AND THE UPDATED SW.JS FILE
WILL THEN RUN THESE FUNCTIONS WHEN ITS ACTIVE.. IS THERE A BETTER
IN-BUILT METHOD TO DO THIS?**/
setTimeout(function(){
if(entApp === true){
requestWakeLock();
const browserFeatures = detectFeatures(reg);
setCompatibilityArray(browserFeatures);
localforage.ready().then(function() {
localforage.getItem('homescreen').then(function (value) {
if(value != 1){
if (platform == 'iPhone' || platform == 'iPad') {
installHome();
} else {
makeProgress('waiting', 'prompt');
waitPrompt();
}
return;
} else {
enterApp();
return;
}
}).catch(function (err) {
alertIt('something went wrong. Please refresh the page to try again. If the problem persists, try another browser.</br>', 'warning', 0);
return;
});
}).catch(function (err) {
alertIt('Something went wrong.<br>Please refresh the page to restart the installation process.<br>'+err, 'danger', 0);
return;
});
}
}, 2500);
}

localStorage results in function being loaded infinite times

I am filtering what users see based on a selected value. Users can choose between consumer and commercial with the code supposed to be adding "/commercial" to the URL of commercial users and "/consumer" being added to the URL of consumer users.
This is the current code I am using, which results in the below functions being run every time the page is loaded, resulting in an infinite loop.
/* Hides non-commercial products */
function commercial() {
window.location.assign(window.location.origin + "/collections/{{ collection.handle }}/commercial");
localStorage.setItem("saved", "0");
}
/* Hides non-consumer products */
function consumer() {
window.location.assign(window.location.origin + "/collections/{{ collection.handle }}/consumer");
localStorage.setItem("saved", "1");
}
/* Shows all products */
function reset() {
window.location.assign(window.location.origin + "/collections/{{ collection.handle }}");
localStorage.removeItem("saved");
}
UPDATED CODE: LocalStorage results in the above functions being run every time the page is loaded, resulting in an infinite loop. Below is mt LocalStorage get the saved value of the key code:
/* Remember last clicked button and store it in LocalStorage */
window.addEventListener('load', (event) => {
const value = localStorage.getItem("saved");
if (value == "0") {
commercial();
} else if (value == "1") {
consumer()
} else {
reset()
}
});
This is what the code outputs: (DJI is just an example of a collection)
saved == "0" outputs /collections/dji/commercial
saved == "1" outputs /collections/dji/consumer
saved == "2" outputs /collections/dji/
window.location.href holds the complete URL thus it's appending to previous URL, instead use window.location.origin.
window.location.assign(window.location.origin + "/commercial");

Problem in reloading page(table) after deleting record from database

I have a ASP.NET CORE MVC application with a table listing some clients (CRUD) and I can delete users when I click in delete.
The problem that i'm trying to resolve is when I refresh the page after delete the records from the listing the listing still show the deleted record when in the database the record is deleted and if I reload (F5) the page manually the record disappear.
I already try the location.reload(), windows.location.reload() and nothing... I can saw that page is reloading but the record don't disappear.
My code is above:
<script type="text/javascript">
function toggleChecked(status) {
$("#checkboxes input").each(function () {
// Set the checked status of each to match the
// checked status of the check all checkbox:
$(this).prop("checked", status);
});
}
function Delete(id) {
var example_table = $("#example1")
var r = confirm("Are you sure you want to Delete?");
if (r == true) {
$.ajax(
{
type: "POST",
url: '#Url.Action("Delete", "Clients")',
data: {
id: id
},
error: function (result) {
alert("error");
},
success: function (result) {
if (result == true) {
example_table.ajax.reload(); // -->> The problem is in this line!
location.reload(); // -->> The problem is in this line!
}
else {
alert("There is a problem, Try Later!");
}
}
});
}
}
$(document).ready(function () {
//Set the default value of the global checkbox to true:
$("#checkall").prop('checked', false);
// Attach the call to toggleChecked to the
// click event of the global checkbox:
$("#checkall").click(function () {
var status = $("#checkall").prop('checked');
toggleChecked(status);
});
});
</script>
The back-end Delete:
[HttpPost]
public bool Delete(int id)
{
try
{
Clients client = db.Clients.Where(s => s.Id == id).First();
db.Clients.Remove(client );
db.SaveChanges();
return true;
}
catch (System.Exception)
{
return false;
}
}
I want that the deleted record disappear in real time without have to refresh the page manually. If you can help me I appreciate.
If the delete is sucessful you could manually remove that item from the list in the sucess of the ajax call.
However with the reload, does the reload of the page call the database and get a list of data? If this is not triggered from the reload then it wont be updating the list. If it is then my only suggestion would be that cookies may be storing the list?
For Server side refresh you can use Response.Redirect(Request.RawUrl)
For Client side refresh you can use window.location.href= window.location
Or document.location.reload() instead of location.reload()

Angular: When does app.config get called?

I am creating a chrome extension UI using angular. I want to make it so when the user clicks the icon in the upper right the correct screen appears. If the user is not logged in they should go to the login page. If the user is logged in and in drawing mode then they should go to the drawing screen, if they are logged in and not drawing then they should go to the main menu.
My main problem is checking whether or not they are already in drawing mode. I am sending a message to my content scripts to check whether or not I am in drawing mode, but for some reason this callback is never getting called! Very disappointing. I'm not sure when code in app.config gets called; when does it?
app.js
app.config(function($stateProvider, $urlRouterProvider) {
var rootRef = new Firebase(mysterious_url);
var user = rootRef.getAuth();
chrome.tabs.sendMessage('isInDrawingMode', {action: 'isInDrawingMode'}, function(response) {
if (!user) {
$urlRouterProvider.otherwise('/login');
} else if (response.inDrawingMode) {
$urlRouterProvider.otherwise('/draw');
} else {
$urlRouterProvider.otherwise('/main');
}
});
contentscripts.js
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse){
// Toggle User Canvas Messages
if ( request.toggle === 'off' ){
// toggleUserCanvasOff();
disableDrawingMode();
sendResponse({confirm:'canvas turned off'});
} else if ( request.toggle === 'on' ){
enableDrawingMode();
// toggleUserCanvasOn();
sendResponse({confirm:'canvas turned on'});
// Initialize toggle status for popup button
} else if ( request.getStatus === true ){
sendResponse({status:toggle});
} else if (request.canvasData) { // new Canvas data
onCanvasData(request.site, request.user, request.data);
} else if (request.erase){
eraseUserCanvas();
} else if (request.changeColor){
lineColor = request.changeColor;
} else if (request.image){
getCurrentUser(function(user){
var userCanvas = $('.'+ user);
addOneTimeClickEvent(userCanvas, addImage, request.image);
});
} else if (request.action) {
sendResponse({inDrawingMode: "true"});
}
}
);

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

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.
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):
Verify
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.
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
});
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.
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);
}
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>
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.
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.
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());
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.

Categories