Displaying nested HTML ul list in columns - javascript

I have an autogenerated nested list structure, like so
<ul>
<li>AAA</li>
<li>BBB
<ul>
<li>111
<ul>
<li>XXX</li>
</ul>
</li>
<li>222</li>
</ul>
</li>
<li>CCC</li>
...etc...
</ul>
I want to layout in columns like so:
AAA 111 XXX
BBB 222
CCC
Using JQuery and a few CSS tags, it's then relatively easy to create a navigation style menu. e.g. select BBB from the first column, then this makes its children appear in the second column. Any other second level depth ULs are hidden.
What's leaving me stuck is simply how to style the list in the first place to put the depths into columns. I can add tags to each UL or LI to show the depth. But if I simply use relative positioning and move each column left, then column 1 will leave a vertical gap where each of the entries have been moved across. Absolute positioning works, but doesn't seem too neat. Any better ideas?

Using recursive functions this can be quite straight-forward: http://jsfiddle.net/uvxfm/1/.
If you want interactivity you could save which elements are children of which parent and show the appropriate ones on click.
var $tr = $("#tr");
function text(x) { // returns text without children
return x.clone()
.children()
.remove()
.end()
.text();
}
function add(elem, level) {
elem.children().each(function() { // iterate children
var $this = $(this);
var appendToThis = $tr.find("td").eq(level); // find td to append this level to
var appendedText = text($this) + "<br>"; // text to append
if(appendToThis.length) { // if td exists
appendToThis.append(appendedText);
} else { // if td doesn't exist yet
$tr.append($("<td>").append(appendedText));
}
var children = $this.children();
if(children.length) { // call recursively for children, if any
add(children, level + 1);
}
});
}
add($("ul:eq(0)"), 0); // start the process
References: http://viralpatel.net/blogs/2011/02/jquery-get-text-element-without-child-element.html

Related

Closing one element when opening another within the same loop using Javascript

