Use Chrome extension to unbind click events? - javascript

I'm trying to make an extension that unbinds a click event added by the website itself.
The website uses jQuery, which would make this insanely simple:
jQuery('a[rel]').unbind('click');
The problem is that my extension (using "content_scripts") can't access the website's jQuery object and therefore doesn't have the event functions to unbind. I can include a jQuery in my extension, but that wouldn't help, because jQuery stores 'data' in the jQuery object (not in the DOM elements). My jQuery will not have those events stored.
Is there another way? It doesn't have to be pretty. Maybe without "content_scripts"?

var unbind_event_listeners = function (node) {
var parent = node.parentNode;
if (parent) {
parent.replaceChild(node.cloneNode(true), node);
} else {
var ex = new Error("Cannot remove event listeners from detached or document nodes");
ex.code = DOMException[ex.name = "HIERARCHY_REQUEST_ERR"];
throw ex;
}
};
Just call unbind_event_listeners(a_node) to unbind any listeners from a node. This will work on every node in the document except document itself. As for window, you're out of luck. unbind_event_listeners(document.documentElement) should remove most event listeners attached to nodes in the document.
In the case of a[rel], you'd want to do this:
var nodes = document.querySelectorAll("a[rel]"), i = nodes.length;
while (i--) {
unbind_event_listeners(nodes.item(i));
}

If it doesn't need to be pretty and you're okay with doing things slightly hack-like, this should forcefully unbind every click listener bound to that element:
var el = document.querySelector('a[rel]');
el.onclick = function() {};
el.addEventListener = function() {};
or for every element:
Array.prototype.slice.call(document.querySelectorAll('a[rel]')).forEach(function(el) {
el.onclick = function() {};
el.addEventListener = function() {};
});
EDIT: You could maybe do something even uglier and have a content script run at "document_start" and do:
Element.prototype.addEventListener = (function() {
var real = Element.prototype.addEventListener;
return function(ev) {
if (ev === 'click' && this.tagName === 'A' && this.hasAttribute('rel')) {
console.log('try again, jquery!');
} else {
return real.apply(this, arguments);
}
};
})();

Related

How to replicate jQuery's $(this)?

