I've been trying to override an event listener on a site for quite some time now and I have a small issue.
On the site there's a video player (jwplayer). If you click into it, the player captures the keyboard in some fashion I haven't been able to override.
Here's my script:
// ==UserScript==
// #name test
// #author test
// #version 1.1.5
// #match *://*.example.com/*
// #require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// ==/UserScript==
/* globals $ */
document.addEventListener('keydown', function(e){
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey)
{
switch (e.keyCode)
{
case 37:
alert('37');
// some code
break;
case 39:
alert('39');
// some code
break;
}
}
}, true);
// some code is simply two lines of code taken directly from the event that changes the page to the next one.
I also have this code that I run together with my script when I'm testing.
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://third-party.example.com/js/jwplayer8/jwplayer.core.controls.html5.js';
document.getElementsByTagName('head')[0].appendChild(script);
script.onload = function() {
alert( 'loaded' );
}
The reason I have this is because I'm pretty sure the code below is the one that's causing the issue, or is connected to the issue. It at least calls the same keys as I'm using.
function(n) {
if (n.ctrlKey || n.metaKey) return !0;
var o = !i.settingsMenu.visible,
a = i.instreamState;
switch (n.keyCode) {
/* ... code ... */
case 37:
!a && o && m(-5);
break;
case 39:
!a && o && m(5);
break;
/* ... code ... */
default:
/* ... code ... */
}
return /13|32|37|38|39|40/.test(n.keyCode) ? (n.preventDefault(), !1) : void 0
}
Both my script and this one calls for the key cases 37 and 39.
When the jwplayer isn't in focus, the script works as expected, but once the player is started and captures a keypress, it breaks the userscript.
On page load, the site's script loads twice. First one opens a dialogue box simply saying "loaded" as it is supposed to. However, on second load the dialogue box, while still saying "loaded" now has a title called "The page at https://third-party.example.com says:" instead of no title like before.
Likewise, using the shortcuts Ctrl + Arrow (left or right) produces identical results. The alerts say either only "37" or "39" like they are supposed to, and the dialogue box has no title, like expected.
Once the jwplayer is in focus, though, the dialogue box reflects the one saying the script loaded. The title becomes "The page at https://third-party.example.com says:" and then either "37" or "39" as the information within.
My original plan had been to simply unfocus the jwplayer, but considering that the alerts execute properly but the code following it doesn't has made that difficult.
There are 2 phases in which you can handle events in JavaScript. One is the event capturing phase where the event flows from the parent to the child. The other is the event bubbling phase, where the event flows bottom up (child to parent).
You can turn on capturing, by setting the third parameter to .addEventListener() to true. You can then use event.stopPropagation() to ensure that the event doesn't trickle down to the videoplayer.
document.addEventListener('keydown', function(e){
e.stopPropagation();
// Any other code
}, true); // <= This tells JS to use the event capturing phase
Related
I've been trying to override an event listener on a site for quite some time now and I have a small issue.
On the site there's a video player (jwplayer). If you click into it, the player captures the keyboard in some fashion I haven't been able to override.
Here's my script:
// ==UserScript==
// #name test
// #author test
// #version 1.1.5
// #match *://*.example.com/*
// #require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// ==/UserScript==
/* globals $ */
document.addEventListener('keydown', function(e){
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey)
{
switch (e.keyCode)
{
case 37:
alert('37');
// some code
break;
case 39:
alert('39');
// some code
break;
}
}
}, true);
// some code is simply two lines of code taken directly from the event that changes the page to the next one.
I also have this code that I run together with my script when I'm testing.
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://third-party.example.com/js/jwplayer8/jwplayer.core.controls.html5.js';
document.getElementsByTagName('head')[0].appendChild(script);
script.onload = function() {
alert( 'loaded' );
}
The reason I have this is because I'm pretty sure the code below is the one that's causing the issue, or is connected to the issue. It at least calls the same keys as I'm using.
function(n) {
if (n.ctrlKey || n.metaKey) return !0;
var o = !i.settingsMenu.visible,
a = i.instreamState;
switch (n.keyCode) {
/* ... code ... */
case 37:
!a && o && m(-5);
break;
case 39:
!a && o && m(5);
break;
/* ... code ... */
default:
/* ... code ... */
}
return /13|32|37|38|39|40/.test(n.keyCode) ? (n.preventDefault(), !1) : void 0
}
Both my script and this one calls for the key cases 37 and 39.
When the jwplayer isn't in focus, the script works as expected, but once the player is started and captures a keypress, it breaks the userscript.
On page load, the site's script loads twice. First one opens a dialogue box simply saying "loaded" as it is supposed to. However, on second load the dialogue box, while still saying "loaded" now has a title called "The page at https://third-party.example.com says:" instead of no title like before.
Likewise, using the shortcuts Ctrl + Arrow (left or right) produces identical results. The alerts say either only "37" or "39" like they are supposed to, and the dialogue box has no title, like expected.
Once the jwplayer is in focus, though, the dialogue box reflects the one saying the script loaded. The title becomes "The page at https://third-party.example.com says:" and then either "37" or "39" as the information within.
My original plan had been to simply unfocus the jwplayer, but considering that the alerts execute properly but the code following it doesn't has made that difficult.
There are 2 phases in which you can handle events in JavaScript. One is the event capturing phase where the event flows from the parent to the child. The other is the event bubbling phase, where the event flows bottom up (child to parent).
You can turn on capturing, by setting the third parameter to .addEventListener() to true. You can then use event.stopPropagation() to ensure that the event doesn't trickle down to the videoplayer.
document.addEventListener('keydown', function(e){
e.stopPropagation();
// Any other code
}, true); // <= This tells JS to use the event capturing phase
I have a page that uses the TradingView widget, which you can type inside it any key on the keyboard to trigger a symbol search. I used Chrome's debugger to breakpoint and know exactly which line of code does that. I want the key event to be bubbled up to the general DOM to be handled by my own custom function.
How to do that? Any sample code would be appreciated. Here is the code snippet from TradingView ('t' is the key event):
a || (o.handle = a = function(t) {
return s === Te || t && Te.event.triggered === t.type ? s : Te.event.dispatch.apply(a.elem, arguments)
}
So instead of the return statement I want to send that key event to my own function. The full TradingView code where this code is: https://static.bitmex.com/assets/tradingView/static/tv-chart.min-6ce28e05fd34b9cf06a4e63b29980a72.js
Rather than altering the page code (which is possible, but can be tedious), consider changing your own listener to run in the capturing phase, rather than the bubbling phase. Eg, instead of
document.body.addEventListener('keypress', fn);
do
document.body.addEventListener('keypress', fn, true);
and it'll run before the event has bubbled down to the inner element.
If the page's listener on the inner element needs to be complete before your fn runs, you can call fn after a small setTimeout:
document.body.addEventListener('keypress', () => setTimeout(fn), true);
If you also want to prevent the widget's code from running, make sure to call stopPropagation in your capturing listener so the event doesn't capture down to the widget:
const fn = (event) => {
event.stopPropagation();
// rest of code
};
If the container you're sending the key event to is inside an iframe, the parent window won't see the event at all, due to security issues.
On most pages, you'd be able to communicate the event up to the parent by using postMessage, eg:
// ==UserScript==
// #name 0 New Userscript
// #include https://www.bitmex.com/app/trade/XBTUSD
// #include https://static.bitmex.com/chartEmbed*
// #grant GM_getValue
// #run-at document-start
// ==/UserScript==
if (window.location.host === 'www.bitmex.com') {
const mainFn = () => {
console.log('b');
};
// We're on the top level
const keydown = (e) => {
e.stopPropagation();
if (!e.altKey && e.key === 'b') {
mainFn();
}
};
window.addEventListener('keydown', keydown, true);
window.addEventListener('message', (e) => {
if (e.origin === 'https://static.bitmex.com' && e.data === 'keydown inside iframe') {
mainFn();
}
});
} else {
// We're in the iframe
// When a keypress is detected, message the parent window
const keydown = (e) => {
console.log('iframe kd');
e.stopPropagation();
if (!e.altKey && e.key === 'b') {
window.top.postMessage('keydown inside iframe', '*');
}
};
window.addEventListener('click', keydown, true);
}
The #run-at document-start is needed because the iframe has a CSP which forbids userscripts without it from running.
But unfortunately, there's another problem: in this particular case, the iframe window also overwrites EventTarget.prototype.addEventListener on pageload, before the userscript has a chance to run. While you could usually use // #run-at document-start to ensure you save a reference to a variable before a page script overwrites it, this isn't possible in an iframe; Chrome userscripts in iframes cannot run at the very start of pageload.
Without a reference to EventTarget.addEventListener (or EventTarget.prototype.onclick or EventTarget.prototype.onkeydown setters, etc), there's no way to access the browser-level API that registers the user's actions.
I don't think what you're looking for is possible in Tampermonkey.
so basically I want to change a W Text into B. My code below is working fine but there is a lag when you refresh the page where it shows the W text then replaces it with a B. My second problem is that every 20 refreshes or so, the B Text doesn't replace the original. Can someone plz help fix my code so it doesn't make these 2 mistakes Thanks.
window.onload = function () {
/* Add your logic here */
var my_div = document.getElementsByClassName('c')[3]
my_div.firstChild.nodeValue = 'B'
}
An onload handler will run once all resources on the page have finished downloading. On larger pages with images and external scripts and such, this could take a while, especially on bad connections.
Attach a DOMContentLoaded listener instead, which will run as soon as the DOM structure has been downloaded and parsed by the browser.
If the document may already be interactive by the time the userscript runs, then don't attach a listener, just run a function that changes the text immediately:
if (window.top === window) {
const fn = () => {
var my_div = document.getElementsByClassName('c')[3];
my_div.firstChild.nodeValue='B'
};
if (document.readyState === 'interactive' || document.readyState === 'complete') {
fn();
} else {
window.addEventListener('DOMContentLoaded', fn);
}
}
But the document in your link is huge, and even DOM parsing takes a long time, then the above may not be fast enough, in which case you could attach a MutationObserver to the document at the beginning of pageload (with #run-at document-start and instant script injection), and change the document.getElementsByClassName('c')[3] as soon as it exists in the document, like this:
// ==UserScript==
// #name New Userscript
// #include /^https://fork-accessuh-uh-edu-e3e0cca90dc751a6.sitemod.io/logout.phpsitemod/
// #run-at document-start
// ==/UserScript==
const cs = document.getElementsByClassName('c');
new MutationObserver((mutations, observer) => {
if (!cs[3]) {
return;
}
cs[3].firstChild.nodeValue='B'
observer.disconnect();
})
.observe(document.documentElement, { childList: true, subtree: true });
document-start isn't entirely reliable, though - it will sometimes take some time to execute such a script. If you happen to be in an environment where the script doesn't run immediately, enable experimental instant script injection too, via Tampermonkey settings (Config mode: Advanced, scroll to the very bottom, Inject Mode: Instant).
So I'm working on a game that will accept user input (up down left right arrow keys). To do so, I will add an event listener that will check when the user presses said keys. I have it set up in the following way:
function gameStart(){
//Generates four random numbers on board
for(gameStart_i = 0; gameStart_i < 4; gameStart_i++){
generateNumber();
}
document.getElementById("start_button").innerHTML = "Reset Game";
document.getElementById("start_button").setAttribute('onclick', 'reset()');
document.getElementById("start_button").id = 'reset_button';
var board = document.getElementById("game_background");
console.log("1");
board.addEventListener("keydown", inputListen(), false);
console.log("1.1");
}
function inputListen(e){
console.log("2");
var code = e.keyCode;
switch(code){
case 37:
alert("left");
break;
case 38:
alert("up");
break;
case 39:
alert("right");
break;
case 40:
alert("down");
break;
}
}
This seems in line with how tutorials show it. However, some tutorials change the addEventListener line with something that would look like this:
board.addEventListener("keydown", inputListen, false); //removes the () after the function
However that doesn't seem to work for me when I look into my console.
When I run my code my console gives me the following error:
1
2
Uncaught TypeError: Cannot read property 'keyCode' of undefined
at inputListen (script.js:86)
at gameStart (script.js:16)
at HTMLButtonElement.onclick (2048Game.html:114)
I think it's because I don't pass any parameters to my function, but none of the online tutorials pass paraements in their addEventListener statement.
Thank you,
Alex
The proper way to do this is indeed to remove the () after inputListen.
Using the () immediately calls the function, which then gives you aforementioned Cannot read property 'keycode' of undefined since no input parameters were given.
You should have also received the error in the console between the two console.log lines, which proves the error came from the addEventListener line.
You want to pass the function without calling it using the line you posted:
board.addEventListener("keydown", inputListen, false); //removes the () after the function
Take a look at this JSFiddle:
function gameStart(){
console.log("before");
document.getElementById("game_background").addEventListener("keydown", inputListen, false);
console.log("after");
}
This should print out to the console
before
after
And then nothing else until it detects a keydown.
It also matters what kind of element you bind this to. An element such as <input> will have no trouble here, but normal non-focusable elements will need the tabindex attribute in order to be focused and respond to a keydown:
<div id="game_background" tabindex="-1">
</div>
You'll need to click the element once to focus onto it, then your events should be captured. More info at this answer.
Yes drop the (), you are using the function itself to the event listener, not the result of the function call.
The keydown event is likely not sent to an arbitrary <div> in the page. Add the event listener to the window itself instead, if you don't want the user to manually click to focus on it.
window.addEventListener('keydown', inputListen);
Let me just first point out to any IE users right now (this is not a problem in Chrome, Safari or Firefox) hint hint ;)
So... I have a issue with my tooltips in IE, I have a onmouseover listener for all the elements which are to be hoverable and then in my mouseover function I have a very basic cross browser declaration as such...
var event = e || window.event,
el = event.target || event.srcElement;
I've been having issues with the window object not existing in IE or something, this has been a issue after I added a flag to ignore mouseover's from one element mouseover on the way to the tooltip itself (during the time cycle allowed, 300ms). In other words the flag is to ignore mouseovers on route to the tooltip from the original mouseover.
So that logic looks like this...
loadtip.refMouseOver = function (e) {
var event = e || window.event, el = event.target || event.srcElement;
//console.log(window); // <-- throws error in IE (Member not found)
// Reset the lastHoveredRef data.
tipManager.lastHoveredRef = null;
tipManager.lastHoveredRef = [el, event];
// true means there is a tip open still, so if no tip is open.
if (tipManager.tipState !== true) {
tipManager.processTip(el, event);
} else {
return; // do nothing
}
}
The "Member not found" error will occur when I hover from one element quickly to the next in IE with the tooltip still open.
I read about window.open and close stuff with a try catch but I didn't see how that was relevant. Any help is greatly appreciated.
Ok I have found the problem.
To sum it up, basically IE will not pass a event to another function if that function call is within a setTimeout.
So you can trick IE by creating a copy of the event and passing that, here is a example of that...
var eventCopy = {};
for (var i in event) {
eventCopy[i] = event[i];
}
Then just send your function the eventCopy, even though this is a 'total' hack.
setTimeout(function () { yourFunction(eventCopy), yourDelayTime);
And voila it will work.
I should add, that Internet Explorer will merely create a reference to the global window event which is why we need the copy of event. This is because by the time setTimeout calls the function, windows.event has already passed,
Bottom line... don't try to send a event inside a setTimeout because IE won't accept it. This is true for IE 6, 7 & 8 from my testing.
I realize this question/answer is pretty old and seems to be resolved. That being said, I have another alternative I've used to handle a similar -- yet slightly different -- issue with 'Member Not Found' in IE versions prior to MSIE 9. I hope this helps someone out! ...this can also be used to work around issues with Firefox not having window.event.
First I extended jQuery and added a function to get the MSIE version or -1 if the browser is non MSIE. You can do the same or just create a pure JS function to accomplish this. Then create an event override function (it might be necessary to add a global 'event' variable in some cases), that's more of a per individual situation basis. Then override the event in your event handler(s) as needed.
Extending jQuery
// So this will give you the version of IE (or for non IE browser -1)
$.fn.msieVersion = function()
{
if ( navigator.userAgent.toLowerCase().indexOf( 'msie' ) !== -1 ) {
return document.documentMode;
}
return -1;
};
Override the global event
var setEvent = function( evt ) {
// Set the event if MSIE version is >= 9 or is -1 which means it's not IE
if ( $.fn.msieVersion() >= 9 || $.fn.msieVersion === -1 ) {
// NOTE: I have a global 'event' variable I'm using that comes from another previously loaded JS file
// Why? I didn't do it. I'm updating some SUPER old code the best I can. (old enough it has references to Netscape....)
event = evt || window.event;
}
return true;
};
Usage Example
$( 'img.myImageID' ).bind('mouseover mouseout', function ( evt ) {
setEvent( evt ); // Override the event
// DO WORK! ...continue all other awesomeness here!
// Maybe setTimeout(...)
};