Sort elements by document order in JavaScript - javascript

I have an array of Elements and I want them in document order. I know this is trivial to achieve in XPath, but the logic I have to implement is a bit complicated for a single expression.
I did find Node.compareDocumentPosition(), but it generates a bit mask of quite a few combinations, so not very ideal for a comparator.
As I final resort, I could probably add a random attribute on to all the elements in the array and select them again using XPath, but I'd rather not do that if possible.

I don't necessarily agree that document.compareDocumentPosition() is insufficient for a comparator. Why do you consider this not ideal?
var elementArray = [];
elementArray.push(document.getElementById('div3'));
elementArray.push(document.getElementById('div2'));
elementArray.push(document.getElementById('div4'));
elementArray.push(document.getElementById('div1'));
function documentPositionComparator (a, b) {
if (a === b) {
return 0;
}
var position = a.compareDocumentPosition(b);
if (position & Node.DOCUMENT_POSITION_FOLLOWING || position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
return -1;
} else if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
return 1;
} else {
return 0;
}
}
console.log('unsorted::', elementArray);
console.log('sorted::', elementArray.sort(documentPositionComparator));
<div id="div1">Div 1
<div id="div2">Div 2
<div id="div3">Div 3</div>
<div id="div4">Div 4</div>
</div>
</div>

Related

Target by ID elements in array assigned by get() jQuery

Apologies if I get the terminology wrong here.
I have a 'grid' of images in html that I want to use jQuery to fade in each element randomly. One item in the grid is a Logo - I want it to fade in last. The grid size could be changed and the position of the 'logo' could also be different. Here is a reduced simplified output of the list.
<ul id="homepage-grid" class="projectsgrid row">
<div id="item1">
</div>
<div id="item2">
</div>
<div id="itemlogo" style="opacity: 0;">
<a href="#" class="block" style="padding-bottom: 100%;">
<div style="background-image:url('logoonly.png')" title="" class="logoblock"></div>
</a>
</div>
<div id="item4">
</div>
<div id="item5">
</div>
</ul>
I have the following script which will collect the elements into an array.
But i can't figure out how to match the element with the 'itemlogo' ID in the collection to split it out and push it to the end of the array so it is last to 'fade in'. I have tried "div#itemlogo", "#itemlogo", "itemlogo" but nothing seems to match, and perhaps not knowing the name of what I am doing I can't find any references.
var elems = $('#homepage-grid > div').get(); // collect elements
console.log(elems);
for (var i = elems.length - 1; i > 1; i--) { // Shuffle the order
var j = Math.floor(Math.random() * (i + 1));
var elem = elems[j];
elems[j] = elems[i];
elems[i] = elem;
}
elms = elems.push(elems.splice(elems.indexOf('div#itemlogo'), 1)[0]); // pull logo to last??
var i = 0;
var timer = setInterval(function() { // animate fade them sequentially
console.log(elems[i]).id();
$(elems[i]).fadeTo( "slow" , 1);
if (i === elems.length) {
clearInterval(timer);
}
i++;
}, 150);
You're on the right path, but the key here is that you need to find a particular item. Those items are DOM elements, not strings or selectors on their own.
elems.push(
elems.splice(
elems.findIndex(node=>node.id === 'itemlogo'),
1
)[0]
);
findIndex allows you to pass a function that should return true for the item you want - in this case, you want the item whose ID is itemlogo. The rest is just the same push-splice thing you have already.
I would also like to praise your correct use of array shuffling. You can simplify it a little bit with destructuring:
[elems[i], elems[j]] = [elems[j], elems[i]];

Apply random class to multiple groups of elements with no repeat

