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