Too many listeners for document? - javascript

Setup
I've got a reusable custom dropdown menu, the rough framework is something like:
var List = (function() {
// Custom list prototype stuffs...
function ListObj(el, options) {
this._init(el);
}
ListObj.prototype._init = function(el) {
var self = this;
self.menu = el;
}
// Expose an init function
return {
init : function(el, options) {
new ListObj(el, options);
}
};
})();
And each menu is initialized with something like:
var ddlist = document.querySelector('.ddlist');
DDList.init(ddlist, options);
This all works as expected.
What I want
One behaviour I'd like each menu to have is to automatically minimize if a click occurs that isn't within the menu itself.
What I've done
Instead of having a single click listener for the document, and then having to add each menu and check where the click was, I've decided to add the document click listener within the _init method above:
ListObj.prototype._init = function(el) {
var self = this;
self.menu = el;
document.addEventListener('click', function(e) {
var el = e.target;
if(self.menu === el || self.menu.contains(el)) {
// Click was inside menu
// Perform whatever tasks
}else {
// Click was outside of (this) menu, so minimize
self.menu.minimize();
}
});
}
This way, each individual menu is automatically initialized with the behaviour to monitor document clicks, and either minimize if click is not within the menu, or perform whatever task is required.
This works perfectly. Of particular interest to me is that I can dynamically create new dropdown menus without having to add them to the document click listener, and I don't have to fight with event.stopPropagation(); (I'd rather cut off my foot).
But... Will my computer explode?
This is all for a single-page webapp, which means that many dozens or hundreds of these menus could be created (and removed). My concern is that all these document click listeners will pile up and cause performance issues.
If I do something like...
document.getElementById('someMenu').remove();
will JS garbage collection know that it can do away with the one document click listener? Or will the listener persist until the end of days? If the latter, is there any way that I can remove that particular listener when the menu is removed?
An important caviate is that the menu will likely never directly be removed, but rather its parent will be removed - so .remove() will never act on the menu directly.
Much thanks!
Just tested...
I created two menus and then deleted one of them (.remove()). Even though the one menu was removed, the document click listener within still fires for every click. This seems to suggest that garbage collection will not handle this mess, and I will end up with countless listeners.
So, now what?

Give the function a name, so you can then remove the event listener.
ListObj.prototype._init = function(el) {
var self = this;
self.menu = el;
self.click_handler = function(e) {
var el = e.target;
if (!document.contains(self.menu)) { // This element has been removed from DOM
document.removeEventListener("click", self.click_handler);
return;
}
if(self.menu === el || self.menu.contains(el)) {
// Click was inside menu
// Perform whatever tasks
}else {
// Click was outside of (this) menu, so minimize
self.menu.minimize();
}
};
document.addEventListener("click", self.click_handler);
}

Related

How to determine which widget was clicked? Dojo

Is there a way to observe the mouse click event and determine which widget was clicked?
So basically I wish I could do something like this (on mouse click anywhere on the page)
on("click", function (e) {
//var aWidget = dijit.getEnclosingWidget(e.target);
//var id = aWidget.id
//do something based on the widget id
});
You can but it will requires some extra steps.
Basically, the logic is:
- When you click on a node, you have to go up in the DOM until you find a node with the attribute widgetId
- When you have it, use dijit/registry::byNode to fetch the widget.
If you skip the node traversing, you will find the widget only if the main domNode of the widget was clicked.
require(['dojo/on', 'dijit/registry'], function(on, registry){
on(document, 'click', function(event){
var target = event.target,
widget;
while(!target.getAttribute('widgetId') && target.parentNode) {
target = target.parentNode;
}
widget = registry.byNode(target);
console.warn(widget);
});
});
Be aware, this method will work ONLY if you have 1 dojo instance loaded on in the page.
If you page have more than one dojo instance then the document click event must be attached by every dojo instances.
Might be a lot of overhead but yes you can
require(["dojox/mobile/deviceTheme","dojo/on","dijit/registry"],function(theme,on,reg){
on(document,'click', function(e){
console.log(e.target);
var widget = reg.byNode(e.target);
console.log("foundWidget::" + widget.id);
});
});

KnockoutJS: Checkbox Inside Click-Bound Anchor-Tag not Selectable