I am creating a mini-library, sort of trying to reconstruct, at least partly, the way jQuery works for learning purposes and to understand better how object-oriented programming works.
I have recreated the jQuery methods click and addClass, but when I call them like:
$(".class1").click(function() {
$(".class1").addClass("class2"); // Works, but it adds class2 to all elements
this.addClass("class2"); // Doesn't work
});
I get an Uncaught Error saying this.addClass is not a function, which is normal, since I shouldn't be able to access another object's methods.
How is $(this) made in jQuery to mean the DOM element that triggered an event, so that in my case I can use it to add class2 only to the element clicked and not all elements that have the class class1?
P.S: I tried reading the jQuery file, but I feel like these waters are currently too deep for me.
Edit:
I always appreciate all the answers and the help I get on Stack Overflow, but telling me to use $(this) instead of this doesn't solve my issue, because $(this) doesn't exist in my code. I'm trying to learn how to create something like jQuery's $(this) & what's the logic behind it.
The click method is defined as follows:
$.prototype.click = function(callback) {
for (var i = 0; i < this.length; i++) {
this[i].onclick = function(event) {
callback.call(this, event);
}
}
};
With an extra 1.5 years of experience, this question becomes rather easy.
Alter $, so that, except string selectors, it can accept HTML elements.
Create a new instance of the object containing the HTML element given.
Call addClass with that as the context.
Code:
;(function() {
/* The object constructor. */
function ElementList(arg) {
/* Cache the context. */
var that = this;
/* Save the length of the object. */
this.length = 0;
/* Check whether the argument is a string. */
if (typeof arg == "string") {
/* Fetch the elements matching the selector and inject them in 'this'. */
[].forEach.call(document.querySelectorAll(arg), function(element, index) {
that[index] = element;
that.length++;
});
}
/* Check whether the argument is an HTML element and inject it into 'this'. */
else if (arg instanceof Element) {
this[0] = arg;
this.length = 1;
}
}
/* The 'click' method of the prototype. */
ElementList.prototype.click = function(callback) {
/* Iterate over every element and set the 'click' event. */
[].forEach.call(this, function(element) {
element.addEventListener("click", function(event) {
callback.call(this, event);
});
});
}
/* The 'addClass' method of the prototype. */
ElementList.prototype.addClass = function(className) {
/* Iterate over every element. */
[].forEach.call(this, function(element) {
/* Cache the classList of the element. */
var list = element.classList;
/* Add the specified className, if it doesn't already exist. */
if (!list.contains(className)) list.add(className);
});
}
/* The global callable. */
window.$ = function(arg) {
return new ElementList(arg);
}
})();
/* Example */
$("#b1").click(function() {
$(this).addClass("clicked");
console.log(this);
});
<button id="b1">Click</button>
You need to use call, apply, bind or some combination of those to set the callback's context to the DOM Node. Here is a contrived example of jquery's each method that sets the context of the callback using call:
var $ = {
each: function(selector, callback) {
var collection = Array.from(document.querySelectorAll(selector));
collection.forEach(function(element, index) {
// the magic...
callback.call(element, index, element);
});
}
}
$.each('.foo', function(idx, el) {
console.log(this.textContent);
});
this is the native JavaScript element and only exposes the native API. You need to pass it to the jQuery constructor in order to access jQuery's API
$(this).addClass("class2"); // This will work
One possible way (only selectors are accepted):
$ = function(selector) {
this.elements = '';//Select the element(s) based on your selector
this.addClass = function(klass) {
//apply your klass to you element(s)
return this;
};
this.click= function(handler) {
//Attach click event to your element(s)
return this;
};
return this;
};
Please keep in mind it's just an example.
Edit 1:
In your click method you are calling the handler in the wrong scope (the anonymous function scope). You need to use the outer scope:
$.prototype = {
click: function(callback) {
console.log(this.length);
var _self = this;
for (var i = 0; i < this.length; i++) {
this[i].onclick = function(event) {
//this here presents the anonymous function scope
//You need to call the handler in the outer scope
callback.call(_self, event);
//If you want to call the handler in the Element scope:
//callback.call(_self[i], event);
}
}
}
}
Note: In your example, this.addClass("class2"); doesn't work because jQuery calls the click handler in the Element scope not jQuery scope. Therefore, this presents the Element which dosen't have the addClass method;
Ok, I understand now your question. Let me try to help you again.
jQuery doesn't knows what DOM element do you use when you give it to selector. It doesn't parsing it or something else. Just save it to the internal property.
Very simplified code to understand:
$ = function(e) {
// finding object. For example "this" is object
if (typeof e !== 'object' || typeof e.className === 'undefined') {
if (typeof e == 'string') {
if (e[0] == '#') {
e = document.getElementById(e.substring(1));
} else if (e[0] == '.') {
e = document.getElementsByClassName(e.substring(1))[0];
} else {
// ... etc
}
}
// ... etc
}
var manager = {
elem: e,
addClass: function(newClass) {
manager.elem.className = manager.elem.className + ' ' + newClass;
return manager;
},
click: function(callback) {
// here is just simple way without queues
manager.elem.onclick = function(event) {
callback.call(manager, event);
}
}
}
return manager;
}

Removing wrapped event handlers

