Detecting "runs" of DOM elements in jQuery - javascript

What is the best way to detect runs of dom elements in jQuery?
For instance, if I have the following list of items
<ol>
<li class="a"></li>
<li class="foo"></li>
<li class="a"></li>
<li class="a"></li>
<li class="foo"></li>
<li class="foo"></li>
<li class="foo"></li>
<li class="a"></li>
<li class="foo"></li>
<li class="foo"></li>
<li class="foo"></li>
</ol>
Say I want to grab all the li.foo elements and wrap them inside their own <ol> block (or any wrapper for that matter) to end up with something like.
<ol>
<li class="a"></li>
<li class="foo"></li>
<li class="a"></li>
<li class="a"></li>
<li><ol>
<li class="foo"></li>
<li class="foo"></li>
<li class="foo"></li>
</ol></li>
<li class="a"></li>
<li><ol>
<li class="foo"></li>
<li class="foo"></li>
<li class="foo"></li>
</ol></li>
</ol>
As you can see from the example, I only want to wrap "runs" of li.foo dom elements (where there are 2 or more li.foo elements in succession.
I'm not sure of the best/most efficient way to accomplish this via jQuery (or just plain javascript for that matter).

Example: http://jsfiddle.net/kNfxs/1/
$('ol .foo').each(function() {
var $th = $(this);
var nextUn = $th.nextUntil(':not(.foo)');
if(!$th.prev('.foo').length && nextUn.length)
nextUn.andSelf().wrapAll('<li><ol></ol></li>');
});
Loop over the .foo elements, and if the previous element is not .foo, and it has at least 1 .foo after it, then grab all the next .foo elements using the nextUntil()(docs) method and include the original using the andSelf()(docs) method, then wrap them using the wrapAll()(docs) method.
Update: This will be a little more efficient because it avoids the nextUntil()(docs) method when there's a previous .foo().
Example: http://jsfiddle.net/kNfxs/2/
$('ol .foo').each(function() {
var $th = $(this);
if($th.prev('.foo').length) return; // return if we're not first in the group
var nextUn = $th.nextUntil(':not(.foo)');
if( nextUn.length)
nextUn.andSelf().wrapAll('<li><ol></ol></li>');
});

Use something like this:
$(li).each(function(){
if($(this).next().hasClass('foo')){
---- Recursively check until the end of the run has been found ----
}
});
To recursively check write a function that checks the next element until the end of the run has been found.

Not sure how well this works for performance:
var $foo = $('.foo + .foo');
$foo.add($foo.prev());
$foo will be the set of ".foo runs"
Edit to add:
I thought of a simpler way:
var $foo = $('.foo + .foo').prev('.foo').andSelf();

Working example: http://jsfiddle.net/v8GY8/
For posterity:
// Takes a jQuery collection and returns an array of arrays of contiguous elems
function groupContiguousElements( $elems ){
var groups = [];
var current, lastElement;
$elems.each(function(){
if ($(this).prev()[0] == lastElement ){
if (!current) groups.push( current=[lastElement] );
current.push( this );
}else{
current = null;
}
lastElement = this;
});
return groups;
}
var groups = groupContiguousElements( $('li.foo') );
$.each( groups, function(){
var wrapper = $('<ol>').insertBefore(this[0]);
$.each(this,function(){
wrapper.append(this);
});
});

Related

append / appendChild not working on all items

I'm trying to move all the list items from an list to another using only javascript but for some reason only half of them are actually moved.
Heres a working example of what I'm doing:
var results_ul = document.getElementById('results');
var stores_li = document.getElementsByClassName('store-list-item');
for (var x = 0; x < stores_li.length; x++) {
document.getElementById('hide').appendChild(stores_li[x]);
stores_li[x].className += ' teste';
}
<p>results</p>
<ul id="results">
<li class="store-list-item">Teste 1</li>
<li class="store-list-item">Teste 2</li>
<li class="store-list-item">Teste 3</li>
<li class="store-list-item">Teste 4</li>
</ul>
<p>Hide:</p>
<ul id="hide"></ul>
What seems to be the problem?
getElementsByClassName returns a live list.
When you append the element to a different element, you change its position in the list.
So it starts off as:
1 2 3 4
Then you move the first one:
2 3 4 1
Then you access the second one … but the second one is now 3 because everything has shuffled down the list.
You could copy each element into an array (which will not be a live list) and then iterate over that array to move them (so they won't change positions as you go).
Alternatively, you could use querySelectorAll which returns a non-live list.
You should better use querySelectorAll than getElementsByClassName
var results_ul = document.getElementById('results');
var stores_li = document.querySelectorAll('.store-list-item');
stores_li.forEach((item)=>{
document.getElementById('hide').appendChild(item);
item.className += ' teste';
});
<p>results</p>
<ul id="results">
<li class="store-list-item">Teste 1</li>
<li class="store-list-item">Teste 2</li>
<li class="store-list-item">Teste 3</li>
<li class="store-list-item">Teste 4</li>
</ul>
<p>Hide:</p>
<ul id="hide"></ul>
Try use querySelectorAll . It'll returns a non-live list. That's what you need.
var stores_li = document.querySelectorAll('.store-list-item');
To increase more information:
Live : when the changes in the DOM are reflected in the collection. The content suffers the change when a node is modified.
Non-Live : when any change in the DOM does not affect the content of the collection.
document.getElementsByClassName() is an HTMLCollection, and is live.
document.querySelectorAll() is a NodeList and is not live.
In your code you are removing each element from the first list and inserting into the new list. After you remove 2 elements it will have only 2 elements in the first list but now you are searching the 3 rd index in the loop which is not there. So to make it work i have prepended each element from the last.
var results_ul = document.getElementById('results');
var stores_li = document.getElementsByClassName('store-list-item');
var hide_ul = document.getElementById('hide');
for (var x = 0, y = stores_li.length; x < y; x++) {
hide_ul.insertBefore(stores_li[y-x-1],hide_ul.firstChild);
stores_li[x].className += ' teste';
}
<p>results</p>
<ul id="results">
<li class="store-list-item">Teste 1</li>
<li class="store-list-item">Teste 2</li>
<li class="store-list-item">Teste 3</li>
<li class="store-list-item">Teste 4</li>
</ul>
<p>Hide:</p>
<ul id="hide"></ul>
Or you may want to clone the element with Jquery and you can push into the clonned ones then delete the orginals from top. I could not find any equivalent of clone() for js but if you want to check link is here
var results_ul = document.getElementById('results');
var stores_li = document.getElementsByClassName('store-list-item');
while(stores_li.length>0) {
document.getElementById('hide').appendChild(stores_li[0]);
stores_li[x].className += ' teste';
}

Find the place of an element

I have a list
<ul>
<li class="list_1">a</li>
<li class="list_2">b</li>
<li class="list_3">c</li>
<li class="list_4">d</li>
</ul>
This is in a carousel, so that the list items change position (1-2-3-4, 2-3-4-1, 3-4-1-2, 4-1-2-3,...)
How can I find out, using javascript, which item is in, let's say, second and third position?
In the beginning, the list_2 and list_3 are in second and third position, after one cycle, the list_3 and list_4 are in second and third position, etc.
How to find out what list is in those positions, while I cycle through? For starters I just need to see it displayed in console with console.log(), something like:
On 2nd place is list_3, and on third is list_4.
Tried with this but doesn't work:
var $list_items = $(this).find('ul li');
$list_items.each(function(i,j){
$(this).addClass('list_' + (i+1));
console.log($list_items.eq(2).attr('class'));
});
I'm using $(this) because my original lists are enclosed in a div, and originally lists had no class, so I added them.
One approach is to use map() and index() to create an array of the element's class-name and index, obviously this depends on what, precisely, you want to find; but your question is somewhat vague on the result you want:
function mapIndices() {
// iterate over the 'ul li' elements, forming a map:
return $('ul li').map(function() {
// returning the className (classes) of the element and its index amongst
// siblings:
return this.className + ': ' + $(this).index();
// converting to an Array:
}).get();
};
// this is just a simple trigger to move the elements,
// to demonstrate binding the function:
$('#change').on('click', function() {
var ul = $('ul');
ul.find('li:first-child').appendTo(ul);
console.log(mapIndices());
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="change">advance positions</button>
<ul>
<li class="list_1">a</li>
<li class="list_2">b</li>
<li class="list_3">c</li>
<li class="list_4">d</li>
</ul>
If, however, you simply want to find out which element is in a specific position:
$('#change').on('click', function() {
var ul = $('ul');
ul.find('li:first-child').appendTo(ul);
// bear in mind that JavaScript has zero-based indexing,
// 2 is the index of third element, not the second:
console.log(ul.find('li').eq(2));
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="change">advance positions</button>
<ul>
<li class="list_1">a</li>
<li class="list_2">b</li>
<li class="list_3">c</li>
<li class="list_4">d</li>
</ul>
References:
eq().
find().
get().
index().
map().
on().
Array manipulation:
var arr = $('li').get(); // http://api.jquery.com/get/
// var arr = ["aa","bb","cc","dd"]; // you can try also using this guy
function doIt(){
if(this.id==="next") arr.push( arr.shift() ); // send first to last
else arr.unshift( arr.pop() ); // send last to first
$('ul').html( arr );
}
$('#prev, #next').on('click', doIt);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="prev">PREV</button>
<button id="next">NEXT</button>
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
<li>d</li>
<li>-----------</li>
</ul>
First give an id/class to the ul:
<ul id="mylist">
<li class="list_1">a</li>
<li class="list_2">b</li>
<li class="list_3">c</li>
<li class="list_4">d</li>
</ul>
Using jQuery to access the 'li' element at 2nd position:
var second_li = $('#mylist').find('li')[1]; // to access 2nd li use index 1
var second_li_class = $(second_li).attr('class'); // second_li_class will be equal to 'list_2'
var second_li_content = $(second_li).html(); // second_li_content will be equal to 'b'

Cycling through a jQuery object with elements grouped together

I have a set of list items that contain nested lists, sort of like this:
<ul class="searchselectors">
<li class="group">Group name 1
<ul>
<li class="item selected">List item 1.1</li>
<li class="item">List item 1.2</li>
<li class="item">List item 1.3</li>
</ul>
</li>
<li class="group">Group name 2
<ul>
<li class="item">List item 2.1</li>
<li class="item">List item 2.2</li>
<li class="item">List item 2.3</li>
</ul>
</li>
<li class="group">Group name 3
<ul>
<li class="item">List item 3.1</li>
</ul>
</li>
</ul>
I want to cycle through all of the .item elements using up/down arrow keys (which I already have set up by using on('keydown') and catching key codes 38 and 40) and set .selected on the next item before or after the currently selected item, wrapping around to the top/bottom as necessary.
Using $().next() and $().prev() will not work, since it will only work on siblings, and not on a jQuery object such as $('.searchselectors .item').\
I was working on the same problem but in my project I'm using KnockoutJS. In fact, the original logic was written with pure jQuery and I refactored it with Knockout. Here's a solution for your problem using jQuery:
Fiddle - http://jsfiddle.net/6QN77/2/
I didn't spend too much time cleaning up the JavaScript, but I'm leaving that to you now.
HTML
<ul id="searchselectors">
<li class="group">Group name 1
<ul>
<li class="item selected">List item 1.1</li>
<li class="item">List item 1.2</li>
<li class="item">List item 1.3</li>
</ul>
</li>
<li class="group">Group name 2
<ul>
<li class="item">List item 2.1</li>
<li class="item">List item 2.2</li>
<li class="item">List item 2.3</li>
</ul>
</li>
<li class="group">Group name 3
<ul>
<li class="item">List item 3.1</li>
</ul>
</li>
</ul>
jQuery
$(function () {
var $menu = $("#searchselectors"),
$items = $menu.find(".item"),
$selectedItem = $menu.find(".selected"),
selectedIndex = $selectedItem.length - 1;
$(document).on("keydown", function (e) {
switch(e.keyCode) {
case 40: // down arrow
$selectedItem.removeClass("selected");
selectedIndex = (selectedIndex + 1) % $items.length;
$selectedItem = $items.eq(selectedIndex).addClass("selected");
break;
case 38: // up arrow
$selectedItem.removeClass("selected");
selectedIndex = (selectedIndex - 1) % $items.length;
$selectedItem = $items.eq(selectedIndex).addClass("selected");
break;
}
});
});
UPDATE: My solution revolves around getting a reference to a wrapper element (#searchselectors) and getting all the LI elements marked with the CSS class .item. Next I get a reference to the currently selected element and its index. Finally, the code listens to the down and up arrow keys being pressed (keydown), decrementing if going up and incrementing if going up. Cycling is achieved via the modulus operator. The selected item's CSS class is removed and put back. The selected item reference is used for performance reasons so I don't have to write $items.removeClass(".selected").eq(selectedIndex).addClass("selected");
In my quest to provide native JS answers to those looking for it, here is #Mario j Vargas' good answer adapted in native Javascript. It only takes 2 lines of extra code.
http://jsfiddle.net/kevinvanlierde/7tQSW/2/
Only putting the JS up here, HTML is the same.
(function () {
var $menu = document.getElementById('searchselectors'),
items = $menu.getElementsByClassName('item'),
$selectedItem = $menu.getElementsByClassName('selected')[0],
selectedIndex = Array.prototype.indexOf.call($selectedItem, items)+1;
document.body.addEventListener('keydown', function (e) {
switch(e.keyCode) {
case 40: // down arrow
$selectedItem.className = $selectedItem.className.replace(' selected','');
selectedIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : selectedIndex;
$selectedItem = items[selectedIndex];
$selectedItem.className += ' selected';
break;
case 38: // up arrow
$selectedItem.className = $selectedItem.className.replace(' selected','');
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : selectedIndex;
$selectedItem = items[selectedIndex];
$selectedItem.className += ' selected';
break;
}
}, false);
}());
Easiest way to get all the list items with class "item" is:
var items = $('.item');
for (var i = 0; i < items.length; i++) {
var item = items[i]; // iterate
}
And then, you can select the next item from the list
OR
you can do this
if (!$().next) {
var nextUL= $().parent.find('ul')
iterateThruList(nextUL);
}
You could use something like
var UL = $(".searchselectors").children().length; //number of UL (group names)
to iterate through items.

Strange behaviour for jQuery find

I got this piece of javascript that works perfectly to one of my websites but give troubles with other.
$(document).ready(function(){
if ($.find(Thesaurus.options.containers).length > 0) {
thes = new Thesaurus(Thesaurus.options);
}
});
These are results when I try to debug by using the old fashioned alerts:
alert(Thesaurus.options.containers); - this returns the string div.content
alert($.find(Thesaurus.options.containers)); - this one returns empty, therefore the length is zero
alert($.find('div.content')); - this one returns [object HTMLdivElement] as I expect
I seem to be unable to understand what is happening.
There is no method $.find() in jQuery it's .find()
$(document).ready(function(){
if ($(document).find(Thesaurus.options.containers).length > 0) {
thes = new Thesaurus(Thesaurus.options);
}
});
or
$(document).ready(function(){
if ($(Thesaurus.options.containers).length > 0) {
thes = new Thesaurus(Thesaurus.options);
}
});
If you visit web page of jQUery you can visualize that you are using wrong $.find... here an example the way to use.
<ul class="level-1">
<li class="item-i">I</li>
<li class="item-ii">II
<ul class="level-2">
<li class="item-a">A</li>
<li class="item-b">B
<ul class="level-3">
<li class="item-1">1</li>
<li class="item-2">2</li>
<li class="item-3">3</li>
</ul>
</li>
<li class="item-c">C</li>
</ul>
</li>
<li class="item-iii">III</li>
</ul>
If we begin at item II, we can find list items within it:
1
$( "li.item-ii" ).find( "li" ).css( "background-color", "red" );
you need to change your code something like this:
if ($(document).find(Thesaurus.options.containers).length > 0) {
thes = new Thesaurus(Thesaurus.options);
}
Hope this works.
Regards

Traversing the Ordered List backwards returning each li text

How can I traverse an ordered list and return the text if I have a scenario where the user can click on a li element like Cat 1-2 and it returns all of the parent li's text into a string or an array. If an array I can reverse the sort but eventually I need it to be a string.
Example:
<ul>
<li>Cat 1
<ul>
<li>Cat 1-1
<ul>
<li>Cat 1-2</li>
</ul>
</li>
</ul>
</li>
<li>Cat 2
<ul>
<li>Cat 2-1
<ul>
<li>Cat 2-2</li>
</ul>
</li>
</ul>
</li>
</ul>
if clicked on Cat 1-2 the desired result would be:
str = "Cat 1/Cat 1-1/Cat 1-2";
if clicked on Cat 2-1 the desired result would be:
str = "Cat 2/Cat 2-1";
What I would like to have is checkboxes on this where I could join all the selections into a string hierarchy like:
str = "Cat 1/Cat 1-1/Cat 1-2|Cat 2/Cat 2-1";
Here; I'll show a down and dirty method to get you rolling, then you can find a better way (maybe using textContent)...
$('li').click(function(e){
var parents = $(this).add($(this).parents('li'))
.map(function(){
return $(this).clone().children().remove().end()
.text().replace(/\r|\n|^\s+|\s+$/g,'');
})
.toArray().join('/');
alert(parents);
e.stopPropagation();
});
I don't know if this is any more efficient than Brad's method, but I started working on this earlier and it bugged me until I had it finished.
jsFiddle example
var ary = [];
$('li').click(function (e) {
ary.push($.trim($(this).clone()
.children()
.remove()
.end()
.text()));
if ($(this).parents('ul').parent('li').length == 0) {
console.log(ary.reverse().join('/'));
ary = [];
}
});

Categories