I have a hierarchy of tags within my HTML which all contain onclick event handlers. The onclick is pushed onto the event stack from the leaf back through the root of the hierarchy. I only want to respond to the leaf onclick event. Can I flush the event stack rather than using a flag?
For instance:
<ul>
<li onclick="nada('1');">1</li>
<li onclick="nada('2');">2
<ul>
<li onclick="nada('2.1');">2.1</li>
<li onclick="nada('2.2');">2.2</li>
<li onclick="nada('2.3');">2.3</li>
</ul>
</li>
<li onclick="nada('4');">4</li>
<li onclick="nada('5');">5</li>
</ul>
Clicking on 2.2 using this function...
function nada(which)
{
alert(which);
}
...will result in two alerts for '2.2' and '2'.
What could I add to the nada function to eliminate the alert for '2'?
To stop the event bubbling up to parent elements you have to tell the event object about it. In IE, you set event.cancelBubble= true. In other browsers, you call event.stopPropagation().
You probably also want to turn off the default link-following action so that the browser doesn't keep jumping up to the top trying to follow the non-existing anchor links like #1. In IE, you set event.returnValue= false. In other browsers, you call event.preventDefault().
The event object is accessible as window.event on IE. On other browsers, it is passed into the event handler function. A way to pass the event into a function that works on both is:
<li onclick="nada('2.1', event);">2.1</li>
function nada(n, event) {
alert(n);
if ('stopPropagation' in event) {
event.stopPropagation();
event.preventDefault();
} else {
event.cancelBubble= true;
event.returnValue= false;
}
}
However it would probably be better all round to put the onclick event on the a element which it usually belongs. This helps for accessibility, as the a element will be focusable and keyboard-operable. And it means you don't have to worry about parents' click handlers being called.
(You can style the a to look like a plain block, if you want.)
You can then also kick out the redundant onclick links with a bit of unobtrusive scripting:
<ul id="nadalist">
<li>1</li>
<li>2
<ul>
<li>2.1</li>
<li>2.2</li>
<li>2.3</li>
</ul>
</li>
<li>4</li>
<li>5</li>
</ul>
<script type="text/javascript">
var links= document.getElementById('nadalist').getElementsByTagName('a');
for (var i= links.length; i-->0;) {
links[i].onclick= function() {
alert(this.hash.substring(1));
return false;
}
}
</script>
Related
I'm moving elements from one list to another, the issue is:
I have javascript binded functions to the <li> depending of the <ul>, if I click one <li> that is inside an <ul> with an specific ID, a function is excecuted.
When I move the <li> with success, it's assumed the <li> is already in the other <ul> (The destination one). but if I click the <li> in the new list, the event still bind with the source one <ul>
How do I do to bind the <li> with the current <ul>?
Thanks for your help.
<ul id="sourceList">
<li>element1</li>
<li>element2</li>
</ul>
<ul id="destinationList"></ul>
<script>
$('#sourceList li').click(function () {
$(this).appendTo('#destinationList');
console.log("from source");
});
$('#destinationList li').on("click", function () {
$(this).appendTo('#sourceList');
console.log("from destination");
});
</script>
Events are binded when the script is executed the first time so, in your code, every li from #sourceList has a click event attached to the first defined event listener, no matter if you move the element to another container or not. You need to use event delegation for that:
$('#sourceList').on('click', 'li', function () {
$(this).appendTo('#destinationList');
console.log("from source");
});
$('#destinationList').on('click', 'li', function () {
$(this).appendTo('#sourceList');
console.log("from destination");
});
I have a list item (#planAdminMenuItem) that has an onclick attribute. This list item has an icon inside of it (.spinner) that will collapse #collapseExample. Whenever the .spinner is clicked, I want it to run bootstrap collapse only. I do not want it to run drawPlanAdmin function. I have tried adding event.stopPropagation to my toggleSpinnerLeftMenu function, but whenever I do that, it also stops the bootstrap collapse. The parent click is blocked, but so is bootstrap collapse.
THE PHP & HTML CODE
<ul>
<li id="planAdminMenuItem" onclick="plan.drawPlanAdmin();">
Book Plan
<span class="icn icn-chevron-down spinner" onclick="ui.toggleSpinnerLeftMenu(this,event);" data-toggle="collapse" data-target="#collapseExample" data-aria-expanded="true" aria-controls="collapseExample"></span>
</li>
<!-- the collapsable area -->
<li id="collapseExample" class="collapse in">
<ul>
<li onclick="plan.drawRunListAdmin();">
Run List View
</li>
<li onclick="plan.drawLadderAdmin();">
Ladder View
</li>
</ul>
</li>
</ul>
THE JS CODE
toggleSpinnerLeftMenu:function(el,event){
el = jQuery(el);
if(el.hasClass('icn-chevron-up')){
el.addClass('icn-chevron-down');
el.removeClass('icn-chevron-up');
}else if(el.hasClass('icn-chevron-down')){
el.addClass('icn-chevron-up');
el.removeClass('icn-chevron-down');
}
event.stopPropagation(); //why is this stopping the collapse also?
},
stopPropagation is doing exactly what it is meant to do.
If you want the parent element to be propagated by the click on the inner element then simply don't do event.stopPropagation at all.
Though for some reasons if you need to have that then my suggestion is: call the function like
toggleSpinnerLeftMenu:function(el,event){
el = jQuery(el);
if(el.hasClass('icn-chevron-up')){
el.addClass('icn-chevron-down');
el.removeClass('icn-chevron-up');
}else if(el.hasClass('icn-chevron-down')){
el.addClass('icn-chevron-up');
el.removeClass('icn-chevron-down');
}
plan.drawPlanAdmin(); // Call the function inside of the child element's click handler.
event.stopPropagation(); //why is this stopping the collapse also?
},
Update: Since you described the issue more clearly in the comment, which has a solution completely south of what I've written above, I am updating with the new content that may be able to help.
Instead of attaching two event handlers, one using an inline onClick attribute and another using Bootstrap's data-collapse, use one:
$(".spinner").on("click", function(event) { // tip: Give a better id or class name
$('#collapseExample').collapse({toggle: true});
ui.toggleSpinnerLeftMenu(this, event);
});
This is the general idea of doing this, you may still have to make some adjustments to your method calls to fit it in.
I have made a basic mobile navigation dropdown menu that makes use of nested tags to open the second (and third) level of the navigation. However because the elements are nested in anchor tags, they also trigger the default anchor events. Is there any way to prevent the anchors default event when clicking on a child element of said anchor ?
HTML
<ul class="main-menu">
<li>
<a href="some-link.html">Some Link
<span class="target"></span>
</a>
<ul class="submenu">
<li>Some Link</li>
<li>Some Link</li>
<li>Some Link</li>
</ul>
</li>
</ul>
Example Javascript
$('span.target').on('click', function(event) {
$(event.target).parent().preventDefault();
// Do somthing
});
You could stop the propagation of the event, and cancel it's default behavior. See this:
$('span.target').on('click', function(event) {
event.stopPropagation();
event.preventDefault();
// the rest of your code here...
});
As mentioned in the comments, I add those 2 lines as part of my 'automatic' workflow, in case I'm binding to an anchor tag (a) and prevent the default behavior of navigating away from the page (that's preventDefault()), and stopPropagation to avoid the parent elements' bound events from being triggered.
You can try this code. When you click on the child on anchor it prevents default behaviour of the parent anchor element. I use core javascript here:
var menuAnchor = document.querySelectorAll('.main-menu a');
for(var i = 0, len = menuAnchor.length; i < len; i++){
menuAnchor[i].addEventListener('click', function(event){
if(event.target != this) event.preventDefault();
});
}
Hey so here is the code demo I made
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
</head>
<body>
<ul contenteditable class="outerList">
<li class="innerElement">Hello</li>
<li class="innerElement">World</li>
<li class="innerElement">Hello World</li>
</ul>
<script>
$(".outerList").keydown(function () {
console.log("I am the outer ul");
});
$(".innerElement").keydown(function() {
console.log("I am an inner element");
});
</script>
</body>
</html>
and here is the jsFiddle to run it
http://jsfiddle.net/scrbovyr/
Basically I have a content editable UL and I want to catch the enter key and pass in my own custom function. But I need to know which LI element the keydown event was thrown on. And as shown in the demo, I can only seem to tie a keydown event listener (or any event listener for that matter) to the outer UL element. Is there a way to attach the keydown event to each LI? Or is there at least a way to attach it to the UL but still tell which child it came from?
Thanks in advance, let me know if any additional information would help!
You will have to add contenteditable to your li elements in order to achieve that. You are setting contenteditable to your ul element, thus, the event will be binded to that element, you may edit the li elements, but they do not have contenteditable set, so the keyboard events won't be triggered for those elements.
<ul class="outerList">
<li contenteditable class="innerElement">Hello</li>
<li contenteditable class="innerElement">World</li>
<li contenteditable class="innerElement">Hello World</li>
</ul>
And then:
$(".innerElement").keydown(function() {
console.log("I am an inner element");
});
You may check the node at the current selection
If you don't want to make each li a contenteditable element, you may get the element at the current selection or caret position and perform a check against it.
The embedded example shows how you would achieve this using the Web API Interface for contenteditable selections. (I tested this in Chrome, but it may need additional logic to achieve cross-browser compatibility).
It is also worth noting that you can bind some event listeners to the children of a contenteditable element. For example, the click event may be bound to the li elements as you can see in the embedded example.
$(document).ready(function() {
function getCurrentNode() {
var node = window.getSelection().getRangeAt(0).commonAncestorContainer;
return node.nodeType === 1 ? node : node.parentNode;
}
$('.outerList').on('click keyup', function (e) {
var $target = $(getCurrentNode()),
$closest = $target.closest('b');
console.log(e.type);
console.log('I am the outer ul');
console.log($target);
// Optional. Filter by clostest selector.
if ($closest.length) {
console.log('Target matches selector', $closest);
}
});
$('.innerElement').on('click', function (e) {
console.log(e.type);
console.log('I am an inner element');
});
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul contenteditable class="outerList">
<li class="innerElement">Hello</li>
<li class="innerElement"><i>Hello</i></li>
<li class="innerElement"><b><i>Hello</i></b></li>
<li class="innerElement"><b>Hello</b></li>
<li class="innerElement">Hello</li>
<li class="innerElement">Hello</li>
</ul>
I keep getting puzzled, can't find a contextmenu that will work for me. Maybe someone can help?
Here's to what I need contextMenu to be added to:
<ul id="list_{id}" class="list">
<li id="Item_{id}"><a ondblclick=""><span>{title}</span></a></li>
</ul>
This is dynamic list, so it will add many more of them on the page and differ them by giving different ID's. So I need a contextMenu which will be added to every list but for each list an unique contextMenu. Basically different instances of contextMenu in every list, by adding dynamic {id} tag to the ID of contextMenu or something like that.
Thanks
It's kind of hard to tell what you're asking, but if you want to hook into the "context menu" event of a browser, you hook the contextmenu event and then do whatever you're going to do (which could include creating a div, for instance, with options on it — e.g., your own context menu). You can either do that on the lists themselves, individually, via getElementById as you indicated in your question, or you can do it by hooking the event on some container that holds all of the lists, and then figuring out when the event is triggered which list it was triggered on ("event delegation").
See the end of this answer for the event delegation approach. But assuming you have a way of knowing the actual IDs used and you want to hook each list specifically for some reason:
HTML:
<ul id='list_1'>
<li>List 1 item 1</li>
<li>List 1 item 2</li>
</ul>
<ul id='list_2'>
<li>List 2 item 1</li>
<li>List 2 item 2</li>
</ul>
JavaScript:
hookEvent(document.getElementById('list_1'), 'contextmenu', function(event) {
event = event || window.event;
if (event.preventDefault) {
event.preventDefault();
}
display("List 1 context menu");
return false;
});
hookEvent(document.getElementById('list_2'), 'contextmenu', function(event) {
event = event || window.event;
if (event.preventDefault) {
event.preventDefault();
}
display("List 2 context menu");
return false;
});
function hookEvent(element, event, handler) {
if (element.addEventListener) {
element.addEventListener( event, handler, false);
}
else if (element.attachEvent) {
element.attachEvent('on' + event, handler);
}
else {
element['on' + event] = handler;
}
}
Live example
Note that only some (most) browsers let you cancel the default context menu.
Update: Re your "but what if the ID is bindable?" question below: I'm afraid I don't know what you mean by "bindable" — none of the tags on your question indicates a specific templating technology. You haven't even mentioned whether the templating is happening server-side or client-side, which makes it hard to help. But basically, by the time the JavaScript is running, there will be real IDs on real elements in the document. You'll have to know what those IDs are in order to use getElementById.
Server-side templating:
If those IDs are going to be completely dynamic and the template is being handled on the server, you can include a small bit of script that passes those IDs on to JavaScript. For instance, near the top of your document you might have:
<script type='text/javascript'>
var mySpecialListIDs = [];
</script>
...and then update your template to add a small script tag each time it's expanded:
<ul id="list_{id}" class="list">
<li id="Item_{id}"><a ondblclick=""><span>{title}</span></a></li>
</ul>
<script type='text/javascript'>
mySpecialListIDs.push("{id}");
</script>
Then your client-side code can loop through mySpecialLitIDs and use each ID when hooking up the handler.
Client-side templating:
If the templating is being done client-side, this gets a bit simpler: Just set up your mySpecialListIDs list at some convenient place in your client-side script, and the append to it each time you call the templating engine.
Event Delegation: Whether you're doing server- or client-side templating, if you're going to have dynamic lists like this, sometimes event delegation is the best way to handle it. The contextmenu event (like most, but not all, events) bubbles up the DOM. So if you hook it on an ancestor element (something that contains all of your lists, like the document body itself or some such), you can then see which actual list was clicked by examining the event object. Like this:
HTML:
<div id='list_container'>
<ul id='list_1'>
<li>List 1 item 1</li>
<li>List 1 item 2</li>
</ul>
<ul id='list_2'>
<li>List 2 item 1</li>
<li>List 2 item 2</li>
</ul>
</div>
JavaScript (using the hookEvent function from above):
// Hook up the contextmenu event on the container, not
// on each list:
hookEvent(document.getElementById('list_container'),
'contextmenu',
handleListContextMenu);
// Our handler function
function handleListContextMenu(event) {
var target;
// Handle IE-vs-the-world difference
event = event || window.event;
// Find out what the actual target element clicked was
target = event.target || event.srcElement;
// See if it or an ancestor of it is one of our lists
while (target &&
(target.tagName !== "UL" || !target.id || target.id.substring(0, 5) !== "list_")) {
target = target.parentNode;
}
// Did we find a list?
if (target) {
// Yes, handle this.
if (event.preventDefault) {
event.preventDefault();
}
display("List '" + target.id + "' context menu");
return false;
}
}
Live example