How to avoid using a global variable, when closing an EventSource in internal links? - javascript

I have a web application, where some internal pages use an EventSource to receive live updates from the server.
The client code looks like this:
var LiveClient = (function() {
return {
live: function(i) {
var source = new EventSource("/stream/tick");
source.addEventListener('messages.keepalive', function(e) {
console.log("Client "+ i + ' received a message.');
});
}
};
})();
You can see a live demo on heroku: http://eventsourcetest.herokuapp.com/test/test/1. If you open the developer console, you will see a message printed every time an event is received.
The problem is that when visiting internal links, the EventSource remains open, causing messages to be printed even after the visitor moves from one page to another - so if you visit the three links on the top, you will get messages from three sources.
How can I close the previous connection after the user moves from one internal page to another?
A hacky workaround that I tried was to use a global variable for the EventSource object, like this:
var LiveClient = (function() {
return {
live_global: function(i) {
// We set source as global, otherwise we were left
// with sources remaining open after visiting internal
// pages
if (typeof source != "undefined" && source != null) {
if (source.OPEN) {
source.close();
console.log("Closed source");
}
}
source = new EventSource("/stream/tick");
source.addEventListener('messages.keepalive', function(e) {
console.log("Client "+ i + ' received a message.');
});
}
};
})();
Demo here: http://eventsourcetest.herokuapp.com/test/test_global/1, but I am looking for a solution that would avoid the use of a global variable if possible.
The HTML code that is generated is:
Page 1 |
Page 2 |
Page 3 |
<p>This is page 3</p>
<script>
$(function() {
LiveClient.live_global(3);
});
</script>
or with LiveClient.live_global(1); for the case with the global variable.

Try this. I haven't tested it. If it works, you might be able to replace LiveClient.source with this.source which is a lot cleaner imo.
var LiveClient = (function() {
return {
source: null,
live_global: function(i) {
// We set source as global, otherwise we were left
// with sources remaining open after visiting internal
// pages
if (typeof LiveClient.source != "undefined" && LiveClient.source != null) {
if (source.OPEN) {
source.close();
console.log("Closed source");
}
}
LiveClient.source = new EventSource("/stream/tick");
LiveClient.source.addEventListener('messages.keepalive', function(e) {
console.log("Client "+ i + ' received a message.');
});
}
};
})();

Related

How can I limit the user to a single session tab? [duplicate]

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.

Service Worker onClick event - How to open url in window withing sw scope?

I have service worker which handles push notification click event:
self.addEventListener('notificationclick', function (e) {
e.notification.close();
e.waitUntil(
clients.openWindow(e.notification.data.url)
);
});
When notification comes it takes url from data and displays it in new window.
The code works, however, I want different behavior. When User clicks on the link, then it should check if there is any opened window within service worker scope. If yes, then it should focus on the window and navigate to the given url.
I have checked this answer but it is not exactly what I want.
Any idea how it can be done?
P.S. I wrote this code but it still doesn't work. The first two messages are however shown in the log.
self.addEventListener('notificationclick', function (e) {
e.notification.close();
var redirectUrl = e.notification.data.redirect_url.toString();
var scopeUrl = e.notification.data.scope_url.toString();
console.log(redirectUrl);
console.log(scopeUrl);
e.waitUntil(
clients.matchAll({type: 'window'}).then(function(clients) {
for (i = 0; i < clients.length; i++) {
console.log(clients[i].url);
if (clients[i].url.toString().indexOf(scopeUrl) !== -1) {
// Scope url is the part of main url
clients[i].navigate(givenUrl);
clients[i].focus();
break;
}
}
})
);
});
Ok, here is the piece of code which works as expected. Notice that I am passing scope_url together with redirect_url into the web notification. After that I am checking if scope_url is part of sw location. Only after that I navigate to redirect_url.
self.addEventListener('notificationclick', function (e) {
e.notification.close();
var redirectUrl = e.notification.data.redirect_url;
var scopeUrl = e.notification.data.scope_url;
e.waitUntil(
clients.matchAll({includeUncontrolled: true, type: 'window'}).then(function(clients) {
for (i = 0; i < clients.length; i++) {
if (clients[i].url.indexOf(scopeUrl) !== -1) {
// Scope url is the part of main url
clients[i].navigate(redirectUrl);
clients[i].focus();
break;
}
}
})
);
});
If I understand you correctly, most of the code you linked to works here.
First retrieve all the clients
If there are more than one, choose one of them
Navigate that to somewhere and focus
Else open a new window
Right?
event.waitUntil(
clients.matchAll({type: 'window'})
.then(clients => {
// clients is an array with all the clients
if (clients.length > 0) {
// if you have multiple clients, decide
// choose one of the clients here
const someClient = clients[..someindex..]
return someClient.navigate(navigationUrl)
.then(client => client.focus());
} else {
// if you don't have any clients
return clients.openWindow(navigationUrl);
}
})
);

