Skip recursion in jQuery.find() for a selector? [duplicate] - javascript

This question already has answers here:
jQuery: find() children until a certain threshold element is encountered
(5 answers)
Closed 16 days ago.
TL;DR: How do I get an action like find(), but block traversal (not full stop, just skip) for a certain selector?
ANSWERS: $(Any).find(Selector).not( $(Any).find(Mask).find(Selector) )
There were many truly great answers, I wish I could some how distribute the bounty points more, maybe I should make some 50 pt bounties in response to some of these ;p I choose Karl-André Gagnon's because this answer managed to make findExclude unrequired in one, slightly long, line. While this uses three find calls and a heavy not filter, in most situations jQuery can use very fast implementation that skips traversal for most find()s.
Especially good answers are listed below:
falsarella: Good improvement on my solution, findExclude(), best in many situatoins
Zbyszek: A filter-based solution similar to falsarella's, also good on efficiency
Justin: A completely different, but manageable and functional solution to the underlaying issues
Each of these have their own unique merits and and are deserving of some mention.
I need to descend into an element fully and compare selectors, returning all matched selectors as an array, but skip descending into the tree when another selector is encountered.
Edit: replacing original code sample with some from my site
This is for a message forum which may have reply message-groups nested inside any message.
Notice, however, we cannot use the message or content classes because the script is also used for other components outside of the forum. Only InterfaceGroup, Interface and controls classes are potentially useful - and preferably just Interface and controls.
Interact with the code and see it in JS Fiddle, thanks Dave A, here Click on the buttons while viewing a JavaScript console to see that the controls class is being bound to one extra time per level of .Interface nesting.
Visual A, Forum Layout Struture:
<li class="InterfaceGroup">
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance"> ... condensed ... </li>
<li class="InterfaceGroup"> ... condensed ...</li>
</ul>
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance"> ... condensed ... </li>
</ul>
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance"> ... condensed ... </li>
<li class="InterfaceGroup"> ... condensed ...</li>
</ul>
</li>
Inside of each <li class="InterfaceGroup"> there could be any number of repetitions of the same structure (each group is a thread of messages) and/or deeper nesting such as..
<li class="InterfaceGroup">
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance"> ... condensed ... </li>
<li class="InterfaceGroup">
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance"> ... condensed ... </li>
<li class="InterfaceGroup"> ... condensed ...</li>
</ul>
</li>
</ul>
</li>
Inside of each <li class="instance"> ... </li> there are arbitrary places decided by another team where class="controls" may appear and an event listener should be bound. Though these contain messages, other components structure their markup arbitrarily but will always have .controls inside of .Interface, which are collected into an .InterfaceGroup.A reduced-complexity version of the inner-content (for forum posts) is below for reference.
Visual B, Message Contents with controls class:
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance">
<ul class="profile"> ...condensed, nothing clickable...</ul>
<ul class="contents">
<li class="heading"><h3>Hi there!</h3></li>
<li class="body"><article>TEST Message here</article></li>
<li class="vote controls">
<button class="up" data-role="VoteUp" ><i class="fa fa-caret-up"> </i><br/>1</button>
<button class="down" data-role="VoteDown" >0<br/><i class="fa fa-caret-down"> </i></button>
</li>
<li class="social controls">
<button class="reply-btn" data-role="ReplyButton" >Reply</button>
</li>
</ul>
</li>
<li class="InterfaceGroup" > <!-- NESTING OCCURRED -->
<ul class="Interface Message" data-role="MessagePost" >
<li class="instance">... condensed ... </li>
<li class="InterfaceGroup" >... condensed ... </li>
</ul>
</li>
</ul>
We can only bind to controls that are within an Interface class, instance may or may not exist but Interface will. Events bubble to .controls elements and have a reference to the .Interface which holds them..
So I am trying to $('.Interface').each( bind to any .controls not inside a deeper .Interface )
That's the tricky part, because
.Interface .controls will select the same .control multiple times in the .each()
.not('.Interface .Interface .controls') cancels out controls in any deeper nesting
How can I do this using jQuery.find() or a similar jQuery method for this?
I have been considering that, perhaps, using children with a not selector could work and could be doing the same thing as find under the hood, but I'm not so sure that it actually is or wont cause horrible performance. Still, an answer recursing .children effectively is acceptable.
UPDATE: Originally I tried to use a psuedo-example for brevity, but hopefully seeing a forum structure will help clarify the issue since they're naturally nested structures. Below I'm also posting partial javascript for reference, line two of the init function is most important.
Reduced JavaScript partial:
var Interface=function()
{
$elf=this;
$elf.call=
{
init:function(Markup)
{
$elf.Interface = Markup;
$elf.Controls = $(Markup).find('.controls').not('.Interface .controls');
$elf.Controls.on('click mouseenter mouseleave', function(event){ $elf.call.events(event); });
return $elf;
},
events:function(e)
{
var classlist = e.target.className.split(/\s+/), c=0, L=0;
var role = $(e.target).data('role');
if(e.type == 'click')
{
CurrentControl=$(e.target).closest('[data-role]')[0];
role = $(CurrentControl).data('role');
switch(role)
{
case 'ReplyButton':console.log('Reply clicked'); break;
case 'VoteUp':console.log('Up vote clicked'); break;
case 'VoteDown':console.log('Down vote clicked'); break;
default: break;
}
}
}
}
};
$(document).ready( function()
{
$('.Interface').each(function(instance, Markup)
{
Markup.Interface=new Interface().call.init(Markup);
});
} );

