source: SRUAggregator/trunk/src/main/resources/assets/js/main.js @ 6132

Last change on this file since 6132 was 6132, checked in by emanuel.dima@uni-tuebingen.de, 9 years ago
  1. beta-34: production settings and developments settings; misc
File size: 21.0 KB
Line 
1/** @jsx React.DOM */
2(function() {
3"use strict";
4
5var VERSION = window.MyAggregator.VERSION = "VERSION 2.0.0-beta-34";
6var URLROOT = window.MyAggregator.URLROOT = 
7        window.location.pathname.substring(0, window.location.pathname.indexOf("/",2)) || 
8        "/Aggregator";
9
10var PT = React.PropTypes;
11
12var ErrorPane = window.MyReact.ErrorPane;
13var AggregatorPage = window.MyAggregator.AggregatorPage;
14
15var Main = React.createClass({displayName: 'Main',
16        componentWillMount: function() {
17                routeFromLocation.bind(this)();
18        },
19
20        getInitialState: function () {
21                return {
22                        navbarCollapse: false,
23                        navbarPageFn: this.renderAggregator,
24                        // navbarPageFn: this.renderStatistics,
25                        errorMessages: [],
26                };
27        },
28
29        error: function(errObj) {
30                var err = "";
31                if (typeof errObj === 'string' || errObj instanceof String) {
32                        err = errObj;
33                } else if (typeof errObj === 'object' && errObj.statusText) {
34                        console.log("ERROR: jqXHR = ", errObj);
35                        err = errObj.statusText;
36                } else {
37                        return;
38                }
39
40                var that = this;
41                var errs = this.state.errorMessages.slice();
42                errs.push(err);
43                this.setState({errorMessages: errs});
44
45                setTimeout(function() {
46                        var errs = that.state.errorMessages.slice();
47                        errs.shift();
48                        that.setState({errorMessages: errs});
49                }, 10000);
50        },
51       
52        ajax: function(ajaxObject) {
53                var that = this;
54                if (!ajaxObject.error) {
55                        ajaxObject.error = function(jqXHR, textStatus, error) {
56                                if (jqXHR.readyState === 0) {
57                                        that.error("Network error, please check your internet connection");
58                                } else if (jqXHR.responseText) {
59                                        that.error(jqXHR.responseText + " ("+error+")");
60                                } else  {
61                                        that.error(error + " ("+textStatus+")");
62                                }
63                                console.log("ajax error, jqXHR: ", jqXHR);
64                        };
65                }
66                // console.log("ajax", ajaxObject);
67                jQuery.ajax(ajaxObject);
68        },
69
70        toggleCollapse: function() {
71                this.setState({navbarCollapse: !this.state.navbarCollapse});
72        },
73
74        renderAggregator: function() {
75                return React.createElement(AggregatorPage, {ajax: this.ajax, error: this.error});
76        },
77
78        renderHelp: function() {
79                return React.createElement(HelpPage, null);
80        },
81
82        renderAbout: function() {
83                return React.createElement(AboutPage, null);
84        },
85
86        renderStatistics: function() {
87                return React.createElement(StatisticsPage, {ajax: this.ajax});
88        },
89
90        renderEmbedded: function() {
91                return React.createElement(AggregatorPage, {ajax: this.ajax, embedded: true});
92        },
93
94        getPageFns: function() { 
95                return {
96                        '': this.renderAggregator,
97                        'help': this.renderHelp,
98                        'about': this.renderAbout,
99                        'stats': this.renderStatistics,
100                        'embed': this.renderEmbedded,
101                };
102        },
103
104        gotoPage: function(doPushHistory, pageFnName) {
105                var pageFn = this.getPageFns()[pageFnName];
106                if (this.state.navbarPageFn !== pageFn) {
107                        if (doPushHistory) {
108                                window.history.pushState({page:pageFnName}, '', URLROOT+"/"+pageFnName);
109                        }
110                        this.setState({navbarPageFn: pageFn});
111                        // console.log("new page: " + document.location + ", name: " + pageFnName);
112                }
113        },
114
115        toAggregator: function(doPushHistory) { this.gotoPage(doPushHistory, ''); },
116        toHelp: function(doPushHistory) { this.gotoPage(doPushHistory, 'help'); },
117        toAbout: function(doPushHistory) { this.gotoPage(doPushHistory, 'about'); },
118        toStatistics: function(doPushHistory) { this.gotoPage(doPushHistory, 'stats'); },
119        toEmbedded: function(doPushHistory) { this.gotoPage(doPushHistory, 'embed'); },
120
121        renderLogin: function() {
122                return false;
123                // return  <li className="unauthenticated">
124                //                      <a href="login" tabIndex="-1"><span className="glyphicon glyphicon-log-in"></span> LOGIN</a>
125                //              </li>;
126        },
127
128        renderCollapsible: function() {
129                var classname = "navbar-collapse collapse " + (this.state.navbarCollapse?"in":"");
130                return (
131                        React.createElement("div", {className: classname}, 
132                                React.createElement("ul", {className: "nav navbar-nav"}, 
133                                        React.createElement("li", {className: this.state.navbarPageFn === this.renderAggregator ? "active":""}, 
134                                                React.createElement("a", {className: "link", tabIndex: "-1", onClick: this.toAggregator.bind(this, true)}, "Aggregator")
135                                        ), 
136                                        React.createElement("li", {className: this.state.navbarPageFn === this.renderHelp ? "active":""}, 
137                                                React.createElement("a", {className: "link", tabIndex: "-1", onClick: this.toHelp.bind(this, true)}, "Help")
138                                        )
139                                ), 
140                                React.createElement("ul", {id: "CLARIN_header_right", className: "nav navbar-nav navbar-right"}, 
141                                this.renderLogin()
142                                )
143                        )
144                );
145        },
146
147        renderTop: function() {
148                if (this.state.navbarPageFn === this.renderEmbedded) {
149                        return false;
150                }
151                return  (
152                        React.createElement("div", null, 
153                                React.createElement("div", {className: "container"}, 
154                                        React.createElement("div", {className: "beta-tag"}, 
155                                                React.createElement("span", null, "BETA")
156                                        )
157                                ), 
158                       
159                                React.createElement("div", {className: "navbar navbar-default navbar-static-top", role: "navigation"}, 
160                                        React.createElement("div", {className: "container"}, 
161                                                React.createElement("div", {className: "navbar-header"}, 
162                                                        React.createElement("button", {type: "button", className: "navbar-toggle", onClick: this.toggleCollapse}, 
163                                                                React.createElement("span", {className: "sr-only"}, "Toggle navigation"), 
164                                                                React.createElement("span", {className: "icon-bar"}), 
165                                                                React.createElement("span", {className: "icon-bar"}), 
166                                                                React.createElement("span", {className: "icon-bar"})
167                                                        ), 
168                                                        React.createElement("a", {className: "navbar-brand", href: URLROOT, tabIndex: "-1"}, React.createElement("header", null, "Federated Content Search"))
169                                                ), 
170                                                this.renderCollapsible()
171                                        )
172                                ), 
173
174                                React.createElement(ErrorPane, {errorMessages: this.state.errorMessages})
175                        )
176                );
177        },
178
179        render: function() {
180                return  (
181                        React.createElement("div", null, 
182                                React.createElement("div", null, " ",  this.renderTop(), " "), 
183
184                                React.createElement("div", {id: "push"}, 
185                                        React.createElement("div", {className: "container"}, 
186                                                this.state.navbarPageFn()
187                                        ), 
188                                        React.createElement("div", {className: "top-gap"})
189                                )
190                        )
191                );
192        }
193});
194
195
196var StatisticsPage = React.createClass({displayName: 'StatisticsPage',
197        propTypes: {
198                ajax: PT.func.isRequired,
199        },
200
201        getInitialState: function () {
202                return {
203                        stats: {},
204                        activeTab: 0,
205                        // searchStats: {},
206                        // lastScanStats: {},
207                };
208        },
209
210        componentDidMount: function() {
211                this.refreshStats();
212        },
213
214        refreshStats: function() {
215                this.props.ajax({
216                        url: 'rest/statistics',
217                        success: function(json, textStatus, jqXHR) {
218                                this.setState({stats: json});
219                                // console.log("stats:", json);
220                        }.bind(this),
221                });
222        },
223
224        renderWaitTimeSecs: function(t) {
225                var hue = t * 4;
226                if (hue > 120) {
227                        hue = 120;
228                }
229                var a = hue/120;
230                hue = 120 - hue;
231                var shue = "hsla("+hue+",100%,80%,"+a+")";
232                return  React.createElement("span", {className: "badge", style: {backgroundColor:shue, color:"black"}}, 
233                                        t.toFixed(3), "s"
234                                );
235        },
236
237        renderCollections: function(colls) {
238                return  React.createElement("div", {style: {marginLeft:40}}, 
239                                         colls.length === 0 ? 
240                                                React.createElement("div", {style: {color:"#a94442"}}, "NO collections found")
241                                                : 
242                                                React.createElement("div", null, 
243                                                        colls.length, " root collection(s):", 
244                                                        React.createElement("ul", {className: "list-unstyled", style: {marginLeft:40}}, 
245                                                                 colls.map(function(name, i) { return React.createElement("div", {key: i}, name); }) 
246                                                        )
247                                                )
248                                       
249                                );
250        },
251
252        renderDiagnostic: function(d) {
253                var classes = "inline alert alert-warning " + (d.diagnostic.uri === 'LEGACY' ? "legacy" : "");
254                return  React.createElement("div", {key: d.diagnostic.uri}, 
255                                        React.createElement("div", {className: classes}, 
256                                                React.createElement("div", null, 
257                                                         d.counter <= 1 ? false : 
258                                                                React.createElement("div", {className: "inline", style: {margin:"5px 5px 5px 5px"}}, 
259                                                                        React.createElement("span", {className: "badge", style: {backgroundColor:'#ae7241'}}, "x ", d.counter)
260                                                                ), 
261                                                       
262                                                        "Diagnostic: ", d.diagnostic.message, ": ", d.diagnostic.diagnostic
263                                                ), 
264                                                React.createElement("div", null, "Context: ", React.createElement("a", {href: d.context}, d.context))
265                                        )
266                                ); 
267        },
268
269        renderError: function(e) {
270                var xc = e.exception;
271                return  React.createElement("div", {key: xc.message}, 
272                                        React.createElement("div", {className: "inline alert alert-danger", role: "alert"}, 
273                                                React.createElement("div", null, 
274                                                         e.counter <= 1 ? false : 
275                                                                React.createElement("div", {className: "inline", style: {margin:"5px 5px 5px 5px"}}, 
276                                                                        React.createElement("span", {className: "badge", style: {backgroundColor:'#c94442'}}, "x ", e.counter, " ")
277                                                                ), 
278                                                       
279                                                        "Exception: ", xc.message
280                                                ), 
281                                                React.createElement("div", null, "Context: ", React.createElement("a", {href: e.context}, e.context)), 
282                                                 xc.cause ? React.createElement("div", null, "Caused by: ", xc.cause) : false
283                                        )
284                                ); 
285        },
286
287        renderEndpoint: function(isScan, endpoint) {
288                var stat = endpoint[1];
289                var errors = _.values(stat.errors);
290                var diagnostics = _.values(stat.diagnostics);
291                return React.createElement("div", {style: {marginTop:10}, key: endpoint[0]}, 
292                                        React.createElement("ul", {className: "list-inline list-unstyled", style: {marginBottom:0}}, 
293                                                React.createElement("li", null, 
294                                                         stat.version == "LEGACY" ? 
295                                                                React.createElement("span", {style: {color:'#a94442'}}, "legacy ", React.createElement("i", {className: "glyphicon glyphicon-thumbs-down"}), " ") 
296                                                                : React.createElement("span", {style: {color:'#3c763d'}}, React.createElement("i", {className: "glyphicon glyphicon-thumbs-up"}), " "), 
297                                                       
298                                                         " "+endpoint[0]
299                                                )
300                                        ), 
301                                        React.createElement("div", {style: {marginLeft:40}}, 
302                                         isScan ? 
303                                                React.createElement("div", null, "Max concurrent scan requests:", " ", " ", stat.maxConcurrentRequests, " ") :
304                                                React.createElement("div", null, "Max concurrent search requests:", " ", " ", stat.maxConcurrentRequests, " ")
305                                       
306                                        ), 
307                                        React.createElement("div", {style: {marginLeft:40}}, 
308                                                React.createElement("span", null, stat.numberOfRequests), " request(s)," + ' ' +
309                                                "average:", this.renderWaitTimeSecs(stat.avgExecutionTime), "," + ' ' + 
310                                                "max: ", this.renderWaitTimeSecs(stat.maxExecutionTime)
311                                        ), 
312                                         isScan ? this.renderCollections(stat.rootCollections) : false, 
313                                                (errors && errors.length) ? 
314                                                React.createElement("div", {className: "inline", style: {marginLeft:40}}, 
315                                                         errors.map(this.renderError) 
316                                                ) : false, 
317                                       
318                                                (diagnostics && diagnostics.length) ? 
319                                                React.createElement("div", {className: "inline", style: {marginLeft:40}}, 
320                                                         diagnostics.map(this.renderDiagnostic) 
321                                                ) : false
322                                       
323                                );
324        },
325
326        renderInstitution: function(isScan, inst) {
327                return  React.createElement("div", {style: {marginTop:30}, key: inst[0]}, 
328                                        React.createElement("h4", null, inst[0]), 
329                                        React.createElement("div", {style: {marginLeft:20}}, " ", _.pairs(inst[1]).map(this.renderEndpoint.bind(this, isScan)) )
330                                );
331        },
332
333        renderStatistics: function(stats) {
334                return  React.createElement("div", {className: "container statistics", style: {marginTop:20}}, 
335                                        React.createElement("div", null, 
336                                                React.createElement("div", null, "Start date: ", new Date(stats.date).toLocaleString()), 
337                                                React.createElement("div", null, "Timeout: ", " ", React.createElement("kbd", null, stats.timeout, " seconds"))
338                                        ), 
339                                        React.createElement("div", null, " ",  _.pairs(stats.institutions).map(this.renderInstitution.bind(this, stats.isScan)), " ")
340                                )
341                                 ;
342        },
343
344        setTab: function(idx) {
345                this.setState({activeTab:idx});
346        },
347
348        render: function() {
349                return  (
350                        React.createElement("div", null, 
351                                React.createElement("div", {className: "top-gap"}, 
352                                        React.createElement("h1", null, "Statistics"), 
353                                        React.createElement("p", null), 
354                                        React.createElement("div", {role: "tabpanel"}, 
355                                                React.createElement("ul", {className: "nav nav-tabs", role: "tablist"}, 
356                                                         _.pairs(this.state.stats).map(function(st, idx){
357                                                                        var classname = idx === this.state.activeTab ? "active":"";
358                                                                        return  React.createElement("li", {role: "presentation", className: classname, key: st[0]}, 
359                                                                                                React.createElement("a", {href: "#", role: "tab", onClick: this.setTab.bind(this, idx)}, st[0])
360                                                                                        );
361                                                                }.bind(this))
362                                                       
363                                                ), 
364
365                                                React.createElement("div", {className: "tab-content"}, 
366                                                         _.pairs(this.state.stats).map(function(st, idx){
367                                                                        var classname = idx === this.state.activeTab ? "tab-pane active" : "tab-pane";
368                                                                        return  React.createElement("div", {role: "tabpanel", className: classname, key: st[0]}, 
369                                                                                                this.renderStatistics(st[1])
370                                                                                        );
371                                                                }.bind(this))
372                                                       
373                                                )
374                                        )
375                                )
376                        )
377                        );
378        },
379});
380
381var HelpPage = React.createClass({displayName: 'HelpPage',
382        openHelpDesk: function() {
383                window.open('http://support.clarin-d.de/mail/form.php?queue=Aggregator&lang=en', 
384                        '_blank', 'height=560,width=370');
385        },
386
387        render: function() {
388                return  (
389                        React.createElement("div", null, 
390                                React.createElement("div", {className: "top-gap"}, 
391                                        React.createElement("h1", null, "Help"), 
392                                        React.createElement("h3", null, "Performing search in FCS corpora"), 
393                                        React.createElement("p", null, "To perform simple keyword search in all CLARIN-D Federated Content Search centers" + ' ' + 
394                                        "and their corpora, go to the search field at the top of the page," + ' ' + 
395                                        "enter your query, and click 'search' button or press the 'Enter' key."), 
396                                       
397                                        React.createElement("p", null, "When the search starts, the page will start filling in with the corpora responses." + ' ' + 
398                                        "After the entire search process has ended you have the option to download the results" + ' ' +
399                                        "in various formats."
400                                        ), 
401
402                                        React.createElement("p", null, "If you are particularly interested in the results returned by a corpus, you have" + ' ' +
403                                        "the option to focus only on the results of that corpus, by clicking on the 'Watch' button." + ' ' +
404                                        "In this view mode you can also download the results of use the WebLicht processing services" + ' ' +
405                                        "to further analyse the results."), 
406
407
408                                        React.createElement("h3", null, "Adjusting search criteria"), 
409                                        React.createElement("p", null, "The FCS Aggregator makes possible to select specific corpora based on their name" + ' ' + 
410                                        "or language and to specify the number of search results (hits) per corpus per page." + ' ' +
411                                        "The user interface controls that allows to change these options are located" + ' ' + 
412                                        "right below the search fiels on the main page. The current options are" + ' ' + 
413                                        "to filter resources based on their language, to select specific resources, and" + ' ' + 
414                                        "to set the maximum number of hits."), 
415
416
417                                        React.createElement("h3", null, "More help"), 
418                                        React.createElement("p", null, "More detailed information on using FCS Aggregator is available" + ' ' + 
419                                        "at the Aggegator wiki page. If you still cannot find an answer to your question," + ' ' + 
420                                        "or if want to send a feedback, you can write to Clarin-D helpdesk: "), 
421                                        React.createElement("button", {type: "button", className: "btn btn-default btn-lg", onClick: this.openHelpDesk}, 
422                                                React.createElement("span", {className: "glyphicon glyphicon-question-sign", 'aria-hidden': "true"}), 
423                                                " HelpDesk"
424                                        )                                       
425                                )
426                        )
427                );
428        }
429});
430
431var AboutPage = React.createClass({displayName: 'AboutPage',
432        render: function() {
433                return  React.createElement("div", null, 
434                                        React.createElement("div", {className: "top-gap"}, 
435                                                React.createElement("h1", null, "About"), 
436                                                React.createElement("h3", null, "Technology"), 
437
438                                                React.createElement("p", null, "The Aggregator uses the following software components:"), 
439
440                                                React.createElement("ul", null, 
441                                                        React.createElement("li", null, 
442                                                                React.createElement("a", {href: "http://dropwizard.io/"}, "Dropwizard"), " ", 
443                                                                "(", React.createElement("a", {href: "http://www.apache.org/licenses/LICENSE-2.0"}, "Apache License 2.0"), ")"
444                                                        ), 
445                                                        React.createElement("li", null, 
446                                                                React.createElement("a", {href: "http://eclipse.org/jetty/"}, "Jetty"), " ", 
447                                                                "(", React.createElement("a", {href: "http://www.apache.org/licenses/LICENSE-2.0"}, "Apache License 2.0"), ")"
448                                                        ), 
449                                                        React.createElement("li", null, 
450                                                                React.createElement("a", {href: "http://jackson.codehaus.org/"}, "Jackson"), " ", 
451                                                                "(", React.createElement("a", {href: "http://www.apache.org/licenses/LICENSE-2.0"}, "Apache License 2.0"), ")"
452                                                        ), 
453                                                        React.createElement("li", null, 
454                                                                React.createElement("a", {href: "https://jersey.java.net/"}, "Jersey"), " ", 
455                                                                "(", React.createElement("a", {href: "https://jersey.java.net/license.html#/cddl"}, "CCDL 1.1"), ")"
456                                                        ), 
457                                                        React.createElement("li", null, 
458                                                                React.createElement("a", {href: "https://github.com/optimaize/language-detector"}, "Optimaize Language Detector"), " ", 
459                                                                "(", React.createElement("a", {href: "http://www.apache.org/licenses/LICENSE-2.0"}, "Apache License 2.0"), ")"
460                                                        ), 
461                                                        React.createElement("li", null, 
462                                                                React.createElement("a", {href: "http://poi.apache.org/"}, "Apache POI"), " ", 
463                                                                "(", React.createElement("a", {href: "http://www.apache.org/licenses/LICENSE-2.0"}, "Apache License 2.0"), ")"
464                                                        )
465                                                ), 
466
467                                                React.createElement("ul", null, 
468                                                        React.createElement("li", null, 
469                                                                React.createElement("a", {href: "http://facebook.github.io/react/"}, "React"), " ", 
470                                                                "(", React.createElement("a", {href: "https://github.com/facebook/react/blob/master/LICENSE"}, "BSD license"), ")"
471                                                        ), 
472                                                        React.createElement("li", null, 
473                                                                React.createElement("a", {href: "http://getbootstrap.com/"}, "Bootstrap"), " ", 
474                                                                "(", React.createElement("a", {href: "http://opensource.org/licenses/mit-license.html"}, "MIT license"), ")"
475                                                        ), 
476                                                        React.createElement("li", null, 
477                                                                React.createElement("a", {href: "http://jquery.com/"}, "jQuery"), " ", 
478                                                                "(", React.createElement("a", {href: "http://opensource.org/licenses/mit-license.html"}, "MIT license"), ")"
479                                                        ), 
480                                                        React.createElement("li", null, 
481                                                                React.createElement("a", {href: "http://glyphicons.com/"}, "GLYPHICONS free"), " ", 
482                                                                "(", React.createElement("a", {href: "https://creativecommons.org/licenses/by/3.0/"}, "CC-BY 3.0"), ")"
483                                                        ), 
484                                                        React.createElement("li", null, 
485                                                                React.createElement("a", {href: "http://fortawesome.github.io/Font-Awesome/"}, "FontAwesome"), " ", 
486                                                                "(", React.createElement("a", {href: "http://opensource.org/licenses/mit-license.html"}, "MIT"), ", ", React.createElement("a", {href: "http://scripts.sil.org/OFL"}, "SIL Open Font License"), ")"
487                                                        )
488                                                ), 
489
490                                                React.createElement("h3", null, "Statistics"), 
491                                                React.createElement("button", {type: "button", className: "btn btn-default btn-lg", onClick: function() {main.toStatistics(true);}}, 
492                                                        React.createElement("span", {className: "glyphicon glyphicon-cog", 'aria-hidden': "true"}, " "), 
493                                                        "View server log"
494                                                )
495                                        )
496                                );
497        }
498});
499
500var Footer = React.createClass({displayName: 'Footer',
501        toAbout: function(e) {
502                main.toAbout(true);
503                e.preventDefault();
504                e.stopPropagation();
505        },
506
507        render: function() {
508                var path = window.location.pathname.split('/');
509                if (path.length === 3 && path[2] === 'embed') {
510                        return false;
511                }
512                return  (
513                        React.createElement("div", {className: "container"}, 
514                                React.createElement("div", {id: "CLARIN_footer_left"}, 
515                                                React.createElement("a", {title: "about", href: "about", onClick: this.toAbout}, 
516                                                React.createElement("span", {className: "glyphicon glyphicon-info-sign"}), 
517                                                React.createElement("span", null, VERSION)
518                                        )
519                                ), 
520                                React.createElement("div", {id: "CLARIN_footer_middle"}, 
521                                        React.createElement("a", {title: "CLARIN ERIC", href: "https://www.clarin.eu/"}, 
522                                        React.createElement("img", {src: "img/clarindLogo.png", alt: "CLARIN ERIC logo", style: {height:80}})
523                                        )
524                                ), 
525                                React.createElement("div", {id: "CLARIN_footer_right"}, 
526                                        React.createElement("a", {title: "contact", href: "mailto:fcs@clarin.eu"}, 
527                                                React.createElement("span", {className: "glyphicon glyphicon-envelope"}), 
528                                                React.createElement("span", null, " CONTACT")
529                                        )
530                                )
531                        )
532                );
533        }
534});
535
536function endsWith(str, suffix) {
537    return str.indexOf(suffix, str.length - suffix.length) !== -1;
538}
539
540var routeFromLocation = function() {
541        // console.log("routeFromLocation: " + document.location);
542        if (!this) throw "routeFromLocation must be bound to main";
543        var path = window.location.pathname.split('/');
544        if (path.length === 3) {
545                var p = path[2];
546                if (p === 'help') {
547                        this.toHelp(false);
548                } else if (p === 'about') {
549                        this.toAbout(false);
550                } else if (p === 'stats') {
551                        this.toStatistics(false);
552                } else if (p === 'embed') {
553                        this.toEmbedded(false);
554                } else {
555                        this.toAggregator(false);
556                }
557        } else {
558                this.toAggregator(false);
559        }
560};
561
562var main = React.render(React.createElement(Main, null),  document.getElementById('body'));
563React.render(React.createElement(Footer, null), document.getElementById('footer') );
564
565window.onpopstate = routeFromLocation.bind(main);
566
567})();
Note: See TracBrowser for help on using the repository browser.