Sorting elements with JavaScript

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Sorting elements with JavaScript

Post by agibsonsw »

Hello.
I'm interested in sorting 'p' elements on a page by their text content (nodeValue).
I was thinking of looping through the relevant paragraphs and adding their nodeValue and the element itself to a SortedList. (Can a SortedList contain elements?)
If I then go through the SortedList and appendChild for each one this should put them in the correct order.

The only examples I've found so far are for tables and are a bit messy.

Can you foresee any problems with my approach? Or is there another way.. Thanks, Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

Oops, I think a SortedList is in .asp.

Could I store them in a hash table and then sort them?
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

No worries. I've sorted it!! The following will sort all (p)aragraphs named 'myPara' according to their text content, in ascending or descending order.

Code: Select all

function Keys(obj)
{
	var keys = [];
	for(var key in obj)
		keys.push(key);
	return keys;
}
function SortElements(btn)	// btn is a button's value (either Asc or Desc)
{
	var myPs = {};
	var allPs = document.myForm.getElementsByTagName('p');
	for ( var i = 0, aP; aP = allPs[i]; i++ )
		if ( aP.name && aP.name == 'myPara' )
			myPs[aP.lastChild.nodeValue] = aP;
	var sortedKeys = Keys(myPs).sort();
	if ( btn.value == 'Desc' ) sortedKeys.reverse();
	btn.value = ( btn.value == 'Asc' ) ? 'Desc' : 'Asc';
	for (var i = 0; i < sortedKeys.length; i++)
		document.myForm.appendChild(myPs[sortedKeys[i]]);
}
I'm on fire :fanfare:
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

.. the only issue remaining is, how can I check if my array myPs is empty? I've tried checking myPs.length == 0, or myPs == null but nothing works.
Any ideas? Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
jscher2000
2StarLounger
Posts: 148
Joined: 26 Dec 2010, 18:17

Re: Sorting elements with JavaScript

Post by jscher2000 »

agibsonsw wrote:.. the only issue remaining is, how can I check if my array myPs is empty? I've tried checking myPs.length == 0, or myPs == null but nothing works.
Is it an array or an object? Maybe change

Code: Select all

var myPs = {};
to

Code: Select all

var myPs = [];
If that breaks it, maybe there is some kind of member count you can use with the object?

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

Hello.

I found a slightly complicated prototype method isEmpty() that was supposed to work, but didn't.

I found the simplest way was just to use a boolean 'foundSomething' within my loop and test if it was set. In the end I didn't need either - the elements correspond to cookies, so if 'document.cookie.length == 0' I know there is nothing to sort :smile:

I realised that the point you raised was the issue - was it an object or an array? Your suggestion would probably have helped, or perhaps I could have just use 'var myPs;'?

I was quite pleased with my sort though - I found some quite extensive code for sorting tables, which seemed unnecessary. I think I could adapt my method 'fairly' easily to sort tables by any column in ascending or descending order. I'll have to give it a go.

Is there a method/function that will confirm if an array is already sorted? I seem to recall something similar (although I might be thinking of PHP). Thanks, Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I tried to roll my own 'is it sorted' method, but not sure if it can be made to work?

Code: Select all

function IsAscending(element, index, array) {
	IsAscending.Small = IsAscending.Small || "aaaa";
	return (element > IsAscending.Small);
	IsAscending.Small = element;
}
var someArray = [ "and", "bats" , "aardvark", "zebra", "zz"  ];
window.alert(someArray.some(IsAscending));
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I think I was overcomplicating it. I think this might do it, but I've yet to put it through its paces:

Code: Select all

function NotSortedAsc(element, index, array) {
      return ( (index != 0) && (element < array[index-1]) );
}
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

Hello again.
I'm using a callback function with the sort() method so that I can sort text case-insensitively.

Code: Select all

function Asc(a, b) {	// used by sort() method (case insensitive)
	a = a.toLowerCase(); b = b.toLowerCase();
	if (a > b) return 1;
	if (a < b) return -1;
	return 0; 
}
Seems to work great. But testing IE8 (what a pain!), if I click my sort button too quickly for IE to cope there is an error in the page - number expected. When I debug I see that 'b' correctly refers to a text item in my list, but 'a' is undefined. Or, more accurately, 'Variable uses an Automation type not supported in JScript'.