If you want to exclude element in you find, you can use a not filter. As for example, I've taken you function that exclude element and made it way shorter :
$.fn.findExclude = function( Selector, Mask,){
return this.find(Selector).not(this.find(Mask).find(Selector))
}
Now, ill be honest with you, I did not fully understand what you want. But, when i took a look at your function, I saw what you were trying to do.
Anyway, take a look at this fiddle, the result is the same as your : http://jsfiddle.net/KX65p/8/

Well, I really don't want to be answering my own question on a bounty, so if anyone can provide a better or alternative implementation please do..
However, being pressed to complete the project, I ended up working on this quite a bit and came up with a fairly clean jQuery plugin for doing a jQuery.find() style search while excluding child branches from the results as you go.
Usage to work with sets of elements inside nested views:
// Will not look in nested ul's for inputs
$('ul').findExclude('input','ul');
// Will look in nested ul's for inputs unless it runs into class="potato"
$('ul').findExclude('input','.potato');
More complex example found at http://jsfiddle.net/KX65p/3/ where I use this to .each() a nested class and bind elements which occur in each nested view to a class. This let me make components server-side and client-side reflect each other's properties and have cheaper nested event handling.
Implementation:
// Find-like method which masks any descendant
// branches matching the Mask argument.
$.fn.findExclude = function( Selector, Mask, result){
// Default result to an empty jQuery object if not provided
result = typeof result !== 'undefined' ?
result :
new jQuery();
// Iterate through all children, except those match Mask
this.children().each(function(){
thisObject = jQuery( this );
if( thisObject.is( Selector ) )
result.push( this );
// Recursively seek children without Mask
if( !thisObject.is( Mask ) )
thisObject.findExclude( Selector, Mask, result );
});
return result;
}
(Condensed Version):
$.fn.findExclude = function( selector, mask, result )
{
result = typeof result !== 'undefined' ? result : new jQuery();
this.children().each( function(){
thisObject = jQuery( this );
if( thisObject.is( selector ) )
result.push( this );
if( !thisObject.is( mask ) )
thisObject.findExclude( selector, mask, result );
});
return result;
}

Maybe something like this would work:
$.fn.findExclude = function (Selector, Mask) {
var result = new jQuery();
$(this).each(function () {
var $selected = $(this);
$selected.find(Selector).filter(function (index) {
var $closest = $(this).closest(Mask);
return $closest.length == 0 || $closest[0] == $selected[0] || $.contains($closest, $selected);
}).each(function () {
result.push(this);
});
});
return result;
}
http://jsfiddle.net/JCA23/
Chooses those elements that are either not in mask parent or their closest mask parent is same as root or their closest mask parent is a parent of root.

I think that this is the closest the findExclude can be optimized:
$.fn.findExclude = function (Selector, Mask) {
var result = $([]);
$(this).each(function (Idx, Elem) {
$(Elem).find(Selector).each(function (Idx2, Elem2) {
if ($(Elem2).closest(Mask)[0] == Elem) {
result = result.add(Elem2);
}
});
});
return result;
}
Also, see its fiddle with added logs with ellapsed time in milliseconds.
I see that you are worried with the performances. So, I've run some tests, and this implementation takes no longer than 2 milliseconds, while your implementation (as the answer you have posted) sometimes takes around 4~7 millisecods.

