How do I intercept pjax page changes from a chrome userscript? - events

Can I detect history.pushState url changes from a chrome user script (not extension), without polling (or breaking out of the script's isolated world to intercept the call in the web page's javascript context)?
My problem, in more detail:
As Github uses history.pushState [WHAT-WG specs] by way of pjax [minimized example] for in-site page-to-page site navigation, instead of reloading the whole page as you browse the site, user scripts targeting a specific url on the site will only fire when you enter the site at that url or manually forcefully reload the page.
I am writing a little user script library on.js for DRY:ing up user scripts, handling that kind of thing for them by intercepting pushState calls to see when we're on a new page the host script wants to run on and only invoking it where relevant.
This would be relatively easy if history.pushState fired events when called, but as best I can tell, no popstate event gets fired except when the first page loads or the user triggers back / forward movement through history, which is fairly useless for my purposes.
I explored limiting it to a pjax/github-only solution too – when a new page loads, github fires a $(document).trigger('pageUpdate') event, but as jQuery implements these internally rather than via DOM events, I can't hear those from the isolated world my user script runs in, unless I inject a little listener in the page itself to have its jQuery instance report this event back to me, and then I might as well hijack the page's history.pushState itself.
Does anyone see other tidy ways of achieving this?

For now, I am taking the not excellent but working latter approach, of injecting a script that intercepts history.pushState calls and fires an event of the same name on the document, once the call has finished (and, optionally, after the pjax pageUpdate event has fired too):
function inject(fn, args) {
var script = document.createElement('script')
, parent = document.documentElement;
args = JSON.stringify(args || []).slice(1, -1);
script.textContent = '('+ fn +')('+ args +');';
parent.appendChild(script);
parent.removeChild(script);
}
inject(function(pjax_event) {
function reportBack() {
var e = document.createEvent('Events');
e.initEvent('history.pushState', !'bubbles', !'cancelable');
document.dispatchEvent(e);
}
var pushState = history.pushState;
history.pushState = function on_pushState() {
if (pjax_event && window.$ && $.pjax)
$(document).one(pjax_event, reportBack);
else
setTimeout(reportBack, 0);
return pushState.apply(this, arguments);
};
}, ['pageUpdated']);
document.addEventListener('history.pushState', function() {
console.log('history.pushState', location.pathname);
// user script code here
}, false);

Before GreaseMonkey changed it's sandbox policy (read: Firefox did, and GM didn't make it backwards compatible) one could use unsafeWindow to fetch an element/window event. Now that that's changed, unsafeWindow isn't working anymore when used with GreaseMonkey API's.
Your proposed answer of injecting a script into the page doesn't work on Github, because of it's very strict content security policy (CSP) not allowing inline scripts;
Content-Security-Policy:default-src *; script-src assets-cdn.github.com collector-cdn.github.com; [...]
My solution is two-fold for two different situations: one with #grant none and one with an GreaseMonkey API #grant.
test-grant-none.user.js
// ==UserScript==
// #name test #grant none
// #namespace https://github.com/jerone/UserScripts
// #include https://github.com/*
// #version 1
// #grant none
// ==/UserScript==
unsafeWindow.$(document).on("pjax:end", function() {
console.log('test #grant none 1');
});
unsafeWindow.$(window).on('popstate', function() {
console.log('test #grant none 2');
});
test-grant-GM.user.js
// ==UserScript==
// #name test #grant GM_*
// #namespace https://github.com/jerone/UserScripts
// #include https://github.com/*
// #version 1
// #grant GM_setValue
// ==/UserScript==
unsafeWindow.$(document).on("pjax:end", exportFunction(function() {
console.log('test #grant GM_* 1');
}, unsafeWindow));
unsafeWindow.$(window).on('popstate', exportFunction(function() {
console.log('test #grant GM_* 2');
}, unsafeWindow));
You can find above scripts here: https://gist.github.com/jerone/e38e8637887559870d84
You can read here more: https://greasyfork.org/en/forum/discussion/3648/hook-into-jquery-existing-events
Thank to Athorcis for pointing me in the right direction.

Related

logout and click back takes me to authenticated pages in Safari browser [duplicate]

