add sorting tables to the pcu view.
[monitor.git] / web / MonitorWeb / monitorweb / static / javascript / sortable_tables.js
diff --git a/web/MonitorWeb/monitorweb/static/javascript/sortable_tables.js b/web/MonitorWeb/monitorweb/static/javascript/sortable_tables.js
new file mode 100644 (file)
index 0000000..6ffbe62
--- /dev/null
@@ -0,0 +1,214 @@
+/*
+
+On page load, the SortableManager:
+
+- Finds the table by its id (sortable_table).
+- Parses its thead for columns with a "mochi:format" attribute.
+- Parses the data out of the tbody based upon information given in the
+  "mochi:format" attribute, and clones the tr elements for later re-use.
+- Clones the column header th elements for use as a template when drawing 
+  sort arrow columns.
+- Stores away a reference to the tbody, as it will be replaced on each sort.
+- Performs the first sort.
+
+
+On sort request:
+
+- Sorts the data based on the given key and direction
+- Creates a new tbody from the rows in the new ordering
+- Replaces the column header th elements with clickable versions, adding an
+   indicator (↑ or ↓) to the most recently sorted column.
+
+*/
+
+SortableManager = function () {
+    this.thead = null;
+    this.tbody = null;
+    this.columns = [];
+    this.rows = [];
+    this.sortState = {};
+    this.sortkey = 0;
+};
+
+mouseOverFunc = function () {
+    addElementClass(this, "over");
+};
+
+mouseOutFunc = function () {
+    removeElementClass(this, "over");
+};
+
+ignoreEvent = function (ev) {
+    if (ev && ev.preventDefault) {
+        ev.preventDefault();
+        ev.stopPropagation();
+    } else if (typeof(event) != 'undefined') {
+        event.cancelBubble = false;
+        event.returnValue = false;
+    }
+};
+
+
+update(SortableManager.prototype, {
+
+    "initWithTable": function (table) {
+        /***
+
+            Initialize the SortableManager with a table object
+        
+        ***/
+        // Ensure that it's a DOM element
+        table = getElement(table);
+        // Find the thead
+        this.thead = table.getElementsByTagName('thead')[0];
+        // get the mochi:format key and contents for each column header
+        var cols = this.thead.getElementsByTagName('th');
+        for (var i = 0; i < cols.length; i++) {
+            var node = cols[i];
+            var attr = null;
+            try {
+                attr = node.getAttribute("mochi:format");
+            } catch (err) {
+                // pass
+            }
+            var o = node.childNodes;
+            this.columns.push({
+                "format": attr,
+                "element": node,
+                "proto": node.cloneNode(true)
+            });
+        }
+        // scrape the tbody for data
+        this.tbody = table.getElementsByTagName('tbody')[0];
+        // every row
+        var rows = this.tbody.getElementsByTagName('tr');
+        for (var i = 0; i < rows.length; i++) {
+            // every cell
+            var row = rows[i];
+            var cols = row.getElementsByTagName('td');
+            var rowData = [];
+            for (var j = 0; j < cols.length; j++) {
+                // scrape the text and build the appropriate object out of it
+                var cell = cols[j];
+                var obj = scrapeText(cell);
+                switch (this.columns[j].format) {
+                    case 'isodate':
+                        obj = isoDate(obj);
+                        break;
+                    case 'int':
+                        obj = parseInt(obj);
+                        break;
+                    case 'str':
+                        break;
+                    case 'istr':
+                        obj = obj.toLowerCase();
+                        break;
+                    // cases for numbers, etc. could be here
+                    default:
+                        break;
+                }
+                rowData.push(obj);
+            }
+            // stow away a reference to the TR and save it
+            rowData.row = row.cloneNode(true);
+            this.rows.push(rowData);
+
+        }
+
+        // do initial sort on first column
+        this.drawSortedRows(this.sortkey, true, false);
+
+    },
+
+    "onSortClick": function (name) {
+        /***
+
+            Return a sort function for click events
+
+        ***/
+        return method(this, function () {
+            log('onSortClick', name);
+            var order = this.sortState[name];
+            if (order == null) {
+                order = true;
+            } else if (name == this.sortkey) {
+                order = !order;
+            }
+            this.drawSortedRows(name, order, true);
+        });
+    },
+
+    "drawSortedRows": function (key, forward, clicked) {
+        /***
+
+            Draw the new sorted table body, and modify the column headers
+            if appropriate
+
+        ***/
+        log('drawSortedRows', key, forward);
+        this.sortkey = key;
+        // sort based on the state given (forward or reverse)
+        var cmp = (forward ? keyComparator : reverseKeyComparator);
+        this.rows.sort(cmp(key));
+        // save it so we can flip next time
+        this.sortState[key] = forward;
+        // get every "row" element from this.rows and make a new tbody
+        var newBody = TBODY(null, map(itemgetter("row"), this.rows));
+        // swap in the new tbody
+        this.tbody = swapDOM(this.tbody, newBody);
+               // replace first column with row count.
+               var rows = this.tbody.getElementsByTagName('tr');
+        for (var i=0; i < rows.length; i++) {
+                       var row = rows[i];
+            var cols = row.getElementsByTagName('td');
+                       // var cell = cols[0];
+                       cols[0].innerHTML = String(i);
+               }
+        for (var i = 0; i < this.columns.length; i++) {
+            var col = this.columns[i];
+            var node = col.proto.cloneNode(true);
+            // remove the existing events to minimize IE leaks
+            col.element.onclick = null;
+            col.element.onmousedown = null;
+            col.element.onmouseover = null;
+            col.element.onmouseout = null;
+            // set new events for the new node
+            node.onclick = this.onSortClick(i);
+            node.onmousedown = ignoreEvent;
+            node.onmouseover = mouseOverFunc;
+            node.onmouseout = mouseOutFunc;
+            // if this is the sorted column
+            if (key == i) {
+                // \u2193 is down arrow, \u2191 is up arrow
+                // forward sorts mean the rows get bigger going down
+                var arrow = (forward ? "\u2193" : "\u2191");
+                // add the character to the column header
+                node.appendChild(SPAN(null, arrow));
+                if (clicked) {
+                    node.onmouseover();
+                }
+            }
+            // swap in the new th
+            col.element = swapDOM(col.element, node);
+        }
+    }
+});
+
+sortableManager = new SortableManager();
+
+addLoadEvent(function () {
+    sortableManager.initWithTable('sortable_table');
+});
+
+// rewrite the view-source links
+addLoadEvent(function () {
+    var elems = getElementsByTagAndClassName("A", "view-source");
+    var page = "sortable_tables/";
+    for (var i = 0; i < elems.length; i++) {
+        var elem = elems[i];
+        var href = elem.href.split(/\//).pop();
+        elem.target = "_blank";
+        elem.href = "../view-source/view-source.html#" + page + href;
+    }
+});