From my understanding, I would bind to the .controls elements and allow the event to bubble up to them. From that, you can get the closest .Interface to get the parent, if needed. This way you are added multiple handlers to the same elements as you go further down the rabbit hole.
While I saw you mention it, I never saw it implemented.
//Attach the event to the controls to minimize amount of binded events
$('.controls').on('click mouseenter mouseleave', function (event) {
var target = $(event.target),
targetInterface = target.closest('.Interface'),
role = target.data('role');
if (event.type == 'click') {
if (role) {
switch (role) {
case 'ReplyButton':
console.log('Reply clicked');
break;
case 'VoteUp':
console.log('Up vote clicked');
break;
case 'VoteDown':
console.log('Down vote clicked');
break;
default:
break;
}
}
}
});
Here is a fiddle showing what I mean. I did remove your js in favor of a simplified display.
It does seem that my solution may be a over simplification though...
Update 2
So here is a fiddle that defines some common functions that will help achieve what you are looking for...I think. The getInterfaces provides a simplified function to find the interfaces and their controls, assuming all interfaces always have controls.
There are probably fringe cases that will creep up though. I also feel I need to apologize if you have already ventured down this path and I'm just not seeing/understanding!
Update 3
Ok, ok. I think I understand what you want. You want to get the unique interfaces and have a collection of controls that belong to it, that make sense now.
Using this fiddle as the example, we select both the .Interface and the .Interface .controls.
var interfacesAndControls = $('.Interface, .Interface .controls');
This way we have a neat collection of the interfaces and the controls that belong to them in order they appear in the DOM. With this we can loop through the collection and check to see if the current element has the .Interface associated with it. We can also keep a reference to the current interface object we create for it so we can add the controls later.
if (el.hasClass('Interface')){
currentInterface = new app.Interface(el, [], eventCallback);
interfaces.push(currentInterface);
//We don't need to do anything further with the interface
return;
};
Now when we don't have the .Interface class associate with the element, we got controls. So let's first modify our Interface object to support adding controls and binding events to the controls as they are being added to the collection.
//The init function was removed and the call to it
self.addControls = function(el){
//Use the mouseover and mouseout events so event bubbling occurs
el.on('click mouseover mouseout', self.eventCallback)
self.controls.push(el);
}
Now all we have to do is add the control to the current interfaces controls.
currentInterface.addControls(el);
After all that, you should get an array of 3 objects (interfaces), that have an array of 2 controls each.
Hopefully, THAT has everything you are looking for!

If I understand you:
understanding your needs better and applying the specific classes you need, I think this is the syntax will work:
var targetsOfTopGroups = $('.InterfaceGroup .Interface:not(.Interface .Interface):not(.Interface .InterfaceGroup)')
This Fiddle is an attempt to reproduce your scenario. Feel free to play around with it.
I think I found the problem. You were not including the buttons in your not selector
I changed the binding to be
var Controls = $('.InterfaceGroup .Interface :button:not(.Interface .Interface :button):not(.Interface .InterfaceGroup :button)');
Fiddle

Why not taking the problem upside down?
Select all $(.target) elements and then discard them from further treatment if their .$parents(.group) is empty, that would give sonething like:
$('.target').each(function(){
if (! $(this).parents('.group').length){
//the jqueryElem is empy, do or do not
} else {
//not empty do what you wanted to do
}
});
Note that don't answer the title but literally gives you "Selector B, inside of a result from Selector A"

If your .interface classes had some kind of identifier this would seem to be rather easy.
Perhabs you already have such an identifier for other reasons or choose to include one.
http://jsfiddle.net/Dc4dz/
<div class="interface" name="a">
<div class="control">control</div>
<div class="branch">
<div class="control">control</div>
<div class="interface">
<div class="branch">
<div class="control">control</div>
</div>
</div>
</div>
<div class="interface" name="c">
<div class="branch">
<div class="control">control</div>
</div>
</div> </div>
$( ".interface[name=c] .control:not(.interface[name=c] .interface .control)" ).css( "background-color", "red" );
$( ".interface[name=a] .control:not(.interface[name=a] .interface .control)" ).css( "background-color", "green" );
Edit: And now Im wondering if you're tackling this problem from the wrong angle.
So I am trying to $('.Interface').each( bind to any .controls not
inside a deeper .Interface )
http://jsfiddle.net/Dc4dz/1/
$(".interface").on("click", ".control", function (event) {
alert($(this).text());
event.stopPropagation();
});
The event would be triggered on a .control; it would then bubble up to its .closest( ".interface" ) where it would be processed and further propagation be stopped. Isn't that what you described?

