1 | /** @jsx React.DOM */ |
---|
2 | (function() { |
---|
3 | "use strict"; |
---|
4 | |
---|
5 | window.MyAggregator = window.MyAggregator || {}; |
---|
6 | |
---|
7 | var PT = React.PropTypes; |
---|
8 | var ReactCSSTransitionGroup = window.React.addons.CSSTransitionGroup; |
---|
9 | // own components |
---|
10 | var Panel = window.MyReact.Panel; |
---|
11 | |
---|
12 | |
---|
13 | ///////////////////////////////// |
---|
14 | |
---|
15 | var SearchCorpusBox = React.createClass({ |
---|
16 | propTypes: { |
---|
17 | search: PT.func.isRequired, |
---|
18 | }, |
---|
19 | |
---|
20 | getInitialState: function () { |
---|
21 | return { |
---|
22 | query: "" |
---|
23 | }; |
---|
24 | }, |
---|
25 | |
---|
26 | handleChange: function(event) { |
---|
27 | var query = event.target.value; |
---|
28 | this.setState({query: query}); |
---|
29 | |
---|
30 | if (query.length === 0 || 2 <= query.length) { |
---|
31 | this.props.search(query); |
---|
32 | } |
---|
33 | event.stopPropagation(); |
---|
34 | }, |
---|
35 | |
---|
36 | handleKey: function(event) { |
---|
37 | if (event.keyCode==13) { |
---|
38 | this.props.search(event.target.value); |
---|
39 | } |
---|
40 | }, |
---|
41 | |
---|
42 | render: function() { |
---|
43 | return <div className="form-group"> |
---|
44 | <input className="form-control search search-collection" type="text" |
---|
45 | value={this.state.query} placeholder="Search for collection" |
---|
46 | onChange={this.handleChange} /> |
---|
47 | </div>; |
---|
48 | } |
---|
49 | }); |
---|
50 | |
---|
51 | var CorpusView = window.MyAggregator.CorpusView = React.createClass({ |
---|
52 | propTypes: { |
---|
53 | corpora: PT.object.isRequired, |
---|
54 | languageMap: PT.object.isRequired, |
---|
55 | }, |
---|
56 | |
---|
57 | toggleSelection: function (corpus, e) { |
---|
58 | var s = !corpus.selected; |
---|
59 | this.props.corpora.recurseCorpus(corpus, function(c) { c.selected = s; }); |
---|
60 | this.props.corpora.update(); |
---|
61 | this.stop(e); |
---|
62 | }, |
---|
63 | |
---|
64 | toggleExpansion: function (corpus) { |
---|
65 | corpus.expanded = !corpus.expanded; |
---|
66 | this.props.corpora.update(); |
---|
67 | }, |
---|
68 | |
---|
69 | selectAll: function(value) { |
---|
70 | this.props.corpora.recurse(function(c) { c.selected = value; }); |
---|
71 | this.props.corpora.update(); |
---|
72 | }, |
---|
73 | |
---|
74 | searchCorpus: function(query) { |
---|
75 | // sort fn: descending priority, stable sort |
---|
76 | var sortFn = function(a, b){ |
---|
77 | if (b.priority === a.priority) { |
---|
78 | return b.index - a.index; // stable sort |
---|
79 | } |
---|
80 | return b.priority - a.priority; |
---|
81 | }; |
---|
82 | |
---|
83 | query = query.toLowerCase(); |
---|
84 | if (!query) { |
---|
85 | this.props.corpora.recurse(function(corpus) {corpus.priority = 1; }); |
---|
86 | this.props.corpora.update(); |
---|
87 | return; |
---|
88 | } |
---|
89 | |
---|
90 | // clean up all priorities |
---|
91 | this.props.corpora.recurse(function(corpus) { |
---|
92 | corpus.priority = 0; |
---|
93 | }); |
---|
94 | |
---|
95 | // find priority for each corpus |
---|
96 | var querytokens = query.split(" ").filter(function(x){ return x.length > 0; }); |
---|
97 | this.props.corpora.recurse(function(corpus){ |
---|
98 | var title = corpus.title; |
---|
99 | querytokens.forEach(function(qtoken){ |
---|
100 | if (title && title.toLowerCase().indexOf(qtoken) >= 0) { |
---|
101 | corpus.priority ++; |
---|
102 | } |
---|
103 | if (corpus.description && corpus.description.toLowerCase().indexOf(qtoken) >= 0) { |
---|
104 | corpus.priority ++; |
---|
105 | } |
---|
106 | if (corpus.institution && corpus.institution.name && |
---|
107 | corpus.institution.name.toLowerCase().indexOf(qtoken) >= 0) { |
---|
108 | corpus.priority ++; |
---|
109 | } |
---|
110 | if (corpus.languages){ |
---|
111 | corpus.languages.forEach(function(lang){ |
---|
112 | if (lang.toLowerCase().indexOf(qtoken) >= 0){ |
---|
113 | corpus.priority ++; |
---|
114 | } |
---|
115 | }); |
---|
116 | corpus.languages.forEach(function(lang){ |
---|
117 | if (this.props.languageMap[lang].toLowerCase().indexOf(qtoken) >= 0){ |
---|
118 | corpus.priority ++; |
---|
119 | } |
---|
120 | }.bind(this)); |
---|
121 | } |
---|
122 | }.bind(this)); |
---|
123 | }.bind(this)); |
---|
124 | |
---|
125 | // ensure parents of visible corpora are also visible; maximum depth = 3 |
---|
126 | var isVisibleFn = function(corpus){ return corpus.priority > 0; }; |
---|
127 | var parentBooster = function(corpus){ |
---|
128 | if (corpus.priority <= 0 && corpus.subCorpora) { |
---|
129 | if (corpus.subCorpora.some(isVisibleFn)) { |
---|
130 | corpus.priority = 0.5; |
---|
131 | } |
---|
132 | } |
---|
133 | }; |
---|
134 | for (var i = 3; i > 0; i --) { |
---|
135 | this.props.corpora.recurse(parentBooster); |
---|
136 | } |
---|
137 | |
---|
138 | this.props.corpora.recurse(function(corpus) { corpus.subCorpora.sort(sortFn); }); |
---|
139 | this.props.corpora.corpora.sort(sortFn); |
---|
140 | |
---|
141 | // display |
---|
142 | this.props.corpora.update(); |
---|
143 | }, |
---|
144 | |
---|
145 | stop: function(e) { |
---|
146 | e.stopPropagation(); |
---|
147 | }, |
---|
148 | |
---|
149 | getMinMaxPriority: function() { |
---|
150 | var min = 1, max = 0; |
---|
151 | this.props.corpora.recurse(function(c) { |
---|
152 | if (c.priority < min) min = c.priority; |
---|
153 | if (max < c.priority) max = c.priority; |
---|
154 | }); |
---|
155 | return [min, max]; |
---|
156 | }, |
---|
157 | |
---|
158 | renderCheckbox: function(corpus) { |
---|
159 | return <button className="btn btn-default"> |
---|
160 | { corpus.selected ? |
---|
161 | <span className="glyphicon glyphicon-check" aria-hidden="true"/> : |
---|
162 | <span className="glyphicon glyphicon-unchecked" aria-hidden="true"/> |
---|
163 | } |
---|
164 | </button>; |
---|
165 | }, |
---|
166 | |
---|
167 | renderExpansion: function(corpus) { |
---|
168 | if (!corpus.subCorpora || corpus.subCorpora.length === 0) { |
---|
169 | return false; |
---|
170 | } |
---|
171 | return <div className="expansion-handle" style={{}}> |
---|
172 | <a> |
---|
173 | {corpus.expanded ? |
---|
174 | <span className="glyphicon glyphicon-minus" aria-hidden="true"/>: |
---|
175 | <span className="glyphicon glyphicon-plus" aria-hidden="true"/> |
---|
176 | } |
---|
177 | {corpus.expanded ? " Collapse ":" Expand "} ({corpus.subCorpora.length} subcollections) |
---|
178 | </a> |
---|
179 | </div>; |
---|
180 | }, |
---|
181 | |
---|
182 | renderLanguages: function(languages) { |
---|
183 | return languages |
---|
184 | .map(function(l) { return this.props.languageMap[l]; }.bind(this)) |
---|
185 | .sort() |
---|
186 | .join(", "); |
---|
187 | }, |
---|
188 | |
---|
189 | renderFilteredMessage: function() { |
---|
190 | var total = 0; |
---|
191 | var visible = 0; |
---|
192 | this.props.corpora.recurse(function(corpus){ |
---|
193 | if (corpus.visible) { |
---|
194 | total ++; |
---|
195 | if (corpus.priority > 0) { |
---|
196 | visible++; |
---|
197 | } |
---|
198 | } |
---|
199 | }); |
---|
200 | if (visible === total) { |
---|
201 | return false; |
---|
202 | } |
---|
203 | return <div> Showing {visible} out of {total} (sub)collections. </div>; |
---|
204 | }, |
---|
205 | |
---|
206 | renderCorpus: function(level, minmaxp, corpus) { |
---|
207 | if (!corpus.visible || corpus.priority <= 0) { |
---|
208 | return false; |
---|
209 | } |
---|
210 | |
---|
211 | var indent = {marginLeft:level*50}; |
---|
212 | var corpusContainerClass = "corpus-container "+(corpus.priority>0?"":"dimmed"); |
---|
213 | |
---|
214 | var hue = 120 * corpus.priority / minmaxp[1]; |
---|
215 | var color = minmaxp[0] === minmaxp[1] ? 'transparent' : 'hsl('+hue+', 50%, 50%)'; |
---|
216 | var priorityStyle = {paddingBottom: 4, paddingLeft: 2, borderBottom: '3px solid '+color }; |
---|
217 | var expansive = corpus.expanded ? {overflow:'hidden'} |
---|
218 | : {whiteSpace:'nowrap', overflow:'hidden', textOverflow: 'ellipsis'}; |
---|
219 | return <div className={corpusContainerClass} key={corpus.title}> |
---|
220 | <div className="row corpus" onClick={this.toggleExpansion.bind(this, corpus)}> |
---|
221 | <div className="col-sm-1 vcenter"> |
---|
222 | <div className="inline" style={priorityStyle} onClick={this.toggleSelection.bind(this,corpus)}> |
---|
223 | {this.renderCheckbox(corpus)} |
---|
224 | </div> |
---|
225 | </div> |
---|
226 | <div className="col-sm-8 vcenter"> |
---|
227 | <div style={indent}> |
---|
228 | <h3 style={expansive}> |
---|
229 | {corpus.title} |
---|
230 | { corpus.landingPage ? |
---|
231 | <a href={corpus.landingPage} onClick={this.stop}> |
---|
232 | <span style={{fontSize:12}}> â Homepage </span><i className="fa fa-home"/> |
---|
233 | </a>: false} |
---|
234 | </h3> |
---|
235 | |
---|
236 | |
---|
237 | <p style={expansive}>{corpus.description}</p> |
---|
238 | {this.renderExpansion(corpus)} |
---|
239 | </div> |
---|
240 | </div> |
---|
241 | <div className="col-sm-3 vcenter"> |
---|
242 | <p style={expansive}> |
---|
243 | <i className="fa fa-institution"/> {corpus.institution.name} |
---|
244 | </p> |
---|
245 | <p style={expansive}> |
---|
246 | <i className="fa fa-language"/> {this.renderLanguages(corpus.languages)} |
---|
247 | </p> |
---|
248 | </div> |
---|
249 | </div> |
---|
250 | {corpus.expanded ? corpus.subCorpora.map(this.renderCorpus.bind(this, level+1, minmaxp)) : false} |
---|
251 | </div>; |
---|
252 | }, |
---|
253 | |
---|
254 | render: function() { |
---|
255 | var minmaxp = this.getMinMaxPriority(); |
---|
256 | return <div style={{margin: "0 30px"}}> |
---|
257 | <div className="row"> |
---|
258 | <div className="float-left inline"> |
---|
259 | <h3 style={{marginTop:10}}> |
---|
260 | {this.props.corpora.getSelectedMessage()} |
---|
261 | </h3> |
---|
262 | </div> |
---|
263 | <div className="float-right inline"> |
---|
264 | <button className="btn btn-default" style={{ marginRight: 10 }} onClick={this.selectAll.bind(this,true)}> |
---|
265 | {" Select all"}</button> |
---|
266 | <button className="btn btn-default" style={{ marginRight: 20 }} onClick={this.selectAll.bind(this,false)}> |
---|
267 | {" Deselect all"}</button> |
---|
268 | </div> |
---|
269 | <div className="float-right inline"> |
---|
270 | <div className="inline" style={{ marginRight: 20 }} > |
---|
271 | <SearchCorpusBox search={this.searchCorpus}/> |
---|
272 | {this.renderFilteredMessage()} |
---|
273 | </div> |
---|
274 | </div> |
---|
275 | </div> |
---|
276 | |
---|
277 | {this.props.corpora.corpora.map(this.renderCorpus.bind(this, 0, minmaxp))} |
---|
278 | </div>; |
---|
279 | } |
---|
280 | }); |
---|
281 | |
---|
282 | })(); |
---|