I understand that IE's sorting is very inefficient and perhaps it can't cope with me clicking the button too quickly. Do you think that there's anything I can add to this callback function to prevent/ignore the error? Or, I hate to say it, should I ignore it as they can just click the button again (a micro-second later) and the problem goes away? Thanks, Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I'm revisiting sorting paragraphs within a DIV element. It seems I can store all the paragraphs into an array, and their child elements are (seemingly) accessible within the array as well. So if I create my own helper function that compares the text within a child element (of each paragraph) I should be able to sort all of the paragraphs using the 'sort' method.

Code: Select all

var allPs = new Array();
allPs = $('excludes').getElementsByTagName('p');
var newPsArray = allPs.sort(AscP);  // where.. 

function AscP(a, b) {		// sort all paragraphs based on lastChild
	var spanTextA = a.lastChild.innerText || a.lastChild.textContent;
	var spanTextB = b.lastChild.innerText || b.lastChild.textContent;
	spanTextA = spanTextA.toLowerCase();
	spanTextB = spanTextB.toLowerCase();
	if (spanTextA > spanTextB) return 1;
	if (spanTextA < spanTextB) return -1;
	return 0; 
}
But Firefox returns ' allPs.sort is not a function'. Does anyone have a rough idea as to what the problem might be? Thanks for any suggestions, Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
jscher2000
2StarLounger
Posts: 148
Joined: 26 Dec 2010, 18:17

Re: Sorting elements with JavaScript

Post by jscher2000 »

agibsonsw wrote:But Firefox returns ' allPs.sort is not a function'. Does anyone have a rough idea as to what the problem might be?
Here's my guess. JS is a weakly typed language, so although allPs starts out as an array, you are overriding it and changing it to a node list. Apparently the sort() method only applies to native arrays and not node lists. The best solution is unclear... (that's my way of punting all the Googling back to you).

Edit: Actually, I think you already wrote the solution earlier in this thread.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I suppose I was a little optimistic in assuming JS would be able to treat a node list as a native array - I suppose it's really an 'array of arrays..'.

As you say, I already have the solution. I might carry on though.. and see if I can loop through and place the paragraphs into the array (indvidually) and then sort by a span within the paragraphs. I'm an optimist :smile:

Thanks, Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I'm posting my routine here for sorting paragraphs in case anyone might be interested - but mainly because I'm quite proud of it (being a relative newbie to JS) :smile:

The function will sort the paragraphs contained within a DIV according the text content of a SPAN. For this version the SPAN happens to be the last one in the paragraph. It also checks if the paragraphs are already in ascending order, in which case it will sort them in descending order.

Code: Select all

function Asc(a, b) {	// used by sort() method (case insensitive)
	a = a.toLowerCase(); b = b.toLowerCase();
	return (a > b) ? 1 : ((a < b) ? -1 : 0); 
}
function NotAscOrder(element, index, array) {	// used by some() method
	return ( index > 0 && element.toLowerCase() < array[index-1].toLowerCase() );
}

function SortElements() {	// sorts paragraphs (in a DIV) according to their last span's text
	var i, j, myPs = {}, aP, allPs, psLen, origKeys=[], origKey, sortedKeys=[], sortedKey, sortedLen;

	allPs = $('excludes').getElementsByTagName('p');	// 'excludes' is the name of my DIV
	
	for ( i = 0, psLen = allPs.length; i < psLen; i++ )	{		// go forward examining each paragraph
		aP = allPs[i];
		if ( aP.className === 'myPara' ) {	// or nodeName == 'P'					
			origKey = aP.lastChild.innerText || aP.lastChild.textContent;	// my span happens to be the last one
			// (this could be easily adapted using an index number as an argument to the function)
			myPs[origKey] = aP;					// innerText (IE) || textContent (FF)
			origKeys.push(origKey);
			sortedLen = sortedKeys.length || 0;
			j = 0;
			while ( j < sortedLen && sortedKeys[j].toLowerCase() < origKey.toLowerCase() ) j++;
			sortedKeys.splice(j,0,origKey);		// insert the key in the correct order
		}
	}
	if ( !origKeys.some(NotAscOrder) ) {		// if they were already in Asc order..
		sortedKeys.reverse();			// .. put them in Desc order
	}
	for (i = 0, sortedLen = sortedKeys.length; i < sortedLen; i++) {
		sortedKey = sortedKeys[i];
		$('excludes').appendChild(myPs[sortedKey]);
	}
	return true;
}
Description:
I loop through all paragraphs and store them in an associative array, using the span text as a key.
With each iteration of the loop I store the key in a new array (purely so I can test later whether they were already in sorted order) and also store the key in its correct sorted order in a second array - using splice() to insert it in the correct position.
I then test if the keys were already sorted - in which case I reverse() the sorted array.
I then loop through the sorted keys appending each corresponding paragraph to the DIV, so that they will appear in the correct order.

