How to know if browser tab is already open using Javascript? - javascript

How to know or check if the two browser tab is already open and if those tab are open, the user will receive an alert box or msg box saying that 'the url is already open', something like that, in pure/native JavaScript? This browser tab is contain an external website which is I don't have any privileges to manipulate or change it. Thanks
Example URLs
yahoo.com and google.com
I want to alert the user if there's already open tab for yahoo.com and google.com
And I want to use tabCreate to open the url like this:
tabCreate("http://maps.google.com/", "tabMapsPermanentAddress");
mean to open a new tab, it is use in creating chrome extension

You may use something like following
<!-- HTML -->
<a id="opener">Open window</a>
// JavaScript
var a = document.getElementById('opener'), w;
a.onclick = function() {
if (!w || w.closed) {
w = window.open("https://www.google.com","_blank","menubar = 0, scrollbars = 0");
} else {
console.log('window is already opened');
}
w.focus();
};
Working jsBin | More on window.open method
If you want to control more than one window, use the snippet below
<!-- HTML -->
Open google.com |
Open yahoo.com
//JavaScript
window.onload = function(){
var a = document.querySelectorAll('.opener'), w = [], url, random, i;
for(i = 0; i < a.length; i++){
(function(i){
a[i].onclick = function(e) {
if (!w[i] || w[i].closed) {
url = this.href;
random = Math.floor((Math.random() * 100) + 1);
w[i] = window.open(url, "_blank", random, "menubar = 0, scrollbars = 0");
} else {
console.log('window ' + url + ' is already opened');
}
e.preventDefault();
w[i].focus();
};
})(i);
}
};
Working jsBin
If you don't want them to load in separated window, just exclude this line
random = Math.floor((Math.random()*100)+1);
and remove random reference from the next line
w[i] = window.open(url, "_blank", random, "menubar=0,scrollbars=0");
Side note: As you can see above, we created two windows with some third party content; you should know that there's no way to get any reference (to the parent/opener window) from them.

One basic idea is to store the tab count in either a cookie or localStorage, incrementing it on page load and decrementing it on page unload:
if (+localStorage.tabCount > 0)
alert('Already open!');
else
localStorage.tabCount = 0;
localStorage.tabCount = +localStorage.tabCount + 1;
window.onunload = function () {
localStorage.tabCount = +localStorage.tabCount - 1;
};
Try opening this fiddle in multiple tabs.
Note that this technique is pretty fragile, though. For example, if for some reason the browser crashes, the unload handler won't run, and it'll go out of sync.

The answer by Casey Chu works fine until the browser crashes with the page open. On any next execution, the localStorage object will have initialized tabCount with non zero value. Therefore a better solution is to store the value in a session cookie. The session cookie will be removed when browser exits successfully. When the browser crashes the session cookie will actually be preserved but fortunately only for one next execution of the browser.
Object sessionStorage is distinct for each tab so it cannot be used for sharing tab count.
This is the improved solution using js-cookie library.
if (+Cookies.get('tabs') > 0)
alert('Already open!');
else
Cookies.set('tabs', 0);
Cookies.set('tabs', +Cookies.get('tabs') + 1);
window.onunload = function () {
Cookies.set('tabs', +Cookies.get('tabs') - 1);
};

This answer: https://stackoverflow.com/a/28230846 is an alternative that doesn't require Cookies/js-cookie library. It better suited my needs. In a nutshell (see linked answer for full description):
$(window).on('storage', message_receive);
...
// use local storage for messaging. Set message in local storage and clear it right away
// This is a safe way how to communicate with other tabs while not leaving any traces
//
function message_broadcast(message)
{
localStorage.setItem('message',JSON.stringify(message));
localStorage.removeItem('message');
}
// receive message
//
function message_receive(ev)
{
if (ev.originalEvent.key!='message') return; // ignore other keys
var message=JSON.parse(ev.originalEvent.newValue);
if (!message) return; // ignore empty msg or msg reset
// here you act on messages.
// you can send objects like { 'command': 'doit', 'data': 'abcd' }
if (message.command == 'doit') alert(message.data);
// etc.
}

