Firefox Extension location.href not redirecting - javascript

I'm using something rather generic here, but essentially I want to be able to load a new tab at my desired URL when selecting my extension, then when at that tab, redirect to a new URL. (The add on should run some code at the first page before redirecting, but that's for another day).
The code I have at the moment is
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
var button = buttons.ActionButton({
id: "redirect",
label: "redirect",
icon: {
"16": "./icon-16.png"
},
onClick: handleClick
});
function handleClick(state) {
tabs.open({
url: "http://www.google.com",
onReady: loadRedirect
});
function loadRedirect(tab) {
tab.attach({
contentScript: "location.href = 'www.youtube.com;'"
});
}
}
When running this however, the 2nd URL appends to the first, rather than replaces, and then gets stuck in an infinite load/refresh loop until I close the browser.
I assume I'm missing something absolutely obvious, but I wasn't able to find anything while searching around.

You are trying to change the location.href to a string that has no scheme. It is assumed that it is a URL within the current domain. Because it also does not start with a / it is assumed to be relative to the current page. Thus, it is appended to the current google.com URL. The page becomes ready; fires the ready event; and your onReady handler is called. Your handler then changes the URL, causing the page to be reloaded, and, again, fire the ready event, which starts the process over again. This is your infinite loop.
To actually get to www.youtube.com, you could change your code to:
function loadRedirect(tab) {
tab.attach({
contentScript: "location.href = 'https://www.youtube.com';"
});
}
Which could have been done without the need to inject a content script by assigning to tab.url:
function loadRedirect(tab) {
tab.url = 'https://www.youtube.com';
}
However, that will not prevent the infinite loop. There are many ways to do so. The easiest is to just remove the ready listener:
function loadRedirect(tab) {
tab.off('ready',loadRedirect);
tab.url = 'https://www.youtube.com';
}

Related

Open Bootstrap tab programmatically with turbolinks

