| 1 | /* |
| 2 | |
| 3 | On page load, the SortableManager: |
| 4 | |
| 5 | - Rips out all of the elements with the mochi-example class. |
| 6 | - Finds the elements with the mochi-template class and saves them for |
| 7 | later parsing with "MochiTAL". |
| 8 | - Finds the anchor tags with the mochi:dataformat attribute and gives them |
| 9 | onclick behvaiors to load new data, using their href as the data source. |
| 10 | This makes your XML or JSON look like a normal link to a search engine |
| 11 | (or javascript-disabled browser). |
| 12 | - Clones the thead element from the table because it will be replaced on each |
| 13 | sort. |
| 14 | - Sets up a default sort key of "domain_name" and queues a load of the json |
| 15 | document. |
| 16 | |
| 17 | |
| 18 | On data load, the SortableManager: |
| 19 | |
| 20 | - Parses the table data from the document (columns list, rows list-of-lists) |
| 21 | and turns them into a list of [{column:value, ...}] objects for easy sorting |
| 22 | and column order stability. |
| 23 | - Chooses the default (or previous) sort state and triggers a sort request |
| 24 | |
| 25 | |
| 26 | On sort request: |
| 27 | |
| 28 | - Replaces the cloned thead element with a copy of it that has the sort |
| 29 | indicator (↑ or ↓) for the most recently sorted column (matched up |
| 30 | to the first field in the th's mochi:sortcolumn attribute), and attaches |
| 31 | onclick, onmousedown, onmouseover, onmouseout behaviors to them. The second |
| 32 | field of mochi:sortcolumn attribute is used to perform a non-string sort. |
| 33 | - Performs the sort on the domains list. If the second field of |
| 34 | mochi:sortcolumn was not "str", then a custom function is used and the |
| 35 | results are stored away in a __sort__ key, which is then used to perform the |
| 36 | sort (read: shwartzian transform). |
| 37 | - Calls processMochiTAL on the page, which finds the mochi-template sections |
| 38 | and then looks for mochi:repeat and mochi:content attributes on them, using |
| 39 | the data object. |
| 40 | |
| 41 | */ |
| 42 | |
| 43 | processMochiTAL = function (dom, data) { |
| 44 | /*** |
| 45 | |
| 46 | A TAL-esque template attribute language processor, |
| 47 | including content replacement and repeat |
| 48 | |
| 49 | ***/ |
| 50 | |
| 51 | // nodeType == 1 is an element, we're leaving |
| 52 | // text nodes alone. |
| 53 | if (dom.nodeType != 1) { |
| 54 | return; |
| 55 | } |
| 56 | var attr; |
| 57 | // duplicate this element for each item in the |
| 58 | // given list, and then process the duplicated |
| 59 | // element again (sans mochi:repeat tag) |
| 60 | attr = getAttribute(dom, "mochi:repeat"); |
| 61 | if (attr) { |
| 62 | dom.removeAttribute("mochi:repeat"); |
| 63 | var parent = dom.parentNode; |
| 64 | attr = attr.split(" "); |
| 65 | var name = attr[0]; |
| 66 | var lst = valueForKeyPath(data, attr[1]); |
| 67 | if (!lst) { |
| 68 | return; |
| 69 | } |
| 70 | for (var i = 0; i < lst.length; i++) { |
| 71 | data[name] = lst[i]; |
| 72 | var newDOM = dom.cloneNode(true); |
| 73 | processMochiTAL(newDOM, data); |
| 74 | parent.insertBefore(newDOM, dom); |
| 75 | } |
| 76 | parent.removeChild(dom); |
| 77 | return; |
| 78 | } |
| 79 | // do content replacement if there's a mochi:content attribute |
| 80 | // on the element |
| 81 | attr = getAttribute(dom, "mochi:content"); |
| 82 | if (attr) { |
| 83 | dom.removeAttribute("mochi:content"); |
| 84 | replaceChildNodes(dom, valueForKeyPath(data, attr)); |
| 85 | return; |
| 86 | } |
| 87 | // we make a shallow copy of the current list of child nodes |
| 88 | // because it *will* change if there's a mochi:repeat in there! |
| 89 | var nodes = list(dom.childNodes); |
| 90 | for (var i = 0; i < nodes.length; i++) { |
| 91 | processMochiTAL(nodes[i], data); |
| 92 | } |
| 93 | }; |
| 94 | |
| 95 | mouseOverFunc = function () { |
| 96 | addElementClass(this, "over"); |
| 97 | }; |
| 98 | |
| 99 | mouseOutFunc = function () { |
| 100 | removeElementClass(this, "over"); |
| 101 | }; |
| 102 | |
| 103 | ignoreEvent = function (ev) { |
| 104 | if (ev && ev.preventDefault) { |
| 105 | ev.preventDefault(); |
| 106 | ev.stopPropagation(); |
| 107 | } else if (typeof(event) != 'undefined') { |
| 108 | event.cancelBubble = false; |
| 109 | event.returnValue = false; |
| 110 | } |
| 111 | }; |
| 112 | |
| 113 | SortTransforms = { |
| 114 | "str": operator.identity, |
| 115 | "istr": function (s) { return s.toLowerCase(); }, |
| 116 | "isoDate": isoDate |
| 117 | }; |
| 118 | |
| 119 | getAttribute = function (dom, key) { |
| 120 | try { |
| 121 | return dom.getAttribute(key); |
| 122 | } catch (e) { |
| 123 | return null; |
| 124 | } |
| 125 | }; |
| 126 | |
| 127 | datatableFromXMLRequest = function (req) { |
| 128 | /*** |
| 129 | |
| 130 | This effectively converts domains.xml to the |
| 131 | same form as domains.json |
| 132 | |
| 133 | ***/ |
| 134 | var xml = req.responseXML; |
| 135 | var nodes = xml.getElementsByTagName("column"); |
| 136 | var rval = {"columns": map(scrapeText, nodes)}; |
| 137 | var rows = []; |
| 138 | nodes = xml.getElementsByTagName("row") |
| 139 | for (var i = 0; i < nodes.length; i++) { |
| 140 | var cells = nodes[i].getElementsByTagName("cell"); |
| 141 | rows.push(map(scrapeText, cells)); |
| 142 | } |
| 143 | rval.rows = rows; |
| 144 | return rval; |
| 145 | }; |
| 146 | |
| 147 | loadFromDataAnchor = function (ev) { |
| 148 | ignoreEvent(ev); |
| 149 | var format = this.getAttribute("mochi:dataformat"); |
| 150 | var href = this.href; |
| 151 | sortableManager.loadFromURL(format, href); |
| 152 | }; |
| 153 | |
| 154 | valueForKeyPath = function (data, keyPath) { |
| 155 | var chunks = keyPath.split("."); |
| 156 | while (chunks.length && data) { |
| 157 | data = data[chunks.shift()]; |
| 158 | } |
| 159 | return data; |
| 160 | }; |
| 161 | |
| 162 | |
| 163 | SortableManager = function () { |
| 164 | this.thead = null; |
| 165 | this.thead_proto = null; |
| 166 | this.tbody = null; |
| 167 | this.deferred = null; |
| 168 | this.columns = []; |
| 169 | this.rows = []; |
| 170 | this.templates = []; |
| 171 | this.sortState = {}; |
| 172 | bindMethods(this); |
| 173 | }; |
| 174 | |
| 175 | SortableManager.prototype = { |
| 176 | |
| 177 | "initialize": function () { |
| 178 | // just rip all mochi-examples out of the DOM |
| 179 | var examples = getElementsByTagAndClassName(null, "mochi-example"); |
| 180 | while (examples.length) { |
| 181 | swapDOM(examples.pop(), null); |
| 182 | } |
| 183 | // make a template list |
| 184 | var templates = getElementsByTagAndClassName(null, "mochi-template"); |
| 185 | for (var i = 0; i < templates.length; i++) { |
| 186 | var template = templates[i]; |
| 187 | var proto = template.cloneNode(true); |
| 188 | removeElementClass(proto, "mochi-template"); |
| 189 | this.templates.push({ |
| 190 | "template": proto, |
| 191 | "node": template |
| 192 | }); |
| 193 | } |
| 194 | // set up the data anchors to do loads |
| 195 | var anchors = getElementsByTagAndClassName("a", null); |
| 196 | for (var i = 0; i < anchors.length; i++) { |
| 197 | var node = anchors[i]; |
| 198 | var format = getAttribute(node, "mochi:dataformat"); |
| 199 | if (format) { |
| 200 | node.onclick = loadFromDataAnchor; |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | // to find sort columns |
| 205 | this.thead = getElementsByTagAndClassName("thead", null)[0]; |
| 206 | this.thead_proto = this.thead.cloneNode(true); |
| 207 | |
| 208 | this.sortkey = "domain_name"; |
| 209 | this.loadFromURL("json", "domains.json"); |
| 210 | }, |
| 211 | |
| 212 | "loadFromURL": function (format, url) { |
| 213 | log('loadFromURL', format, url); |
| 214 | var d; |
| 215 | if (this.deferred) { |
| 216 | this.deferred.cancel(); |
| 217 | } |
| 218 | if (format == "xml") { |
| 219 | var d = doXHR(url, { |
| 220 | mimeType: 'text/xml', |
| 221 | headers: {Accept: 'text/xml'} |
| 222 | }); |
| 223 | d.addCallback(datatableFromXMLRequest); |
| 224 | } else if (format == "json") { |
| 225 | d = loadJSONDoc(url); |
| 226 | } else { |
| 227 | throw new TypeError("format " + repr(format) + " not supported"); |
| 228 | } |
| 229 | // keep track of the current deferred, so that we can cancel it |
| 230 | this.deferred = d; |
| 231 | var self = this; |
| 232 | // on success or error, remove the current deferred because it has |
| 233 | // completed, and pass through the result or error |
| 234 | d.addBoth(function (res) { |
| 235 | self.deferred = null; |
| 236 | log('loadFromURL success'); |
| 237 | return res; |
| 238 | }); |
| 239 | // on success, tag the result with the format used so we can display |
| 240 | // it |
| 241 | d.addCallback(function (res) { |
| 242 | res.format = format; |
| 243 | return res; |
| 244 | }); |
| 245 | // call this.initWithData(data) once it's ready |
| 246 | d.addCallback(this.initWithData); |
| 247 | // if anything goes wrong, except for a simple cancellation, |
| 248 | // then log the error and show the logger |
| 249 | d.addErrback(function (err) { |
| 250 | if (err instanceof CancelledError) { |
| 251 | return; |
| 252 | } |
| 253 | logError(err); |
| 254 | logger.debuggingBookmarklet(); |
| 255 | }); |
| 256 | return d; |
| 257 | }, |
| 258 | |
| 259 | "initWithData": function (data) { |
| 260 | /*** |
| 261 | |
| 262 | Initialize the SortableManager with a table object |
| 263 | |
| 264 | ***/ |
| 265 | |
| 266 | // reformat to [{column:value, ...}, ...] style as the domains key |
| 267 | var domains = []; |
| 268 | var rows = data.rows; |
| 269 | var cols = data.columns; |
| 270 | for (var i = 0; i < rows.length; i++) { |
| 271 | var row = rows[i]; |
| 272 | var domain = {}; |
| 273 | for (var j = 0; j < cols.length; j++) { |
| 274 | domain[cols[j]] = row[j]; |
| 275 | } |
| 276 | domains.push(domain); |
| 277 | } |
| 278 | data.domains = domains; |
| 279 | this.data = data; |
| 280 | // perform a sort and display based upon the previous sort state, |
| 281 | // defaulting to an ascending sort if this is the first sort |
| 282 | var order = this.sortState[this.sortkey]; |
| 283 | if (typeof(order) == 'undefined') { |
| 284 | order = true; |
| 285 | } |
| 286 | this.drawSortedRows(this.sortkey, order, false); |
| 287 | |
| 288 | }, |
| 289 | |
| 290 | "onSortClick": function (name) { |
| 291 | /*** |
| 292 | |
| 293 | Return a sort function for click events |
| 294 | |
| 295 | ***/ |
| 296 | // save ourselves from doing a bind |
| 297 | var self = this; |
| 298 | // on click, flip the last sort order of that column and sort |
| 299 | return function () { |
| 300 | log('onSortClick', name); |
| 301 | var order = self.sortState[name]; |
| 302 | if (typeof(order) == 'undefined') { |
| 303 | // if it's never been sorted by this column, sort ascending |
| 304 | order = true; |
| 305 | } else if (self.sortkey == name) { |
| 306 | // if this column was sorted most recently, flip the sort order |
| 307 | order = !((typeof(order) == 'undefined') ? false : order); |
| 308 | } |
| 309 | self.drawSortedRows(name, order, true); |
| 310 | }; |
| 311 | }, |
| 312 | |
| 313 | "drawSortedRows": function (key, forward, clicked) { |
| 314 | /*** |
| 315 | |
| 316 | Draw the new sorted table body, and modify the column headers |
| 317 | if appropriate |
| 318 | |
| 319 | ***/ |
| 320 | log('drawSortedRows', key, forward); |
| 321 | |
| 322 | // save it so we can flip next time |
| 323 | this.sortState[key] = forward; |
| 324 | this.sortkey = key; |
| 325 | var sortstyle; |
| 326 | |
| 327 | // setup the sort columns |
| 328 | var thead = this.thead_proto.cloneNode(true); |
| 329 | var cols = thead.getElementsByTagName("th"); |
| 330 | for (var i = 0; i < cols.length; i++) { |
| 331 | var col = cols[i]; |
| 332 | var sortinfo = getAttribute(col, "mochi:sortcolumn").split(" "); |
| 333 | var sortkey = sortinfo[0]; |
| 334 | col.onclick = this.onSortClick(sortkey); |
| 335 | col.onmousedown = ignoreEvent; |
| 336 | col.onmouseover = mouseOverFunc; |
| 337 | col.onmouseout = mouseOutFunc; |
| 338 | // if this is the sorted column |
| 339 | if (sortkey == key) { |
| 340 | sortstyle = sortinfo[1]; |
| 341 | // \u2193 is down arrow, \u2191 is up arrow |
| 342 | // forward sorts mean the rows get bigger going down |
| 343 | var arrow = (forward ? "\u2193" : "\u2191"); |
| 344 | // add the character to the column header |
| 345 | col.appendChild(SPAN(null, arrow)); |
| 346 | if (clicked) { |
| 347 | col.onmouseover(); |
| 348 | } |
| 349 | } |
| 350 | } |
| 351 | this.thead = swapDOM(this.thead, thead); |
| 352 | |
| 353 | // apply a sort transform to a temporary column named __sort__, |
| 354 | // and do the sort based on that column |
| 355 | if (!sortstyle) { |
| 356 | sortstyle = "str"; |
| 357 | } |
| 358 | var sortfunc = SortTransforms[sortstyle]; |
| 359 | if (!sortfunc) { |
| 360 | throw new TypeError("unsupported sort style " + repr(sortstyle)); |
| 361 | } |
| 362 | var domains = this.data.domains; |
| 363 | for (var i = 0; i < domains.length; i++) { |
| 364 | var domain = domains[i]; |
| 365 | domain.__sort__ = sortfunc(domain[key]); |
| 366 | } |
| 367 | |
| 368 | // perform the sort based on the state given (forward or reverse) |
| 369 | var cmp = (forward ? keyComparator : reverseKeyComparator); |
| 370 | domains.sort(cmp("__sort__")); |
| 371 | |
| 372 | // process every template with the given data |
| 373 | // and put the processed templates in the DOM |
| 374 | for (var i = 0; i < this.templates.length; i++) { |
| 375 | log('template', i, template); |
| 376 | var template = this.templates[i]; |
| 377 | var dom = template.template.cloneNode(true); |
| 378 | processMochiTAL(dom, this.data); |
| 379 | template.node = swapDOM(template.node, dom); |
| 380 | } |
| 381 | |
| 382 | |
| 383 | } |
| 384 | |
| 385 | }; |
| 386 | |
| 387 | // create the global SortableManager and initialize it on page load |
| 388 | sortableManager = new SortableManager(); |
| 389 | addLoadEvent(sortableManager.initialize); |
| 390 | |
| 391 | // rewrite the view-source links |
| 392 | addLoadEvent(function () { |
| 393 | var elems = getElementsByTagAndClassName("A", "view-source"); |
| 394 | var page = "ajax_tables/"; |
| 395 | for (var i = 0; i < elems.length; i++) { |
| 396 | var elem = elems[i]; |
| 397 | var href = elem.href.split(/\//).pop(); |
| 398 | elem.target = "_blank"; |
| 399 | elem.href = "../view-source/view-source.html#" + page + href; |
| 400 | } |
| 401 | }); |