I have multiple groups of divs that I need to apply a random class to with no repeats in each of the groups. Once I've done this, I then need to 'reset' the Array back to the original values, and move onto the next group of divs where I apply a random class to each div again.
The idea is to get to something that looks like this:
<div class="mini-thumbnail-container">
<div class="mini-thumbnail-individual-image left">
<div class="mini-thumbnail-individual-image centre">
<div class="mini-thumbnail-individual-image right">
</div>
<div class="mini-thumbnail-container">
<div class="mini-thumbnail-individual-image centre">
<div class="mini-thumbnail-individual-image right">
<div class="mini-thumbnail-individual-image left">
</div>
<div class="mini-thumbnail-container">
<div class="mini-thumbnail-individual-image right">
<div class="mini-thumbnail-individual-image left">
<div class="mini-thumbnail-individual-image centre">
</div>
etc.
I've shamelessly taken code taken from another stackoverflow question to do with this question, but I can't figure out how to get it to work over all my elements, instead of the first three.
Here's the jQuery:
function shuffle(obj) {
var l = obj.length,
i = 0,
rnd,
tmp;
while (i < l) {
rnd = Math.floor(Math.random() * i);
tmp = obj[i];
obj[i] = obj[rnd];
obj[rnd] = tmp;
i += 1;
}
}
var classes = ["centre", "left", "right"];
shuffle(classes);
jQuery(".mini-thumbnail-individual-image").each(function() {
jQuery(this).addClass(classes.pop());
});
And here's a basic outline of my div structure – I have multiples of these which I want to iterate over and apply the random class to each <div class="mini-thumbnail-individual-image">
HTML:
<div class="col-1-6">
<div class="mini-thumbnail-container">
<div class="mini-thumbnail-individual-image">
<img class="mini-thumbnail-image" src="" />
</div>
<div class="mini-thumbnail-individual-image">
<img class="mini-thumbnail-image" src="" />
</div>
<div class="mini-thumbnail-individual-image">
<img class="mini-thumbnail-image" src="" />
</div>
</div>
</div>
I think I need to create a loop that looks is triggered after each iteration over the array, and once it spots the array is empty, I need to push the class names back in, and then loop over the next group of divs, until each group of divs has a random class applied, but maybe there is a simpler way that I haven't thought of yet.
Thanks in advance!
You're on the right track.
Without speaking of methods to optimize the code, here's (one) way to get quickly to where you need to be:
function shuffle(obj) {
var l = obj.length,
i = 0,
rnd,
tmp;
while (i < l) {
rnd = Math.floor(Math.random() * i);
tmp = obj[i];
obj[i] = obj[rnd];
obj[rnd] = tmp;
i += 1;
}
}
// declare OUTSIDE the function for correct scope
var classes;
// Simple function to set up the classes variable and shuffle.
function setUpClasses() {
classes = ["centre", "left", "right"];
shuffle(classes);
}
jQuery(".mini-thumbnail-individual-image").each(function() {
// Check if classes is set / empty. If so, set up the classes again.
if (!classes || classes.length < 1) {
setUpClasses();
}
jQuery(this).addClass(classes.pop());
});
If you want to look at cleaner / briefer ways to shuffle the array, this article has some other techniques. Here's one of them:
yourArray.sort(function() { return 0.5 - Math.random() });
So you could literally remove your shuffle function, and just do this:
function setUpClasses() {
classes = ["centre", "left", "right"];
classes.sort(function() { return 0.5 - Math.random() });
}
Or, if you wanted maximum brevity:
function setUpClasses() {
classes = ["centre", "left", "right"].sort(function() { return 0.5 - Math.random() });
}
Here is a working Fiddle

Hide element if it has an attribute value within an array