I'm tryng to open a Bootstrap 4 tab after page load. It does work if I refresh the page, but if I navigate within the site, I get a query selector empty error.
This is my code, that's basically a port I did of this https://webdesign.tutsplus.com/tutorials/how-to-add-deep-linking-to-the-bootstrap-4-tabs-component--cms-31180 because I'm using Bootstrap-native, so I had to rewrite it in vanilla Javascript.
document.addEventListener("turbolinks:load", function() {
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
document.querySelector('#nav-tab a[href="#'+hash[1]+'"]').Tab.show();
url = location.href.replace(/\/#/, "#");
history.replaceState(null, null, url);
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
});
I placed it just before the closing body tag. If I write http://www.myURL#mytab and click refresh, the page refresh and the tab is changed, but if I get there from a (turbo)link, it does not find the tab to query select. I'm afraid the problem is with the "load" event, I tried different methods but I can't get it to work.
If you are including this code in a <script> at the end of the <body>, you probably don't need to listen for the turbolinks:load event. Why? On the first load, the browser will be able to query any elements positioned before the script element. On Turbolinks loads scripts in the <body> will have access to all rendered elements on the page.
It's worth adding that by calling document.addEventListener("turbolinks:load", …) in a body <script> element, the listener will be called on every subsequent page load, not just on the page where the script is rendered. If the
#nav-tab elements don't exist on a subsequent page load, then you'll see the querySelector error. What's more, if you include the script on more than one page, then the listener will be duplicated again and again and again, which is probably not what you want!
So the first step to fix your issue is to remove the event listener. We'll wrap your code in an immediately invoked function to prevent polluting the global scope:
;(function() {
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
document.querySelector('#nav-tab a[href="#'+hash[1]+'"]').Tab.show();
url = location.href.replace(/\/#/, "#");
history.replaceState(null, null, url);
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
})();
The next thing to know is that Turbolinks manages its own cache of visited pages, so that when a user taps "Back", a page is rendered from this cache. To do this, it has a system for adding to the browser's own history stack. If you bypass the Turbolinks system, and call history.replaceState (or history.pushState) yourself, then you could end up breaking the "Back" navigations. Turbolinks doesn't have a documented way to manually add to its history stack, but you could try the following:
;(function() {
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
document.querySelector('#nav-tab a[href="#'+hash[1]+'"]').Tab.show();
url = location.href.replace(/\/#/, "#");
Turbolinks
.controller
.replaceHistoryWithLocationAndRestorationIdentifier(url, Turbolinks.uuid())
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
})();
Note, this is undocumented so may not be publically available in future versions.
Finally, it might be worth considering including this snippet in your main application JavaScript bundle, and load it in the <head> rather than in the body. In this case you would need to use a `turbolinks:load handler. It might look something like:
document.addEventListener('turbolinks:load', function () {
let url = location.href.replace(/\/$/, "");
const hash = url.split("#");
const navLink = document.querySelector('#nav-tab a[href="#'+hash[1]+'"]')
if (location.hash && navLink) {
navLink.Tab.show();
url = location.href.replace(/\/#/, "#");
Turbolinks
.controller
.replaceHistoryWithLocationAndRestorationIdentifier(url, Turbolinks.uuid())
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
});

How can I prevent multiple injection/execution of content script(s) in response to a single event?

I am building a chrome extension that responds to click events on a context menu.
My background script creates the context menu using the chrome.contextMenus.create api method call and sets a click handler as shown in the code below:
//event.js
function onItemClick(info, tab){
// Inject the content script into the current page
chrome.tabs.executeScript(null, { file: 'content.js' });
// Perform the callback when a message is received from the content script
chrome.runtime.onMessage.addListener(function(message){
var url = "data:text/html;charset=utf8,";
function append(key, value){
var input = document.createElement('textarea');
input.setAttribute('name', key);
input.textContent = value;
form.appendChild(input);
}
var form = document.createElement('form');
form.method = 'POST';
form.action = 'http://localhost/myapp/myapp.php';
form.style.visibility = "hidden";
append('url', message.url);
append('text', message.selectedText);
url = url + encodeURIComponent(form.outerHTML);
url = url + encodeURIComponent('<script>document.forms[0].submit();</script>');
chrome.tabs.create({url: url, active: true});
});
}
var context = "selection";
var title = "Share in new tab";
var id = chrome.contextMenus.create({"title": title, "contexts": [context], "onclick": onItemClick});
The background script above programmatically creates a form that automatically gets submitted in a new tab. In doing so, it calls a "content script" below to get some information from the current page/tab.
//content.js
chrome.runtime.sendMessage({
'url': window.location.href,
'selectedText': window.getSelection().toString()
});
The problem is this. The click handler in the background script injects the "content script" into the current page multiple times (that is, every time the click handler is called). As a result of this multiple injection, each injected instance of the "content script" is executed resulting in multiple new tabs/pages being opened. The number of new tabs opened increases by one each time the context menu item is clicked suggesting the problem is indeed multiple injection and execution of the content script. How can I inject the content script only once, or at least ensure that only one "instance" of the injected scripts sends a message back to my background script?
I have tried to automatically inject the script in the manifest, but calling chrome.tabs.executeScript thereafter results in an endless creation of tabs. So, I really need to be able to inject the script on demand, but find a way to either prevent multiple injections or at least ensure only one "injection" sends a message back. Please help!
The solution can be achieved easily: you can create a global control variable in the content script. Check for it at the beginning, and if it is undefined then set it to true and proceed. The first content script that gets executed will set the variable and prevent others from doing anything.
By the way, I see you're adding a listener to chrome.runtime.onMessage inside another listener: that is not good practice, because it will add multiple listeners for the same event and result in executing them multiple times. You should instead declare the listener outside, sending a different message saying "do something" or "do something else".
In the content script:
if (window.messageSent === undefined) {
window.messageSent = true;
chrome.runtime.sendMessage({
action: "submit the form",
url: window.location.href,
selectedText: window.getSelection().toString()
});
}
In the background.js:
chrome.runtime.onMessage.addListener(function(message){
if (message.action == "submit the form") {
// do what you need to submit the form
var url = "data:text/html;charset=utf8,";
function append(key, value){
...
}
});

Ajax with history.pushState and popstate - what do I do when popstate state property is null?

I'm trying out the HTML5 history API with ajax loading of content.
I've got a bunch of test pages connected by relative links. I have this JS, which handles clicks on those links. When a link is clicked the handler grabs its href attribute and passes it to ajaxLoadPage(), which loads content from the requested page into the content area of the current page. (My PHP pages are set up to return a full HTML page if you request them normally, but only a chunk of content if ?fragment=true is appended to the URL of the request.)
Then my click handler calls history.pushState() to display the URL in the address bar and add it to the browser history.
$(document).ready(function(){
var content = $('#content');
var ajaxLoadPage = function (url) {
console.log('Loading ' + url + ' fragment');
content.load(url + '?fragment=true');
}
// Handle click event of all links with href not starting with http, https or #
$('a').not('[href^=http], [href^=https], [href^=#]').on('click', function(e){
e.preventDefault();
var href = $(this).attr('href');
ajaxLoadPage(href);
history.pushState({page:href}, null, href);
});
// This mostly works - only problem is when popstate happens and state is null
// e.g. when we try to go back to the initial page we loaded normally
$(window).bind('popstate', function(event){
console.log('Popstate');
var state = event.originalEvent.state;
console.log(state);
if (state !== null) {
if (state.page !== undefined) {
ajaxLoadPage(state.page);
}
}
});
});
When you add URLs to the history with pushState you also need to include an event handler for the popstate event to deal with clicks on the back or forward buttons. (If you don't do this, clicking back shows the URL you pushed to history in the address bar, but the page isn't updated.) So my popstate handler grabs the URL saved in the state property of each entry I created, and passes it to ajaxLoadPage to load the appropriate content.
This works OK for pages my click handler added to the history. But what happens with pages the browser added to history when I requested them "normally"? Say I land on my first page normally and then navigate through my site with clicks that do that ajax loading - if I then try to go back through the history to that first page, the last click shows the URL for the first page, but doesn't load the page in the browser. Why is that?
I can sort of see this has something to do with the state property of that last popstate event. The state property is null for that event, because it's only entries added to the history by pushState() or replaceState() that can give it a value. But my first loading of the page was a "normal" request - how come the browser doesn't just step back and load the initial URL normally?
This is an older question but there is a much simpler answer using native javascript for this issue.
For the initial state you should not be using history.pushState but rather history.replaceState.
All arguments are the same for both methods with the only difference is that pushState creates a NEW history record and thus is the source of your problem. replaceState only replaces the state of that history record and will behave as expected, that is go back to the initial starting page.
I ran into the same issue as the original question. This line
var initialPop = !popped && location.href == initialURL;
should be changed to
var initialPop = !popped;
This is sufficient to catch the initial pop. Then you do not need to add the original page to the pushState. i.e. remove the following:
var home = 'index.html';
history.pushState({page:home}, null, home);
The final code based on AJAX tabs (and using Mootools):
if ( this.supports_history_api() ) {
var popped = ('state' in window.history && window.history.state !== null)
, changeTabBack = false;
window.addEvent('myShowTabEvent', function ( url ) {
if ( url && !changingTabBack )
setLocation(url);
else
changingTabBack = false;
//Make sure you do not add to the pushState after clicking the back button
});
window.addEventListener("popstate", function(e) {
var initialPop = !popped;
popped = true;
if ( initialPop )
return;
var tabLink = $$('a[href="' + location.pathname + '"][data-toggle*=tab]')[0];
if ( tabLink ) {
changingTabBack = true;
tabLink.tab('show');
}
});
}
I still don't understand why the back button behaves like this - I'd have thought the browser would be happy to step back to an entry that was created by a normal request. Maybe when you insert other entries with pushState the history stops behaving in the normal way. But I found a way to make my code work better. You can't always depend on the state property containing the URL you want to step back to. But stepping back through history changes the URL in the address bar as you would expect, so it may be more reliable to load your content based on window.location. Following this great example I've changed my popstate handler so it loads content based on the URL in the address bar instead of looking for a URL in the state property.
One thing you have to watch out for is that some browsers (like Chrome) fire a popstate event when you initially hit a page. When this happens you're liable to reload your initial page's content unnecessarily. So I've added some bits of code from the excellent pjax to ignore that initial pop.
$(document).ready(function(){
// Used to detect initial (useless) popstate.
// If history.state exists, pushState() has created the current entry so we can
// assume browser isn't going to fire initial popstate
var popped = ('state' in window.history && window.history.state !== null), initialURL = location.href;
var content = $('#content');
var ajaxLoadPage = function (url) {
console.log('Loading ' + url + ' fragment');
content.load(url + '?fragment=true');
}
// Handle click event of all links with href not starting with http, https or #
$('a').not('[href^=http], [href^=https], [href^=#]').on('click', function(e){
e.preventDefault();
var href = $(this).attr('href');
ajaxLoadPage(href);
history.pushState({page:href}, null, href);
});
$(window).bind('popstate', function(event){
// Ignore inital popstate that some browsers fire on page load
var initialPop = !popped && location.href == initialURL;
popped = true;
if (initialPop) return;
console.log('Popstate');
// By the time popstate has fired, location.pathname has been changed
ajaxLoadPage(location.pathname);
});
});
One improvement you could make to this JS is only to attach the click event handler if the browser supports the history API.
I actually found myself with a similar need today and found the code you provided to be very useful. I came to the same problem you did, and I believe all that you're missing is pushing your index file or home page to the history in the same manner that you are all subsequent pages.
Here is an example of what I did to resolve this (not sure if it's the RIGHT answer, but it's simple and it works!):
var home = 'index.html';
history.pushState({page:home}, null, home);
Hope this helps!
I realize this is an old question, but when trying to manage state easily like this, it might be better to take the following approach:
$(window).on('popstate',function(e){
var state = e.originalEvent.state;
if(state != null){
if(state.hasOwnProperty('window')){
//callback on window
window[state.window].call(window,state);
}
}
});
in this way, you can specify an optional callback function on the state object when adding to history, then when popstate is trigger, this function would be called with the state object as a parameter.
function pushState(title,url,callback)
{
var state = {
Url : url,
Title : title,
};
if(window[callback] && typeof window[callback] === 'function')
{
state.callback = callback;
}
history.pushState(state,state.Title,state.Url);
}
You could easily extend this to suit your needs.
And Finally says:
I'd have thought the browser would be happy to step back to an entry that was created by a normal request.
I found an explanation of that strange browser's behavior here. The explanation is
you should save the state when your site is loaded the first time and thereafter every time it changes state
I tested this - it works.
It means there is no need in loading your content based on window.location.
I hope I don't mislead.

Accessing every document that a user currently views from an extension

I'm writing an extension that checks every document a user views on certain data structures, does some back-end server calls and displays the results as a dialog.The problem is starting and continuing the sequence properly with event listeners. My actual idea is:
Load: function()
{
var Listener = function(){ Fabogore.Start();};
var ListenerTab = window.gBrowser.selectedTab;
ListenerTab.addEventListener("load",Listener,true);
}
(...)
ListenerTab.removeEventListener("load", Listener, true);
Fabogore.Load();
The Fabogore.Load function is first initialized when the browser gets opened. It works only once I get these data structures, but not afterwards. But theoretically the script should initialize a new listener, so maybe it's the selectedTab. I also tried listening to focus events.
If someone has got an alternative solution how to access a page a user is currently viewing I would feel comfortable as well.
The common approach is using a progress listener. If I understand correctly, you want to get a notification whenever a browser tab finished loading. So the important method in your progress listener would be onStateChange (it needs to have all the other methods as well however):
onStateChange: function(aWebProgress, aRequest, aFlag, aStatus)
{
if ((aFlag & Components.interfaces.nsIWebProgressListener.STATE_STOP) &&
(aFlag & Components.interfaces.nsIWebProgressListener.STATE_IS_WINDOW) &&
aWebProgress.DOMWindow == aWebProgress.DOMWindow.top)
{
// A window finished loading and it is the top-level frame in its tab
Fabogore.Start(aWebProgress.DOMWindow);
}
},
Ok, I found a way which works from the MDN documentation, and achieves that every document a user opens can be accessed by your extension. Accessing every document a user focuses is too much, I want the code to be executed only once. So I start with initializing the Exentsion, and Listen to DOMcontentloaded Event
window.addEventListener("load", function() { Fabogore.init(); }, false);
var Fabogore = {
init: function() {
var appcontent = document.getElementById("appcontent"); // browser
if(appcontent)
appcontent.addEventListener("DOMContentLoaded", Fabogore.onPageLoad, true);
},
This executes the code every Time a page is loaded. Now what's important is, that you execute your code with the new loaded page, and not with the old one. You can acces this one with the variable aEvent:
onPageLoad: function(aEvent)
{
var doc = aEvent.originalTarget;//Document which initiated the event
With the variable "doc" you can check data structures using XPCNativeWrapper etc. Thanks Wladimir for bringing me in the right direction, I suppose if you need a more sophisticated event listening choose his way with the progress listeners.

Display Webpage current URL with Firefox extension

I've written the following code for the purpose of the title of the post but instead of having the real URL I get the previous URL (e.g. If I'm on Google and type "car" in the search field and type "Enter" I get "http://www.google.fr" and not the URL from the search).
code :
window.addEventListener("change", function() { myExtension_with_change.init(); }, false);
var myExtension_with_change = {
init: function() {
var url = window.location.href;
alert(url);
}
}
You might need to add an event listener inside the first to wait for the window to load, such as:
window.addEventListener("change", function()
{
window.addEventListener("load", function()
{
myExtension_with_change.init();
}, false);
}, false);
I doubt that window is the correct anchor to listen for changes of the URL. My first try would be listen to change events at #urlbar (I didn't try that, though):
window.getElementById('#urlbar').addEventListener("change", function() {
myExtension_with_change.init(); }, false);
If your ultimate goal is to listen to URL changes on every tab I suggest you also have look at the Tabbed Browser documentation and this code snippet on location changes.
In another post https://stackoverflow.com/users/785541/wladimir-palant gave a perfect answer ( Get URL of a webpage dynamically with Javascript on a Firefox extension ) .
In my case, I followed the recommendation in http://forums.mozillazine.org/viewtopic.php?f=9&t=194671. Simply calling the following code snippet gives me the current url
gBrowser.mCurrentBrowser.currentURI.QueryInterface(Components.interfaces.nsIURI);
var currentUrl = gBrowser.mCurrentBrowser.currentURI.spec;

Categories

Resources