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

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