I have a function which wraps a function around another one, then attaches it to a element.
function addCustomEvent(element, eventName, handler, useCapture) {
var wrappedHandler = function () {
// Do something here.
handler.call();
};
element.addEventListener(eventName, wrappedHandler, useCapture);
}
This works great and I also want to implement this function:
removeCustomEvent(element, eventName, handler, useCapture)
So I want to do something like this.
var clickHandler= function () { /* ... */ };
addCustomEvent(someElement, "click", clickHandler, false);
removeCustomEvent(someElement, "click", clickHandler, false);
There is a problem with this because I don't have a reference to the wrappedHandler in removeCustomEvent.
The only way I can think of now is to keep track of handlers and their corresponding wrappedHandlers in a dictionary so that I can find wrappedHandler from handler within the function, and remove it.
But I'm not fond of this approach because browser must have information about what handlers are attached, so creating a new dictionary seems redundant and waste of memory.
Is there a better, and much cleaner way?
Personally, I'd simply wrap the addCustomEvent and removeCustomEvent to a single module, and keep an object that tracks the bound handlers. You consider this "a waste of resources", but really, the impact of this approach would be negligible.
The upsides are: you have the beginning of a module that can easily be expanded on, to handle more complex event handlers (like simulating a tab event for mobile devices using the touchstart and touchend events).
An alternative approach would be to unbind the event handler internally, depending on the event object itself.
Then, you'll have to re-write your removeCustomEvent function to trigger a special event, that lets the bound handler know that you want to remove the event listener.
//in the wrappedHandler:
var wrappedHandler = function(e)
{
e = e || window.event;
if (e.synthetic === true)
{
e.preventDefault();
e.stopPropagation();
element.removeEventListener(eventName, wrappedHandler, useCapture);//<-- use closure vars
return e;//or return false.
}
//do normal event
handler.apply(this, [e]);//pass event object, and call handler in the same context!
};
var removeCustomEvent = function(event, node, capture)
{
var e, eClass,
doc = node.ownerDocument || (node.nodeType === (document.DOCUMENT_NODE || 9) ? node : document);
if (node.dispatchEvent)
{
if (event === 'click' || event.indexOf('mouse') >= 0)
eClass = 'MouseEvents';
else
eClass = 'HTMLEvents';
e = doc.createEvent(eClass);
e.initEvent(event, !(event === 'change'), true);
//THIS IS THE TRICK:
e.synthetic = true;
node.dispatchEvent(e, true);
return true;
}
if (node.fireEvent)
{
e = doc.createEventObject();
e.synthetic = true;
node.fireEvent('on' + event, e);
return true;
}
event = 'on' + event;
return node[event]();
};
here's a version of this code that is actually documented
I've set a synthetic property on the event object that will be passed to the event handler. the handler checks for this property, and if it's set to true, it will unbind the listener and return. This doesn't require you to keep DOM references and handlers in an object, but this is, I think you'll agree, quite a lot of work, too.
It also feels quite hacky, if you don't mind my saying so...
Compared to:
var binderModule = (function()
{
var module = {},
eventMap = {},
addEvent = function (elem, eventName, handler, capture)
{
var i, wrappedHandler;
if (!eventMap.hasOwnProperty(eventName))
eventMap[eventName] = [];
for (i=0;i<eventMap[eventName].length;++i)
{//look for elem reference
if (eventMap[eventName][i].node === elem)
break;
}
if (i>= eventMap[eventName].length)
{
i = eventMap[eventName].length;//set i to key
eventMap[eventName].push({
node: elem,
handlers: []//keep handlers here, in array for multiple handlers
});
}
wrappedHandler = function(e)
{
//stuff
return handler.apply(this, [e || window.event]);//pass arguments!
};
eventMap[eventNAme][i].handlers.push(wrappedHandler);
return elem.addEventListener(eventName, wrappedHandler, capture);
},
removeEvent(elem, eventName, capture)
{
var i, temp;
if (!eventMap.hasOwnProperty(eventName))
return;//no handlers bound, end here
for (i=0;i<eventMap[eventName].length;++i)
if (eventMap[eventName][i].node === elem)
break;
if (i < eventMap[eventName].length)
{//found element, remove listeners!
//get handlers
temp = eventMap[eventName][i].handlers;
//remove element + handlers from eventMap:
eventMap[evetnName][i] = undefined;
for (i=0;i<temp.length;++i)
elem.removeEventListener(eventName, temp[i], capture);
}
};
module.addCustomEvent = addEvent;
module.removeCustomEvent = removeEvent;
//or, perhaps better:
Object.defineProperty(module, 'addCustomEvent', {value: addEvent});//read-only
Object.defineProperty(module, 'removeCustomEvent', {value: removeEvent});
return module;
}());
Note that this is the basic setup to keep track of event handlers that are bound to particular DOM nodes, and how to mangage them. This code is not finished and is not tested. It probably contains typo's, syntax errors and some consistency issues. But this should be more than enough to get you started.