Got an issue with safari loading old youtube videos when back button is clicked. I have tried adding onunload="" (mentioned here Preventing cache on back-button in Safari 5) to the body tag but it doesn't work in this case.
Is there any way to prevent safari loading from cache on a certain page?
Your problem is caused by back-forward cache. It is supposed to save complete state of page when user navigates away. When user navigates back with back button page can be loaded from cache very quickly. This is different from normal cache which only caches HTML code.
When page is loaded for bfcache onload event wont be triggered. Instead you can check the persisted property of the onpageshow event. It is set to false on initial page load. When page is loaded from bfcache it is set to true.
Kludgish solution is to force a reload when page is loaded from bfcache.
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload()
}
};
If you are using jQuery then do:
$(window).bind("pageshow", function(event) {
if (event.originalEvent.persisted) {
window.location.reload()
}
});
All of those answer are a bit of the hack. In modern browsers (safari) only on onpageshow solution work,
window.onpageshow = function (event) {
if (event.persisted) {
window.location.reload();
}
};
but on slow devices sometimes you will see for a split second previous cached view before it will be reloaded. Proper way to deal with this problem is to set properly Cache-Control on the server response to one bellow
'Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store'
Yes the Safari browser does not handle back/foreward button cache the same like Firefox and Chrome does. Specially iframes like vimeo or youtube videos are cached hardly although there is a new iframe.src.
I found three ways to handle this. Choose the best for your case.
Solutions tested on Firefox 53 and Safari 10.1
1. Detect if user is using the back/foreward button, then reload whole page or reload only the cached iframes by replacing the src
if (!!window.performance && window.performance.navigation.type === 2) {
// value 2 means "The page was accessed by navigating into the history"
console.log('Reloading');
//window.location.reload(); // reload whole page
$('iframe').attr('src', function (i, val) { return val; }); // reload only iframes
}
2. reload whole page if page is cached
window.onpageshow = function (event) {
if (event.persisted) {
window.location.reload();
}
};
3. remove the page from history so users can't visit the page again by back/forward buttons
$(function () {
//replace() does not keep the originating page in the session history,
document.location.replace("/Exercises#nocache"); // clear the last entry in the history and redirect to new url
});
You can use an anchor, and watch the value of the document's location href;
Start off with http://acme.co/, append something to the location, like '#b';
So, now your URL is http://acme.co/#b, when a person hits the back button, it goes back to http://acme.co, and the interval check function sees the lack of the hash tag we set, clears the interval, and loads the referring URL with a time-stamp appended to it.
There are some side-effects, but I'll leave you to figure those out ;)
<script>
document.location.hash = "#b";
var referrer = document.referrer;
// setup an interval to watch for the removal of the hash tag
var hashcheck = setInterval(function(){
if(document.location.hash!="#b") {
// clear the interval
clearInterval(hashCheck);
var ticks = new Date().getTime();
// load the referring page with a timestamp at the end to avoid caching
document.location.href.replace(referrer+'?'+ticks);
}
},100);
</script>
This is untested but it should work with minimal tweaking.
The behavior is related to Safari's Back/Forward cache. You can learn about it on the relevant Apple documentation: http://web.archive.org/web/20070612072521/http://developer.apple.com/internet/safari/faq.html#anchor5
Apple's own fix suggestion is to add an empty iframe on your page:
<iframe style="height:0px;width:0px;visibility:hidden" src="about:blank">
this frame prevents back forward cache
</iframe>
(The previous accepted answer seems valid too, just wanted to chip in documentation and another potential fix)
I had the same issue with using 3 different anchor links to the next page. When coming back from the next page and choosing a different anchor the link did not change.
so I had
House 1
View House 2
View House 3
Changed to
House 1
View House 2
View House 3
Also used for safety:
// Javascript
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload()
}
};
// JQuery
$(window).bind("pageshow", function(event) {
if (event.originalEvent.persisted) {
window.location.reload()
}
});
None of the solutions found online to unload, reload and reload(true) singularily didn't work. Hope this helps someone with the same situation.
First of all insert field in your code:
<input id="reloadValue" type="hidden" name="reloadValue" value="" />
then run jQuery:
jQuery(document).ready(function()
{
var d = new Date();
d = d.getTime();
if (jQuery('#reloadValue').val().length == 0)
{
jQuery('#reloadValue').val(d);
jQuery('body').show();
}
else
{
jQuery('#reloadValue').val('');
location.reload();
}
});
There are many ways to disable the bfcache. The easiest one is to set an 'unload' handler. I think it was a huge mistake to make 'unload' and 'beforeunload' handlers disable the bfcache, but that's what they did (if you want to have one of those handlers and still make the bfcache work, you can remove the beforeunload handler inside the beforeunload handler).
window.addEventListener('unload', function() {})
Read more here:
https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/1.5/Using_Firefox_1.5_caching

Tampermonkey script - Refresh needed