Related

Accessing a reference to the onclick event from a li a click

This is the first time I’ve thought about moving my events outside of the normal HTML onClick=”” event but I cant seem to find any references as to how I would do this with a li list.
Basically I’m trying to get the number associated with the scrollToArtical(#) in to myElement.onclick. How would you rewrite this so that the event is in the .js file.
I’ve tried variations of to get at the element but these don’t work:
var objScrollToNav = document.getElementById("id_ScrollToNav").children;
var objScrollToNav = document.querySelector("#id_ScrollToNav a");
Any help would be greatly appreciated – CES
My old code is:
<ul id="id_ScrollToNav" role="list">
<li class="sectionNavOff"><a onclick="scrollToArticle(0)" role="link">•</a></li>
<li class="sectionNavOn"><a onclick="scrollToArticle(1)" role="link">•</a></li>
<li class="sectionNavOff"><a onclick="scrollToArticle(2)" role="link">•</a></li>
</ul>
Use document.querySelectorAll to get an array-like list, then loop over them. To keep a reference to the index, make sure you also pass the index into a new closure (the addEvent function below creates a new closure).
function scrollToArticle(index) { console.log("Scrolling to:", index); }
// Select all the elements.
var links = document.querySelectorAll("#id_ScrollToNav a");
// This function adds event listener, and holds a reference to the index.
function addEvent(el, index) {
el.addEventListener("click", function() {
scrollToArticle(index);
});
}
// Loop over the elements.
for (var i = 0; i < links.length; i++) {
addEvent(links[i], i);
}
<ul id="id_ScrollToNav" role="list">
<li class="sectionNavOff"><a role="link">•</a></li>
<li class="sectionNavOn"><a role="link">•</a></li>
<li class="sectionNavOff"><a role="link">•</a></li>
</ul>
Since your li elements can be gathered up into an array and arrays have indexes, you really don't need to pass a hard-coded number to your function. You can just pass the index of the li that is being clicked to the function.
Also, don't use <a> elements when they are not directly navigating you anywhere. This can cause problems for people who use screen readers. Instead, set up the click event directly on the li elements and eliminate the a elements completely.
Lastly, don't use inline HTML event attributes (onclick). That is how we did event handlers 20 years ago and, unfortunately, this technique just won't die. There are many reasons not to use them. Instead, follow modern standards and separate your JavaScript from your HTML.
// Get all the li elements into an array
var items = Array.prototype.slice.call(document.querySelectorAll("#id_ScrollToNav > li"));
// Loop over the list items
items.forEach(function(item, index){
// Assign each item a click event handler that uses the index of the current item
item.addEventListener("click", function(){ scrollToArticle(index) });
});
// Just for testing
function scrollToArticle(articleNumber){
console.log(articleNumber);
}
#id_ScrollToNav > li {
cursor:pointer;
}
<ul id="id_ScrollToNav" role="list">
<li class="sectionNavOff" role="link">•</li>
<li class="sectionNavOn" role="link">•</li>
<li class="sectionNavOff" role="link">•</li>
</ul>
To add to the above, use data- attributes to separate css styles from javascript (meaning, html class tags should be used for html/css things only).
<li data-element="sectionNavOff">
<li data-element="sectionNavOn">
There are some minor downsides to using data- tags, mainly speed, but many enterprise applications and frameworks (e.g. Bootstrap) tend to believe the upside to separating styles from javascript completely outweighs the downsides. If I knew whether or not you use jQuery I could give you a detailed usage example.

Find if ancestor has a certain CSS class

I would like to know if there is an easier way to check if an element has an ancestor with a particular class.
Consider the following HTML code:
<ul id="uniqueID" class="parentClass">
<li class="subclassA">
<div class="subclassB">
<nobr>
MyText
</nobr>
</div>
</li>
<li class="subclassA"> ... </li>
<li class="subclassA"> ... </li>
<li class="subclassA"> ... </li>
</ul>
<div>other elements in this page which I want to select</div>
Right now, I can select the element MyText by using a jQuery selector checking the href for a particular format. What I can then do is do .parent() a known number of times (4) and then check the class attribute of that particular element that I've now moved to. While this is working just fine, I am curious if there is a better way to do it, perhaps one that lets me be a bit more dynamic?
PS. There are a lot of elements that I'm selecting that'll fit this $('[href *= index.php]') format, so I want to keep those but remove the ones that fall under the categorization where they are a descendant of a member of class listclass. Currently I'm just selecting all of the elements with the selector above, then using an if statement to check through and see if it fits this condition. Again, if there is a more efficient way to do this (perhaps select these certain elements in the first place?) I would love to hear about it.
Current code:
$('[href *= "index.php"]').each(function(){
if ($(this).parent().parent().parent().parent().attr('class') != 'parentClass'){
//do things
}
});
To generalise you can use
.closest(".parentClass")
You can use closest and is:
$('[href*="index.php"]').each(function(){
if ($(this).closest('ul').is('.parentClass')) {
//do things''
}
});
if($(this).parents("ul.parentClass").length == 0){
//do something
}