Just going to throw this up here, because I wish I had something like it. Make what you will of it.
If you want a solution for checking if you are the active tab that doesn't require a cookie, works as a React hook, and works whether or not the browser crashes, you can use this useIsActiveTab webhook which returns true if you are the most recent active tab/window. You can also set yourself as the active tab with activateTab.
import { useEffect, useState } from 'react';
const CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const CHARACTERS_LENGTH = CHARACTERS.length;
function generateTabId() {
let result = '';
const prefix = 'TAB_';
const length = 15;
for (let i = 0; i < length - prefix.length; i++) {
result += CHARACTERS.charAt(Math.floor(Math.random() * CHARACTERS_LENGTH));
}
if (prefix.includes('_')) {
return `${prefix}${result}`;
}
return `${prefix}_${result}`;
};
const tabId = generateTabId();
export function activateTab(): void {
localStorage.setItem('activeTab', tabId);
const event = new Event('thisStorage');
window.dispatchEvent(event);
}
export function useIsActiveTab(): boolean {
const [isActiveTab, setIsActiveTab] = useState(false);
useEffect(() => {
setActiveTab();
function updateIsActiveTab() {
setIsActiveTab(checkIfActiveTab());
}
window.addEventListener('storage', updateIsActiveTab);
window.addEventListener('thisStorage', updateIsActiveTab);
updateIsActiveTab();
return () => {
window.removeEventListener('storage', updateIsActiveTab);
window.removeEventListener('thisStorage', updateIsActiveTab);
};
}, []);
return isActiveTab;
}
function checkIfActiveTab(): boolean {
const activeTab = localStorage.getItem('activeTab');
if (!activeTab) {
console.error('localStorage.activeTab is not set');
return true;
}
if (activeTab === tabId) {
return true;
}
return false;
}
function setActiveTab(): void {
localStorage.setItem('activeTab', tabId);
}

Related

How to change value of global var when it is being used as a parameter in a function? (Javascript)

