I'm trying to build a custom navigation menu with 3 options. Initially, only the active option is visible. Clicking on the active option shows the other options, and upon clicking on another one, it is prepended at the beginning of the menu and the other list items are once again hidden.
// html
<ul>
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
// css
li {
display: block;
}
li:not(:first-child) {
display: none;
}
// js
$(function(){
$('li:first-child').on('click', function(){
$(this).siblings().toggle()
});
$('li').not(':first-child').on('click', function(){
$(this).prependTo('ul')
$(this).siblings().hide()
});
});
http://jsfiddle.net/H85Yj/
However, the only issue is that after it executes once, it won't run again. I'm guessing that the li:first-child still remains as the first option. Any way I can work around this?
The (undocumented, so far as I can see, in the API for prependTo()) problem you appear to be experiencing is that, once you move the li element from the HTML, the event-binding is not transferred with them; therefore clicking on the li no longer triggers an event. The easiest way around that is to bind the events to the parent ul element, and handle the events there (as the click events bubble up through the DOM and are acted upon by the ancestor).
Therefore, I'd suggest:
$('ul').on('click', 'li', function(){
var self = $(this),
siblings = self.siblings();
if (siblings.filter(':visible').length) {
self.prependTo(self.parent());
siblings.hide();
}
else {
siblings.toggle();
}
});
JS Fiddle demo.
Although, on reflection, the following seems more simple:
$('ul').on('click', 'li', function(){
var _self = $(this);
if (_self.is(':first-child')) {
_self.siblings().toggle();
}
else {
_self.prependTo(_self.parent()).siblings().hide();
}
});
JS Fiddle demo.
Note that I've adjusted the CSS a little, too (to use simple CSS rather than SCSS); setting the display: none as the default rule for the li elements, and display: block for the li:first-child element (as opposed to the needlessly-complex :not(:first-child) rule you used originally).
References:
:first-child selector.
:visible selector.
filter().
is.
on().
parent().
prependTo().
siblings().
Related
I have a button with toggle class attached to it - and it should work only when the borwser size is below 1200px. It works after refresh but somehow when I resize the window it sometimes works and sometimes doesn't - can't see the pattern. I see on the dev tools the the element is higlighted (so hte script is doing something) but I doesn't toggle the class. Tried to change it to addClass/removeClass but the result is the same. Any advice how to make it work would be much appreciated.
CodePen: http://codepen.io/miunik/pen/oLWOLY
HTML:
<ul class="level-1">
<li class="btn">1 level item
<ul class="level-2">
<li>2 level item</li>
<li>2 level item</li>
<li>2 level item</li>
<li>2 level item</li>
</ul>
</li>
</ul>
CSS
ul.level-2 {
display: none;
}
ul.level-2.open {
display: block;
}
jQuery:
$(document).ready(function() {
function setNav() {
if (window.outerWidth < 1200) {
$('.btn').on({
click: function() {
$(this).children('.level-2').toggleClass('open');
}
});
}
}
setNav()
$(window).resize(function() {
setNav();
console.log(window.outerWidth);
});
});
The problem with your code is, that you bind a lot of event handlers: every time the window resize event occurs, each <li> tag (not only the one in level 1) gets a new one. So it depends on the number of event handlers if toggleClass() actually toggles or doesn't.
I would only bind one handler, preferably on the document in conjunction with a selector which identifies only the <li> tags directly below .level-1 and ask for the screen size in this handler. You don't even need a resize handler for that.
$(document).ready(function() {
$(document).on("click", ".btn", function () {
if (window.outerWidth < 1200) {
$(this).children('.level-2').toggleClass('open');
}
});
});
See a working example here: https://jsfiddle.net/wu1unvek/
It might be that you want a resize() handler anyway to remove the open class if the window gets larger:
$(window).resize(function () {
if (window.outerWidth >= 1200) {
$(".level-2").removeClass("open");
}
});
EDIT: Adapted to modified question code: Use .btn instead of .level-1 > li
EDIT 2: Added example for resize() resetting the open class if window gets larger
if i make a hover over my menulinks currently all submenues which are on the first level will be shown
i dont know whats wrong, see my code:
$('#menu li').hover(function () {
//show its submenu
$('ul', this).slideDown(100);
}, function () {
//hide its submenu
$('ul', this).slideUp(100);
});
so in my opinion it must work very well because a hover over a link should only display this first submenu. But also the submenu of this first link will show directly by a hover and i dont know how to fix it better than yet.
need some help please.
For a better understanging i hve created a fiddle here.
Your selector in your hover functions are finding all ul elements that are descendants of the li element. You want to show only direct children. Try this instead:
$('#menu li').hover(function() {
//show its submenu
$(this).children('ul').slideDown(100);
}, function() {
//hide its submenu
$(this).children('ul').slideUp(100);
});
The <ul> which is holding your submenus also contains the sub-submenus. So if you display the first list, it automatically also shows all elements contained in that list.
you should separate the ul for different levels of submenu using different class for different levels of menus.
if you want to just change your code you might want to try this change:
//show its submenu
$('ul', this).eq(0).slideDown(100);
You need to specify the list you want to show. Use $(this) as context to find the <ul> inside, and then filter the result with the :first pseudo-selector.
Try something like this for both hoverIn and hoverOut events:
$("#menu").on('hover', 'li', function(e){
// $(this) refers to the <li> being hovered
$(this).find('ul:first').slideToggle(100);
});
See the docs for on() and slideToggle() methods.
As the jQuery API is currently down, is anyone able to assist me with the below? I am ajax loading an unordered list into the web page and need to be able to attach hover and click events to the list items.
<ul>
<li class="option">Item 1</li>
<li class="option">Item 1</li>
<li class="option">Item 1</li>
</ul>
So far I have tried a few variations of the below jQuery code using .on for version 1.7+
$("ul").on("click", "li .option", function(){
alert($(this).text());
});
Can anyone point me in the right direction? I'm aware that .live has been deprecated and that .delegate has been superseded so really only looking for a solution that will allow me to use .on.
Not li .option, because it find element within li with class option, but you have this class to li, so it will be li.option or .option.
So for .on(), it looks like:
$("ul").on("click", "li.option", function(){
alert($(this).text());
});
But for .delegate(), it looks like:
$("ul").delegate("li.option", "click", function(){
alert($(this).text());
});
According to you edit
you're trying to bind click to li.option with container reference ul, which is also append to DOM alter. So you can go for #content, which already exists in DOM ans where you append you whole list.
So delegate event will looks like:
$("#content").delegate("ul > li.option", "click", function(){
alert($(this).text());
});
http://jsfiddle.net/walkerneo/QqkkA/
I've seen many questions here either asking about or being answered with event delegation in javascript, but I've yet to see, however, how to use event delegation for elements that aren't going to be the targets of the click event.
For example:
HTML:
<ul>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
<li><div class="d"></div></li>
</ul>
CSS:
ul{
padding:20px;
}
li{
margin-left:30px;
margin-bottom:10px;
border:1px solid black;
}
.d{
padding:10px;
background:gray;
}
What if I want to add a click event to handle the li elements when they're clicked? If I attach an event handler to the ul element, the divs will always be the target elements. Apart from checking every parent of the target element in a click function, how can I accomplish this?
edit:
I want to use event delegation instead of:
var lis = document.getElementsByTagName('li');
for(var i=0;i<lis.length;i++){
lis[i].onclick = function(){};
}
But if I do:
document.getElementsByTagName('ul')[0].addEventListener('click',function(e){
// e.target is going to be the div, not the li
if(e.target.tagName=='LI'){
}
},false);
EDIT: I'm not interested in how to use Javascript libraries for this, I'm interested in how they do it and how it can be done with pure js.
Here's one way to solve it:
var list = document.getElementsByTagName('ul')[0]
list.addEventListener('click', function(e){
var el = e.target
// walk up the tree until we find a LI item
while (el && el.tagName !== 'LI') {
el = el.parentNode
}
console.log('item clicked', el)
}, false)
This is overly simplified, the loop will continue up the tree even past the UL element. See the implementation in rye/events for a more complete example.
The Element.matches, Node.contains and Node.compareDocumentPosition methods can help you implement this type of features.
There is now a method on elements called closest, which does exactly this. It takes a CSS selector as parameter and finds the closest matching ancestor, which can be the element itself. All current versions of desktop browsers support it, but in general it is not ready for production use. The MDN page linked above contains a polyfill.
Mootools Events works just on first click, after stops working.
Hope someone have issue for that: http://jsfiddle.net/3j3Ws/
CSS
ul li,li.selected div{
width:22px;
height:22px;
display:block;
background:#000;
color:#fff;
text-align:center;
border-radius:3px;
}
ul#list{
display:none;
opacity:0;
float:left;
}
HTML
<ul id="option">
<li class="selected" id="a">a</li>
<ul id="list">
<li id="b">b</li>
<li id="c">c</li>
<li id="d">d</li>
</ul>
</ul>
Mootools JavaScript
window.addEvent('domready', function(){
var x = '<div>v</div>';
$$('ul#option li.selected').set('html',x);
var opt = $$('ul#option li.selected div');
var d = opt.getStyle('display');
var l = document.id('list');
var list = opt.set('morph').addEvents({
click:function(){
l.store('timerA',(function(){
l.morph({
'display':'block',
'opacity':1
});
$$('ul#option li.selected').setStyle('background-color','#fff');
$$('ul#option li.selected div').destroy();
}).delay(10,this));//$clear(this.retrieve('timerA'));
}
}
);
l.set('morph').addEvents({
mouseleave:function(el){
this.store('timerB',(function(){
this.morph({
'display':d,
'opacity':0
});
$$('ul#option li.selected').removeProperties('style');
$$('ul#option li.selected').set('html',x);
}).delay(500,this));//$clear(this.retrieve('timerB'));
}
});
});
odd writing style you have.
anyway. it is the destroy. the events are not delegated. i.e. your selector is the first div but that's a physical element that gets a UID and a functional cllback against that.
by doing .destroy() you are removing this div from the dom and even if you reinsert it after, because you don't use event delegation, the event will no longer work (events are part of element storage so destroy removes them too).
check out http://jsfiddle.net/dimitar/3j3Ws/1/ -> proves it can work fine (i added mootools more for easy .show() and .hide() but you can just use .setStyle("display", "none").
alternatively, look at doing an event for document.id("option") as click:relay(div.down) and mod the x html to have class='down' - then the code you have at the moment will keep.
It's most likely this:
$$('ul#option li.selected div').destroy();
At that point, you're deleting the <div>v</div> that you inserted earlier and had attached the click event to.
In the mouseleave later, you do:
$$('ul#option li.selected').set('html',x);
which recreates the div, but has not also reattached the click handler to the new copy.
comment followup:
when you use the .set('html', x), you're replacing the original node with a new one, which also replaces the event handlers. Handlers are attached to an actual node, not to the node's location in the DOM tree.