user1728278
user1728278

Reputation: 625

Sort array containing DOM elements according to their position in the DOM

Context

I've structured a jQuery plugin I'm currently working on in a way that has me storing DOM elements in an array, mostly for being able to store more information next to these elements without having to use the not-so-fast data().

That array looks like:

[
    { element: DOMElement3, additionalData1: …, additionalData2: … },
    { element: DOMElement1, additionalData1: …, additionalData2: … },
    { element: DOMElement2, additionalData1: …, additionalData2: … },
]

The way this plugin works prevents me from pushing these elements to the array in a predictable order, which means DOMElement3 can in fact find itself at an index lower than DOMElement2's.

However, I need these array elements to be sorted in the same order as the DOM elements they contain appear in the DOM. The previous example array, once sorted, would look like this:

[
    { element: DOMElement1, additionalData1: …, additionalData2: … },
    { element: DOMElement2, additionalData1: …, additionalData2: … },
    { element: DOMElement3, additionalData1: …, additionalData2: … },
]

This is, of course, if DOMElement1 appears before DOMElement2 in the DOM, and DOMElement2 before DOMElement3.

Discarded solution

The jQuery add() method returns a set of DOM elements in the same order as they appear in the DOM. I could be using this, but the requirement is that I work with a jQuery collection – which means I'd have to refactor a huge chunk of the abovementioned plugin to use a different storage format. That's why I consider this a last-resort solution.

Another solution?

I would have imagined that map() and a sort of global DOM index tied to each DOM element would have done the trick, but there doesn't appear to be such a "global DOM index".

What approach can you think of? (If writing code, both vanilla JS and jQuery are welcome.)

Upvotes: 14

Views: 4261

Answers (4)

Niet the Dark Absol
Niet the Dark Absol

Reputation: 324650

There is a very useful function called compareDocumentPosition, which returns a number based on where two elements are relative to each other. You can use this in a .sort callback:

yourArray.sort(function(a,b) {
    if( a === b) return 0;
    if( !a.compareDocumentPosition) {
        // support for IE8 and below
        return a.sourceIndex - b.sourceIndex;
    }
    if( a.compareDocumentPosition(b) & 2) {
        // b comes before a
        return 1;
    }
    return -1;
});

Upvotes: 27

svidgen
svidgen

Reputation: 14302

If you can assume that the visible location on the page is the same, that could be a relatively quick solution:

function compareByVisibleLocation(a, b) {
  if (a.offsetTop > b.offsetTop) {
    return 1;
  } else if (a.offsetTop < b.offsetTop) {
    return -1;
  }

  if (a.offsetLeft > b.offsetLeft) {
    return 1;
  } else if (a.offsetLeft < b.offsetLeft) {
    return -1;
  } else {
    return 0;
  }
}

nodes.sort(compareByVisibleLocation);

Else, you can use querySelectorAll() to quickly build an index (it's a depth-first pre-order traversal) either by explicitly marking the nodes you want to index with an indexed-node classname.

var nodes = document.querySelectorAll('.indexed-node');

Or by grabbing all of a single node type, if that's works:

var nodes = document.querySelectorAll('div');

Or by just grabbing all nodes:

var nodes = document.querySelectorAll('*');

And then adding an index attribute to each node:

for (var i = 0; i < nodes.length; i++) { nodes[i].__DomIndex = i; }

You can sort like this then:

var nodes = yourGetMethod().sort(function(a,b) {
  if (a === b) {
    return 0;
  } else if (a.__DomIndex > b.__DomIndex) {
    return 1;
  } else {
    return -1;
  }
});

Should work down to IE8.

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074475

You don't have to do a lot of refactoring to take advantage of jQuery's sorting. You can use it as a temporary sorting mechanism. Here's an off-the-cuff:

function putInDocumentOrder(a) {
      var elements;

    // Get elements in order and remember the index of
    // the entry in `a`
    elements = $().add(a.map(function(entry, index){
        entry.element.__index = index;
        return entry.element;
    }));

    // Build array of entries in element order
    a = elements.map(function(){
        return a[this.__index];
    }).get();

    return a;
}

It requires an expando, but it does the job. Live Example

This has the advantage that it works on all browsers that jQuery supports, rather than your having to deal with the edge cases (IE8 not supporting compareDocumentPosition, for instance) yourself.

Upvotes: 1

James Montagne
James Montagne

Reputation: 78650

While I don't suggest you go with this approach, it is possible with jquery to get a "global DOM index":

var $all = $("*");

// use index to get your element's index within the entire DOM
$all.index($YOUR_EL);

Upvotes: 1

Related Questions