I want to be able to change the value of a global variable when it is being used by a function as a parameter.
My javascript:
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
playAudio(audioFilePitch01, canPlayPitch01);
});
My HTML:
<body>
<button id="btnPitch01">Play Pitch01</button>
<button id="btnPitch02">Play Pitch02</button>
<script src="js/js-master.js"></script>
</body>
My scenario:
I'm building a Musical Aptitude Test for personal use that won't be hosted online. There are going to be hundreds of buttons each corresponding to their own audio files. Each audio file may only be played twice and no more than that. Buttons may not be pressed while their corresponding audio files are already playing.
All of that was working completely fine, until I optimised the function to use parameters. I know this would be good to avoid copy-pasting the same function hundreds of times, but it has broken the solution I used to prevent the audio from being played more than once. The "canPlayPitch01" variable, when it is being used as a parameter, no longer gets incremented, and therefore makes the [if (canPlay < 2)] useless.
How would I go about solving this? Even if it is bad coding practise, I would prefer to keep using the method I'm currently using, because I think it is a very logical one.
I'm a beginner and know very little, so please forgive any mistakes or poor coding practises. I welcome corrections and tips.
Thank you very much!
It's not possible, since variables are passed by value, not by reference. You should return the new value, and the caller should assign it to the variable.
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
return canPlay;
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
canPlayPitch01 = playAudio(audioFilePitch01, canPlayPitch01);
});
A little improvement of the data will fix the stated problem and probably have quite a few side benefits elsewhere in the code.
Your data looks like this:
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
// and, judging by the naming used, there's probably more like this:
const btnPitch02 = document.getElementById("btnPitch02");
const audioFilePitch02 = new Audio("../aud/Pitch02.wav");
var canPlayPitch02 = 0;
// and so on
Now consider that global data looking like this:
const model = {
btnPitch01: {
canPlay: 0,
el: document.getElementById("btnPitch01"),
audioFile: new Audio("../aud/Pitch01.wav")
},
btnPitch02: { /* and so on */ }
}
Your event listener(s) can say:
btnPitch01.addEventListener("click", function(event) {
// notice how (if this is all that's done here) we can shrink this even further later
playAudio(event);
});
And your playAudio function can have a side-effect on the data:
function playAudio(event) {
// here's how we get from the button to the model item
const item = model[event.target.id];
if (item.canPlay < 2 && item.audioFile.paused) {
item.canPlay++;
item.audioFile.play();
} else {
if (item.canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
Side note: the model can probably be built in code...
// you can automate this even more using String padStart() on 1,2,3...
const baseIds = [ '01', '02', ... ];
const model = Object.fromEntries(
baseIds.map(baseId => {
const id = `btnPitch${baseId}`;
const value = {
canPlay: 0,
el: document.getElementById(id),
audioFile: new Audio(`../aud/Pitch${baseId}.wav`)
}
return [id, value];
})
);
// you can build the event listeners in a loop, too
// (or in the loop above)
Object.values(model).forEach(value => {
value.el.addEventListener("click", playAudio)
})
below is an example of the function.
btnPitch01.addEventListener("click", function() {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio(audioFilePitch01, canPlayPitch01);
this.dataset.numberOfPlays++;
});
you would want to select all of your buttons and assign this to them after your html is loaded.
https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName
const listOfButtons = document.getElementsByClassName('pitchButton');
listOfButtons.forEach( item => {
item.addEventListener("click", () => {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio("audioFilePitch" + this.id);
this.dataset.numberOfPlays++;
});

setTimeout not working on production but it works fine on local enviroment

I had a react-app that is being used as a widget in Iframe. And there I had a report page for which we have an option to print that.
I am using this StackOverflow solution to use the print function. Additionally, I have just added a timeout function to let the page load API data first and then open the print function. This piece of code works very fine when tested in the local environment, that is, the print popup appears after 2.7 seconds (desired behavior) but in production, the print popup opens instantly (undesired behavior).
const onClickOfPrint = () => {
var title = questionnaireQuestionsSuccessData.name;
try {
// same-origin frame
document.title = window.parent.document.title = title; // to check if same origin
// handlePrint();
} catch (err) {
// cross-origin frame
var p = window.open(reportPageUrl);
p.onload = function () {
p.document.title = title;
const closePopup = () => {
p.close();
};
if ('onafterprint' in p) {
// FF and IE
p.onafterprint = closePopup;
} else {
// webkit don't support onafterprint
var mediaQueryList = p.matchMedia('print');
mediaQueryList.addEventListener('change', mqlListener);
const mqlListener = (mql) => {
if (!mql.matches) {
closePopup();
mediaQueryList.removeEventListener('change', mqlListener);
}
};
}
};
setTimeout(() => {
p.print();
}, 2700);
}
};

Given a list of links, how do I click and validate each one using C# or Protractor, with Selenium?

Code below is in C#, but I also know javascript/protractor. Looking for any pattern that works.
var links = driver.FindElements(By.TagName("a"));
foreach (var ele in links)
{
if (ele.Displayed == false) continue;
if (ele.Enabled) ele.Click();
System.Threading.Thread.Sleep(3000);
driver.Navigate().Back();
System.Threading.Thread.Sleep(3000);
}
Without the sleep above (which I don't like) the page hasn't settled down enough to Navigate Back. With the sleep values in, I can click the link, and go back but only one time! The error on 2nd iteration tells me that the page is stale.
Question: Using Selenium with C# or Protractor how do I go through entire list of links?
If these links are regular links with href attributes, you can use map() to get the array of hrefs, and navigate to each of them one by one. protractor-specific solution:
element.all(by.tagName("a")).map(function (a) {
return a.getAttribute("href");
}).then(function (links) {
for (i = 0; i < links.length; i++) {
browser.get(links[i]);
// TODO: some logic here
}
});
This solution below works for C# without the MAP option pointed out above. Design was to first find the links and put each element's location and text values into a list named "Locators". Then for each tuple in that "Locators" list, pull a fresh copy each time before the click and page back methods.
var links = driver.FindElements(By.TagName("a"));
var Locators = new List<Tuple<Point, string>>();
foreach (var thing in links)
{
var tup = new Tuple<Point, string>(thing.Location, thing.Text);
Locators.Add(tup);
}
foreach (var thing in Locators)
{
var pt = thing.Item1;
var reassess = driver.FindElements(By.TagName("a"));
var filtered = reassess.ToList<IWebElement>().Where(
p =>
p.Location == thing.Item1 &&
p.Text == thing.Item2 &&
p.Displayed == true
);
// Debugger.Break();
if (filtered.Count() == 0) continue;
filtered.First().Click();
driver.WaitForPageToLoad();
AssessNewPageContent();
driver.Navigate().Back();
driver.WaitForPageToLoad();
}
The AssessNewPageContent does assertions and could be a callback if you prefer that.
The code for WaitForPageToLoad was lifted from the internet somewhere and looks like this:
public static IWebDriver WaitForPageToLoad(this IWebDriver driver)
{
TimeSpan timeout = new TimeSpan(0, 0, 30);
WebDriverWait wait = new WebDriverWait(driver, timeout);
IJavaScriptExecutor javascript = driver as IJavaScriptExecutor;
if (javascript == null)
throw new ArgumentException("driver", "Driver must support javascript execution");
wait.Until((d) =>
{
try
{
string readyState = javascript.ExecuteScript("if (document.readyState) return document.readyState;").ToString();
return readyState.ToLower() == "complete";
}
catch (InvalidOperationException e)
{
//Window is no longer available
return e.Message.ToLower().Contains("unable to get browser");
}
catch (WebDriverException e)
{
//Browser is no longer available
return e.Message.ToLower().Contains("unable to connect");
}
catch (Exception)
{
return false;
}
});
return driver;
}

Routing knockout.js app with Sammy.js and history with html4 support

I just started playing around with sammy.js and first thing I want to do is to test how history changes works. And it works as expected, even better, but once I open IE10 and switched to IE9 browser mode, everything felled apart. If I'm not setting the links with hash, IE9 just keeps following the links. Same problem with IE8 of course.
At this moment I only have this bit of code related with sammy
App.sm = $.sammy('#content', function() {
this.get('/', function(context) {
console.log('Yo yo yo')
});
this.get('/landing', function(context) {
console.log('landing page')
});
this.get('/:user', function(context) {
console.log(context)
});
});
And initiator
$(function() {
App.sm.run('/');
});
I also looked at this example which contains three types of the links, normal ones, hash and again normal ones, but working properly on IE9 and IE8. that makes me think that somehow it should be possible to make sammy.js support html5 history and html4 at the same time.
So my question would be, how I can do achieve that?
Update
I found the way to make it work on IE
I just added this snippet:
this.bind('run', function(e) {
var ctx = this;
$('body').on('click', 'a', function(e) {
e.preventDefault();
ctx.redirect($(e.target).attr('href'));
return false;
});
});
Anyway, I'm still having a problem with entry to the website, html5 history supporting browsers is always redirected to the domain.com, no matter what was initial url.
So I wonder how I should configure sammy.js to work peroperly. Or maybe anyone could recommend
some other router which would work nicely with knockout.js.
For a number of reasons, including search engine spiders and link sharing; your site should work without the History API. If a user sees http://example.org/poodles/red and wants to show someone else the red poodles on your web site, they copy the link. The other visitor needs to be able to see the same content at the same URL; even if they don't start at the homepage.
For this reason, I suggest using the History API as a progressive enhancement. Where it's available, you should use it to provide a better UX. Where it's not available, links should function as normal.
Here's an example Router (like Sammy) which simply allows the default browser navigation if history.pushState isn't available.
And about the Knockout part; I have used this in a KnockoutJS project and it works well.
(function($){
function Route(path, callback) {
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
// replace "/:something" with a regular expression fragment
var expression = escapeRegExp(path).replace(/\/:(\w+)+/g, "/(\\w+)*");
this.regex = new RegExp(expression);
this.callback = callback;
}
Route.prototype.test = function (path) {
this.regex.lastIndex = 0;
var match = this.regex.exec(path);
if (match !== null && match[0].length === path.length) {
// call it, passing any matching groups
this.callback.apply(this, match.slice(1));
return false;
}
};
function Router(paths) {
var self = this;
self.routes = [];
$.each(paths, function (path, callback) {
self.routes.push(new Route(path, callback));
});
self.listen();
self.doCallbacks(location.pathname);
}
Router.prototype.listen = function () {
var self = this, $document = $(document);
// watch for clicks on links
// does AJAX when ctrl is not down
// nor the href ends in .html
// nor the href is blank
// nor the href is /
$document.ready(function(e){
$document.on("click", "[href]", function(e){
var href = this.getAttribute("href");
if ( !e.ctrlKey && (href.indexOf(".html") !== href.length - 5) && (href.indexOf(".zip") !== href.length - 4) && href.length > 0 && href !== "/") {
e.preventDefault();
self.navigate(href);
}
});
});
window.addEventListener("popstate", function(e) {
self.doCallbacks(location.pathname);
});
};
Router.prototype.navigate = function(url) {
if (window.history && window.history.pushState) {
history.pushState(null, null, url);
this.doCallbacks(location.pathname);
}
};
Router.prototype.doCallbacks = function(url) {
var routes = this.routes;
for (var i=0; i<routes.length; i++){
var route = routes[i];
// it returns false when there's a match
if (route.test(url) === false) {
console.log("nav matched " + route.regex);
return;
}
}
if (typeof this.fourOhFour === "function") {
this.fourOhFour(url);
} else {
console.log("404 at ", url);
}
};
window.Router = Router;
}).call(this, jQuery);
Example usage:
router = new Router({
"/": function () {
},
"/category/:which": function (category) {
},
"/search/:query": function(query) {
},
"/search/:category/:query": function(category, query) {
},
"/:foo/:bar": function(foo, bar) {
}
});
router.fourOhFour = function(requestURL){
};

Chrome extension, using localStorage to save ip, tabid, serverfingerprint per tab

I should mention up front I'm new to code/stackoverflow so my apologies if this question doesn't makes sense. I'm beyond stumped, I'm trying to build a chrome extension that saves the ip address, url and a server finger print. The serverfingerprint is a field that lives within the response headers. Using my background.js and localStorage I can save this information and then display it in my pop up window. This is all fine and dandy except I can't figure out how to save it on a per tab basis, aka... if I have 5 tabs open, I'd like to click my extension and have it display the url for each corresponding tab. example: click tab4 and shows tab4's url, then click tab2 and it shows the url of tab2.
the below code works except for it doesn't tie to the tabId so it's not exactly ideal. Any ideas of where to start researching would be very appreciated!
what i've done thus far:
background.js:
chrome.experimental.webRequest.onCompleted.addListener(function (details)
{
var headers = details.responseHeaders;
localStorage['ip'] = details.ip;
localStorage['url'] = details.url;
for (var i = 0, length = headers.length; i < length; i++)
{
var header = headers[i];
if (header.name == 'X-Server-Fingerprint')
{
localStorage['XServerFingerprint'] = header.value.toString();
break;
}
}
},{'urls': ['http://www.someurl.com/*']},['responseHeaders']);
popup.js:
document.getElementById('url').innerText = localStorage['url'];
document.getElementById('ip').innerText = localStorage['ip'];
document.getElementById('XServerFingerPrint').innerText = localStorage['XServerFingerPrint'];
As each tab has unique id (until browser restart), you can use it to identify tabs.
You are probably interested only in current tabs, which makes things simpler as you don't need localStorage for this (which persists data between browser restarts). Just use background page's namespace to store data about all tabs:
// background.js
var tabs = {}; //all tab data
chrome.experimental.webRequest.onCompleted.addListener(function (details)
{
var tabInfo = {};
tabInfo["ip"] = ...;
tabInfo["url"] = ...;
tabInfo["XServerFingerprint"] = ...;
tabs[details.tabId] = tabInfo;
}
// popup.js
chrome.tabs.getSelected(null, function(tab){
var tabInfo = chrome.extension.getBackgroundPage().tabs[tab.id]; // get from bg page
document.getElementById('url').innerText = tabInfo['url'];
document.getElementById('ip').innerText = tabInfo['ip'];
document.getElementById('XServerFingerPrint').innerText = tabInfo['XServerFingerPrint'];
});
If you do need localStorage then you can convert tabs object to json string and store it there.
Ok, so I've sorted out my issues! Well the ones concerning chrome extensions haha, which appears to be pretty much exactly what Serg is saying (thx Serg!!) I wrote it a bit different tho.
// background.js
chrome.experimental.webRequest.onCompleted.addListener(function (details)
{
var headers = details.responseHeaders;
var tabId = details.tabId;
var ip = details.ip;
var url = details.url;
for (var i = 0, length = headers.length; i < length; i++) {
var header = headers[i];
//custom field in response headers from my site
if (header.name == 'X-Server-Fingerprint') {
var XServerFingerprint = header.value.toString();
var data = {
ip: ip,
url: url,
fingerprint: XServerFingerprint
}
//store it
localStorage[tabId] = JSON.stringify(data);
break;
}
}
},{'urls': ['http://www.corbisimages.com/*']},['responseHeaders']);
}
// and then on my popup.js
chrome.tabs.getSelected(null, function(tab) {
var parseData = JSON.parse(localStorage[tab.id]);
document.getElementById('XServerFingerprint').innerText = parseData.fingerprint;
document.getElementById('url').innerText = parseData.url;
document.getElementById('ip').innerText = parseData.ip;
});

Categories