Using jQuery appendTo() while sorting html elements

I need to effectively sort html nodes inside some container. Here's a simplified version of what I did:
<html>
<ul class="navigation">
<li class="first">Main</li>
<li class="second">HTML и CSS tricks</li>
<li class="third">Study</li>
<li class="fourth">HTML reference</li>
</ul>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script src="http://code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script>
var rows = $( ".navigation" ).children();
function sortAlpha(a,b){
return a.innerHTML.toLowerCase() > b.innerHTML.toLowerCase() ? 1 : -1;
};
rows.sort(sortAlpha).appendTo('.navigation');
</script>
</html>
Some of the people I work with are suspicious about the line
rows.sort(sortAlpha).appendTo('.navigation');
They ask is it ok to use appendTo this way, maybe there are (or will be in the future) some drawbacks of using this method, how come using appendTo replaces the content of the parent container and why it won't just add the sorted things to the parent container? What if it's a temporary feature of jQuery and it won't work like that in the future?
I need to say that the real case is more complex, e.g. we use the library that allows us to have object-oriented programming at the front-end side, also each element which needs to be sorted is a row of a table that contains some controls that have event bindings.
The idea was to replace the content of the dom node with sorted elements so this library wouldn't notice that. I used the approach described above in the real code and it's bugless at least for now, but the question remains: 1) what are the drawbacks of this approach, 2) what can be a better option?
This is covered in the documentation for .appendTo():
We can also select an element on the page and insert it into another:
$( "h2" ).appendTo( $( ".container" ) );
If an element selected this way is inserted into a single location elsewhere in the DOM, it will be moved into the target (not cloned)...
So, since you're selecting elements that already exist on the page, and calling .appendTo() with a selector that only matches a single element, those selected elements are moved into that new target element, in the order they're in inside of your set of matched elements. It's irrelevant that you're putting them back into the same element.
What are the drawbacks?
None that I can think of. You're using a documented aspect of what the function does. It's unlikely that the way that .appendTo() works is ever going to be changed.
appendTo is supposed to appendTo...I'm surprised it just acts like .html(). To make them happy why dont you just do $('.navigation').empty().append(rows.sort(sortAlpha));
Basically, appendTo() will insert html nodes at the end of the specified element, here : .navigation
Assuming you're using jQuery Sort plugin (http://github.com/jamespadolsey/jQuery-Plugins/tree/master/sort/), if you need to reorder your navigation menu, you could simply do :
$('.navigation li').sort(function(a, b){
return $(a).text() > $(b).text() ? 1 : -1;
});
By the way, if you want to replace the full content of an element, consider html() method, which empty the node and insert the content.
Try this,
$(function() {
$.fn.sortList = function() {
var mylist = $(this);
var listitems = $('li', mylist).get();
listitems.sort(function(a, b) {
var compA = $(a).text().toUpperCase();
var compB = $(b).text().toUpperCase();
return (compA < compB) ? -1 : 1;
});
$.each(listitems, function(i, itm) {
mylist.append(itm);
});
}
$("ul.navigation").sortList();
});
Demo

(Adding and) Removing list items with JavaScript / jQuery