first time on here so i'll try my best to explain what I'm asking.
So I have 3 list items with the same class name. I've put them in a looping function so that when you click on one it will display a sub set of list items for that specific list item. I also have them inside an if statement that adds a new class name to the specific list item that was clicked. It allows opening and closing of the sub list items when you click the corresponding parent element.
My question is; how can I use this same principle of checking for the additional class name, when the user clicks any of the list items. In other words, I am trying to code it in a way that will allow me to close any of the open sub list items when the user clicks a new list item.
This is what I came up with but it doesn't know what button[i] is when I include it within the "click" function. What I was trying to do with this code is to take whatever list item was clicked, and then check the previous and next iterations of the class name "button" to see if any of the contain also contain the class name "clicked.
HTML
<div class="main">
<ul>
<li>One
<ul>
<li>One-1</li>
<li>One-2</li>
</ul>
</li>
<li>Two
<ul>
<li>Two-1</li>
<li>Two-2</li>
</ul>
</li>
<li>Three
<ul>
<li>Three-1</li>
<li>Three-2</li>
</ul>
</li>
</ul>
</div>
CSS
.main ul ul {
display: none;
}
.main ul ul li {
display: block;
}
Javascript
var button = document.getElementsByClassName("button");
for (i = 0; i < button.length; i++) {
button[i].addEventListener("click", function() {
var prevItem = button[i - 1];
var nextItem = button[i + 1];
if (prevItem.className !== "button") {
prevItem.className = "button";
prevItem.nextElementSibling.style.display = "none";
}
if (nextItem.className !== "button") {
nextItem.className = "button";
nextItem.nextElementSibling.style.display = "none";
}
if (this.className === "button") {
this.className += " clicked";
this.nextElementSibling.style.display = "block";
}
});
}
I am wanting to make this code usable no matter how many list items you add. So checking exactly button[0] button[1] and button[2] wasn't really an option, but I can see how button[i + 1] might not check every list item after it but rather just the next one. I tried adding another loop but ran into similar issues. anyway that's why I'm here. Thanks for any help in advance.
Since I am not sure whether I understood your question correctly, I quickly rephrase it in my own words.
Question: "I have an arbitrary number of list elements, of which each contains a button and a nested list. The button is always visible, the nested list is hidden by default. When the user clicks on a button, the corresponding nested list should be shown. At the same time, all other shown nested lists should be hidden again. How can I achieve this?"
The original HTML looks fine:
<div class="main">
<ul>
<li>One
<ul>
<li>One-1</li>
<li>One-2</li>
</ul>
</li>
<li>Two
<ul>
<li>Two-1</li>
<li>Two-2</li>
</ul>
</li>
<li>Three
<ul>
<li>Three-1</li>
<li>Three-2</li>
</ul>
</li>
</ul>
</div>
The CSS I did not fully understand, but I suggest the following:
.main ul ul {
display: none;
}
.main li.is-active ul {
display: block;
}
.main ul ul li {
display: block;
}
By adding the "is-active" class to an LI element, it is shown. This way, the CSS controls the visibility.
For the JavaScript part, I suggest this:
const buttonElements = Array.from(document.querySelectorAll('.button'));
buttonElements.forEach(buttonElement => {
buttonElement.addEventListener('click', () => {
const activeElements = Array.from(document.querySelectorAll('.is-active'));
activeElements.forEach(activeElement => {
activeElement.classList.remove('is-active');
});
buttonElement.parentElement.classList.add('is-active');
});
});
This solution assumes you can use newer versions of JavaScript/ECMAScript. Overall, it makes use of const and arrow functions.
First, we get all elements with the class "button" by using document.querySelectorAll(). Since the result is a NodeList and no array, we convert it using Array.from(). Afterwards, we loop through the array by using Array.prototpye.forEach(). We add an event listener for the "click" event. When a button is clicked, we search for all elements with the "is-active" class and for each one remove it. Finally, we add the "is-active" class to the parent element of the clicked button using Node.prototype.parentElement().
Here is another solution that works in older browsers:
var buttonElements = document.getElementsByClassName('button');
for (var i = 0; i < buttonElements.length; i++) {
buttonElements[i].addEventListener('click', function(event) {
var activeListElements = document.getElementsByClassName('is-active');
for (var i = 0; i < activeListElements.length; i++) {
activeListElements[i].setAttribute('class', '');
}
event.target.parentNode.setAttribute('class', 'is-active');
});
}
This is pretty much the same as the other approach but works with older versions of JavaScript.
Generally, the idea is to focus on an arbitrary sum of elements instead of an array with a specific length. In natural language something like: "Give me all buttons. For every button, add an event listener. When a button is clicked, give me all active list elements and remove their active status. Then, mark the list item above the button as active".
Hope this helps

Why is the outcome different

Building a simple dropdown menu with JS for Wordpress
I was wondering why do I get a different outcome when
function intDropDownMobile() {
$('.menu-item-has-children').click(function () {
// e.preventDefault();
var subMenus = $(this).siblings();
var subMenuItems = $('.menu-mobile .sub-menu');
$(this).children('.menu-mobile .sub-menu').slideToggle(300);
if (subMenus.children('.menu-mobile .sub-menu').is(':visible')); {
subMenus.children('.menu-mobile .sub-menu').hide(300);
}
});
}
and this
// Te submenu
function intDropDownMobile() {
$('.menu-item-has-children').click(function () {
// e.preventDefault();
var subMenus = $(this).siblings();
var subMenuItems = $('.menu-mobile .sub-menu');
$(this).children(subMenuItems).slideToggle(300);
if (subMenus.children(subMenuItems).is(':visible')); {
subMenus.children(subMenuItems).hide(300);
}
});
}
They both work but in the second option acts different and collapse the all menu also the non submenu items, was wondering why this happens,
Thanks
Both look suspect in that selector sub-menu probably should be .sub-menu (prefixed with a .)
In the second example, you're using subMenuItems to filter the children of $(this), but subMenuItems is probably going to always contain no elements because of the issue I pointed out above. Accordingly, $(this).children(subMenuItems). will contain no elements as well.
-- EDIT --
This is from your first example:
var subMenuItems = $('.menu-mobile .sub-menu');
$(this).children('.menu-mobile .sub-menu').slideToggle(300);
I don't think that logically, this selector can work when used with the children() function. Because there is a space in the selector, its results span TWO levels of hierarchy (the elements with class menu-mobile, and their respective sub-elements with class sub-menu). Since the results span two levels, it'd be ambiguous to the function as to which -- the inner, or outer -- to return as 'child'. So none are returned!!!
This is from your second example:
var subMenuItems = $('.menu-mobile .sub-menu');
$(this).children(subMenuItems).slideToggle(300);
In this example, all elements in subMenuItems will have class sub-menu. SOME of them may be children of $(this), and those are returned.
So in the second example, you selected all elements matching $('.menu-mobile .sub-menu') in the document, and filtered to children of $(this). But in the first, you gave a selector that will never work.