testing 2-player socket.io game. updating a label is affecting both pages

I don't understand why updating a label on one page is affecting the label on another page. I did not think the DOM was shared like that. Opening one tab or page successfully updates the label to 'player1', but when I open another tab/pg, it updates both labels to 'player2'.
<script>
var socket = io.connect('http://localhost:3000');
socket.on('connect', function() {
socket.emit('join');
socket.on('joinSuccess', function (playerSlot) {
if (playerSlot === 'player1') {
$("#playerID").text("you are player1");
} else if (playerSlot === 'player2') {
$("#playerID").text("you are player2");
}
}); //end joinSuccess
}); //end connect
I am merely trying to notify the user which player they are.
solution:
else if (playerSlot === 'player2') {
var elm = $("#playerID");
var empty = !elm.text().trim();
if (empty) {
elm.text("you are " + playerSlot);
}
}
Are you pushing the 'joinSuccess' message when new user joins? In such case this message will be passed to both the pages with same playerSlot value. So, all pages will be updated last joined player name.
In such case you can handle this with simple condition,
socket.on('joinSuccess', function (playerSlot) {
var elm = $("#playerID");
if (!elm.text().trim()) {
elm.text("you are " + playerSlot);
}
});

'load' event not firing when iframe is loaded in Chrome

I am trying to display a 'mask' on my client while a file is dynamically generated server side. Seems like the recommend work around for this (since its not ajax) is to use an iframe and listen from the onload or done event to determine when the file has actually shipped to the client from the server.
here is my angular code:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
e.load(function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
});
angular.element('body').append(e);
This works great in Firefox but no luck in Chrome. I have also tried to use the onload function:
e.onload = function() { //unmask here }
But I did not have any luck there either.
Ideas?
Unfortunately it is not possible to use an iframe's onload event in Chrome if the content is an attachment. This answer may provide you with an idea of how you can work around it.
I hate this, but I couldn't find any other way than checking whether it is still loading or not except by checking at intervals.
var timer = setInterval(function () {
iframe = document.getElementById('iframedownload');
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Check if loading is complete
if (iframeDoc.readyState == 'complete' || iframeDoc.readyState == 'interactive') {
loadingOff();
clearInterval(timer);
return;
}
}, 4000);
You can do it in another way:
In the main document:
function iframeLoaded() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
}
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element('body').append(e);
In the iframe document (this is, inside the html of the page referenced by url)
window.onload = function() {
parent.iframeLoaded();
}
This will work if the main page, and the page inside the iframe are in the same domain.
Actually, you can access the parent through:
window.parent
parent
//and, if the parent is the top-level document, and not inside another frame
top
window.top
It's safer to use window.parent since the variables parent and top could be overwritten (usually not intended).
you have to consider 2 points:
1- first of all, if your url has different domain name, it is not possible to do this except when you have access to the other domain to add the Access-Control-Allow-Origin: * header, to fix this go to this link.
2- but if it has the same domain or you have added Access-Control-Allow-Origin: * to the headers of your domain, you can do what you want like this:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element(document.body).append(e);
e[0].contentWindow.onload = function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
};
I have done this in all kinds of browsers.
I had problems with the iframe taking too long to load. The iframe registered as loaded while the request wasn't handled. I came up with the following solution:
JS
Function:
function iframeReloaded(iframe, callback) {
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframeReloaded(iframe[0], function () {
console.log('Reloaded');
})
JQuery
Function:
$.fn.iframeReloaded = function (callback) {
if (!this.is('iframe')) {
throw new Error('The element is not an iFrame, please provide the correct element');
}
let iframe = this[0];
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframe.iframeReloaded(function () {
console.log('Reloaded');
})
I've just noticed that Chrome is not always firing the load event for the main page so this could have an effect on iframes too as they are basically treated the same way.
Use Dev Tools or the Performance api to check if the load event is being fired at all.
I just checked http://ee.co.uk/ and if you open the console and enter window.performance.timing you'll find the entries for domComplete, loadEventStart and loadEventEnd are 0 - at least at this current time:)
Looks like there is a problem with Chrome here - I've checked it on 2 PCs using the latest version 31.0.1650.63.
Update: checked ee again and load event fired but not on subsequent reloads so this is intermittent and may possibly be related to loading errors on their site. But the load event should fire whatever.
This problem has occurred on 5 or 6 sites for me now in the last day since I noticed my own site monitoring occasionally failed. Only just pinpointed the cause to this. I need some beauty sleep then I'll investigate further when I'm more awake.

Override "Error Loading Page" for network failure in js file

I have JQuery Mobile-1.0.js file.
// Load a page into the DOM.
$.mobile.loadPage = function (url, options) {
// This function uses deferred notifications to let callers
// know when the page is done loading, or if an error has occurred.
var deferred = $.Deferred(),
// The default loadPage options with overrides specified by
// the caller.
settings = $.extend({}, $.mobile.loadPage.defaults, options),
// The DOM element for the page after it has been loaded.
page = null,
// If the reloadPage option is true, and the page is already
// in the DOM, dupCachedPage will be set to the page element
// so that it can be removed after the new version of the
// page is loaded off the network.
dupCachedPage = null,
// determine the current base url
findBaseWithDefault = function () {
var closestBase = ($.mobile.activePage && getClosestBaseUrl($.mobile.activePage));
return closestBase || documentBase.hrefNoHash;
},
// The absolute version of the URL passed into the function. This
// version of the URL may contain dialog/subpage params in it.
absUrl = path.makeUrlAbsolute(url, findBaseWithDefault());
// If the caller provided data, and we're using "get" request,
// append the data to the URL.
if (settings.data && settings.type === "get") {
absUrl = path.addSearchParams(absUrl, settings.data);
settings.data = undefined;
}
// If the caller is using a "post" request, reloadPage must be true
if (settings.data && settings.type === "post") {
settings.reloadPage = true;
}
// The absolute version of the URL minus any dialog/subpage params.
// In otherwords the real URL of the page to be loaded.
var fileUrl = path.getFilePath(absUrl),
// The version of the Url actually stored in the data-url attribute of
// the page. For embedded pages, it is just the id of the page. For pages
// within the same domain as the document base, it is the site relative
// path. For cross-domain pages (Phone Gap only) the entire absolute Url
// used to load the page.
dataUrl = path.convertUrlToDataUrl(absUrl);
// Make sure we have a pageContainer to work with.
settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
// Check to see if the page already exists in the DOM.
page = settings.pageContainer.children(":jqmData(url='" + dataUrl + "')");
// If we failed to find the page, check to see if the url is a
// reference to an embedded page. If so, it may have been dynamically
// injected by a developer, in which case it would be lacking a data-url
// attribute and in need of enhancement.
if (page.length === 0 && dataUrl && !path.isPath(dataUrl)) {
page = settings.pageContainer.children("#" + dataUrl)
.attr("data-" + $.mobile.ns + "url", dataUrl);
}
// If we failed to find a page in the DOM, check the URL to see if it
// refers to the first page in the application. If it isn't a reference
// to the first page and refers to non-existent embedded page, error out.
if (page.length === 0) {
if ($.mobile.firstPage && path.isFirstPageUrl(fileUrl)) {
// Check to make sure our cached-first-page is actually
// in the DOM. Some user deployed apps are pruning the first
// page from the DOM for various reasons, we check for this
// case here because we don't want a first-page with an id
// falling through to the non-existent embedded page error
// case. If the first-page is not in the DOM, then we let
// things fall through to the ajax loading code below so
// that it gets reloaded.
if ($.mobile.firstPage.parent().length) {
page = $($.mobile.firstPage);
}
} else if (path.isEmbeddedPage(fileUrl)) {
deferred.reject(absUrl, options);
return deferred.promise();
}
}
// Reset base to the default document base.
if (base) {
base.reset();
}
// If the page we are interested in is already in the DOM,
// and the caller did not indicate that we should force a
// reload of the file, we are done. Otherwise, track the
// existing page as a duplicated.
if (page.length) {
if (!settings.reloadPage) {
enhancePage(page, settings.role);
deferred.resolve(absUrl, options, page);
return deferred.promise();
}
dupCachedPage = page;
}
var mpc = settings.pageContainer,
pblEvent = new $.Event("pagebeforeload"),
triggerData = { url: url, absUrl: absUrl, dataUrl: dataUrl, deferred: deferred, options: settings };
// Let listeners know we're about to load a page.
mpc.trigger(pblEvent, triggerData);
// If the default behavior is prevented, stop here!
if (pblEvent.isDefaultPrevented()) {
return deferred.promise();
}
if (settings.showLoadMsg) {
// This configurable timeout allows cached pages a brief delay to load without showing a message
var loadMsgDelay = setTimeout(function () {
$.mobile.showPageLoadingMsg();
}, settings.loadMsgDelay),
// Shared logic for clearing timeout and removing message.
hideMsg = function () {
// Stop message show timer
clearTimeout(loadMsgDelay);
// Hide loading message
$.mobile.hidePageLoadingMsg();
};
}
if (!($.mobile.allowCrossDomainPages || path.isSameDomain(documentUrl, absUrl))) {
deferred.reject(absUrl, options);
} else {
// Load the new page.
$.ajax({
url: fileUrl,
type: settings.type,
data: settings.data,
dataType: "html",
success: function (html, textStatus, xhr) {
//pre-parse html to check for a data-url,
//use it as the new fileUrl, base path, etc
var all = $("<div></div>"),
//page title regexp
newPageTitle = html.match(/<title[^>]*>([^<]*)/) && RegExp.$1,
// TODO handle dialogs again
pageElemRegex = new RegExp("(<[^>]+\\bdata-" + $.mobile.ns + "role=[\"']?page[\"']?[^>]*>)"),
dataUrlRegex = new RegExp("\\bdata-" + $.mobile.ns + "url=[\"']?([^\"'>]*)[\"']?");
// data-url must be provided for the base tag so resource requests can be directed to the
// correct url. loading into a temprorary element makes these requests immediately
if (pageElemRegex.test(html)
&& RegExp.$1
&& dataUrlRegex.test(RegExp.$1)
&& RegExp.$1) {
url = fileUrl = path.getFilePath(RegExp.$1);
}
if (base) {
base.set(fileUrl);
}
//workaround to allow scripts to execute when included in page divs
all.get(0).innerHTML = html;
page = all.find(":jqmData(role='page'), :jqmData(role='dialog')").first();
//if page elem couldn't be found, create one and insert the body element's contents
if (!page.length) {
page = $("<div data-" + $.mobile.ns + "role='page'>" + html.split(/<\/?body[^>]*>/gmi)[1] + "</div>");
}
if (newPageTitle && !page.jqmData("title")) {
if (~newPageTitle.indexOf("&")) {
newPageTitle = $("<div>" + newPageTitle + "</div>").text();
}
page.jqmData("title", newPageTitle);
}
//rewrite src and href attrs to use a base url
if (!$.support.dynamicBaseTag) {
var newPath = path.get(fileUrl);
page.find("[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]").each(function () {
var thisAttr = $(this).is('[href]') ? 'href' :
$(this).is('[src]') ? 'src' : 'action',
thisUrl = $(this).attr(thisAttr);
// XXX_jblas: We need to fix this so that it removes the document
// base URL, and then prepends with the new page URL.
//if full path exists and is same, chop it - helps IE out
thisUrl = thisUrl.replace(location.protocol + '//' + location.host + location.pathname, '');
if (!/^(\w+:|#|\/)/.test(thisUrl)) {
$(this).attr(thisAttr, newPath + thisUrl);
}
});
}
//append to page and enhance
// TODO taging a page with external to make sure that embedded pages aren't removed
// by the various page handling code is bad. Having page handling code in many
// places is bad. Solutions post 1.0
page
.attr("data-" + $.mobile.ns + "url", path.convertUrlToDataUrl(fileUrl))
.attr("data-" + $.mobile.ns + "external-page", true)
.appendTo(settings.pageContainer);
// wait for page creation to leverage options defined on widget
page.one('pagecreate', $.mobile._bindPageRemove);
enhancePage(page, settings.role);
// Enhancing the page may result in new dialogs/sub pages being inserted
// into the DOM. If the original absUrl refers to a sub-page, that is the
// real page we are interested in.
if (absUrl.indexOf("&" + $.mobile.subPageUrlKey) > -1) {
page = settings.pageContainer.children(":jqmData(url='" + dataUrl + "')");
}
//bind pageHide to removePage after it's hidden, if the page options specify to do so
// Remove loading message.
if (settings.showLoadMsg) {
hideMsg();
}
// Add the page reference and xhr to our triggerData.
triggerData.xhr = xhr;
triggerData.textStatus = textStatus;
triggerData.page = page;
// Let listeners know the page loaded successfully.
settings.pageContainer.trigger("pageload", triggerData);
deferred.resolve(absUrl, options, page, dupCachedPage);
},
error: function (xhr, textStatus, errorThrown) {
//set base back to current path
if (base) {
base.set(path.get());
}
// Add error info to our triggerData.
triggerData.xhr = xhr;
triggerData.textStatus = textStatus;
triggerData.errorThrown = errorThrown;
var plfEvent = new $.Event("pageloadfailed");
// Let listeners know the page load failed.
settings.pageContainer.trigger(plfEvent, triggerData);
// If the default behavior is prevented, stop here!
// Note that it is the responsibility of the listener/handler
// that called preventDefault(), to resolve/reject the
// deferred object within the triggerData.
if (plfEvent.isDefaultPrevented()) {
return;
}
// Remove loading message.
if (settings.showLoadMsg) {
// Remove loading message.
hideMsg();
//show error message
$("<div class='ui-loader ui-overlay-shadow ui-body-e ui-corner-all'><h1>" + $.mobile.pageLoadErrorMessage + "</h1></div>")
.css({ "display": "block", "opacity": 0.96, "top": $window.scrollTop() + 100 })
.appendTo(settings.pageContainer)
.delay(800)
.fadeOut(400, function () {
$(this).remove();
});
}
deferred.reject(absUrl, options);
}
});
}
return deferred.promise();
};
This is the code for showing an error message "Error Loading Page" for error in page. Here i want to show alert message for net connection failure as "Please check your net connection" instead of the below image.
Note: I dont want to change the pageloaderrormessage. want to stop to get the page error messages, instead of that i will enable my network error condition as in Show Network Error in android. If the user pressed "Ok" in alert dialog i'll navigate them into Reload.html.
Please tell me where i can check that condition and where i have to change the error message?
As both #shkschneider and #codemonkey have suggested you need to set this option on mobileinit
Example:
$(document).bind("mobileinit", function(){
$.mobile.pageLoadErrorMessage = "Please check your net connection";
});
Linking the jQM 1.0.1 docs:
http://jquerymobile.com/demos/1.0.1/docs/api/globalconfig.html
Here is a example:
http://jquerymobile.com/demos/1.0.1/docs/config/pageLoadErrorMessage.html ( click the "or Try this broken link" button )
Now if you have the ability to upgrade jQM to 1.1.1 you might try something like this:
//use theme swatch "b", a custom message, and no spinner
$.mobile.showPageLoadingMsg("b", "Please check your net connection", true);
// hide after delay
setTimeout( $.mobile.hidePageLoadingMsg, 1500 );
Docs:
http://jquerymobile.com/demos/1.1.1/docs/api/methods.html
UPDATE:
Another thought is to use a plugin to achieve something like you want, Does something like this work?
http://dev.jtsage.com/jQM-SimpleDialog/demos/bool.html
Simply use:
$(document).bind("mobileinit", function(){
$.mobile.pageLoadErrorMessage("Please check your netconnection");
});
http://jquerymobile.com/test/docs/api/globalconfig.html
Set the pageLoadErrorMessage as described here http://jquerymobile.com/demos/1.1.1/docs/api/globalconfig.html
EDIT
If you want to handle the behaviour in a custom way, set loadingMessage to false. This prevents the loading message from being displayed. You can bind to the pageloadfailed (described here http://jquerymobile.com/demos/1.1.1/docs/api/events.html) and add add your custom handling logic in the event handler.

Categories

Resources