I am almost a noob at JavaScript and jQuery, (so I apologize if I didn't recognize a suiting answer to my question, in similar posts).
Here is the thing. I have a list with lots of stuff in each list item:
<ul id="fruit_list">
<li>
<h4> Fruit 1: remove </h4>
<p> blablabla </p>
</li>
<li>
<h4> Fruit 2: remove </h4>
<p> blablabla </p>
</li>
</ul>
add
What I want to do, is when I click on the anchor 'remove', to remove the list item containing it.
(Optionally I would like to manipulate the incremental number at Fruit 1, Fruit 2 etc, in a way that when I remove item #2, then the next one becomes the #2 etc. But anyway.)
So here is what I've written so far:
$(function(){
var i = $('#fruit_list li').size() + 1;
$('a.add').click(function() {
$('<li><h4>Fruit '+i+':<a href="#" class="remove">
remove</a></h4><p>Blabla</p></li>')
.appendTo('#fruit_list');
i++;
});
$('a.remove').click(function(){
$(this).parentNode.parentNode.remove();
/* The above obviously does not work.. */
i--;
});
});
The 'add' anchor works as expected. The 'remove' drinks a lemonade..
So, any ideas?
Thanks
EDIT: Thanks for your answers everybody!
I took many of your opinions into account (so I won't be commenting on each answer separately) and finally got it working like this:
$('a.remove').live('click', function(){
$(this).closest('li').remove();
i--;
});
Thank you for your rapid help!
The a.remove event binding needs to be a live http://api.jquery.com/live/ binding. The nodes are added to the DOM after doc ready is called.
Additionally, I think you want to use parent() instead of parentNode. Unless I'm behind on my jQuery, parentNode is just DOM manipulation and there's no standard remove(), it's removeChild(). Here you need a jQuery collection returned from parent().
Try $(this).parents("LI").remove();
The reason is that $('a.remove') is only executed once, and so only found at the moment you don't have any remove links yet. To solve this rewrite your ADD function like this:
$('a.add').click(function() {
var $li = $('<li><h4>Fruit '+i+':<a href="#" class="remove">
remove</a></h4><p>Blabla</p></li>');
$li.appendTo('#fruit_list');
$li.find('a.remove').click(function() {
$li.remove();
i--;
});
i++;
});
And just remove your old remove function.
EDIT: Oh, this will only work for items you add, if you already load some list items in the html before any Javascript is executed add this function under the $('a.add').click:
$('a.remove').click(function(){
$(this).parent().parent().remove();
i--;
});

Sorting in the DOM with adjacent elements

I have the the following DOM structure which I want to sort according to the data-created attribute.
<a id="comment-34" href="#"/>
<li data-created="12342342" />
<a id="comment-35" href="#"/>
<li data-created="89342342" />
<a id="comment-36" href="#"/>
<li data-created="45363342" />
I CANNOT (for various reasons) wrap the <a> and <li> in an outer <div>. I want to do javascript sorting. All the jQuery sorting plugins can do the sorting if I just had the <li>. E.g. using the tinysort jQuery plugin ( http://tinysort.sjeiti.com/ ) I can do
$('li').tsort({order:'desc', attr:'data-created'});
However what happens after the sort is that <a> are no longer associated with their original siblings. I also evaluated https://github.com/jamespadolsey/jQuery-Plugins/tree/master/sortElements/ but it may suffer from the same problem.
Any way to do this? Again, I cannot wrap the <a> and <li> in an outer <div>. I also don't want to dynamically wrap a <div> so that I can use tsort.
Any clean solutions :-) ?
Something like this should work for you:
var elms = [];
$('a').each(function() { //create the array of a and li
var pair = {
aTag: $(this),
liTag: $(this).next("li")
};
elms.push(pair);
});
$("a, li").detach(); //detach them from the dom
elms.sort(function(a, b) {
return a.liTag.data("created") < b.liTag.data("created"); //sort based upon the created data
});
$.each(elms , function(){
$("ul").append(this.aTag).append(this.liTag); //push them back to the dom.
});
Code example on jsfiddle.
You can't really have <li> and <a> elements as siblings in the first place. A list item must be inside a list (<ol>,<ul>), where other elements are not allowed.
Ignoring that, you can simply grab each pair, remove from the DOM, reorder, then put them back. It's quite straight-forward. Example:
var items = [];
$('#sortme a').each(function(){
// grab the element and its next sibling
var self = $(this)
, next = self.next('div');
items.push([
self.remove().get(0),
next.remove().get(0)
]);
});
items.sort(function(a,b){
return a[1].getAttribute('data-created') > b[1].getAttribute('data-created');
});
$.each(items, function(){
$('#sortme').append(this[0], this[1]);
});
Test here: http://jsbin.com/okajo4/edit
Edit: a simpler version :)
var sorted = $('#sortme');
sorted.find('div')
.sort(function(a,b){
return $(a).data('created') > $(b).data('created');
})
.each(function(){
$(this).prev('a').andSelf().appendTo(sorted);
});
Move the a elements inside their respective list items. Then you ought to be able to use the sort you mentioned.
Even if having the elements as siblings is valid, it still strikes me as bad form.

Categories