Remove All Event Listeners of Specific Type

I want to remove all event listeners of a specific type that were added using addEventListener(). All the resources I'm seeing are saying you need to do this:
elem.addEventListener('mousedown',specific_function);
elem.removeEventListener('mousedown',specific_function);
But I want to be able to clear it without knowing what it is currently, like this:
elem.addEventListener('mousedown',specific_function);
elem.removeEventListener('mousedown');
That is not possible without intercepting addEventListener calls and keep track of the listeners or use a library that allows such features unfortunately. It would have been if the listeners collection was accessible but the feature wasn't implemented.
The closest thing you can do is to remove all listeners by cloning the element, which will not clone the listeners collection.
Note: This will also remove listeners on element's children.
var el = document.getElementById('el-id'),
elClone = el.cloneNode(true);
el.parentNode.replaceChild(elClone, el);
If your only goal by removing the listeners is to stop them from running, you can add an event listener to the window capturing and canceling all events of the given type:
window.addEventListener(type, function(event) {
event.stopImmediatePropagation();
}, true);
Passing in true for the third parameter causes the event to be captured on the way down. Stopping propagation means that the event never reaches the listeners that are listening for it.
Keep in mind though that this has very limited use as you can't add new listeners for the given type (they will all be blocked). There are ways to get around this somewhat, e.g., by firing a new kind of event that only your listeners would know to listen for. Here is how you can do that:
window.addEventListener('click', function (event) {
// (note: not cross-browser)
var event2 = new CustomEvent('click2', {detail: {original: event}});
event.target.dispatchEvent(event2);
event.stopPropagation();
}, true);
element.addEventListener('click2', function(event) {
if (event.detail && event.detail.original) {
event = event.detail.original
}
// Do something with event
});
However, note that this may not work as well for fast events like mousemove, given that the re-dispatching of the event introduces a delay.
Better would be to just keep track of the listeners added in the first place, as outlined in Martin Wantke's answer, if you need to do this.
You must override EventTarget.prototype.addEventListener to build an trap function for logging all 'add listener' calls. Something like this:
var _listeners = [];
EventTarget.prototype.addEventListenerBase = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener)
{
_listeners.push({target: this, type: type, listener: listener});
this.addEventListenerBase(type, listener);
};
Then you can build an EventTarget.prototype.removeEventListeners:
EventTarget.prototype.removeEventListeners = function(targetType)
{
for(var index = 0; index != _listeners.length; index++)
{
var item = _listeners[index];
var target = item.target;
var type = item.type;
var listener = item.listener;
if(target == this && type == targetType)
{
this.removeEventListener(type, listener);
}
}
}
In ES6 you can use a Symbol, to hide the original function and the list of all added listener directly in the instantiated object self.
(function()
{
let target = EventTarget.prototype;
let functionName = 'addEventListener';
let func = target[functionName];
let symbolHidden = Symbol('hidden');
function hidden(instance)
{
if(instance[symbolHidden] === undefined)
{
let area = {};
instance[symbolHidden] = area;
return area;
}
return instance[symbolHidden];
}
function listenersFrom(instance)
{
let area = hidden(instance);
if(!area.listeners) { area.listeners = []; }
return area.listeners;
}
target[functionName] = function(type, listener)
{
let listeners = listenersFrom(this);
listeners.push({ type, listener });
func.apply(this, [type, listener]);
};
target['removeEventListeners'] = function(targetType)
{
let self = this;
let listeners = listenersFrom(this);
let removed = [];
listeners.forEach(item =>
{
let type = item.type;
let listener = item.listener;
if(type == targetType)
{
self.removeEventListener(type, listener);
}
});
};
})();
You can test this code with this little snipper:
document.addEventListener("DOMContentLoaded", event => { console.log('event 1'); });
document.addEventListener("DOMContentLoaded", event => { console.log('event 2'); });
document.addEventListener("click", event => { console.log('click event'); });
document.dispatchEvent(new Event('DOMContentLoaded'));
document.removeEventListeners('DOMContentLoaded');
document.dispatchEvent(new Event('DOMContentLoaded'));
// click event still works, just do a click in the browser
Remove all listeners on a global event
element.onmousedown = null;
now you can go back to adding event listeners via
element.addEventListener('mousedown', handler, ...);
This solution only works on "Global" events. Custom events won't work. Here's a list of all global events: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers
I know this is old, but I had a similar issue with no real answers, where I wanted to remove all keydown event listeners from the document. Instead of removing them, I override the addEventListener to ignore them before they were even added, similar to Toms answer above, by adding this before any other scripts are loaded:
<script type="text/javascript">
var current = document.addEventListener;
document.addEventListener = function (type, listener) {
if(type =="keydown")
{
//do nothing
}
else
{
var args = [];
args[0] = type;
args[1] = listener;
current.apply(this, args);
}
};
</script>
A modern way to remove event listeners without referencing the original function is to use AbortController. A caveat being that you can only abort the listeners that you added yourself.
const buttonOne = document.querySelector('#button-one');
const buttonTwo = document.querySelector('#button-two');
const abortController = new AbortController();
// Add multiple click event listeners to button one
buttonOne.addEventListener(
'click',
() => alert('First'),
{ signal: abortController.signal }
);
buttonOne.addEventListener(
'click',
() => alert('Second'),
{ signal: abortController.signal }
);
// Add listener to remove first button's listeners
buttonTwo.addEventListener(
'click',
() => abortController.abort()
);
<p>The first button will fire two alert dialogs when clicked. Click the second button to remove those listeners from the first button.</p>
<button type="button" id="button-one">Click for alerts</button>
<button type="button" id="button-two">Remove listeners</button>
Remove all listeners in element by one js line:
element.parentNode.innerHTML += '';
You cant remove a single event, but all? at once? just do
document.body.innerHTML = document.body.innerHTML
In the extreme case of not knowing which callback is attached to a window listener, an handler can be wrapper around window addEventListener and a variable can store ever listeners to properly remove each one of those through a removeAllEventListener('scroll') for example.
var listeners = {};
var originalEventListener = window.addEventListener;
window.addEventListener = function(type, fn, options) {
if (!listeners[type])
listeners[type] = [];
listeners[type].push(fn);
return originalEventListener(type, fn, options);
}
var removeAllEventListener = function(type) {
if (!listeners[type] || !listeners[type].length)
return;
for (let i = 0; i < listeners[type].length; i++)
window.removeEventListener(type, listeners[type][i]);
}
So this function gets rid of most of a specified listener type on an element:
function removeListenersFromElement(element, listenerType){
const listeners = getEventListeners(element)[listenerType];
let l = listeners.length;
for(let i = l-1; i >=0; i--){
removeEventListener(listenerType, listeners[i].listener);
}
}
There have been a few rare exceptions where one can't be removed for some reason.
You could alternatively overwrite the 'yourElement.addEventListener()' method and use the '.apply()' method to execute the listener like normal, but intercepting the function in the process. Like:
<script type="text/javascript">
var args = [];
var orginalAddEvent = yourElement.addEventListener;
yourElement.addEventListener = function() {
//console.log(arguments);
args[args.length] = arguments[0];
args[args.length] = arguments[1];
orginalAddEvent.apply(this, arguments);
};
function removeListeners() {
for(var n=0;n<args.length;n+=2) {
yourElement.removeEventListener(args[n], args[n+1]);
}
}
removeListeners();
</script>
This script must be run on page load or it might not intercept all event listeners.
Make sure to remove the 'removeListeners()' call before using.
var events = [event_1, event_2,event_3] // your events
//make a for loop of your events and remove them all in a single instance
for (let i in events){
canvas_1.removeEventListener("mousedown", events[i], false)
}

