// This is from the MochiKit ajax_tables example /* On page load, the SortableManager: - Rips out all of the elements with the mochi-example class. - Finds the elements with the mochi-template class and saves them for later parsing with "MochiTAL". - Finds the anchor tags with the mochi:dataformat attribute and gives them onclick behvaiors to load new data, using their href as the data source. This makes your XML or JSON look like a normal link to a search engine (or javascript-disabled browser). - Clones the thead element from the table because it will be replaced on each sort. - Sets up a default sort key of "object_name" and queues a load of the json document. On data load, the SortableManager: - Parses the table data from the document (columns list, rows list-of-lists) and turns them into a list of [{column:value, ...}] objects for easy sorting and column order stability. - Chooses the default (or previous) sort state and triggers a sort request On sort request: - Replaces the cloned thead element with a copy of it that has the sort indicator (↑ or ↓) for the most recently sorted column (matched up to the first field in the th's mochi:sortcolumn attribute), and attaches onclick, onmousedown, onmouseover, onmouseout behaviors to them. The second field of mochi:sortcolumn attribute is used to perform a non-string sort. - Performs the sort on the objects list. If the second field of mochi:sortcolumn was not "str", then a custom function is used and the results are stored away in a __sort__ key, which is then used to perform the sort (read: shwartzian transform). - Calls processMochiTAL on the page, which finds the mochi-template sections and then looks for mochi:repeat and mochi:content attributes on them, using the data object. */ processMochiTAL = function (dom, data) { /*** A TAL-esque template attribute language processor, including content replacement and repeat ***/ // nodeType == 1 is an element, we're leaving // text nodes alone. if (dom.nodeType != 1) { return; } var attr; // duplicate this element for each item in the // given list, and then process the duplicated // element again (sans mochi:repeat tag) attr = getAttribute(dom, "mochi:repeat"); if (attr) { dom.removeAttribute("mochi:repeat"); var parent = dom.parentNode; attr = attr.split(" "); var name = attr[0]; var lst = valueForKeyPath(data, attr[1]); if (!lst) { return; } for (var i = 0; i < lst.length; i++) { data[name] = lst[i]; var newDOM = dom.cloneNode(true); processMochiTAL(newDOM, data); parent.insertBefore(newDOM, dom); } parent.removeChild(dom); return; } // do content replacement if there's a mochi:content attribute // on the element attr = getAttribute(dom, "mochi:content"); if (attr) { dom.removeAttribute("mochi:content"); replaceChildNodes(dom, valueForKeyPath(data, attr)); return; } // we make a shallow copy of the current list of child nodes // because it *will* change if there's a mochi:repeat in there! var nodes = list(dom.childNodes); for (var i = 0; i < nodes.length; i++) { processMochiTAL(nodes[i], data); } }; 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; } }; SortTransforms = { "str": operator.identity, "istr": function (s) { return s.toLowerCase(); }, /* "isoDate": isoDate*/ }; getAttribute = function (dom, key) { try { return dom.getAttribute(key); } catch (e) { return null; } }; loadFromDataAnchor = function (ev) { ignoreEvent(ev); var format = this.getAttribute("mochi:dataformat"); var href = this.href; sortableManager.loadFromURL(format, href); }; valueForKeyPath = function (data, keyPath) { var chunks = keyPath.split("."); while (chunks.length && data) { data = data[chunks.shift()]; } return data; }; SortableManager = function () { this.thead = null; this.thead_proto = null; this.tbody = null; this.deferred = null; this.columns = []; this.rows = []; this.templates = []; this.sortState = {}; bindMethods(this); }; SortableManager.prototype = { "initialize": function (prefix, sortkey) { // just rip all mochi-examples out of the DOM var examples = getElementsByTagAndClassName(null, prefix+"-example"); while (examples.length) { swapDOM(examples.pop(), null); } // make a template list var templates = getElementsByTagAndClassName(null, prefix+"-template"); for (var i = 0; i < templates.length; i++) { var template = templates[i]; var proto = template.cloneNode(true); removeElementClass(proto, prefix+"-template"); this.templates.push({ "template": proto, "node": template }); } // set up the data anchors to do loads var anchors = getElementsByTagAndClassName("a", null); for (var i = 0; i < anchors.length; i++) { var node = anchors[i]; var format = getAttribute(node, "mochi:dataformat"); if (format) { node.onclick = loadFromDataAnchor; } } // to find sort columns this.thead = getElementsByTagAndClassName("thead", prefix)[0]; this.thead_proto = this.thead.cloneNode(true); this.sortkey = sortkey; /* this.loadFromURL("json", "objects.json");*/ }, "loadFromURL": function (format, url) { log('loadFromURL', format, url); var d; if (this.deferred) { this.deferred.cancel(); } if (format == "xml") { var d = doXHR(url, { mimeType: 'text/xml', headers: {Accept: 'text/xml'} }); d.addCallback(datatableFromXMLRequest); } else if (format == "json") { d = loadJSONDoc(url); } else { throw new TypeError("format " + repr(format) + " not supported"); } // keep track of the current deferred, so that we can cancel it this.deferred = d; var self = this; // on success or error, remove the current deferred because it has // completed, and pass through the result or error d.addBoth(function (res) { self.deferred = null; log('loadFromURL success'); return res; }); // on success, tag the result with the format used so we can display // it d.addCallback(function (res) { res.format = format; return res; }); // call this.initWithData(data) once it's ready d.addCallback(this.initWithData); // if anything goes wrong, except for a simple cancellation, // then log the error and show the logger d.addErrback(function (err) { if (err instanceof CancelledError) { return; } logError(err); logger.debuggingBookmarklet(); }); return d; }, "initWithData": function (data) { /*** Initialize the SortableManager with a table object ***/ // reformat to [{column:value, ...}, ...] style as the objects key var objects = []; var rows = data.rows; var cols = data.columns; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var object = {}; for (var j = 0; j < cols.length; j++) { object[cols[j]] = row[j]; } objects.push(object); } data.objects = objects; this.data = data; // perform a sort and display based upon the previous sort state, // defaulting to an ascending sort if this is the first sort var order = this.sortState[this.sortkey]; if (typeof(order) == 'undefined') { order = true; } this.drawSortedRows(this.sortkey, order, false); }, "onSortClick": function (name) { /*** Return a sort function for click events ***/ // save ourselves from doing a bind var self = this; // on click, flip the last sort order of that column and sort return function () { log('onSortClick', name); var order = self.sortState[name]; if (typeof(order) == 'undefined') { // if it's never been sorted by this column, sort ascending order = true; } else if (self.sortkey == name) { // if this column was sorted most recently, flip the sort order order = !((typeof(order) == 'undefined') ? false : order); } self.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); // save it so we can flip next time this.sortState[key] = forward; this.sortkey = key; var sortstyle; // setup the sort columns var thead = this.thead_proto.cloneNode(true); var cols = thead.getElementsByTagName("th"); for (var i = 0; i < cols.length; i++) { var col = cols[i]; var sortinfo = getAttribute(col, "mochi:sortcolumn").split(" "); var sortkey = sortinfo[0]; col.onclick = this.onSortClick(sortkey); col.onmousedown = ignoreEvent; col.onmouseover = mouseOverFunc; col.onmouseout = mouseOutFunc; // if this is the sorted column if (sortkey == key) { sortstyle = sortinfo[1]; // \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 col.appendChild(SPAN(null, arrow)); if (clicked) { col.onmouseover(); } } } this.thead = swapDOM(this.thead, thead); // apply a sort transform to a temporary column named __sort__, // and do the sort based on that column if (!sortstyle) { sortstyle = "str"; } var sortfunc = SortTransforms[sortstyle]; if (!sortfunc) { throw new TypeError("unsupported sort style " + repr(sortstyle)); } var objects = this.data.objects; for (var i = 0; i < objects.length; i++) { var object = objects[i]; object.__sort__ = sortfunc(object[key]); } // perform the sort based on the state given (forward or reverse) var cmp = (forward ? keyComparator : reverseKeyComparator); objects.sort(cmp("__sort__")); // process every template with the given data // and put the processed templates in the DOM for (var i = 0; i < this.templates.length; i++) { log('template', i, template); var template = this.templates[i]; var dom = template.template.cloneNode(true); processMochiTAL(dom, this.data); template.node = swapDOM(template.node, dom); } //permission based coloring } }; // create the global SortableManager and initialize it on page load sortableManager = new SortableManager(); sortableManager2 = new SortableManager(); addLoadEvent(function() {sortableManager.initialize("notebook", "dateModified")}); addLoadEvent(function() {sortableManager2.initialize("user", "username")});