I am trying to bind checked on a checkbox input which resides inside an anchor tag which, itself, is click bound.
Whilst I am aware that this may not be entirely valid (interactive content may not be descendant of anchor-tags), I would still like to get it to work as intended - even if just to understand it.
Currently, only the outside click event is handled and the click never arrives at my checkbox.
An example of what I am trying to achieve is here: http://jsfiddle.net/fzmppu93/2/
Having had a look through the KnockoutJS documentation, I tried clickBubble: true on the anchor-tag's click binding - to no avail.
The use case, if you're interested, is an unordered list containing links - each of these "links" contains information on a TV show: title, actors, image, synopsis. The show is selectable, but there are also 'quick-actions' to mark it as seen, star it, and so forth.
Is there another way of making a checkbox work inside an anchor-tag?
I have written a custom binding handler that is similar to "clickBubble", however mines allows to you to prevent the propagation of any event.
Here is the binding handler:
ko.bindingHandlers.preventBubble = {
init: function (element, valueAccessor) {
var eventName = ko.utils.unwrapObservable(valueAccessor());
var arr = eventName;
if (!eventName.pop) {
arr = [arr];
}
for (var p in arr) {
ko.utils.registerEventHandler(element, arr[p], function (event) {
event.cancelBubble = true;
if (event.stopPropagation) {
event.stopPropagation();
}
});
}
}
};
And here is a working fiddle of your example.

Sencha Touch list and scrollToRecord

I have a long sencha touch list and I am trying desperately to scroll that list into somewhere in the middle on startup.
The function
btnScroll:function() {
var list = Ext.getCmp("myList");
var position = 4;
list.scrollToRecord(list.getStore().getAt(position));
}
works if I tap the button: scrollBtn.setHandler(btnScroll);, but it does not work in
MyList.constructor:function() {
...
this.callParent();
this.btnScroll();
}
It just fails without error message. Same goes if I do it in the calling function:
btnOpenList:function() {
var list = new MyList();
list.show();
list.btnScroll();
}
It works sometimes(!) if I use setTimeout(this.btnScroll,70); instead, but never if I use setTimeout(this.btnScroll,50); or setTimeout(this.btnScroll,120);
I think it happens because the list elements are not rendered/positioned. But I did not find any afterrender event.
So how would I define a scroll position in which the list should start?
You could do it in painted event. just add to your list or to container that contains all components that u need to bind the painted event.. and then get your list there and add event to btn.. or whatever you want.. by the template:
element.clearListeners();
element.addListener('tap',function(){
//...
});

Make (possibly dynamically loaded) element clickable via JavaScript, but give precedence to links contained within

