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

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