Dynamic event building in JavaScript

I'd like to create an event for each function in a script, then inject the event trigger into the end of the function.
This way I can see exactly when each function has completed and use the events like hooks for other functions
If I can do it dynamically, I can add as many new functions as I like without having to create and append these events.
Here's a basic example of what I'm trying to do, this won't actually work, but it gives you an idea. I have been using jQuery, but I'll accept any JavaScript framework at all, and any method.
var obj = {};
(function()
{
this.init = function()
{
// loop through every function
$.each(this, function(k, v)
{
// create an event for every function
$('body').bind(v, function()
{
console.log('Event: ' + v + ' Finished');
});
// Add a event trigger into each specific function in the loop
this[v].call($('body').trigger(v));
});
}
this.another_function = function()
{
// do something
}
this.some_function = function()
{
/do something
}
}).apply(obj);
obj.init();
(edit) The script itself basically renders a Calendar, but there are a lot of callbacks, ajax requests, buttons. etc... If I could tie each feature down to an event, it would make my life easier when extending it, adding new features etc...
Loop through every function, replace it with new one, which calls original function on the same object and triggers event on body.
var obj = { };
(function()
{
this.init = function()
{
var self = this;
foreach(var name in this) {
if (typeof k !== 'Function') continue;
if (name ==='init') continue;
var original = this[name];
var newFunc = function() {
original.apply(self, arguments);
$('body').trigger(name);
}
this[name] = newFunc;
}
}
this.another_function = function()
{
// do something
}
this.some_function = function()
{
/do something
}
}).apply(obj);
obj.init();