I'm trying to create a simple script to autoclick a buy button as soon as the browser goes from the cart page to the checkout page. The script doesn't work unless I manually refresh the checkout page. It appears the site uses AJAX.
I started with this simple script
// ==UserScript==
// #name Auto place order
// #version 0.1
// #description Autoclicks on buy button
// #author You
// #match https://www.mywebsite.com/buy/placeOrder
// #grant none
// ==/UserScript==
(function() {
'use strict';
alert("Matched"); //debug
const buybutton = document.querySelector('#submitOrderButtonId-announce');
if(buybutton) {
buybutton.click()
}
})();
Upon reading other suggestions I tried the below
// ==UserScript==
// #name Auto place order
// #version 0.2
// #description Autoclicks on buy button
// #author You
// #match https://www.mywebsite.com/buy/placeOrder
// #grant none
// #require http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
// #require https://gist.github.com/raw/2625891/waitForKeyElements.js
// ==/UserScript==
waitForKeyElements ("#submitOrderButtonId", () => {
alert("Matched");
document.querySelector('#submitOrderButtonId-announce').click(); });
but it didn't help. Still needs refresh.
If I use the main domain in #match it works but I can't set it like that.
Could anyone please suggest the exact code I should write to avoid refreshing?

pushState change - equivalent to Chrome Extension onHistoryStateUpdated

I'm porting a Chrome extension to a Firefox extension and due to the nature of the website that it runs on, I need to monitor the pushState.
Chrome Extensions has a handy way to handle this: chrome.webNavigation.onHistoryStateUpdated. The way that I use it in the Chrome extension is as follows:
chrome.webNavigation.onHistoryStateUpdated.addListener(function(details) {
var tabUrl = details.url;
if (isTabUrlValid(tabUrl)) {
$.get(tabUrl, function(data) {
var videoUrl = $(data).find('meta[itemprop=contentURL]').prop('content');
videoUrl = validateUrl(videoUrl);
videoUrl5k = make5kUrl(videoUrl);
});
}
});
I need to do the same thing for the Firefox Extension, but I haven't found any good answers. I've tried doing the answer mentioned here: How to get notified about changes of the history via history.pushState?
(function(history) {
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
var tabUrl = tabs.activeTab.url;
console.log("UPDATED TAB URL: " + tabUrl);
if (isTabUrlValid(tabUrl)) {
$.get(tabUrl, function(data) {
var videoUrl = $(data).find('meta[itemprop=contentURL]').prop('content');
videoUrl = validateUrl(videoUrl);
videoUrl5k = make5kUrl(videoUrl);
});
}
return pushState.apply(history, arguments);
};
})(window.history);
The problem is that when I do cfx run it complains that history/window is undefined and therefore never gets detected. I think this is due to it being within the SDK, but I don't know of a good workaround.
Any thoughts?
Edit: I looked at #willma's answer below and I don't think that would work for me. The issue is that the URL is updated via pushState and the DOM is not... Is there any good way replicate what I do in the chrome extension?
Edit: Here's the pageMod portion
pageMod.PageMod({
attachTo: 'top', // Don't attach to iFrames --> http://goo.gl/b6b1Iv
include: [URLs],
contentScriptFile: [data.url("jquery-2.1.1.min.js"),
data.url("csScript.js")],
onAttach: function(worker) {
worker.port.on('url', function(url) {
var videoUrl = validateUrl(url);
videoUrl5k = make5kUrl(videoUrl);
console.log("--5K URL--: " + videoUrl5k);
});
}
});
That history code needs to get injected into a tab using a content script. Right now your logic says when the history event occurs, check to see if the tab URL is valid.
In Firefox, the logic will be the other way around: when a tab is opened, check if its URL is valid, and if so, then attach a script to it that will monitor for the history event. To do so you'll need to use a Page Mod.
Edit: All the code
One key concept you're missing is the difference between a content script and a main/library script. The library scripts are stored in lib and have access to all the SDK modules, but don't have access to the DOM, window object… The content scripts are stored in data, are injected into a page using the PageMod or tabs modules, can access the dom and window objects, but have no access to any SDK modules. Content scripts are essentially like the page scripts you'd attach your standard HTML page (with <script></script>) with the caveats that they can't share variables other page scripts but they can communicate with the main scripts.
The only reason I bring this up is because your initial problem was trying to access the window object from a main script and the problem in your fiddle is that you're trying to access the tabs module inside a content script. It's worth reading the topmost link in this answer if this is still confusing.
main.js
const { PageMod } = require('sdk/page-mod');
var sendXHR = function(url) {
// Do something with the new URL
// See Request Module docs (below) for sending XHRs from main script.
}
const pageMod = PageMod({
attachTo: 'top',
include: '*',
onAttach: function(worker) {
worker.port.on('newURL', sendXHR);
}
});
content.js
var sendNewUrlToMain = function() {
self.port.emit('newURL', location.href);
}
var pushState = window.history.pushState;
window.history.pushState = function(state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
sendNewUrlToMain();
return pushState.apply(history, arguments);
}
window.addEventListener('hashchange', sendNewUrlToMain);
Here are the request module docs, for making XHRs.
NB: if you don't want to use the request module (the only reason being that you already have standard XHR code for your chrome extension and don't want to take the time to learn/rewrite that code), you can send a standard XHR from the content script, but in doing so, you risk allowing the user to close the tab and thus destroy the script before your XHR callbacks are executed.

