Table Sort
See the demo page for the finished version of the code.Getting Values
First, in order to sort the table, we need a way to get the data within it.
We know that we can use the rows
and cells
collections to access each TD or TH cell in the table, but we need a way to
extract the actual text contained within those elements.
In the previous samples, we simply took the nodeValue
of the
cell's firstChild
property. This works because each TD or TH tag
contained plain text. So in the DOM, each cell had a single child node which
happened to be a text node.
If we had HTML like this:
<td>sample <b>text</b></td> <td><a href="page.html">sample text</a></td>
that code would either have failed to get all the text or even, in the case of the second cell above, caused a scripting error.
While the planets table also uses plain text in each cell, it would be nice to have a generalized function for extracting all the text from any given cell, no matter how its contents are arranged. The following function does just that.
function getTextValue(el) { var i; var s; // Find and concatenate the values of all text nodes contained // within the element. s = ""; for (i = 0; i < el.childNodes.length; i++) if (el.childNodes[i].nodeType == document.TEXT_NODE) s += el.childNodes[i].nodeValue; else if (el.childNodes[i].nodeType == document.ELEMENT_NODE && el.childNodes[i].tagName == "BR") s += " "; else // Use recursion to get text within sub-elements. s += getTextValue(el.childNodes[i]); return normalizeString(s); }
This function looks at each child node of the cell in order, building a text string as it goes along. When a child is a text node, it appends that text to the string. When the child is some other type of node, it first calls itself recursively to extract any text from that child and then appends that text to the string.
One exception is for BR tags. For these it appends a single space to the string to represent the line break implied by that tag.
document.TEXT_NODE
and
document.ELEMENT_NODE
in code above. These are constants defined
in the DOM standard, along with others, which correlate to values the DOM uses
for the nodeType
property.
Some browsers (notably IE) do not define these properties. You'll need to
define them yourself in order for the script to work on these browsers. The
code below does just that.
// This code is necessary for browsers that don't reflect the DOM // constants (like IE). if (document.ELEMENT_NODE == null) { document.ELEMENT_NODE = 1; document.TEXT_NODE = 3; }So if
document.ELEMENT_NODE
is not defined by a given browser, the
script defines both it and document.TEXT_NODE.
The call to normalizeString()
at the end of the function cleans
up any white space (spaces, tabs, newlines, etc.) in the string. That function
simply replaces multiple white space characters with a single space and removes
any leading or trailing white space.
// Regular expressions for normalizing white space. var whtSpEnds = new RegExp("^\\s*|\\s*$", "g"); var whtSpMult = new RegExp("\\s\\s+", "g"); function normalizeString(s) { s = s.replace(whtSpMult, " "); // Collapse any multiple whites space. s = s.replace(whtSpEnds, ""); // Remove leading or trailing white // space. return s; }
The end result is similar to what you would get by stripping out any HTML tags between the <TD> and </TD> (or the <TH> and </TH>) tag pair for a cell.
Comparing Values
Once we have the values, we need to be able to compare them in order to do
any sorting. For the "Name" column in the planets table, we can just compare
the two strings. But for the other columns, the values are numeric. The problem
is that the values returned by getTextValue()
are strings and
would normally be compared lexicographically in JavaScript, not numerically.
The code below illustrates the problem.
alert(1000 > 3); // <== displays 'true.' alert("1000" > "3"); // <== displays 'false.'
Show (1000 > 3) | Show ("1000" > "3")
We want to be able to compare values either as strings or, when appropriate, as numbers. So we define a function to compare two string values based on whether or not they can be interpreted as numeric values.
function compareValues(v1, v2) { var f1, f2; // If the values are numeric, convert them to floats. f1 = parseFloat(v1); f2 = parseFloat(v2); if (!isNaN(f1) && !isNaN(f2)) { v1 = f1; v2 = f2; } // Compare the two values. if (v1 == v2) return 0; if (v1 > v2) return 1 return -1; }
The compareValues()
function first checks to see if the two
string values passed to it can be converted to numbers using the
parseFloat()
function. If either call to
parseFloat()
returns the value NaN
(a JavaScript
constant meaning "not a number") it leaves them unchanged. But if both can be
converted to numbers, it converts them.
You could take this function further, say by using regular expressions to
recognize different data formats. For example, you could test if a string is in
a currency format like "$1234.56" which could be converted to a numeric value.
Or you could check for a date format like "07/04/1776" and create
Date
objects for comparison. It all depends on what kind of data
you have in your table.
The function compares the two values and returns a value using the same
convention defined for comparison functions used with the Array
object's sort()
method. In other words, it returns a negative
value if v1
is less than v2,
a positive value if
v1
is greater than v2
or zero if v1
is
equal to v2.
Arrays vs. Collections
Incidentally, you may be wondering why we don't just use the Array object's
sort()
method on the rows
property of the table
element. The problem is that the rows
property is not a JavaScript
array, it is an HTMLCollection
object.
Even though you access the individual rows in this collection using array
notation (i.e., using square brackets with a numeric index) and it has a
length
property (as arrays do) it is not an Array
object. HTML collections do not have a sort()
method. We'll just
have to provide that functionality ourselves.