What I'm trying to achieve is hiding specific divs if it's specified attribute matches a value within an array.
The information being used will be dynamic and the divs that need to hide may change. Therefore it has to be extensible.
What I have so far is grabbing the attributes and putting them into an array. Then I'm matching what was put into that array against another array that specifies what divs will need to hide. If a divs attribute matches hide that div otherwise let the div render.
Right now I'm getting an all or none on hiding the divs.
Here is the code
var matching = ['2', '3'];
var mids = [];
$('.merch-tile').each(function(i, e) {
mids.push($(e).attr('m_mid'));
});
//alert(mids);
for (var c1 = 0; c1 < mids.length; c1++) {
//alert('running');
var nm_arg = matching[c1];
//alert('still running');
if ($.inArray(nm_arg, mids) === -1)
$('.merch-tile').hide();
alert('something matches');
}
$(document.body).append(mids);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<div class="merch-tile" m_mid="1">Tile 1</div>
<div class="merch-tile" m_mid="2">Tile 2</div>
<div class="merch-tile" m_mid="3">Tile 3</div>
You can
var matching = ['2', '3'];
$('.merch-tile').filter(function (i, e) {
return matching.indexOf($(this).attr('m_mid')) > -1
}).hide();
var matching = ['2', '3'];
$('.merch-tile').filter(function (i, e) {
return matching.indexOf($(this).attr('m_mid')) > -1
}).hide();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<div class="merch-tile" m_mid="1">Tile 1</div>
<div class="merch-tile" m_mid="2">Tile 2</div>
<div class="merch-tile" m_mid="3">Tile 3</div>
There's no need for the mids array. Just loop over the DOM elements, testing if the attribute is in the array.
I also don't understand $(document.body).append(mids) at the end -- mids is not HTML, it's an array.
I've also replaced your custom attribute with data-mid. data-XXX attributes are reserved for the programmer to use as extensions.
If you want to hide the DIVs that are in the array, you should test != -1, not == -1.
var matching = ['2', '3'];
var something_matches = false;
$('.merch-tile').each(function() {
if ($.inArray($(this).attr('data-mid'), matching) != -1) {
$(this).hide();
something_matches = true;
}
});
if (something_matches) {
alert('something matches');
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<div class="merch-tile" data-mid="1">Tile 1</div>
<div class="merch-tile" data-mid="2">Tile 2</div>
<div class="merch-tile" data-mid="3">Tile 3</div>
Beware of using .data('mid') instead of .attr('data-mid'). .data() returns the value as an integer, because an attribute value that's valid JSON is parsed as JSON, but matching contains strings.
Look at your code :
if ($.inArray(nm_arg, mids) === -1)
$('.merch-tile').hide(); // Look at here,you hide all, because of your have class name
alert('something matches');

Better (more performant) way to get specific DIVs?

I want to add a specific DIV to other DIVs of a defined class. Because the page changes regularly I do this on every DOM-Change. This happens quite often and there are a lot of DIVs (up to a few 1000) that meet the criteria.
(This is an extension so I cannot modifiy the source)
I do it this way:
$('.Qg').each(function() {
if ($(this).parent().find('.quickShare').length === 0)
{
$(this).before('<div class="quickShare">(some more html)<br/></div>');
}
});
That works but does not seem to be very performant, mainly because of the "each" - Loop
Is there a more elegant (and especially performant) way to get only those DIVs which's parent do not contain my DIV (something like $('.Qg').parent().without('quickShare').each(function(){}); (pseudocode)?
Update: To make it clearer a DOM-Example:
<div class="anOuterDiv>
<div class="Qg">something here</div>
</div>
<div class="anotherOuterDiv">
<div class="quickShare">already added</div>
<div class="Qg">something here</div>
</div>
I want to Add the "quickShare" div before the "Qg", but only if it does not exist. (So I want to get the upper Qg, but not the lower Qg)
Give all the parents of .Qg the class QgContainer, then do:
$(".QgContainer:not(:has(.quickShare)) > .Qg").each(function() {
...
});
Since you can't change the site, try:
$(".Qg").filter(function() {
return $(this).siblings(".quickShare").length == 0);
}).each(function() {
...
});
As you wanted better(more perfomant) then you could consider using pure Javascript.
HTML
<div class="anOuterDiv1">
<div class="Qg">something here</div>
</div>
<div class="anOuterDiv2">
<div class="quickShare">already added</div>
<div class="Qg">something here</div>
</div>
<div class="anOuterDiv3">
<div class="Qg">something here</div>
</div>
<div class="anOuterDiv4">
<div class="quickShare">already added</div>
<div class="Qg">something here</div>
</div>
Javascript
Array.prototype.forEach.call(document.getElementsByClassName('Qg'), function (Qg) {
var parentNode = Qg.parentNode,
quickShares = parentNode.getElementsByClassName('quickShare'),
newQuickShare;
if(!quickShares.length) {
newQuickShare = document.createElement('div');
newQuickShare.className = 'quickShare';
newQuickShare.textContent = 'Newly added';
parentNode.insertBefore(newQuickShare, Qg);
}
});
On jsFiddle
Next we should actually compare it against some jQuery, so we will use the accepted answer.
$(".Qg").filter(function() {
return $(this).siblings(".quickShare").length == 0;
}).each(function() {
$(this).before('<div class="quickShare">Newly added</div>');
});
On jsFiddle
And now lets see how they perform on jsPerf
You can filter each .Qg that's not preceded by a .quickShare sibling and then apply .before() on that:
$('.Qg')
.filter(function() {
var node = this.previousSibling; // start with previous sibling
while (node) {
if (node.className == 'quickShare') {
return false; // we found one
}
node = node.previousSibling; // keep trying with previous sibling
}
return true;
})
.before('<div class="quickShare">(some more html)<br/></div>');
This time it will definitely work:
$('div:only-child.Qg').each(function(){
$(this).before('<div class="quickShare">(some more html)<br/></div>');
});
Try this. This is very easy and readable and small and performant.
jsFiddle Demo http://jsfiddle.net/VS6mG/

jQuery function similar to closest that will return elements outside of the parent chain

