Commit | Line | Data |
---|---|---|
6a1aa64f DV |
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 | }); |