I am adding a custom data attribute data-js-href to various HTML elements, and these elements should behave just like a link when clicked. If a link within such an element is clicked, the link should take precedence and the data-js-href functionality should be ignored, though. Furthermore, the solution also needs to work with elements that are dynamically added at a later time.
So far, I have come up with the following solution. It basically checks if the click was performed on a link, or any child element of a link (think <a href='…'><img src='…' alt='…' /></a>).
// Make all elements with a `data-js-href` attribute clickable
$$('body').addEvent('click:relay([data-js-href])',
function(event, clicked) {
var link = clicked.get('data-js-href');
if (link && !event.target.match('a')) {
var parents = event.target.getParents();
for (var i = 0; i < parents.length && parents[i] != clicked; i++) {
if (parents[i].match('a')) {
return;
}
}
document.location.href = link;
}
});
It works, but it feels very clumsy, and I think that there has to be a more elegant solution. I tried something along the lines of
$$('body').addEvent('click:relay([data-js-href] a)',
function(event, clicked) {
event.stopPropagation();
}
but to no avail. (I littered the code with some console.log() messages to verify the behavior.) Any idea is welcome.
you can do this with 2 delegated events - no reverse lookups and it's cheap as they will share the same event. the downside is, it is the same event so it will fire for both and there's no stopping it via the event methods (already bubbled, it's a single event that stacks up multiple pseudo event callbacks and executes them in order--the event has stopped but the callbacks continue) That's perhaps an inconsistency in mootools event vs delegation implementation but it's a subject of another issue.
Workarounds for now can be:
to have the 2 event handlers communicate through each other. It will scale and work with any new els added.
to add the delegators on 2 different elements. eg. document.body and #mainWrap.
http://jsfiddle.net/dimitar/J59PD/4/
var showURL = function(howLong) {
// debug.
return function() {
console.log(window.location.href);
}.delay(howLong || 1000);
};
document.id(document.body).addEvents({
"click:relay([data-js-href] a))": function(e) {
// performance on lookup for repeat clicks.
var parent = this.retrieve("parent");
if (!parent) {
parent = this.getParent("[data-js-href]");
this.store("parent", parent);
}
// communicate it's a dummy event to parent delegator.
parent.store("linkEvent", e);
// let it bubble...
},
"click:relay([data-js-href])": function(e) {
// show where we have gone.
showURL(1500);
if (this.retrieve("linkEvent")) {
this.eliminate("linkEvent");
return;
}
var prop = this.get("data-js-href");
if (prop)
window.location.href = prop;
}
});
Discussed this with Ibolmo and Keeto from the mootools team on IRC as well when my initial attempt failed to work and both callbacks fired despite the event.stop: http://jsfiddle.net/dimitar/J59PD/
As a result, there was briefly a ticket open on the mootools github issues: https://github.com/mootools/mootools-core/issues/2105 but it then went into a discussion of what the right thing to do from the library standpoint is and how viable it is to pursue changing the way things work so...

preventing effects to be applied to the same object twice when adding objects with ajax

I'm having a little issue with an application I'm making. I have a page where the user edits a document via dragging modules into the page or "canvas" area.
http://thinktankdesign.ca/temp_img.jpg
When the page is loaded, javascript haves the modules collapsible (like above). However after the user drags in a new module the effect is applied again some new modules can collapse as well. here is the problem. each time a module loads the same effect gets applied to the modules that already can collapse. It ends up breaking their animations.
heres the code that gets executed on page load.
//make colapsible
$("h1.handle").click(function() {
var object = $(this);
v$(this).next().toggle("fast", colapsible_class(object));
vreturn false;
}).addClass("open");
and heres the code that gets executed in the creation of a module via ajax
function get_module(id){
var template = $('input[name=template]').val();
$.post(window.location.href, { template: template, module: id, mode: 'create' },
function(data){
$(data).insertBefore(".target_wrapper");
//enable deletion of module
$(".js_no_modules").slideUp("slow");
$(enable_module_deletion());
//show delete button
$("button[name=delete]").show();
//make colapsible
$("h1.handle").click(function() {
var object = $(this);
$(this).next().toggle("fast", colapsible_class(object));
return false;
}).addClass("open");
}
);
}
I need a solid way of preventing the toggle effect to be applied to the same module twice
Use jQuery 1.3 live events instead.
//make colapsible
$("h1.handle").live("click", function() {
var object = $(this);
v$(this).next().toggle("fast", colapsible_class(object));
vreturn false;
}).addClass("open");
and then eliminate the click declaration in the second block of code, changing it to $("h1.handle").addClass("open");
Live events bind all current and future matching elements with an event.
In your Ajax success handler try the following:
//make collapsible
$("h1.handle:not(.open)").click(function() {
var object = $(this);
$(this).next().toggle("fast", colapsible_class(object));
return false;
}).addClass("open");
The best way to solve your problem is, instead of using $("h1.handle") on the AJAX callback, go for $(data).find("h1.handle"). Something like,
var x = $(data);
x.insertBefore(...);
/* your other code */
x.find('h1.handle').click(...).addClass(...);
Like that, only the newly added items will have the event bounded. The already present ones will not be touched.
If we want to answer your question instead of just solving your problem, then we have several alternatives, such as:
store, in your objects, that the onclick event handler has been set so that you don't set it twice
always bind the onclick event, but always unbind it first
use jQuery's live events and the addClass open only on the newly created items.
IMO, the first one is the easiest. You can accomplish it by using jQuery's data(). Then you could do something like:
$("h1.handle").each(function() {
var me = $(this);
// if already has click handler, don't do anything
if (me.data('click_set') != null) { return true; }
// otherwise, store the data and bind the click event
me.data('click_set', true).click(function() {
/* the code you already have on the click handler */
}).addClass('open');
}
The second alternative involves storing the function that you pass inline to the click event binder in a variable, and then using jQuery's unbind to disable it.

Categories