Is there any jQuery function similar to closest() that will return elements outside of the parent chain, traversing sideways? For example, I want to call a function foo() on the div source that would return the div target. I know I could navigate using parent() and siblings(), but I need something generic that would go as many levels as needed, up, sideways and down?
var allsources = $('.source');
allsources.click(function()){
$(this).closest('.target').hide();
});
<div class="row">
<div>
<div class="target" ></div>
</div>
<div>
<div>
<div class="source"></div>
</div>
</div>
</div>
<div class="row">
<div>
<div class="target" ></div>
</div>
<div>
<div>
<div class="source"></div>
</div>
</div>
</div>
EDIT:
My definition of closest: you have an element source. Try to find it down. If find more than one, return one that is less node hoops down/next/prev. If not found, go one level up, and try to find again. Repeat until no parent.
If, by closest, you mean "travel up as little as possible, then anywhere downwards", then you can do
$("#source")
.closest(":has(.target)")
.find(".target:first") //make sure we only select one element in case of a tie
In your case, it would be better to specify the common parent directly:
$(this)
.closest(".row")
.find(".target") //there's no tie here, no need to arbitrate
This is a tricky one. As has been commented, how do you define closest in this context? Assuming you can decide on some rules; for example:
Traverse up: 3pt
Traverse down: 2pts
Move sideways: 1pts
And then consider the item with the lowest points to be "closest" then it would be easy enough to author a plugin, named something such as closestAll, which would do the recursive traversal of the whole dom tree to determine the closest item.
However, looking at your recent edit, one (of many!) right solutions to the problem stated is:
var allsources = $('.source');
allsources.click(function(){
$(this).parents('.row').find('.target').hide();
});
Live example: http://jsfiddle.net/zCvJM/ (Source A only hides Target A, Same for B)
If you know exactly the structure of the dom and level of nesting, have you consider to use the eq() method
$(this).parents().eq(1).prev().children(".target")
I don't think there is a way to do this other than basically querying the whole DOM:
$('#target')
Because if you want to go up and across (never mind down as well) then the target element isn't related to the child element. If you also want to check for the presence of the child element you will have to do that separately.
-Edit:
After reading your comment on wanting to find the closest element regardless of whether it is a parent, I think you will have to write a custom function to crawl back up the dom one node at a time. I have tested the following and it works:
Markup
<div id="parent">
<div id="child1">
<div id="source"></div>
</div>
<div id="child2">
<div class="target" rel="right"></div>
</div>
<div id="child3">
<div>
<div class="target" rel="wrong"></div>
</div>
</div>
</div>
Script
$(document).ready(function () {
var tgt = findClosest($('#source'), '.target');
if (tgt != undefined) {
alert(tgt.attr('rel'));
}
});
function findClosest(source, targetSel) {
var crawledNodes = $();
var target = null;
// Go up
source.parents().each(function () {
console.log(crawledNodes.index($(this)));
if (crawledNodes.index($(this)) == -1 && target == null) {
crawledNodes.add($(this));
target = findTarget($(this), targetSel);
// Go across
$(this).siblings().each(function () {
console.log("Sibling");
if (crawledNodes.index($(this)) == -1 && target == null) {
crawledNodes.add($(this));
target = findTarget($(this), targetSel);
}
});
}
});
return target;
}
function findTarget(el, targetSel) {
console.log(targetSel);
var target = el.find(targetSel);
if (target.size() > 0) {
return target.eq(0);
}
else
{
return null;
}
}
If I understood the specification correctly you mean something like the function closest defined below:
var allsources = $(".source");
function closest($source,selector) {
if($source == null) return $([]);
var $matchingChildren = $source.find(selector);
if($matchingChildren.length != 0) return $($matchingChildren.get(0));
else return closest($source.parent(), selector)
}
allsources.click(closest($(this),'.target').hide();});
You can see it working at http://jsfiddle.net/y2wJV/1/
Your definition requires that when choosing among matching children the function must return one that is less node hoops down/next/prev. This requirement has not been met, but this function is quite flexible and seems to do what you want to do in the case of the example you provided.
I found this code that is simple but does not solve the tie issue (returns the first)...
(function ($) {
$.fn.findClosest = function (filter) {
var $found = $(),
$currentSet = this; // Current place
while ($currentSet.length) {
$found = $currentSet.find(filter);
if ($found.length) break; // At least one match: break loop
// Get all children of the current set
$currentSet = $currentSet.parent();
}
return $found.first(); // Return first match of the collection
};
})(jQuery);
I encountered a similar problem, i had a table i needed to find the next element which may be outside the current td, so i made a jquery function:
$.fn.nextAllLevels = function(sel) {
if ($(this).nextAll(sel).length != 0) {
return $(this).nextAll(sel).eq(0);
} else if ($(this).nextAll(':has(' + sel + ')').length != 0) {
return $(this).nextAll(':has(' + sel + ')').find(sel).eq(0);
} else {
return $(this).parent().nextAllLevels(sel);
}
So to use this you simply call
$('#current').nextAllLevels('.target');
To give you the element closest in the foward direction, regardsless of whether in is in the current parent or not.

Categories