source: SRUAggregator/trunk/src/main/resources/assets/js/search.jsx @ 5954

Last change on this file since 5954 was 5954, checked in by emanuel.dima@uni-tuebingen.de, 9 years ago
  1. alpha14: collections view stable sorting, better filtering; upfront diagnostic messages for search
File size: 20.7 KB
Line 
1/** @jsx React.DOM */
2(function() {
3"use strict";
4
5window.MyAggregator = window.MyAggregator || {};
6
7var React = window.React;
8var PT = React.PropTypes;
9var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
10
11var CorpusSelection = window.MyAggregator.CorpusSelection;
12var HitNumber = window.MyAggregator.HitNumber;
13var CorpusView = window.MyAggregator.CorpusView;
14var Popover = window.MyReact.Popover;
15var InfoPopover = window.MyReact.InfoPopover;
16var Panel = window.MyReact.Panel;
17var Modal = window.MyReact.Modal;
18
19var multipleLanguageCode = "mul"; // see ISO-693-3
20
21var layers = [
22        {
23                id: "sampa",
24                name: "Phonetic Transcriptions",
25                searchPlaceholder: "stA:z",
26                searchLabel: "SAMPA query",
27                searchLabelBkColor: "#eef",
28        },
29        {
30                id: "text",
31                name: "Text Resources",
32                searchPlaceholder: "Elephant",
33                searchLabel: "Search text",
34                searchLabelBkColor: "#fed",
35        },
36];
37var layerMap = {
38        sampa: layers[0],
39        text: layers[1],
40};
41
42function Corpora(corpora, updateFn) {
43        var that = this;
44        this.corpora = corpora;
45        this.update = function() {
46                updateFn(that);
47        };
48       
49        var sortFn = function(x, y) {
50                var r = x.institution.name.localeCompare(y.institution.name);
51                if (r !== 0) {
52                        return r;
53                }
54                var t1 = x.title ? x.title : x.displayName;
55                var t2 = y.title ? y.title : y.displayName;
56                return t1.toLowerCase().localeCompare(t2.toLowerCase());
57        };
58
59        this.recurse(function(corpus) { corpus.subCorpora.sort(sortFn); });
60        this.corpora.sort(sortFn);
61
62        this.recurse(function(corpus, index) {
63                corpus.visible = true; // visible in the corpus view
64                corpus.selected = true; // selected in the corpus view
65                corpus.expanded = false; // not expanded in the corpus view
66                corpus.priority = 1; // priority in corpus view
67                corpus.index = index; // original order, used for stable sort
68        });
69}
70
71Corpora.prototype.recurseCorpus = function(corpus, fn) {
72        if (false === fn(corpus)) {             
73                // no recursion
74        } else {
75                this.recurseCorpora(corpus.subCorpora, fn);
76        }
77};
78
79Corpora.prototype.recurseCorpora = function(corpora, fn) {
80        var recfn = function(corpus, index){
81                if (false === fn(corpus, index)) {
82                        // no recursion
83                } else {
84                        corpus.subCorpora.forEach(recfn);
85                }
86        };
87        corpora.forEach(recfn);
88};
89
90Corpora.prototype.recurse = function(fn) {
91        this.recurseCorpora(this.corpora, fn);
92};
93
94Corpora.prototype.getLanguageCodes = function() {
95        var languages = {};
96        this.recurse(function(corpus) {
97                corpus.languages.forEach(function(lang) {
98                        languages[lang] = true;
99                });
100                return true;
101        });
102        return languages;
103};
104
105Corpora.prototype.isCorpusVisible = function(corpus, layerId, languageCode) {
106        if (layerId !== "text") {
107                return false;
108        }
109        // yes for any language
110        if (languageCode === multipleLanguageCode) {
111                return true;
112        }
113        // yes if the corpus is in only that language
114        if (corpus.languages && corpus.languages.length === 1 && corpus.languages[0] === languageCode) {
115                return true;
116        }       
117
118        // ? yes if the corpus also contains that language
119        if (corpus.languages && corpus.languages.indexOf(languageCode) >=0) {
120                return true;
121        }
122
123        // ? yes if the corpus has no language
124        // if (!corpus.languages || corpus.languages.length === 0) {
125        //      return true;
126        // }
127        return false;
128};
129
130Corpora.prototype.setVisibility = function(layerId, languageCode) {
131        // top level
132        this.corpora.forEach(function(corpus) {
133                corpus.visible = this.isCorpusVisible(corpus, layerId, languageCode);
134                this.recurseCorpora(corpus.subCorpora, function(c) { c.visible = corpus.visible; });
135        }.bind(this));
136};
137
138Corpora.prototype.getSelectedIds = function() {
139        var ids = [];
140        this.recurse(function(corpus) {
141                if (corpus.visible && corpus.selected) {
142                        ids.push(corpus.id);
143                        return false; // top-most collection in tree, don't delve deeper
144                }
145                return true;
146        });
147
148        // console.log("ids: ", ids.length, {ids:ids});
149        return ids;
150};
151
152Corpora.prototype.getSelectedMessage = function() {
153        var selected = this.getSelectedIds().length;
154        if (this.corpora.length === selected) {
155                return "All available collections";
156        } else if (selected === 1) {
157                return "1 selected collection";
158        }
159        return selected+" selected collections";
160};
161
162
163var AggregatorPage = window.MyAggregator.AggregatorPage = React.createClass({
164        propTypes: {
165                ajax: PT.func.isRequired
166        },
167
168        timeout: 0,
169        nohits: {
170                requests: [],
171                results: [],
172        },
173        anyLanguage: [multipleLanguageCode, "Any Language"],
174
175        getInitialState: function () {
176                return {
177                        corpora: new Corpora([], this.updateCorpora),
178                        languageMap: {},
179                        language: this.anyLanguage,
180                        languageFilter: 'byMeta',
181                        searchLayerId: "text",
182                        numberOfResults: 10,
183
184                        searchId: null,
185                        hits: this.nohits,
186                };
187        },
188
189        componentDidMount: function() {
190                this.refreshCorpora();
191                this.refreshLanguages();
192        },
193
194        refreshCorpora: function() {
195                this.props.ajax({
196                        url: 'rest/corpora',
197                        success: function(json, textStatus, jqXHR) {
198                                this.setState({corpora : new Corpora(json, this.updateCorpora)});
199                        }.bind(this),
200                });
201        },
202
203        refreshLanguages: function() {
204                this.props.ajax({
205                        url: 'rest/languages',
206                        success: function(json, textStatus, jqXHR) {
207                                this.setState({languageMap : json});
208                        }.bind(this),
209                });
210        },
211
212        updateCorpora: function(corpora) {
213                this.setState({corpora:corpora});
214        },
215
216        search: function(query) {
217                // console.log(query);
218                if (!query) {
219                        this.setState({ hits: this.nohits, searchId: null });
220                        return;                 
221                }
222                this.props.ajax({
223                        url: 'rest/search',
224                        type: "POST",
225                        data: {
226                                layer: this.state.searchLayerId,
227                                language: this.state.language[0],
228                                query: query,
229                                numberOfResults: this.state.numberOfResults,
230                                corporaIds: this.state.corpora.getSelectedIds(),
231                        },
232                        success: function(searchId, textStatus, jqXHR) {
233                                // console.log("search ["+query+"] ok: ", searchId, jqXHR);
234                                this.setState({searchId : searchId});
235                                this.timeout = 250;
236                                setTimeout(this.refreshSearchResults, this.timeout);
237                        }.bind(this),
238                });
239        },
240
241        refreshSearchResults: function() {
242                if (!this.state.searchId) {
243                        return;
244                }
245                this.props.ajax({
246                        url: 'rest/search/'+this.state.searchId,
247                        success: function(json, textStatus, jqXHR) {
248                                if (json.requests.length > 0) {
249                                        if (this.timeout < 10000) {
250                                                this.timeout = 1.5 * this.timeout;
251                                        }
252                                        setTimeout(this.refreshSearchResults, this.timeout);
253                                        // console.log("new search in: " + this.timeout+ "ms");
254                                } else {
255                                        // console.log("search ended");
256                                }
257                                this.setState({hits:json});
258                                // console.log("hits:", json);
259                        }.bind(this),
260                });
261        },
262
263        setLanguageAndFilter: function(languageObj, languageFilter) {
264                this.state.corpora.setVisibility(this.state.searchLayerId,
265                        languageFilter === 'byGuess' ? multipleLanguageCode : languageObj[0]);
266                this.setState({language: languageObj, languageFilter: languageFilter});
267                this.state.corpora.update();
268        },
269
270        setLayer: function(layerId) {
271                this.state.corpora.setVisibility(layerId, this.state.language[0]);
272                this.state.corpora.update();
273                this.setState({searchLayerId: layerId});
274        },
275
276        setNumberOfResults: function(e) {
277                var n = e.target.value;
278                if (n < 10) n = 10;
279                if (n > 250) n = 250;
280                this.setState({numberOfResults: n});
281                e.preventDefault();
282                e.stopPropagation();
283        },
284
285        stop: function(e) {
286                e.stopPropagation();
287        },
288
289        filterResults: function() {
290                var langCode = this.state.language[0];
291                return this.state.hits.results.map(function(corpusHit) {
292                        return {
293                                corpus: corpusHit.corpus,
294                                startRecord: corpusHit.startRecord,
295                                endRecord: corpusHit.endRecord,
296                                exception: corpusHit.exception,
297                                diagnostics: corpusHit.diagnostics,
298                                searchString: corpusHit.searchString,
299                                kwics: corpusHit.kwics.filter(function(kwic){
300                                        return kwic.language === langCode || langCode === multipleLanguageCode || langCode === null;
301                                }),
302                        };
303                });
304        },
305
306        toggleLanguageSelection: function(e) {
307                $(this.refs.languageModal.getDOMNode()).modal();
308                e.preventDefault();
309                e.stopPropagation();
310        },
311
312        toggleCorpusSelection: function(e) {
313                $(this.refs.corporaModal.getDOMNode()).modal();
314                e.preventDefault();
315                e.stopPropagation();
316        },
317
318        renderAggregator: function() {
319                var layer = layerMap[this.state.searchLayerId];
320                return  (
321                        <div className="top-gap">
322                                <div className="row">
323                                        <div className="aligncenter" style={{marginLeft:16, marginRight:16}}>
324                                                <div className="input-group">
325                                                        <span className="input-group-addon" style={{backgroundColor:layer.searchLabelBkColor}}>
326                                                                {layer.searchLabel}
327                                                        </span>
328
329                                                        <SearchBox search={this.search} placeholder={layer.searchPlaceholder} />
330                                                        <div className="input-group-btn">
331                                                                <button className="btn btn-default input-lg" type="button" onClick={this.search}>
332                                                                        <i className="glyphicon glyphicon-search"></i>
333                                                                </button>
334                                                        </div>
335                                                </div>
336                                        </div>
337                                </div>
338
339                                <div className="wel" style={{marginTop:20}}>
340                                        <div className="aligncenter" >
341                                                <form className="form-inline" role="form">
342
343                                                        <div className="input-group">
344                                                               
345                                                                <span className="input-group-addon nobkg" >Search for</span>
346                                                               
347                                                                <div className="input-group-btn">
348                                                                        <button className="form-control btn btn-default"
349                                                                                        onClick={this.toggleLanguageSelection}>
350                                                                                {this.state.language[1]} <span className="caret"/>
351                                                                        </button>
352                                                                        <span/>
353                                                                </div>
354
355                                                                <div className="input-group-btn">
356                                                                        <ul ref="layerDropdownMenu" className="dropdown-menu">
357                                                                                {       layers.map(function(l) {
358                                                                                                return <li key={l.id}> <a tabIndex="-1" href="#"
359                                                                                                        onClick={this.setLayer.bind(this, l.id)}> {l.name} </a></li>;
360                                                                                        }.bind(this))
361                                                                                }
362                                                                        </ul>                                                           
363                                                                        <button className="form-control btn btn-default"
364                                                                                        aria-expanded="false" data-toggle="dropdown" >
365                                                                                {layer.name} <span className="caret"/>
366                                                                        </button>
367                                                                </div>
368
369                                                        </div>
370
371                                                        <div className="input-group">
372                                                                <span className="input-group-addon nobkg">in</span>
373                                                                <button type="button" className="btn btn-default" onClick={this.toggleCorpusSelection}>
374                                                                        {this.state.corpora.getSelectedMessage()} <span className="caret"/>
375                                                                </button>
376                                                        </div>                                                 
377
378                                                        <div className="input-group">
379                                                                <span className="input-group-addon nobkg">and show up to</span>
380                                                                <div className="input-group-btn">
381                                                                        <input type="number" className="form-control input" min="10" max="250"
382                                                                                style={{width:60}}
383                                                                                onChange={this.setNumberOfResults} value={this.state.numberOfResults}
384                                                                                onKeyPress={this.stop}/>
385                                                                </div>
386                                                                <span className="input-group-addon nobkg">hits</span>
387                                                        </div>
388                                                </form>
389                                        </div>
390                                </div>
391
392                    <Modal ref="corporaModal" title="Collections">
393                                        <CorpusView corpora={this.state.corpora} languageMap={this.state.languageMap} />
394                    </Modal>
395
396                    <Modal ref="languageModal" title="Select Language">
397                                        <LanguageSelector anyLanguage={this.anyLanguage}
398                                                                          languageMap={this.state.languageMap}
399                                                                          selectedLanguage={this.state.language}
400                                                                          languageFilter={this.state.languageFilter}
401                                                                          languageChangeHandler={this.setLanguageAndFilter} />
402                    </Modal>
403
404                                <div className="top-gap">
405                                        <Results requests={this.state.hits.requests}
406                                                 results={this.filterResults()}
407                                                 searchedLanguage={this.state.language}/>
408                                </div>
409                        </div>
410                        );
411        },
412        render: function() {
413                return this.renderAggregator();
414        }
415});
416
417
418
419/////////////////////////////////
420
421var LanguageSelector = React.createClass({
422        propTypes: {
423                anyLanguage: PT.array.isRequired,
424                languageMap: PT.object.isRequired,
425                selectedLanguage: PT.array.isRequired,
426                languageFilter: PT.string.isRequired,
427                languageChangeHandler: PT.func.isRequired,
428        },
429        mixins: [React.addons.LinkedStateMixin],
430
431        selectLang: function(language) {
432                this.props.languageChangeHandler(language, this.props.languageFilter);
433        },
434
435        setFilter: function(filter) {
436                this.props.languageChangeHandler(this.props.selectedLanguage, filter);
437        },
438
439        renderLanguageObject: function(lang) {
440                var desc = lang[1] + " [" + lang[0] + "]";
441                var style = {
442                        whiteSpace: "nowrap",
443                        fontWeight: lang[0] === this.props.selectedLanguage[0] ? "bold":"normal",
444                };
445                return  <div key={lang[0]}>
446                                        <a tabIndex="-1" href="#" style={style} onClick={this.selectLang.bind(this, lang)}>{desc}</a>
447                                </div>;
448        },
449
450        renderRadio: function(option) {
451                return  this.props.languageFilter === option ?
452                                <input type="radio" name="filterOpts" value={option} checked onChange={this.setFilter.bind(this, option)}/>
453                                : <input type="radio" name="filterOpts" value={option} onChange={this.setFilter.bind(this, option)} />;
454        },
455
456        render: function() {
457                var languages = _.pairs(this.props.languageMap)
458                                 .sort(function(l1, l2){return l1[1].localeCompare(l2[1]); });
459                languages.unshift(this.props.anyLanguage);
460                languages = languages.map(this.renderLanguageObject);
461                var third = Math.round(languages.length/3);
462                var l1 = languages.slice(0, third);
463                var l2 = languages.slice(third, 2*third);
464                var l3 = languages.slice(2*third, languages.length);
465
466                return  <div>
467                                        <div className="row">
468                                                <div className="col-sm-4">{l1}</div>
469                                                <div className="col-sm-4">{l2}</div>
470                                                <div className="col-sm-4">{l3}</div>
471                                                <div className="col-sm-12" style={{marginTop:10, marginBottom:10, borderBottom:"1px solid #eee"}}/>
472                                        </div>
473                                        <form className="form" role="form">
474                                                <div className="input-group">
475                                                        <div>
476                                                        <label style={{color:'black'}}>
477                                                                { this.renderRadio('byMeta') }{" "}
478                                                                Use the collections{"'"} specified language to filter results
479                                                        </label>
480                                                        </div>
481                                                        <div>
482                                                        <label style={{color:'black'}}>
483                                                                { this.renderRadio('byGuess') }{" "}
484                                                                Filter results by using a language detector
485                                                        </label>
486                                                        </div>
487                                                        <div>
488                                                        <label style={{color:'black'}}>
489                                                                { this.renderRadio('byMetaAndGuess') }{" "}
490                                                                First use the collections{"'"} specified language then also use a language detector
491                                                        </label>
492                                                        </div>
493                                                </div>
494                                        </form>
495                                </div>;
496        }
497});
498/////////////////////////////////
499
500var SearchBox = React.createClass({
501        propTypes: {
502                search: PT.func.isRequired,
503                placeholder: PT.string.isRequired,
504        },
505
506        getInitialState: function () {
507                return {
508                        query: "",
509                };
510        },
511
512        handleChange: function(event) {
513        this.setState({query: event.target.value});
514        },
515
516        handleKey: function(event) {
517        if (event.keyCode==13) {
518                this.search();
519        }
520        },
521
522        search: function() {
523                this.props.search(this.state.query);
524        },
525
526        render: function() {
527                return  <input className="form-control input-lg search"
528                                        name="query"
529                                        type="text"
530                                        value={this.state.query}
531                                        placeholder={this.props.placeholder}
532                                        tabIndex="1"
533                                        onChange={this.handleChange}
534                                        onKeyDown={this.handleKey} />  ;
535        }
536});
537
538/////////////////////////////////
539
540var Results = React.createClass({
541        propTypes: {
542                requests: PT.array.isRequired,
543                results: PT.array.isRequired,
544                searchedLanguage: PT.array.isRequired,
545        },
546
547        getInitialState: function () {
548                return {
549                        displayKwic: false,
550                };
551        },
552
553        toggleKwic: function() {
554                this.setState({displayKwic:!this.state.displayKwic});
555        },
556
557        renderRowLanguage: function(hit) {
558                return false; //<span style={{fontFace:"Courier",color:"black"}}>{hit.language} </span> ;
559        },
560
561        renderRowsAsHits: function(hit,i) {
562                function renderTextFragments(tf, idx) {
563                        return <span key={idx} className={tf.hit?"keyword":""}>{tf.text}</span>;
564                }
565                return  <p key={i} className="hitrow">
566                                        {this.renderRowLanguage(hit)}
567                                        {hit.fragments.map(renderTextFragments)}
568                                </p>;
569        },
570
571        renderRowsAsKwic: function(hit,i) {
572                var sleft={textAlign:"left", verticalAlign:"top", width:"50%"};
573                var scenter={textAlign:"center", verticalAlign:"top", maxWidth:"50%"};
574                var sright={textAlign:"right", verticalAlign:"top", maxWidth:"50%"};
575                return  <tr key={i} className="hitrow">
576                                        <td>{this.renderRowLanguage(hit)}</td>
577                                        <td style={sright}>{hit.left}</td>
578                                        <td style={scenter} className="keyword">{hit.keyword}</td>
579                                        <td style={sleft}>{hit.right}</td>
580                                </tr>;
581        },
582
583        renderPanelTitle: function(corpus) {
584                var inline = {display:"inline-block"};
585                return  <div style={inline}>
586                                        <span className="corpusName"> {corpus.title ? corpus.title : corpus.displayName}</span>
587                                        <span className="institutionName"> — {corpus.institution.name}</span>
588                                </div>;
589        },
590
591        renderPanelInfo: function(corpus) {
592                var inline = {display:"inline-block"};
593                return  <div>
594                                        <InfoPopover placement="left"
595                                                        title={corpus.title ? corpus.title : corpus.displayName}>
596                                                <dl className="dl-horizontal">
597                                                        <dt>Institution</dt>
598                                                        <dd>{corpus.institution.name}</dd>
599
600                                                        {corpus.description ? <dt>Description</dt>:false}
601                                                        {corpus.description ? <dd>{corpus.description}</dd>: false}
602
603                                                        {corpus.landingPage ? <dt>Landing Page</dt> : false }
604                                                        {corpus.landingPage ?
605                                                                <dd><a href={corpus.landingPage}>{corpus.landingPage}</a></dd>:
606                                                                false}
607
608                                                        <dt>Languages</dt>
609                                                        <dd>{corpus.languages.join(", ")}</dd>
610                                                </dl>
611                                        </InfoPopover>
612                                        {" "}
613                                        <div style={inline}>
614                                                <button className="btn btn-default btn-xs" onClick={this.zoom}>
615                                                        <span className="glyphicon glyphicon-fullscreen"/>
616                                                </button>
617                                        </div>
618                                </div>;
619        },
620
621        renderDiagnostics: function(corpusHit) {
622                if (!corpusHit.diagnostics || corpusHit.diagnostics.length === 0) {
623                        return false;
624                }
625
626                return corpusHit.diagnostics.map(function(d) {
627                        return  <div className="alert alert-danger" role="alert">
628                                                {d.dgnMessage}{": "}{d.dgnDiagnostic}
629                                        </div>;
630                });
631        },
632
633        renderPanelBody: function(corpusHit) {
634                var fulllength = {width:"100%"};
635                if (this.state.displayKwic) {
636                        return  <div>
637                                                {this.renderDiagnostics(corpusHit)}
638                                                <table className="table table-condensed table-hover" style={fulllength}>
639                                                        <tbody>{corpusHit.kwics.map(this.renderRowsAsKwic)}</tbody>
640                                                </table>
641                                        </div>;
642                } else {
643                        return  <div>
644                                                {this.renderDiagnostics(corpusHit)}
645                                                {corpusHit.kwics.map(this.renderRowsAsHits)}
646                                        </div>;
647                }
648        },
649
650        renderResultPanels: function(corpusHit) {
651                if (corpusHit.kwics.length === 0 &&
652                        corpusHit.diagnostics.length === 0) {
653                        return false;
654                }
655                return  <Panel key={corpusHit.corpus.displayName}
656                                                title={this.renderPanelTitle(corpusHit.corpus)}
657                                                info={this.renderPanelInfo(corpusHit.corpus)}>
658                                        {this.renderPanelBody(corpusHit)}
659                                </Panel>;
660        },
661
662        renderProgressBar: function() {
663                var percents = 100 * this.props.results.length / (this.props.requests.length + this.props.results.length);
664                var sperc = Math.round(percents);
665                var styleperc = {width: sperc+"%"};
666                return this.props.requests.length > 0 ?
667                        <div className="progress" style={{marginBottom:10}}>
668                                <div className="progress-bar progress-bar-striped active" role="progressbar"
669                                        aria-valuenow={sperc} aria-valuemin="0" aria-valuemax="100" style={styleperc} />
670                        </div> :
671                        <span />;
672        },
673
674        renderSearchingMessage: function() {
675                return false;
676                // if (this.props.requests.length === 0)
677                //      return false;
678                // return "Searching in " + this.props.requests.length + " collections...";
679        },
680
681        renderFoundMessage: function(hits) {
682                if (this.props.results.length === 0)
683                        return false;
684                var total = this.props.results.length;
685                return hits + " collections with results found in " + total + " searched collections";
686        },
687
688        renderKwicCheckbox: function() {
689                return  <div className="float-right" style={{marginRight:17}}>
690                                        <div className="btn-group" style={{display:"inline-block"}}>
691                                                <label forHtml="inputKwic" className="btn-default">
692                                                        { this.state.displayKwic ?
693                                                                <input id="inputKwic" type="checkbox" value="kwic" checked onChange={this.toggleKwic} /> :
694                                                                <input id="inputKwic" type="checkbox" value="kwic" onChange={this.toggleKwic} />
695                                                        }
696                                                        &nbsp;
697                                                        Display as Key Word In Context
698                                                </label>
699                                        </div>
700                                </div>;
701        },
702
703        render: function() {
704                var hits = this.props.results.filter(function(corpusHit) { return corpusHit.kwics.length > 0; }).length;
705                var margintop = {marginTop:"10px"};
706                var margin = {marginTop:"0", padding:"20px"};
707                var inlinew = {display:"inline-block", margin:"0 5px 0 0", width:"240px;"};
708                var right= {float:"right"};
709                return  <div>
710                                        <ReactCSSTransitionGroup transitionName="fade">
711                                                <div key="-searching-message-" style={margintop}>{this.renderSearchingMessage()} </div>
712                                                <div key="-found-message-" style={margintop}>{this.renderFoundMessage(hits)} </div>
713                                                <div key="-progress-" style={margintop}>{this.renderProgressBar()}</div>
714                                                {hits > 0 ?
715                                                        <div key="-option-KWIC-" className="row">
716                                                                {this.renderKwicCheckbox()}
717                                                        </div>
718                                                        : false }
719                                                {this.props.results.map(this.renderResultPanels)}
720                                        </ReactCSSTransitionGroup>
721                                </div>;
722        }
723});
724
725var _ = window._ = window._ || {
726        keys: function() {
727                var ret = [];
728                for (var x in o) {
729                        if (o.hasOwnProperty(x)) {
730                                ret.push(x);
731                        }
732                }
733                return ret;
734        },
735
736        pairs: function(o){
737                var ret = [];
738                for (var x in o) {
739                        if (o.hasOwnProperty(x)) {
740                                ret.push([x, o[x]]);
741                        }
742                }
743                return ret;
744        },
745};
746
747})();
Note: See TracBrowser for help on using the repository browser.