Hide <h3> when all ul>li of group does not contain filter term

I've created a small script that filters my ul with a nested ul inside of it. The only issue with my script is I want to hide the title of the nested ul if none of the li's contain the search term, but I am not sure how to go about checking the li's of each "group" as opposed to each li individually. The way it stands, it will display the title if it finds an li in the group matching the search term, but it will immediately turn around and hide the title if the same group contains an li that DOES NOT contain the search term. I know what I'm doing wrong, but I am not as skilled in jquery and cannot seem to visualize how to go about this.
Any help would be great. My code is below:
HTML:
<div id="sitemap">
<h3>Hospital Data Solutions Interactive Site Map</h3>
<hr/>
<p id="header"><input type="text" placeholder="Filter Site Map"> - Use this field filter our list of databases: Search by Topic or Topic Subgroup</p>
<ul id="toplist">
<li class="group">
<h3 class="sTitle">Available Beds - <a style="font-size:18px;">Go to Section</a></h3>
<ul class="sublist">
<li>General</li>
<li>ICU</li>
<li>CCU</li>
<li>BICU</li>
<li>SICU</li>
<li>Other</li>
<li>Hospice</li>
<li>Total</li>
</ul>
</li>
<hr/>
<li class="group">
<h3 class="sTitle">Discharges - <a style="font-size:18px;">Go to Section</a></h3>
<ul class="sublist">
<li>Medicare</li>
<li>Medicaid</li>
<li>Other</li>
<li>Total</li>
</ul>
</li>
</ul>
</div>
Jquery:
$(function(){
$('input[type="text"]').keyup(function(){
var searchText = $(this).val().toLowerCase();
$('.sublist>li').each(function(){
var currentLi = $(this).text().toLowerCase();
if(currentLi.search(searchText) != -1){
$(this).slideDown();
$(this).closest('.group').children('.sTitle').show();
} else { $(this).slideUp(); $(this).closest('.group').children('.sTitle').hide();}
});
});
});
First, select the .sublist elements instead of the lis.
Then iterate that collection using .each(), and use .children() to test each li like you currently are, except use .filter() instead of .each(). This will give you a collection as a result. If the collection is empty, there were no matches. If not, then there was a match.
$('input[type="text"]').keyup(function(){
var searchText = $(this).val().toLowerCase();
$('.sublist').each(function(i, sub){
var matches = $(sub).children().filter(function(i, li) {
return $(li).text().toLowerCase().search(searchText) != -1;
});
if (matches.length) {
$(sub).slideDown().prev().show();
} else {
$(sub).slideUp().prev().hide();
}
});
});
Now the slideDown/Up and show/hide are happening once per sublist instead of on every child li. And I just used .prev() to get back to the h3 element.
If you're going to be hiding those list items that don't match your search you're going to want to deal with them individually anyway, so I wouldn't abandon that approach. So you just need a way to check to see if the term was found somewhere in the search of the nested list. Here's what I might do to utilize what you already have.
After you capture the search term, loop through each of the sublists and set a flag to false; this will be where we capture whether there were any matches. Then loop through that sublist's items, and if you find a match set the flag to true, showing or hiding the item as necessary. Then, after you've checked all the items show or hide the heading based on that flag. It might look something like this:
$('.sublist').each(function(){
found = false;
$(this).children("li").each( function() {
var currentLi = $(this).text().toLowerCase();
if(currentLi.search(searchText) != -1){
$(this).slideDown();
found = true;
} else {
$(this).slideUp();
}
});
if(found) {
$(this).closest('.group').children('.sTitle').show();
} else {
$(this).closest('.group').css("list-style-type", "none");
$(this).closest('.group').children('.sTitle').hide();
}
});
I added a css line to show/hide the header's disc to avoid having that hanging there if everything else disappears. Hope this helps! Let me know if you have any questions.

