Initial check-in
[dygraphs.git] / mochikit_v14 / examples / ajax_tables / ajax_tables.js
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 });