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

Last change on this file since 7034 was 7034, checked in by Leif-Jöran, 8 years ago

Added fully visible but unstyled ADV view. Very similar to the spreadsheet export. Updated some of the paragraphs on the help page. Preparing for new version number.

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