how to append data from stating in js tree using jquery?

I Found one example of js tree in jquery ? in this user can add new data inside after selecting the
the row.
here http://jsfiddle.net/fuu94/
But When I remove all row (remove ul and li from mark up)and start making from first it will not work why ?
http://jsfiddle.net/fuu94/1/
$('#tree').jstree({
"core": {
"check_callback": true
},
"plugins": ["dnd"]
});
$('button').click(function () {
var ref = $('#tree').jstree(true),
sel = ref.get_selected();
if (!sel.length) {
return false;
}
sel = sel[0];
sel = ref.create_node(sel);
if (sel) {
ref.edit(sel);
}
});
According to the jstree documentation
"The minimal required markup is a < ul > node with some nested < li > nodes with some text inside."
-> http://www.jstree.com/docs/html/
As long as you make the menu using UL and LI it should do the rest for you (as in creating the tree).
So basically, if you remove the text from the LI and UL nodes and make your own text, duplicate the structure, you could make something like this:
-> http://jsfiddle.net/fuu94/3/
but the minimum requirements is something like:
<ul>
<li></li>
</ul>
and if you want to use a submenu, add one of these :
<li> Title Here
<ul>
<li></li>
<li></li>
</ul>
</li>

Select all "li" that do NOT contain a "ul" within

I'm building a nested drop down navigation for products and I want to automatically hide any categories that are empty.
Unfortunately server side language does not allow efficient way of doing this, so I thought I could output number of products each category has directly, then use jQuery to remove any empty nodes.
I want to target only the li's within nav#top_nav:
<nav id="top_nav">
<nav class="nav1">
<ul>
<li data-num-products="0">
AAA
<ul>
<li data-num-products="3">BBB</li>
<li data-num-products="0">CCC</li>
</ul>
</li>
<li data-num-products="7">DDD</li>
</ul>
<nav>
</nav>
Given 1 level of nesting ul's, I want to remove any li's that...
have no nested ul's within them and
have data-num-products == 0.
So in the example above AAA is retained because it has ul children, but CCC is removed because it has no ul children and no products.
UPDATE:
It might require 2 passes of removal because if a li contains a ul whose li elements are all removed, then we'll want to remove the ul too.
$( "#top_nav li").filter( function(){
return !this.getElementsByTagName("ul").length && !+this.getAttribute("data-num-products");
}).remove();
http://jsfiddle.net/X2D7y/
This will only remove if there are no UL descendants AND have attribute value of 0
Like this:
$('#top_nav li[data-num-products="0"]:not(:has(ul))').remove();
The selector breakdown is...
'#top_nav' // start at element with "top_nav" id
' ' // and select descendant...
'li' // li elements...
'[data-num-products="0"]' // ...where attribute "data-num-products" is "0"
':not(' // ...but exclude li elements that...
':has(ul)' // ...have descendant ul elements
')'
Regarding your updated question, just change :not(:has(ul)) to :not(:has(li)).
$('#top_nav li[data-num-products="0"]:not(:has(li))').remove();
http://jsfiddle.net/4YFDd/
$("#top_nav li[data-num-products='0']").filter(function() {
return $(this).children("ul").length === 0;
}).remove();
In Javascript:
var lis = document.querySelectorAll( '#top_nav li[data-num-products="0"]' );
for( var li = 0; li < lis.length; li++ ) {
!lis[li].getElementsByTagName( 'ul' ).length && lis[li].parentNode.removeChild( lis[li] );
};
Demo: http://jsfiddle.net/ThinkingStiff/2zS9B/

Categories

Resources