This is example code for sorting and filtering a custom tree view, that is, a tree whose values are loaded via JavaScript. This will not work for other types of trees, for example RDF-backed or ones created with DOM methods.
For the sake of simplicity, strings are not localized and the data loaded is hard coded.
Because of bug 340478, this code will only work from privileged script.
sort.xul
<?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <!DOCTYPE window> <window title="Sorting a custom tree view example" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="init()"> <script type="application/javascript" src="sort.js"/> <hbox align="center" id="search-box"> <label accesskey="F" control="filter">Filter</label> <textbox id="filter" oninput="inputFilter(event)" flex="1"/> <button id="clearFilter" oncommand="clearFilter()" label="Clear" accesskey="C" disabled="true"/> </hbox> <tree id="tree" flex="1" persist="sortDirection sortResource" sortDirection="ascending" sortResource="description"> <treecols> <treecol id="name" label="Name" flex="1" persist="width ordinal hidden" onclick="sort(this)" class="sortDirectionIndicator" sortDirection="ascending"/> <splitter class="tree-splitter"/> <treecol id="description" label="description" flex="1" persist="width ordinal hidden" onclick="sort(this)" class="sortDirectionIndicator"/> <splitter class="tree-splitter"/> <treecol id="weapon" label="Weapon" flex="1" persist="width ordinal hidden" onclick="sort(this)" class="sortDirectionIndicator"/> </treecols> <treechildren id="tree-children"/> </tree> </window>
sort.js
var table = null; var data = null; var tree; var filterText = ""; function init() { tree = document.getElementById("tree"); loadTable(); } //this function is called every time the tree is sorted, filtered, or reloaded function loadTable() { //remember scroll position. this is useful if this is an editable table //to prevent the user from losing the row they edited var topVisibleRow = null; if (table) { topVisibleRow = getTopVisibleRow(); } if (data == null) { //put object loading code here. for our purposes, we'll hard code it. data = []; //the property names match the column ids in the xul. this way, we don't have to deal with //mapping between the two data.push({name: "Leonardo", description: "Leader", weapon: "Dual katanas"}); data.push({name: "Michaelangelo", description: "Party dude", weapon: "Nunchaku"}); data.push({name: "Donatello", description: "Does machines", weapon: "Bo"}); data.push({name: "Raphael", description: "Cool, but rude", weapon: "Sai"}); data.push({name: "Splinter", description: "Rat", weapon: "Walking stick"}); data.push({name: "Shredder", description: "Armored man", weapon: "Blades"}); data.push({name: "Casey Jones", description: "Goalie masked man", weapon: "Hockey stick"}); data.push({name: "April O'Neil", description: "Journalist", weapon: "None"}); } if (filterText == "") { //show all of them table = data; } else { //filter out the ones we want to display table = []; data.forEach(function(element) { //we'll match on every property for (var i in element) { if (prepareForComparison(element[i]).indexOf(filterText) != -1) { table.push(element); break; } } }); } sort(); //restore scroll position if (topVisibleRow) { setTopVisibleRow(topVisibleRow); } } //generic custom tree view stuff function treeView(table) { this.rowCount = table.length; this.getCellText = function(row, col) { return table[row][col.id]; }; this.getCellValue = function(row, col) { return table[row][col.id]; }; this.setTree = function(treebox) { this.treebox = treebox; }; this.isEditable = function(row, col) { return col.editable; }; this.isContainer = function(row){ return false; }; this.isSeparator = function(row){ return false; }; this.isSorted = function(){ return false; }; this.getLevel = function(row){ return 0; }; this.getImageSrc = function(row,col){ return null; }; this.getRowProperties = function(row,props){}; this.getCellProperties = function(row,col,props){}; this.getColumnProperties = function(colid,col,props){}; this.cycleHeader = function(col, elem) {}; } function sort(column) { var columnName; var order = tree.getAttribute("sortDirection") == "ascending" ? 1 : -1; //if the column is passed and it's already sorted by that column, reverse sort if (column) { columnName = column.id; if (tree.getAttribute("sortResource") == columnName) { order *= -1; } } else { columnName = tree.getAttribute("sortResource"); } function columnSort(a, b) { if (prepareForComparison(a[columnName]) > prepareForComparison(b[columnName])) return 1 * order; if (prepareForComparison(a[columnName]) < prepareForComparison(b[columnName])) return -1 * order; //tie breaker: name ascending is the second level sort if (columnName != "name") { if (prepareForComparison(a["name"]) > prepareForComparison(b["name"])) return 1; if (prepareForComparison(a["name"]) < prepareForComparison(b["name"])) return -1; } return 0; } table.sort(columnSort); //setting these will make the sort option persist tree.setAttribute("sortDirection", order == 1 ? "ascending" : "descending"); tree.setAttribute("sortResource", columnName); tree.view = new treeView(table); //set the appropriate attributes to show to indicator var cols = tree.getElementsByTagName("treecol"); for (var i = 0; i < cols.length; i++) { cols[i].removeAttribute("sortDirection"); } document.getElementById(columnName).setAttribute("sortDirection", order == 1 ? "ascending" : "descending"); } //prepares an object for easy comparison against another. for strings, lowercases them function prepareForComparison(o) { if (typeof o == "string") { return o.toLowerCase(); } return o; } function getTopVisibleRow() { return tree.treeBoxObject.getFirstVisibleRow(); } function setTopVisibleRow(topVisibleRow) { return tree.treeBoxObject.scrollToRow(topVisibleRow); } function inputFilter(event) { //do this now rather than doing it at every comparison var value = prepareForComparison(event.target.value); setFilter(value); document.getElementById("clearFilter").disabled = value.length == 0; } function clearFilter() { document.getElementById("clearFilter").disabled = true; var filterElement = document.getElementById("filter"); filterElement.focus(); filterElement.value = ""; setFilter(""); } function setFilter(text) { filterText = text; loadTable(); }