Get the caller of the event from attachEvent

I am trying to do something simple: I have a bunch of Images which are being load through JS.
I attach an event listener to the load event, and after the Image is being loaded, in the listener function I would like to get the calling Image and retrieve properties from it.
Here is my code, simplified:
function loadImages() {
for (var i = 0; i < arrDownloadQueueBasic.length; i++) {
var path = arrDownloadQueueBasic[i].path;
var img = new Image();
img.type = arrDownloadQueueBasic[i].type;
img.attachEvent(img, 'load', setBasicElement);
img.src = path;
}
}
function setBasicElement(e) {
var caller = e.target || e.srcElement;
alert(caller); // THIS DOESNT WORK - RETURN NULL
alert(caller.type) // OF COURSE THIS DOESNT WORK AS WELL...
}
There are a couple of things that you need to correct. First, the attachEvent method should not be used for browsers other than IE. You should structure your code to check if the method is implemented and then act accordingly like so:
if(img.addEventListener) {
img.addEventListener('load', setBasicElement, false);
}
else if(img.attachEvent) {
img.attachEvent('onload', setBasicElement);
}
else {
img.onload = setBasicElement;
}
The other issue is that you need to prefix the event name with "on" when using attachEvent.
EDIT
You can get the caller by using the following code in the setBasicElement function:
var caller = e.target || e.srcElement || window.event.target || window.event.srcElement;
Here is a working example - http://jsfiddle.net/BMsXR/3/
Try this:
var caller = window.event ? window.event.srcElement : e.target;
If I remember rightly IE doesn't pass the event object as a parameter when you've used attachEvent(), but it has a global event object.

Categories