Back button not updating hash in iframe on IE

I have an application that uses a combination of the onhashchange event (for new browsers) and the hashchange plugin by Ben Alman (for old browsers) to track the history while making ajax calls or actions. Works like a charm in all browsers, back and forward buttons let the user navigate the actions that get recorded by changing the hash. So far so good. Now our page will be hosted in an iframe on a clients page in a diff domain(cross domain). Chrome kind of works but if you put to many changes in the history it stops working at some point (we can live with that). IE dosen't work at all. When I navigate our application by clicking on links and updating the hash new history items get created in the parent page but when I hit the back button the hash in the nested page is not updated therefore the hashchange event never fires. Anyone solved this problem before? Many thanks
Initialize the hash change event handling
if ("onhashchange" in window && !($j.browser.msie && $j.browser.version == '7.0')) {
window.onhashchange = function() {
var params = parseHash(location.hash)
if (params.tabId) {
if (getSelectedTabId() == params.tabId) return;
reloadPage(params.tabId);
}
};
}
else {// Plugin for older browsers
$j(window).bind('hashchange', function() {
var params = parseHash(location.hash)
if (params.tabId) {
if (getSelectedTabId() == params.tabId) return;
reloadPage(params.tabId);
}
});
}

jQuery — trigger a live event only once per element on the page?

Here's the scenario
$("p").live('customEvent', function (event, chkSomething){
//this particular custom event works with live
if(chkSomething){
doStuff();
// BUT only per element
// So almost like a .one(), but on an elemental basis, and .live()?
}
})
Here's some background
The custom event is from a plugin called inview
The actual issue is here http://syndex.me
In a nutshell, new tumblr posts are being infnitely scrolled via
javascript hack (the only one out there for tumblr fyi.)
The inview plugin listens for new posts to come into the viewport, if the top of an image is shown, it makes it visible.
It's kinda working, but if you check your console at http://.syndex.me check how often the event is being fired
Maybe i'm also being to fussy and this is ok? Please let me know your professional opinion. but ideally i'd like it to stop doing something i dont need anymore.
Some things I've tried that did not work:
stopPropagation
.die();
Some solutions via S.O. didnt work either eg In jQuery, is there any way to only bind a click once? or Using .one() with .live() jQuery
I'm pretty surprised as to why such an option isnt out there yet. Surely the .one() event is also needed for future elements too? #justsayin
Thanks.
Add a class to the element when the event happens, and only have the event happen on elements that don't have that class.
$("p:not(.nolive)").live(event,function(){
$(this).addClass("nolive");
dostuff();
});
Edit: Example from comments:
$("p").live(event,function(){
var $this = $(this);
if ($this.data("live")) {
return;
}
$this.data("live",true);
doStuff();
});
This one works (see fiddle):
jQuery(function($) {
$("p").live('customEvent', function(event, chkSomething) {
//this particular custom event works with live
if (chkSomething) {
doStuff();
// BUT only per element
// So almost like a .one(), but on an elemental basis, and .live()?
$(this).bind('customEvent', false);
}
});
function doStuff() {
window.alert('ran dostuff');
};
$('#content').append('<p>Here is a test</p>');
$('p').trigger('customEvent', {one: true});
$('p').trigger('customEvent', {one: true});
$('p').trigger('customEvent', {one: true});
});
This should also work for your needs, although it's not as pretty :)
$("p").live('customEvent', function (event, chkSomething){
//this particular custom event works with live
if(chkSomething && $(this).data('customEventRanAlready') != 1){
doStuff();
// BUT only per element
// So almost like a .one(), but on an elemental basis, and .live()?
$(this).data('customEventRanAlready', 1);
}
})
Like Kevin mentioned, you can accomplish this by manipulating the CSS selectors, but you actually don't have to use :not(). Here's an alternative method:
// Use an attribute selector like so. This will only select elements
// that have 'theImage' as their ONLY class. Adding another class to them
// will effectively disable the repeating calls from live()
$('div[class=theImage]').live('inview',function(event, visible, visiblePartX, visiblePartY) {
if (visiblePartY=="top") {
$(this).animate({ opacity: 1 });
$(this).addClass('nolive');
console.log("look just how many times this is firing")
}
});
I used the actual code from your site. Hope that was okay.

Resources