I think it should be fairly efficient as I'm doing most of the work within one loop, although I suspect that 'splicing' might be an expensive operation?

I should be able to adapt the function to accept arguments and possibly sort tables, lists, etc. Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I can now use my function to sort a table as well, by any column in ascending or descending order - using 'SortElements('tableid','tr','td',0)' where 0 is a column index. It should work with a list as well.

I hit a stumbling block with duplicate values, as array keys must be unique. But I temporarily add a sequence number "1001",etc., to the end of each key to resolve this. (This does limit the sorting to 999 rows.) I can also sort numerically by attaching a 'sort' attribute to td tags.

Code: Select all

function SortElements(parent, childTag, colTag, colIndex) {		// example use: SortElements('table1','tr','td',3)
	var i, j, parent, cTags = {}, colTag;
	var childLen, aChild, elem;
	var origKey, sortedKeys=[], sortedKey, keyNo='';
	var sortedLen, sorted = true;

	parent = document.getElementById(parent);
	cTags = parent.getElementsByTagName(childTag);
	for ( i = 0, childLen = cTags.length; i < childLen; i++ )	{		// go forward examining each child
		aChild = cTags[i];
		elem = aChild.getElementsByTagName(colTag)[colIndex];
		if ( elem ) {
			origKey = elem.getAttribute('sort') || elem.firstChild.nodeValue;
			// you can supply 'sort' attributes to enable correct sorting of numbers or dates.
			// For example, <td sort='20110212'> for a date.
			keyNo = '' + (1000 + i);		// need to ensure unique keys - means we can only sort 999 items
			cTags[origKey + keyNo] = aChild;
			sortedLen = sortedKeys.length || 0;
			for ( j = 0; j < sortedLen && (Left(sortedKeys[j],sortedKeys[j].length-4)).toLowerCase() <= (origKey).toLowerCase(); j++) ;
			sortedKeys.splice(j,0,origKey + keyNo);				// insert the key in its sorted position
			if ( sorted ) sorted = ( j == sortedLen );	// if the sorted key is not inserted at the end, then
														// the keys were not already in asc. order
		}
	}
	if ( sorted ) {			// if they were already in Asc order..
		sortedKeys.reverse();
	}
	for (i = 0, sortedLen = sortedKeys.length; i < sortedLen; i++) {
		sortedKey = sortedKeys[i];
		parent.appendChild(cTags[sortedKey]);
	}
	return true;
}
// you'll need this as well:
function Left(str, n) {		// returns chars from the left of text
	if ( n <= 0 ) return "";
	return ( (n > String(str).length) ? str : String(str).substring(0,n) );
}
I wonder if my routine is the fastest :scratch: :grin:

Edited: swapped 'document.getElementById(parent)' for '$(parent)' as you may not have this function.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

Yep, can sort a list as well. Just needed to amend it slightly to:

Code: Select all

elem = (colTag) ? aChild.getElementsByTagName(colTag)[colIndex] : aChild;
Then I can call it with

Code: Select all

SortElements('list1','li')
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

And finally.. using a document fragment it should run at lightning speed :cheers:

Code: Select all

frag = document.createDocumentFragment();
for (i = 0, sortedLen = sortedKeys.length; i < sortedLen; i++) {
	sortedKey = sortedKeys[i];
	frag.appendChild(cTags[sortedKey]);
}
parent.appendChild(frag);
return true;
Can anyone point me towards a large table (several hundred rows) please? Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

No worries, I found one.
Sorted 1300 rows and 10 columns in about 3 seconds. Perhaps a litle disappointing but at least it works :fanfare:
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

I'm posting this final version for 'closure' - sorry to bore you again :sad:

In Firefox I'm able to sort a table of 1300 rows and 10 columns (of detailed text) in a fraction of a second (250ms avg). I can use it to sort tables, lists or paragraphs containing, for example, a span. By including a 'sort' attribute it could sort numbers, dates, etc.

Code: Select all

function Asc(a, b) {		// used by sort() method (case insensitive)
	a = a.toLowerCase(); b = b.toLowerCase();
	return (a > b) ? 1 : ((a < b) ? -1 : 0); 
}
function Desc(a, b) {		// used by sort() method (case insensitive)
	a = a.toLowerCase(); b = b.toLowerCase();
	return (a < b) ? 1 : ((a > b) ? -1 : 0); 
}
function IsAscending(element, index, array) {		// used by every() method
	return ( index == 0 || element.toLowerCase() >= array[index-1].toLowerCase() );
}
function IsDescending(element, index, array) {		// used by every() method
	return ( index == 0 || element.toLowerCase() <= array[index-1].toLowerCase() );
}
function SortElements(parent, childTag, colTag, colIndex) {		// example use: SortElements('table1','tr','td',3)
	var i, j, parent, cTags = {}, colTag;						// or SortElements('list1','li')
	var childLen, aChild, elem;
	var origKey, origKeys={}, sortedKeys=[], keyNo='';
	var sortedLen, sorted, frag;

	parent = $(parent);    // or document.getElementById(parent);
	cTags = parent.getElementsByTagName(childTag);
	for ( i = 0, childLen = cTags.length; i < childLen; i++ )	{		// go forward examining each child
		aChild = cTags[i];
		elem = (colTag) ? aChild.getElementsByTagName(colTag)[colIndex] : aChild;
		if ( elem ) {
			origKey = elem.getAttribute('sort') || elem.firstChild.nodeValue;
			// you can supply 'sort' attributes to enable correct sorting of numbers or dates.
			// For example, <td sort='20110212'> for a date.
			keyNo = origKey + (10000 + i);		// need to ensure unique keys - means we can only sort 9999 items!
			origKeys[keyNo] = aChild;
			sortedKeys[sortedKeys.length] = keyNo;
		}
	}
	sorted = sortedKeys.every(IsAscending);
	sortedKeys.sort(Asc);
	if ( sorted ) {			// if they were already in Asc order..
		sortedKeys.reverse();
	}
	frag = document.createDocumentFragment();
	for (i = 0, sortedLen = sortedKeys.length; i < sortedLen; i++) {
		frag.appendChild(origKeys[sortedKeys[i]]);
	}
	parent.appendChild(frag);
	return true;
}
Internet Explorer struggles to even load such a large table, but the sort still works in it. I shall leave quietly now, Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.

User avatar
jscher2000
2StarLounger
Posts: 148
Joined: 26 Dec 2010, 18:17

Re: Sorting elements with JavaScript

Post by jscher2000 »

If you feel the need to work on this further, you could package it up for easy adoption. :grin:

I recently added the venerable sorttable.js to one of our intranet pages. I had to tweak it a bit but it does the job. The key feature of sorttable.js that make it easy to implement is that the script automatically looks for indicated tables/columns to make sortable on window.load. So for example:

(1) Add script tag to import the script

Code: Select all

<script type="text/javascript" src="misc/sorttable.js"></script>
(2) Add the class name sortable to the table (if you already have a class name, you need a space delimiter)

Code: Select all

<table class="btable sortable" cellspacing="1" width="100%">
(3) Mark the column header cells that you do not want to be sortable with class="sorttable_nosort"

(4) If you want your text sorts to be case insensitive, manually edit the code

Well, as you can see, it's user friendly up to a point. I did change some things for reasons of aesthetic consistency and reduced user confusion. :smile:
sorttable.zip
You do not have the required permissions to view the files attached to this post.

User avatar
agibsonsw
SilverLounger
Posts: 2403
Joined: 05 Feb 2010, 22:21
Location: London ENGLAND

Re: Sorting elements with JavaScript

Post by agibsonsw »

Hi. I downloaded this earlier today coincidentally. I should race it against my version :)

My version is already case-insensitive and will ignore any headers, ha ha! But I could package it better as you suggest. I like the idea of adding a 'sortable' class. I could then assign the click event to the headers.

Andy.
"I'm here to save your life. But if I'm going to do that, I'll need total uninanonynymity." Me Myself & Irene.