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

Last change on this file since 6097 was 6097, checked in by emanuel.dima@uni-tuebingen.de, 9 years ago
  1. beta-29: better language detection for weblicht
File size: 29.2 KB
Line 
1/** @jsx React.DOM */
2(function() {
3"use strict";
4
5var NO_MORE_RECORDS_DIAGNOSTIC_URI = "info:srw/diagnostic/1/61";
6
7window.MyAggregator = window.MyAggregator || {};
8
9var React = window.React;
10var PT = React.PropTypes;
11var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
12
13var CorpusSelection = window.MyAggregator.CorpusSelection;
14var HitNumber = window.MyAggregator.HitNumber;
15var CorpusView = window.MyAggregator.CorpusView;
16var Popover = window.MyReact.Popover;
17var InfoPopover = window.MyReact.InfoPopover;
18var Panel = window.MyReact.Panel;
19var ModalMixin = window.MyReact.ModalMixin;
20var Modal = window.MyReact.Modal;
21
22var multipleLanguageCode = "mul"; // see ISO-693-3
23
24var layers = [
25        {
26                id: "text",
27                name: "Text Resources",
28                searchPlaceholder: "Elephant",
29                searchLabel: "Search text",
30                searchLabelBkColor: "#fed",
31                className: '',
32        },
33        {
34                id: "sampa",
35                name: "Phonetic Transcriptions",
36                searchPlaceholder: "stA:z",
37                searchLabel: "SAMPA query",
38                searchLabelBkColor: "#eef",
39                disabled: true,
40        },
41];
42var layerMap = {
43        text: layers[0],
44        sampa: layers[1],
45};
46
47function Corpora(corpora, updateFn) {
48        var that = this;
49        this.corpora = corpora;
50        this.update = function() {
51                updateFn(that);
52        };
53       
54        var sortFn = function(x, y) {
55                var r = x.institution.name.localeCompare(y.institution.name);
56                if (r !== 0) {
57                        return r;
58                }
59                return x.title.toLowerCase().localeCompare(y.title.toLowerCase());
60        };
61
62        this.recurse(function(corpus) { corpus.subCorpora.sort(sortFn); });
63        this.corpora.sort(sortFn);
64
65        this.recurse(function(corpus, index) {
66                corpus.visible = true; // visible in the corpus view
67                corpus.selected = true; // selected in the corpus view
68                corpus.expanded = false; // not expanded in the corpus view
69                corpus.priority = 1; // used for ordering search results in corpus view
70                corpus.index = index; // original order, used for stable sort
71        });
72}
73
74Corpora.prototype.recurseCorpus = function(corpus, fn) {
75        if (false === fn(corpus)) {             
76                // no recursion
77        } else {
78                this.recurseCorpora(corpus.subCorpora, fn);
79        }
80};
81
82Corpora.prototype.recurseCorpora = function(corpora, fn) {
83        var recfn = function(corpus, index){
84                if (false === fn(corpus, index)) {
85                        // no recursion
86                } else {
87                        corpus.subCorpora.forEach(recfn);
88                }
89        };
90        corpora.forEach(recfn);
91};
92
93Corpora.prototype.recurse = function(fn) {
94        this.recurseCorpora(this.corpora, fn);
95};
96
97Corpora.prototype.getLanguageCodes = function() {
98        var languages = {};
99        this.recurse(function(corpus) {
100                corpus.languages.forEach(function(lang) {
101                        languages[lang] = true;
102                });
103                return true;
104        });
105        return languages;
106};
107
108Corpora.prototype.isCorpusVisible = function(corpus, layerId, languageCode) {
109        if (layerId !== "text") {
110                return false;
111        }
112        // yes for any language
113        if (languageCode === multipleLanguageCode) {
114                return true;
115        }
116        // yes if the corpus is in only that language
117        if (corpus.languages && corpus.languages.length === 1 && corpus.languages[0] === languageCode) {
118                return true;
119        }       
120
121        // ? yes if the corpus also contains that language
122        if (corpus.languages && corpus.languages.indexOf(languageCode) >=0) {
123                return true;
124        }
125
126        // ? yes if the corpus has no language
127        // if (!corpus.languages || corpus.languages.length === 0) {
128        //      return true;
129        // }
130        return false;
131};
132
133Corpora.prototype.setVisibility = function(layerId, languageCode) {
134        // top level
135        this.corpora.forEach(function(corpus) {
136                corpus.visible = this.isCorpusVisible(corpus, layerId, languageCode);
137                this.recurseCorpora(corpus.subCorpora, function(c) { c.visible = corpus.visible; });
138        }.bind(this));
139};
140
141Corpora.prototype.getSelectedIds = function() {
142        var ids = [];
143        this.recurse(function(corpus) {
144                if (corpus.visible && corpus.selected) {
145                        ids.push(corpus.id);
146                        return false; // top-most collection in tree, don't delve deeper
147                }
148                return true;
149        });
150
151        // console.log("ids: ", ids.length, {ids:ids});
152        return ids;
153};
154
155Corpora.prototype.getSelectedMessage = function() {
156        var selected = this.getSelectedIds().length;
157        if (this.corpora.length === selected) {
158                return "All available collections";
159        } else if (selected === 1) {
160                return "1 selected collection";
161        }
162        return selected+" selected collections";
163};
164
165function encodeQueryData(data)
166{
167        var ret = [];
168        for (var d in data) {
169                ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
170        }
171        return ret.join("&");
172}
173
174
175var AggregatorPage = window.MyAggregator.AggregatorPage = React.createClass({
176        propTypes: {
177                ajax: PT.func.isRequired
178        },
179
180        nohits: {
181                results: null,
182        },
183        anyLanguage: [multipleLanguageCode, "Any Language"],
184
185        getInitialState: function () {
186                return {
187                        corpora: new Corpora([], this.updateCorpora),
188                        languageMap: {},
189                        weblichtLanguages: [],
190                        query: "",
191                        language: this.anyLanguage,
192                        languageFilter: 'byMeta',
193                        searchLayerId: "text",
194                        numberOfResults: 10,
195
196                        searchId: null,
197                        timeout: 0,
198                        hits: this.nohits,
199
200                        zoomedCorpusHit: null,
201                };
202        },
203
204        componentDidMount: function() {
205                this.props.ajax({
206                        url: 'rest/init',
207                        success: function(json, textStatus, jqXHR) {
208                                if (this.isMounted()) {
209                                        this.setState({
210                                                corpora : new Corpora(json.corpora, this.updateCorpora),
211                                                languageMap: json.languages,
212                                                weblichtLanguages: json.weblichtLanguages,
213                                        });
214                                }
215                        }.bind(this),
216                });
217        },
218
219        updateCorpora: function(corpora) {
220                this.setState({corpora:corpora});
221        },
222
223        search: function() {
224                var query = this.state.query;
225                if (!query) {
226                        this.setState({ hits: this.nohits, searchId: null });
227                        return;                 
228                }
229                var selectedIds = this.state.corpora.getSelectedIds();
230                // console.log("searching in the following corpora:", selectedIds);
231                this.props.ajax({
232                        url: 'rest/search',
233                        type: "POST",
234                        data: {
235                                layer: this.state.searchLayerId,
236                                language: this.state.language[0],
237                                query: query,
238                                numberOfResults: this.state.numberOfResults,
239                                corporaIds: selectedIds,
240                        },
241                        success: function(searchId, textStatus, jqXHR) {
242                                // console.log("search ["+query+"] ok: ", searchId, jqXHR);
243                                var timeout = 250;
244                                setTimeout(this.refreshSearchResults, timeout);
245                                this.setState({ searchId: searchId, timeout: timeout });
246                        }.bind(this),
247                });
248        },
249        nextResults: function(corpusId) {
250                // console.log("searching next results in corpus:", corpusId);
251                this.props.ajax({
252                        url: 'rest/search/'+this.state.searchId,
253                        type: "POST",
254                        data: {
255                                corpusId: corpusId,
256                                numberOfResults: this.state.numberOfResults,
257                        },
258                        success: function(searchId, textStatus, jqXHR) {
259                                // console.log("search ["+query+"] ok: ", searchId, jqXHR);
260                                var timeout = 250;
261                                setTimeout(this.refreshSearchResults, timeout);
262                                this.setState({ searchId: searchId, timeout: timeout });
263                        }.bind(this),
264                });
265        },
266
267        refreshSearchResults: function() {
268                if (!this.state.searchId || !this.isMounted()) {
269                        return;
270                }
271                this.props.ajax({
272                        url: 'rest/search/'+this.state.searchId,
273                        success: function(json, textStatus, jqXHR) {
274                                var timeout = this.state.timeout;
275                                if (json.inProgress) {
276                                        if (timeout < 10000) {
277                                                timeout = 1.5 * timeout;
278                                        }
279                                        setTimeout(this.refreshSearchResults, timeout);
280                                        // console.log("new search in: " + this.timeout + "ms");
281                                } else {
282                                        console.log("search ended; hits:", json);
283                                }
284                                var corpusHit = this.state.zoomedCorpusHit;
285                                if (corpusHit) {
286                                        for (var resi = 0; resi < json.results.length; resi++) {
287                                                var res = json.results[resi];
288                                                if (res.corpus.id === corpusHit.corpus.id) {
289                                                        corpusHit = res;
290                                                        break;
291                                                }
292                                        }
293                                }
294                                this.setState({ hits: json, timeout: timeout, zoomedCorpusHit: corpusHit});
295                        }.bind(this),
296                });
297        },
298
299        getExportParams: function(corpusId, format, filterLanguage) {
300                var params = corpusId ? {corpusId:corpusId}:{};
301                if (format) params.format = format;
302                if (filterLanguage) {
303                        params.filterLanguage = filterLanguage;
304                } else if (this.state.languageFilter === 'byGuess' || this.state.languageFilter === 'byMetaAndGuess') {
305                        params.filterLanguage = this.state.language[0];
306                }
307                return encodeQueryData(params);
308        },
309
310        getDownloadLink: function(corpusId, format) {
311                return 'rest/search/'+this.state.searchId+'/download?' +
312                        this.getExportParams(corpusId, format);
313        },
314
315        getToWeblichtLink: function(corpusId, forceLanguage) {
316                return 'rest/search/'+this.state.searchId+'/toWeblicht?' +
317                        this.getExportParams(corpusId, null, forceLanguage);
318        },
319
320        setLanguageAndFilter: function(languageObj, languageFilter) {
321                this.state.corpora.setVisibility(this.state.searchLayerId,
322                        languageFilter === 'byGuess' ? multipleLanguageCode : languageObj[0]);
323                this.setState({
324                        language: languageObj,
325                        languageFilter: languageFilter,
326                        corpora: this.state.corpora, // === this.state.corpora.update();
327                });
328        },
329
330        setLayer: function(layerId) {
331                this.state.corpora.setVisibility(layerId, this.state.language[0]);
332                this.setState({
333                        searchLayerId: layerId,
334                        hits: this.nohits,
335                        searchId: null,
336                        corpora: this.state.corpora, // === this.state.corpora.update();
337                });
338        },
339
340        setNumberOfResults: function(e) {
341                var n = e.target.value;
342                if (n < 10) n = 10;
343                if (n > 250) n = 250;
344                this.setState({numberOfResults: n});
345                e.preventDefault();
346                e.stopPropagation();
347        },
348
349        stop: function(e) {
350                e.stopPropagation();
351        },
352
353        filterResults: function() {
354                var noLangFiltering = this.state.languageFilter === 'byMeta';
355                var langCode = this.state.language[0];
356                var results = null, inProgress = 0, hits = 0;
357                if (this.state.hits.results) {
358                        results = this.state.hits.results.map(function(corpusHit) {
359                                return {
360                                        corpus: corpusHit.corpus,
361                                        inProgress: corpusHit.inProgress,
362                                        exception: corpusHit.exception,
363                                        diagnostics: corpusHit.diagnostics,
364                                        kwics: noLangFiltering ? corpusHit.kwics :
365                                                corpusHit.kwics.filter(function(kwic) {
366                                                        return kwic.language === langCode ||
367                                                               langCode === multipleLanguageCode ||
368                                                               langCode === null;
369                                                }),
370                                };
371                        });
372                        for (var i = 0; i < results.length; i++) {
373                                var result = results[i];
374                                if (result.inProgress) {
375                                        inProgress++;
376                                }
377                                if (result.kwics.length > 0) {
378                                        hits ++;
379                                }
380                        }
381                }
382                return {
383                        results: results,
384                        hits: hits,
385                        inProgress: inProgress,
386                };
387        },
388
389        toggleLanguageSelection: function(e) {
390                $(this.refs.languageModal.getDOMNode()).modal();
391                e.preventDefault();
392                e.stopPropagation();
393        },
394
395        toggleCorpusSelection: function(e) {
396                $(this.refs.corporaModal.getDOMNode()).modal();
397                e.preventDefault();
398                e.stopPropagation();
399        },
400
401        toggleResultModal: function(e, corpusHit) {
402                $(this.refs.resultModal.getDOMNode()).modal();
403                this.setState({zoomedCorpusHit: corpusHit});
404                e.preventDefault();
405                e.stopPropagation();
406        },
407
408        onQuery: function(event) {
409                this.setState({query: event.target.value});
410        },
411
412        handleKey: function(event) {
413                if (event.keyCode==13) {
414                        this.search();
415                }
416        },
417
418        renderZoomedResultTitle: function(corpusHit) {
419                if (!corpusHit) return <span/>;
420                var corpus = corpusHit.corpus;
421                return <h3 style={{fontSize:'1em'}}>
422                                        {corpus.title}
423                                        { corpus.landingPage ?
424                                                <a href={corpus.landingPage} onClick={this.stop} style={{fontSize:12}}>
425                                                        <span> – Homepage </span>
426                                                        <i className="glyphicon glyphicon-home"/>
427                                                </a>: false}
428                                </h3>;
429        },
430
431        render: function() {
432                var layer = layerMap[this.state.searchLayerId];
433                return  (
434                        <div className="top-gap">
435                                <div className="row">
436                                        <div className="aligncenter" style={{marginLeft:16, marginRight:16}}>
437                                                <div className="input-group">
438                                                        <span className="input-group-addon" style={{backgroundColor:layer.searchLabelBkColor}}>
439                                                                {layer.searchLabel}
440                                                        </span>
441
442                                                        <input className="form-control input-lg search" name="query" type="text"
443                                                                value={this.state.query} placeholder={this.props.placeholder}
444                                                                tabIndex="1" onChange={this.onQuery} onKeyDown={this.handleKey} />
445                                                        <div className="input-group-btn">
446                                                                <button className="btn btn-default input-lg" type="button" onClick={this.search}>
447                                                                        <i className="glyphicon glyphicon-search"></i>
448                                                                </button>
449                                                        </div>
450                                                </div>
451                                        </div>
452                                </div>
453
454                                <div className="wel" style={{marginTop:20}}>
455                                        <div className="aligncenter" >
456                                                <form className="form-inline" role="form">
457
458                                                        <div className="input-group">
459                                                               
460                                                                <span className="input-group-addon nobkg" >Search for</span>
461                                                               
462                                                                <div className="input-group-btn">
463                                                                        <button className="form-control btn btn-default"
464                                                                                        onClick={this.toggleLanguageSelection}>
465                                                                                {this.state.language[1]} <span className="caret"/>
466                                                                        </button>
467                                                                        <span/>
468                                                                </div>
469
470                                                                <div className="input-group-btn">
471                                                                        <ul ref="layerDropdownMenu" className="dropdown-menu">
472                                                                                {       layers.map(function(l) {
473                                                                                                var cls = l.disabled ? 'disabled':'';
474                                                                                                var handler = function() { if (!l.disabled) this.setLayer(l.id); }.bind(this);
475                                                                                                return <li key={l.id} className={cls}> <a tabIndex="-1" href="#"
476                                                                                                        onClick={handler}> {l.name} </a></li>;
477                                                                                        }.bind(this))
478                                                                                }
479                                                                        </ul>                                                           
480                                                                        <button className="form-control btn btn-default"
481                                                                                        aria-expanded="false" data-toggle="dropdown" >
482                                                                                {layer.name} <span className="caret"/>
483                                                                        </button>
484                                                                </div>
485
486                                                        </div>
487
488                                                        <div className="input-group">
489                                                                <span className="input-group-addon nobkg">in</span>
490                                                                <button type="button" className="btn btn-default" onClick={this.toggleCorpusSelection}>
491                                                                        {this.state.corpora.getSelectedMessage()} <span className="caret"/>
492                                                                </button>
493                                                        </div>                                                 
494
495                                                        <div className="input-group">
496                                                                <span className="input-group-addon nobkg">and show up to</span>
497                                                                <div className="input-group-btn">
498                                                                        <input type="number" className="form-control input" min="10" max="250"
499                                                                                style={{width:60}}
500                                                                                onChange={this.setNumberOfResults} value={this.state.numberOfResults}
501                                                                                onKeyPress={this.stop}/>
502                                                                </div>
503                                                                <span className="input-group-addon nobkg">hits</span>
504                                                        </div>
505                                                </form>
506                                        </div>
507                                </div>
508
509                                <Modal ref="corporaModal" title={<span>Collections</span>}>
510                                        <CorpusView corpora={this.state.corpora} languageMap={this.state.languageMap} />
511                                </Modal>
512
513                                <Modal ref="languageModal" title={<span>Select Language</span>}>
514                                        <LanguageSelector anyLanguage={this.anyLanguage}
515                                                                          languageMap={this.state.languageMap}
516                                                                          selectedLanguage={this.state.language}
517                                                                          languageFilter={this.state.languageFilter}
518                                                                          languageChangeHandler={this.setLanguageAndFilter} />
519                                </Modal>
520
521                                <Modal ref="resultModal" title={this.renderZoomedResultTitle(this.state.zoomedCorpusHit)}>
522                                        <ZoomedResult corpusHit={this.state.zoomedCorpusHit}
523                                                                  nextResults={this.nextResults}
524                                                                  getDownloadLink={this.getDownloadLink}
525                                                                  getToWeblichtLink={this.getToWeblichtLink}
526                                                                  searchedLanguage={this.state.language}
527                                                                  weblichtLanguages={this.state.weblichtLanguages}
528                                                                  languageMap={this.state.languageMap} />
529                                </Modal>
530
531                                <div className="top-gap">
532                                        <Results collhits={this.filterResults()}
533                                                         toggleResultModal={this.toggleResultModal}
534                                                         getDownloadLink={this.getDownloadLink}
535                                                         getToWeblichtLink={this.getToWeblichtLink}
536                                                         searchedLanguage={this.state.language}/>
537                                </div>
538                        </div>
539                        );
540        },
541});
542
543
544
545/////////////////////////////////
546
547var LanguageSelector = React.createClass({
548        propTypes: {
549                anyLanguage: PT.array.isRequired,
550                languageMap: PT.object.isRequired,
551                selectedLanguage: PT.array.isRequired,
552                languageFilter: PT.string.isRequired,
553                languageChangeHandler: PT.func.isRequired,
554        },
555        mixins: [React.addons.LinkedStateMixin],
556
557        selectLang: function(language) {
558                this.props.languageChangeHandler(language, this.props.languageFilter);
559        },
560
561        setFilter: function(filter) {
562                this.props.languageChangeHandler(this.props.selectedLanguage, filter);
563        },
564
565        renderLanguageObject: function(lang) {
566                var desc = lang[1] + " [" + lang[0] + "]";
567                var style = {
568                        whiteSpace: "nowrap",
569                        fontWeight: lang[0] === this.props.selectedLanguage[0] ? "bold":"normal",
570                };
571                return  <div key={lang[0]}>
572                                        <a tabIndex="-1" href="#" style={style} onClick={this.selectLang.bind(this, lang)}>{desc}</a>
573                                </div>;
574        },
575
576        renderRadio: function(option) {
577                return  this.props.languageFilter === option ?
578                                <input type="radio" name="filterOpts" value={option} checked onChange={this.setFilter.bind(this, option)}/>
579                                : <input type="radio" name="filterOpts" value={option} onChange={this.setFilter.bind(this, option)} />;
580        },
581
582        render: function() {
583                var languages = _.pairs(this.props.languageMap)
584                                                 .sort(function(l1, l2){return l1[1].localeCompare(l2[1]); });
585                languages.unshift(this.props.anyLanguage);
586                languages = languages.map(this.renderLanguageObject);
587                var third = Math.round(languages.length/3);
588                var l1 = languages.slice(0, third);
589                var l2 = languages.slice(third, 2*third);
590                var l3 = languages.slice(2*third, languages.length);
591
592                return  <div>
593                                        <div className="row">
594                                                <div className="col-sm-4">{l1}</div>
595                                                <div className="col-sm-4">{l2}</div>
596                                                <div className="col-sm-4">{l3}</div>
597                                                <div className="col-sm-12" style={{marginTop:10, marginBottom:10, borderBottom:"1px solid #eee"}}/>
598                                        </div>
599                                        <form className="form" role="form">
600                                                <div className="input-group">
601                                                        <div>
602                                                        <label style={{color:'black'}}>
603                                                                { this.renderRadio('byMeta') }{" "}
604                                                                Use the collections{"'"} specified language to filter results
605                                                        </label>
606                                                        </div>
607                                                        <div>
608                                                        <label style={{color:'black'}}>
609                                                                { this.renderRadio('byGuess') }{" "}
610                                                                Filter results by using a language detector
611                                                        </label>
612                                                        </div>
613                                                        <div>
614                                                        <label style={{color:'black'}}>
615                                                                { this.renderRadio('byMetaAndGuess') }{" "}
616                                                                First use the collections{"'"} specified language then also use a language detector
617                                                        </label>
618                                                        </div>
619                                                </div>
620                                        </form>
621                                </div>;
622        }
623});
624
625/////////////////////////////////
626
627var ResultMixin = window.MyReact.ResultMixin = {
628        // getDefaultProps: function(){
629        //      return {hasPopover: true};
630        // },
631 
632        getInitialState: function () {
633                return {
634                        displayKwic: false,
635                };
636        },
637
638        toggleKwic: function() {
639                this.setState({displayKwic:!this.state.displayKwic});
640        },
641
642        renderPanelTitle: function(corpus) {
643                return  <div className='inline'>
644                                        <span className="corpusName"> {corpus.title}</span>
645                                        <span className="institutionName"> — {corpus.institution.name}</span>
646                                </div>;
647        },
648
649        renderRowLanguage: function(hit) {
650                return false; //<span style={{fontFace:"Courier",color:"black"}}>{hit.language} </span> ;
651        },
652
653        renderRowsAsHits: function(hit,i) {
654                function renderTextFragments(tf, idx) {
655                        return <span key={idx} className={tf.hit?"keyword":""}>{tf.text}</span>;
656                }
657                return  <p key={i} className="hitrow">
658                                        {this.renderRowLanguage(hit)}
659                                        {hit.fragments.map(renderTextFragments)}
660                                </p>;
661        },
662
663        renderRowsAsKwic: function(hit,i) {
664                var sleft={textAlign:"left", verticalAlign:"top", width:"50%"};
665                var scenter={textAlign:"center", verticalAlign:"top", maxWidth:"50%"};
666                var sright={textAlign:"right", verticalAlign:"top", maxWidth:"50%"};
667                return  <tr key={i} className="hitrow">
668                                        <td>{this.renderRowLanguage(hit)}</td>
669                                        <td style={sright}>{hit.left}</td>
670                                        <td style={scenter} className="keyword">{hit.keyword}</td>
671                                        <td style={sleft}>{hit.right}</td>
672                                </tr>;
673        },
674
675        renderDiagnostic: function(d, key) {
676                if (d.uri === NO_MORE_RECORDS_DIAGNOSTIC_URI) {
677                        return false;
678                }
679                return  <div className="alert alert-warning" key={key}>
680                                        <div>Diagnostic: {d.message}</div>
681                                </div>;
682        },
683
684        renderDiagnostics: function(corpusHit) {
685                if (!corpusHit.diagnostics || corpusHit.diagnostics.length === 0) {
686                        return false;
687                }
688                return corpusHit.diagnostics.map(this.renderDiagnostic);
689        },
690
691        renderErrors: function(corpusHit) {
692                var xc = corpusHit.exception;
693                if (!xc) {
694                        return false;
695                }
696                return  (
697                        <div className="alert alert-danger" role="alert">
698                                <div>Exception: {xc.message}</div>
699                                { xc.cause ? <div>Caused by: {xc.cause}</div> : false}
700                        </div>
701                );
702        },
703
704        renderPanelBody: function(corpusHit) {
705                var fulllength = {width:"100%"};
706                if (this.state.displayKwic) {
707                        return  <div>
708                                                {this.renderErrors(corpusHit)}
709                                                {this.renderDiagnostics(corpusHit)}
710                                                <table className="table table-condensed table-hover" style={fulllength}>
711                                                        <tbody>{corpusHit.kwics.map(this.renderRowsAsKwic)}</tbody>
712                                                </table>
713                                        </div>;
714                } else {
715                        return  <div>
716                                                {this.renderErrors(corpusHit)}
717                                                {this.renderDiagnostics(corpusHit)}
718                                                {corpusHit.kwics.map(this.renderRowsAsHits)}
719                                        </div>;
720                }
721        },
722
723        renderDisplayKWIC: function() {
724                return  <div className="inline btn-group" style={{display:"inline-block"}}>
725                                        <label forHtml="inputKwic" className="btn btn-flat">
726                                                { this.state.displayKwic ?
727                                                        <input id="inputKwic" type="checkbox" value="kwic" checked onChange={this.toggleKwic} /> :
728                                                        <input id="inputKwic" type="checkbox" value="kwic" onChange={this.toggleKwic} />
729                                                }
730                                                &nbsp;
731                                                Display as Key Word In Context
732                                        </label>
733                                </div>;
734        },
735
736        renderDownloadLinks: function(corpusId) {
737                return (
738                        <div className="dropdown">
739                                <button className="btn btn-flat" aria-expanded="false" data-toggle="dropdown">
740                                        <span className="glyphicon glyphicon-download-alt" aria-hidden="true"/>
741                                        {" "} Download {" "}
742                                        <span className="caret"/>
743                                </button>
744                                <ul className="dropdown-menu">
745                                        <li> <a href={this.props.getDownloadLink(corpusId, "csv")}>
746                                                        {" "} As CSV file</a></li>
747                                        <li> <a href={this.props.getDownloadLink(corpusId, "excel")}>
748                                                        {" "} As Excel file</a></li>
749                                        <li> <a href={this.props.getDownloadLink(corpusId, "tcf")}>
750                                                        {" "} As TCF file</a></li>
751                                        <li> <a href={this.props.getDownloadLink(corpusId, "text")}>
752                                                        {" "} As Plain Text file</a></li>
753                                </ul>
754                        </div>
755                );
756        },
757
758        renderToWeblichtLinks: function(corpusId, forceLanguage, error) {
759                return (
760                        <div className="dropdown">
761                                <button className="btn btn-flat" aria-expanded="false" data-toggle="dropdown">
762                                        <span className="glyphicon glyphicon-export" aria-hidden="true"/>
763                                        {" "} Use Weblicht {" "}
764                                        <span className="caret"/>
765                                </button>
766                                <ul className="dropdown-menu">
767                                        <li>
768                                                {error ?
769                                                        <div className="alert alert-danger" style={{margin:10, width:200}}>{error}</div> :
770                                                        <a href={this.props.getToWeblichtLink(corpusId, forceLanguage)} target="_blank">{" "}
771                                                                Send to Weblicht</a>
772                                                }
773                                        </li>
774                                </ul>
775                        </div>
776                );
777        },
778
779};
780
781var ZoomedResult = React.createClass({
782        propTypes: {
783                corpusHit: PT.object,
784                nextResults: PT.func.isRequired,
785                languageMap: PT.object.isRequired,
786                weblichtLanguages: PT.array.isRequired,
787                searchedLanguage: PT.array.isRequired,
788                getDownloadLink: PT.func.isRequired,
789                getToWeblichtLink: PT.func.isRequired,
790        },
791        mixins: [ResultMixin],
792
793        getInitialState: function() {
794                return {
795                        inProgress: false,
796                };
797        },
798
799        componentWillReceiveProps: function() {
800                this.setState({inProgress: false});
801        },
802
803        nextResults: function(e) {
804                this.setState({inProgress: true});
805                this.props.nextResults(this.props.corpusHit.corpus.id);
806        },
807
808        renderLanguages: function(languages) {
809                return languages
810                                .map(function(l) { return this.props.languageMap[l]; }.bind(this))
811                                .sort()
812                                .join(", ");
813        },
814
815        renderMoreResults:function(){
816                if (this.state.inProgress || this.props.corpusHit.inProgress)
817                        return <span style={{fontStyle:'italic'}}>Retrieving results, please wait...</span>;
818
819                var moreResults = true;
820                for (var i = 0; i < this.props.corpusHit.diagnostics.length; i++) {
821                        var d = this.props.corpusHit.diagnostics[i];
822                        if (d.uri === NO_MORE_RECORDS_DIAGNOSTIC_URI) {
823                                moreResults = false;
824                                break;
825                        }
826                }
827                if (!moreResults)
828                        return <span style={{fontStyle:'italic'}}>No other results available for this query</span>;
829                return  <button className="btn btn-default" onClick={this.nextResults}>
830                                        <span className="glyphicon glyphicon-option-horizontal" aria-hidden="true"/> More Results
831                                </button>;
832        },
833
834        render: function() {
835                var corpusHit = this.props.corpusHit;
836                if (!corpusHit) {
837                        return false;
838                }
839
840                var forceLanguage = null, wlerror = null;
841                if (this.props.weblichtLanguages.indexOf(this.props.searchedLanguage[0]) < 0) {
842                        // the search language is either AnyLanguage or unsupported
843                        if (this.props.searchedLanguage[0] === multipleLanguageCode) {
844                                if (corpusHit.corpus.languages && corpusHit.corpus.languages.length === 1) {
845                                        forceLanguage = corpusHit.corpus.languages[0];
846                                } else {
847                                        var langs = corpusHit.kwics.map(function(kwic) {return kwic.language;});
848                                        langs = _.uniq(langs.filter(function(l){ return l !== null; }));
849                                        console.log("languages:", langs);
850                                        if (langs.length === 1) {
851                                                forceLanguage = langs[0];
852                                        }
853                                }
854                        }
855                        if (!forceLanguage) {
856                                wlerror = "Cannot use WebLicht: unsupported language ("+this.props.searchedLanguage[1]+")";
857                        }
858                }
859                var corpus = corpusHit.corpus;
860                return  <div>
861                                        <ReactCSSTransitionGroup transitionName="fade">
862                                                <div className='corpusDescription'>
863                                                        <p><i className="fa fa-institution"/> {corpus.institution.name}</p>
864                                                        {corpus.description ?
865                                                                <p><i className="glyphicon glyphicon-info-sign"/> {corpus.description}</p>: false}
866                                                        <p><i className="fa fa-language"/> {this.renderLanguages(corpus.languages)}</p>
867                                                </div>
868                                                <div style={{marginBottom:2}}>
869                                                        <div className="float-right">
870                                                                <div>
871                                                                        { this.renderDisplayKWIC() }
872                                                                        <div className="inline"> {this.renderDownloadLinks(corpusHit.corpus.id)} </div>
873                                                                        <div className="inline"> {this.renderToWeblichtLinks(corpus.id, forceLanguage, wlerror)} </div>
874                                                                </div>
875                                                        </div>
876                                                        <div style={{clear:'both'}}/>
877                                                </div>
878                                                <div className="panel">
879                                                        <div className="panel-body corpusResults">{this.renderPanelBody(corpusHit)}</div>
880                                                </div>
881
882                                                <div style={{textAlign:'center', marginTop:10}}>
883                                                        { this.renderMoreResults() }
884                                                </div>
885
886                                        </ReactCSSTransitionGroup>
887                                </div>;
888        },
889});
890
891var Results = React.createClass({
892        propTypes: {
893                collhits: PT.object.isRequired,
894                searchedLanguage: PT.array.isRequired,
895                toggleResultModal: PT.func.isRequired,
896                getDownloadLink: PT.func.isRequired,
897                getToWeblichtLink: PT.func.isRequired,
898        },
899        mixins: [ResultMixin],
900
901        renderPanelInfo: function(corpusHit) {
902                var corpus = corpusHit.corpus;
903                var inline = {display:"inline-block"};
904                return  <div>
905                                        {" "}
906                                        <div style={inline}>
907                                                <button className="btn btn-default zoomResultButton"
908                                                                onClick={function(e){this.props.toggleResultModal(e,corpusHit)}.bind(this)}>
909                                                                <span className="glyphicon glyphicon-eye-open"/> View
910                                                </button>
911                                        </div>
912                                </div>;
913        },
914
915        renderResultPanel: function(corpusHit) {
916                if (corpusHit.kwics.length === 0 &&
917                        !corpusHit.exception &&
918                        corpusHit.diagnostics.length === 0) {
919                                return false;
920                }
921                return  <Panel key={corpusHit.corpus.id}
922                                                title={this.renderPanelTitle(corpusHit.corpus)}
923                                                info={this.renderPanelInfo(corpusHit)}>
924                                        {this.renderPanelBody(corpusHit)}
925                                </Panel>;
926        },
927
928        renderProgressMessage: function() {
929                var collhits = this.props.collhits;
930                var done = collhits.results.length - collhits.inProgress;
931                var msg = collhits.hits + " matching collections found in " + done + " searched collections";
932                var percents = Math.round(100 * collhits.hits / collhits.results.length);
933                var styleperc = {width: percents+"%"};
934                return  <div style={{marginTop:10}}>
935                                        <div>{msg}</div>
936                                        {collhits.inProgress > 0 ?
937                                                <div className="progress" style={{marginBottom:10}}>
938                                                        <div className="progress-bar progress-bar-striped active" role="progressbar"
939                                                                aria-valuenow={percents} aria-valuemin="0" aria-valuemax="100" style={styleperc} />
940                                                        {percents > 2 ? false :
941                                                                <div className="progress-bar progress-bar-striped active" role="progressbar"
942                                                                        aria-valuenow='100' aria-valuemin="0" aria-valuemax="100"
943                                                                        style={{width: '100%', backgroundColor:'#888'}} />
944                                                        }
945                                                </div> :
946                                                false}
947                                </div>;
948        },
949
950        render: function() {
951                var collhits = this.props.collhits;
952                if (!collhits.results) {
953                        return false;
954                }
955                var showprogress = collhits.inProgress > 0;
956                return  <div>
957                                        <ReactCSSTransitionGroup transitionName="fade">
958                                                { showprogress ? this.renderProgressMessage() : <div style={{height:20}} />}
959                                                <div style={{marginBottom:2}}>
960                                                        { showprogress ? false :
961                                                                <div className="float-left"> {collhits.hits + " matching collections found"} </div>
962                                                        }
963                                                        { collhits.hits === 0 ? false :
964                                                                <div className="float-right">
965                                                                        <div>
966                                                                                { this.renderDisplayKWIC() }
967                                                                                { collhits.inProgress === 0 ?
968                                                                                        <div className="inline"> {this.renderDownloadLinks()} </div>
969                                                                                        :false
970                                                                                }
971                                                                        </div>
972                                                                </div>
973                                                        }
974                                                        <div style={{clear:'both'}}/>
975                                                </div>
976                                                {collhits.results.map(this.renderResultPanel)}
977                                        </ReactCSSTransitionGroup>
978                                </div>;
979        }
980});
981
982var _ = window._ = window._ || {
983        keys: function() {
984                var ret = [];
985                for (var x in o) {
986                        if (o.hasOwnProperty(x)) {
987                                ret.push(x);
988                        }
989                }
990                return ret;
991        },
992
993        pairs: function(o){
994                var ret = [];
995                for (var x in o) {
996                        if (o.hasOwnProperty(x)) {
997                                ret.push([x, o[x]]);
998                        }
999                }
1000                return ret;
1001        },
1002
1003        values: function(o){
1004                var ret = [];
1005                for (var x in o) {
1006                        if (o.hasOwnProperty(x)) {
1007                                ret.push(o[x]);
1008                        }
1009                }
1010                return ret;
1011        },
1012
1013        uniq: function(a) {
1014                var r = [];
1015                for (var i = 0; i < a.length; i++) {
1016                        if (r.indexOf(a[i]) < 0) {
1017                                r.push(a[i]);
1018                        }
1019                }
1020                return r;
1021        },
1022};
1023
1024})();
Note: See TracBrowser for help on using the repository browser.