I'm trying to modify all links on a page so they perform some additional work when they are clicked.
A trivial approach might be something like this:
function adaptLinks()
{
var links = document.getElementsByTagName('a');
for(i = 0; i != links.length; i++)
{
links[i].onclick = function (e)
{
<do some work>
return true;
}
}
}
But some of the links already have an onClick handler that should be preserved. I tried the following:
function adaptLinks()
{
var links = document.getElementsByTagName('a');
for(i = 0; i != links.length; i++)
{
var oldOnClick = links[i].onclick;
links[i].onclick = function (e)
{
if(oldOnClick != null && !oldOnClick())
{
return false;
}
<do some work>
return true;
}
}
}
But this doesn't work because oldOnClick is only evaluated when the handler is called (it contains the value of the last link as this point).
Don't assign to an event handler directly: use the subscribe model addEventListener / attachEvent instead (which also have remove pairs!).
Good introduction here.
You need to create a closure to preserve the original onclick value of each link:
Hi
There
<script type="text/javascript">
function adaptLinks() {
var links = document.getElementsByTagName('a');
for (i = 0; i != links.length; i++) {
links[i].onclick = (function () {
var origOnClick = links[i].onclick;
return function (e) {
if (origOnClick != null && !origOnClick()) {
return false;
}
// do new onclick handling only if
// original onclick returns true
alert('some work');
return true;
}
})();
}
}
adaptLinks();
</script>
Note that this implementation only performs the new onclick handling if the original onclick handler returns true. That's fine if that's what you want, but keep in mind you'll have to modify the code slightly if you want to perform the new onclick handling even if the original handler returns false.
More on closures at the comp.lang.javascript FAQ and from Douglas Crockford.
Use a wrapper around addEventListener (DOM supporting browsers) or attachEvent (IE).
Note that if you ever want to store a value in a variable without overwriting the old value, you can use closures.
function chain(oldFunc, newFunc) {
if (oldFunc) {
return function() {
oldFunc.call(this, arguments);
newFunc.call(this, arguments);
}
} else {
return newFunc;
}
}
obj.method = chain(obj.method, newMethod);
In Aspect Oriented Programming, this is known as "advice".
how about setting oldClick = links[i].onclick or an empty function. Like so
var oldOnClick = links[i].onclick || function() { return true; };
links[i].onclick = function (e)
{
if (!oldOnClick())
return false;
//<do some work>
return true;
}
Or you could use attachEvent and addEventListener as others have recommended
function addEvent(obj, type, fn) {
if (obj.addEventListener)
obj.addEventListener(type, fn, false);
else if (obj.attachEvent)
obj.attachEvent('on' + type, function() { return fn.apply(obj, [window.event]);});
}
and use like so
addEvent(links[i], 'click', [your function here]);
Using JQuery, the following code works:
function adaptLinks(table, sortableTable)
{
$('a[href]').click(function (e)
{
if(!e.isDefaultPrevented())
{
<do some work>
}
});
}
This requires using an extra library but avoids some issues that exist with addEventListener/attachEvent (like the latter's problem with this references).
There is just one pitfall: if the original onClick handler is assigned using "normal" JavaScript, the line
...
if(!e.isDefaultPrevented())
...
will always resolve to true, even in case the original handler canceled the event by returning false. To fix this, the original handler has to use JQuery as well.
This function should be usable (event listeners approach):
function addEventListener(element, eventType, eventHandler, useCapture) {
if (element.addEventListener) {
element.addEventListener(eventType, eventHandler, useCapture);
return true;
} else if (element.attachEvent) {
return element.attachEvent('on' + eventType, eventHandler);
}
element['on' + eventType] = eventHandler;
}
or you can save some more code adding this function (if you need to add the same event listener to many elements):
function addClickListener(element) {
addEventListener(element, 'click', clickHandler, false);
}
I had problems with overloading in the simple way - this page was a great resource
http://www.quirksmode.org/js/events_advanced.html
Related
It's possible what I'm describing is ill-advised with more standard approaches, and if so that would be a valid answer.
Say one is creating their own custom event, but in order for it to work the on() and off() steps need to perform actions on the element.
In my example scenario I want to create a custom jquery event called 'multiclick' that fires when an element is clicked 1, 2, or 3 times within a 500 ms timespan.
My initial idea would be to extend the functionality of on, off, and trigger and add my own event like so:
(function($)
{
var oldOn = $.fn.on;
var oldOff = $.fn.off;
var oldTrigger = $.fn.trigger;
// events [, selector ] [, data ], handler
// events [, selector ] [, data ]
// Note: this does not implement selector or data for multiclick
$.fn.on = function(events, handler)
{
if (events.split(/\s+/).some(function(event) { return event == 'multiclick'; }))
{
$(this).each(function()
{
if (!Array.isArray($(this).data('multiclick-state')))
{
$(this).data('multiclick-state', []);
}
var clicks = 0;
var clicksTimer = null;
var clickHandler = function(e)
{
clicks++;
if (clicks == 1)
{
clicksTimer = setTimeout(function()
{
handler(e, clicks);
clicks = 0;
}, 500);
}
else if (clicks == 3)
{
clearTimeout(clicksTimer);
handler(e, clicks);
clicks = 0;
}
};
$(this).data('multiclick-state').push(
{
handler: handler,
clickHandler: clickHandler
});
$(this).click(clickHandler);
});
arguments[0] = arguments[0].replace('multiclick', '');
}
return oldOn.apply(this, arguments);
};
// events [, selector ] [, handler ]
// events [, selector ]
// events
// Note: this does not implement selector
$.fn.off = function(events, handler)
{
if (events.split(/\s+/).some(function(event) { return event == 'multiclick'; }))
{
$(this).each(function()
{
if (!Array.isArray($(this).data('multiclick-state')))
{
$(this).data('multiclick-state', []);
}
$(this).data('multiclick-state', $(this).data('multiclick-state').filter(function(state)
{
if (handler == undefined || state.handler === handler)
{
$(this).off('click', state.clickHandler);
return false;
}
return true;
}.bind(this)));
});
arguments[0] = arguments[0].replace('multiclick', '');
}
return oldOff.apply(this, arguments);
};
// eventType [, extraParameters ]
// event, [, extraParameters ]
// Note: I wrote this in case trigger is changed to handle multiple events. Currently it does not. Also I don't support the second overloaded signature
$.fn.trigger = function(events)
{
if (typeof events == 'string' && events.split(/\s+/).some(function(event) { return event == 'multiclick'; }))
{
$(this).each(function()
{
if (!Array.isArray($(this).data('multiclick-state')))
{
$(this).data('multiclick-state', []);
}
// This doesn't support disabling bubbling or stopPropogation, would have to think about it. Very ad-hoc.
$(this).data('multiclick-state').forEach(function(data)
{
data.clickHandler();
});
});
arguments[0] = arguments[0].replace('multiclick', '');
}
return oldTrigger.apply(this, arguments);
};
$.fn.multiclick = function(callback)
{
if (arguments.length == 1)
{
return $(this).on('multiclick', callback);
}
else
{
return $(this).trigger('multiclick');
}
return this;
};
}(jQuery));
I'm not really guaranteeing this is the most complete solution, but it shows the basic approach. Then it would be used like a normal jquery event:
function multiclick(e, clicks)
{
console.log(clicks);
}
// $(document).on('multiclick', multiclick);
$(document).multiclick(multiclick);
//$(document).off('multiclick');
//$(document).off('multiclick', multiclick);
jsfiddle example: https://jsfiddle.net/rhsswfho/1/
This all just seems like a lot of code for something that should be fairly straightforward or even built into jquery with helper functions, yet I find none. Not to mention it's somewhat fragile and adds a performance penalty to all on, off, and trigger calls.
What would be the standard approach to doing this, or the more standard approach for handling such a problem?
It is quite straightforward, like you said. You can use the .on() for any kind of event listening. So you could do something like:
$('body').on('takeMeToDinner', function() {
alert('who will pay the bills?');
});
and then trigger that event via
$('body').trigger('takeMeToDinner');
Jquery isn't designed around adding custom operations to on/off events since it's only implementing the DOM events. (Meaning there's no extra code being ran on a per event type basis). Rather than creating a localized solution on the elements this can be accomplished simply by creating a document or body listener like so:
var clicks = 0;
var lastTarget = null;
var clicksTimer = null;
$('body').click(function(e)
{
if (e.target !== lastTarget)
{
clearTimeout(clicksTimer);
lastTarget = e.target;
clicks = 0;
}
clicks++;
if (clicks == 1)
{
clicksTimer = setTimeout(function()
{
$(e.target).trigger('multiclick', clicks);
clicks = 0;
lastTarget = null;
}, 500);
}
else if (clicks == 3)
{
$(e.target).trigger('multiclick', clicks);
clearTimeout(clicksTimer);
clicks = 0;
lastTarget = null;
}
});
Then used like normal:
$('div').on('multiclick', function(e, clicks)
{
console.log(clicks);
});
$('span').on('multiclick', function(e, clicks)
{
console.log(clicks);
});
Example on jsfiddle: http://jsfiddle.net/f35u0fw2/
What's happening for this specific case is the user can click anywhere and then for elements that are listening they'll get the 'multiclick' event after 1, 2, or 3 clicks as expected.
I have to call another function before the original onclick event fires, I've tried a lot of different paths before I've come to following solution:
function bindEnableFieldToAllLinks() {
var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
var link = links[i];
var onclick = link.getAttribute('onclick');
link.onclick = new Function("if(linkClickHandler()){"+onclick+"}");
console.log(link.getAttribute('onclick'));
}
}
This does the trick in Firefox and Chrome but IE8 is acting strange, it seems that the function that's in the onclick variable isn't executed.
I've already added console.log messages that get fired after the if statement is true and if I print out the onclick attribute I get following:
LOG: function anonymous() {
if(linkClickHandler()){function onclick()
{
if(typeof jsfcljs == 'function'){jsfcljs(document.getElementById('hoedanigheidForm'), {'hoedanigheidForm:j_id_jsp_443872799_27':'hoedanigheidForm:j_id_jsp_443872799_27'},'');}return false
}}
}
So it seems that the function is on the onclick of the link and the old onclick function is on it as well.
Can anyone help me out with this please?
Say you have an onclick attribute on a HTMLElement..
<span id="foo" onclick="bar"></span>
Now,
var node = document.getElementById('foo');
node.getAttribute('onclick'); // String "bar"
node.onclick; // function onclick(event) {bar}
The latter looks more useful to what you're trying to achieve as using it still has it's original scope and you don't have to re-evaluate code with Function.
function bindEnableFieldToAllLinks() {
var links = document.getElementsByTagName('a'),
i;
for (i = 0; i < links.length; i++) function (link, click) { // scope these
link.onclick = function () { // this function literal has access to
if (linkClickHandler()) // variables in scope so you can re-
return click.apply(this, arguments); // invoke in context
};
}(links[i], links[i].onclick); // pass link and function to scope
}
Further, setting a named function inside an onclick attribute (i.e. as a String) doesn't achieve anything; the function doesn't invoke or even enter the global namespace because it gets wrapped.
Setting an anonymous one is worse and will throw a SyntaxError when onclick tries to execute.
This will do what you want, executing what is inside linkClickHandler first, and then executing the onclick event. I put in a basic cross browser event subscribing function for your reuse.
bindEnableFieldToAllLinks();
function bindEnableFieldToAllLinks() {
var links = document.getElementsByTagName('a');
for (var i = 0; i < links.length; i++) {
var link = links[i];
var onclick = link.getAttribute('onclick');
onEvent(link, 'click', function() {
linkClickHandler(onclick);
});
link.onclick = undefined;
}
}
function onEvent(obj, name, func) {
if (obj.attachEvent) obj.attachEvent('on' + name, func);
else if (obj.addEventListener) obj.addEventListener(name, func);
}
function linkClickHandler(funcText) {
alert('before');
var f = Function(funcText);
f();
return true;
}
jsFiddle
I understand the proper way to handle event.stopPropagation for IE is
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.returnValue = false;
}
But is it possible to prototype Event so that I don't have to do the check each and everytime I use stopPropagation?
This question seemed helpful: JavaScript Event prototype in IE8 however I don't quite understand the accepted answer and how it is a prototype that can essentially be set and forget.
Probably this:
Event = Event || window.Event;
Event.prototype.stopPropagation = Event.prototype.stopPropagation || function() {
this.cancelBubble = true;
}
returnValue = false is an analogue for preventDefault:
Event.prototype.preventDefault = Event.prototype.preventDefault || function () {
this.returnValue = false;
}
If you're doing your own event handling in plain javascript, then you probably already have a cross browser routine for setting event handlers. You can put the abstraction in that function. Here's one that I use that mimics the jQuery functionality (if the event handler returns false, then both stopPropagation() and preventDefault() are triggered. You can obviously modify it however you want it to behave:
// refined add event cross browser
function addEvent(elem, event, fn) {
// allow the passing of an element id string instead of the DOM elem
if (typeof elem === "string") {
elem = document.getElementById(elem);
}
function listenHandler(e) {
var ret = fn.apply(this, arguments);
if (ret === false) {
e.stopPropagation();
e.preventDefault();
}
return(ret);
}
function attachHandler() {
// normalize the target of the event
window.event.target = window.event.srcElement;
// make sure the event is passed to the fn also so that works the same too
// set the this pointer same as addEventListener when fn is called
var ret = fn.call(elem, window.event);
// support an optional return false to be cancel propagation and prevent default handling
// like jQuery does
if (ret === false) {
window.event.returnValue = false;
window.event.cancelBubble = true;
}
return(ret);
}
if (elem.addEventListener) {
elem.addEventListener(event, listenHandler, false);
} else {
elem.attachEvent("on" + event, attachHandler);
}
}
Or, you can just make a utility function like this:
function stopPropagation(e) {
if(e.stopPropagation) {
e.stopPropagation();
} else {
e.returnValue = false;
}
}
And just call that function instead of operating on the event in each function.
We can use this alone: event.cancelBubble = true.
Tested working in all browsers.
I create a hammer instance like so:
var el = document.getElementById("el");
var hammertime = Hammer(el);
I can then add a listener:
hammertime.on("touch", function(e) {
console.log(e.gesture);
}
However I can't remove this listener because the following does nothing:
hammertime.off("touch");
What am I doing wrong? How do I get rid of a hammer listener? The hammer.js docs are pretty poor at the moment so it explains nothing beyond the fact that .on() and .off() methods exist. I can't use the jQuery version as this is a performance critical application.
JSFiddle to showcase this: http://jsfiddle.net/LSrgh/1/
Ok, I figured it out. The source it's simple enough, it's doing:
on: function(t, e) {
for (var n = t.split(" "), i = 0; n.length > i; i++)
this.element.addEventListener(n[i], e, !1);
return this
},off: function(t, e) {
for (var n = t.split(" "), i = 0; n.length > i; i++)
this.element.removeEventListener(n[i], e, !1);
return this
}
The thing to note here (apart from a bad documentation) it's that e it's the callback function in the on event, so you're doing:
this.element.addEventListener("touch", function() {
//your function
}, !1);
But, in the remove, you don't pass a callback so you do:
this.element.removeEventListener("touch", undefined, !1);
So, the browser doesn't know witch function are you trying to unbind, you can fix this not using anonymous functions, like I did in:
Fiddle
For more info: Javascript removeEventListener not working
In order to unbind the events with OFF, you must:
1) Pass as argument to OFF the same callback function set when called ON
2) Use the same Hammer instance used to set the ON events
EXAMPLE:
var mc = new Hammer.Manager(element);
mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
mc.add(new Hammer.Tap());
var functionEvent = function(ev) {
ev.preventDefault();
// .. do something here
return false;
};
var eventString = 'panstart tap';
mc.on(eventString, functionEvent);
UNBIND EVENT:
mc.off(eventString, functionEvent);
HammerJS 2.0 does now support unbinding all handlers for an event:
function(events, handler) {
var handlers = this.handlers;
each(splitStr(events), function(event) {
if (!handler) {
delete handlers[event];
} else {
handlers[event].splice(inArray(handlers[event], handler), 1);
}
});
return this;
}
Here's a CodePen example of what Nico posted. I created a simple wrapper for "tap" events (though it could easily be adapted to anything else), to keep track of each Hammer Manager. I also created a kill function to painlessly stop the listening :P
var TapListener = function(callbk, el, name) {
// Ensure that "new" keyword is Used
if( !(this instanceof TapListener) ) {
return new TapListener(callbk, el, name);
}
this.callback = callbk;
this.elem = el;
this.name = name;
this.manager = new Hammer( el );
this.manager.on("tap", function(ev) {
callbk(ev, name);
});
}; // TapListener
TapListener.prototype.kill = function () {
this.manager.off( "tap", this.callback );
};
So you'd basically do something like this:
var myEl = document.getElementById("foo"),
myListener = new TapListener(function() { do stuff }, myEl, "fooName");
// And to Kill
myListener.kill();
I have quite a few of these:
function addEventsAndStuff() {
// bla bla
}
addEventsAndStuff();
function sendStuffToServer() {
// send stuff
// get HTML in response
// replace DOM
// add events:
addEventsAndStuff();
}
Re-adding the events is necessary because the DOM has changed, so previously attached events are gone. Since they have to be attached initially as well (duh), they're in a nice function to be DRY.
There's nothing wrong with this set up (or is there?), but can I smooth it a little bit? I'd like to create the addEventsAndStuff() function and immediately call it, so it doesn't look so amateuristic.
Both following respond with a syntax error:
function addEventsAndStuff() {
alert('oele');
}();
(function addEventsAndStuff() {
alert('oele');
})();
Any takers?
There's nothing wrong with the example you posted in your question.. The other way of doing it may look odd, but:
var addEventsAndStuff;
(addEventsAndStuff = function(){
// add events, and ... stuff
})();
There are two ways to define a function in JavaScript. A function declaration:
function foo(){ ... }
and a function expression, which is any way of defining a function other than the above:
var foo = function(){};
(function(){})();
var foo = {bar : function(){}};
...etc
function expressions can be named, but their name is not propagated to the containing scope. Meaning this code is valid:
(function foo(){
foo(); // recursion for some reason
}());
but this isn't:
(function foo(){
...
}());
foo(); // foo does not exist
So in order to name your function and immediately call it, you need to define a local variable, assign your function to it as an expression, then call it.
There is a good shorthand to this (not needing to declare any variables bar the assignment of the function):
var func = (function f(a) { console.log(a); return f; })('Blammo')
There's nothing wrong with this set up (or is there?), but can I smooth it a little bit?
Look at using event delegation instead. That's where you actually watch for the event on a container that doesn't go away, and then use event.target (or event.srcElement on IE) to figure out where the event actually occurred and handle it correctly.
That way, you only attach the handler(s) once, and they just keep working even when you swap out content.
Here's an example of event delegation without using any helper libs:
(function() {
var handlers = {};
if (document.body.addEventListener) {
document.body.addEventListener('click', handleBodyClick, false);
}
else if (document.body.attachEvent) {
document.body.attachEvent('onclick', handleBodyClick);
}
else {
document.body.onclick = handleBodyClick;
}
handlers.button1 = function() {
display("Button One clicked");
return false;
};
handlers.button2 = function() {
display("Button Two clicked");
return false;
};
handlers.outerDiv = function() {
display("Outer div clicked");
return false;
};
handlers.innerDiv1 = function() {
display("Inner div 1 clicked, not cancelling event");
};
handlers.innerDiv2 = function() {
display("Inner div 2 clicked, cancelling event");
return false;
};
function handleBodyClick(event) {
var target, handler;
event = event || window.event;
target = event.target || event.srcElement;
while (target && target !== this) {
if (target.id) {
handler = handlers[target.id];
if (handler) {
if (handler.call(this, event) === false) {
if (event.preventDefault) {
event.preventDefault();
}
return false;
}
}
}
else if (target.tagName === "P") {
display("You clicked the message '" + target.innerHTML + "'");
}
target = target.parentNode;
}
}
function display(msg) {
var p = document.createElement('p');
p.innerHTML = msg;
document.body.appendChild(p);
}
})();
Live example
Note how if you click the messages that get dynamically added to the page, your click gets registered and handled even though there's no code to hook events on the new paragraphs being added. Also note how your handlers are just entries in a map, and you have one handler on the document.body that does all the dispatching. Now, you probably root this in something more targeted than document.body, but you get the idea. Also, in the above we're basically dispatching by id, but you can do matching as complex or simple as you like.
Modern JavaScript libraries like jQuery, Prototype, YUI, Closure, or any of several others should offer event delegation features to smooth over browser differences and handle edge cases cleanly. jQuery certainly does, with both its live and delegate functions, which allow you to specify handlers using a full range of CSS3 selectors (and then some).
For example, here's the equivalent code using jQuery (except I'm sure jQuery handles edge cases the off-the-cuff raw version above doesn't):
(function($) {
$("#button1").live('click', function() {
display("Button One clicked");
return false;
});
$("#button2").live('click', function() {
display("Button Two clicked");
return false;
});
$("#outerDiv").live('click', function() {
display("Outer div clicked");
return false;
});
$("#innerDiv1").live('click', function() {
display("Inner div 1 clicked, not cancelling event");
});
$("#innerDiv2").live('click', function() {
display("Inner div 2 clicked, cancelling event");
return false;
});
$("p").live('click', function() {
display("You clicked the message '" + this.innerHTML + "'");
});
function display(msg) {
$("<p>").html(msg).appendTo(document.body);
}
})(jQuery);
Live copy
Your code contains a typo:
(function addEventsAndStuff() {
alert('oele');
)/*typo here, should be }*/)();
so
(function addEventsAndStuff() {
alert('oele');
})();
works. Cheers!
[edit] based on comment: and this should run and return the function in one go:
var addEventsAndStuff = (
function(){
var addeventsandstuff = function(){
alert('oele');
};
addeventsandstuff();
return addeventsandstuff;
}()
);
You might want to create a helper function like this:
function defineAndRun(name, func) {
window[name] = func;
func();
}
defineAndRun('addEventsAndStuff', function() {
alert('oele');
});
Even simpler with ES6:
var result = ((a, b) => `${a} ${b}`)('Hello','World')
// result = "Hello World"
var result2 = (a => a*2)(5)
// result2 = 10
var result3 = (concat_two = (a, b) => `${a} ${b}`)('Hello','World')
// result3 = "Hello World"
concat_two("My name", "is Foo")
// "My name is Foo"
If you want to create a function and execute immediately -
// this will create as well as execute the function a()
(a=function a() {alert("test");})();
// this will execute the function a() i.e. alert("test")
a();
Try to do like that:
var addEventsAndStuff = (function(){
var func = function(){
alert('ole!');
};
func();
return func;
})();
For my application I went for the easiest way. I just need to fire a function immediately when the page load and use it again also in several other code sections.
function doMyFunctionNow(){
//for example change the color of a div
}
var flag = true;
if(flag){
doMyFunctionNow();
}