72920506 by Adam Heath

Split files.

1 parent bbb7f7fb
1 {
2 "directory": "src/lib/bower"
3 }
...@@ -11,7 +11,7 @@ module.exports = function(grunt) { ...@@ -11,7 +11,7 @@ module.exports = function(grunt) {
11 browserOptions: { 11 browserOptions: {
12 }, 12 },
13 model: [ 13 model: [
14 'src/scripts/solr-search/model/**/*.js', 14 'src/scripts/solr/model/**/*.js',
15 ], 15 ],
16 }; 16 };
17 /* 17 /*
......
...@@ -3,1133 +3,588 @@ define(function(require) { ...@@ -3,1133 +3,588 @@ define(function(require) {
3 var _ = require('underscore'); 3 var _ = require('underscore');
4 var Backbone = require('backbone'); 4 var Backbone = require('backbone');
5 var NestedModels = require('backbone-nested-models'); 5 var NestedModels = require('backbone-nested-models');
6 //var module = require('module');
7 6
8 function getField(obj, key) { 7 function getField(obj, key) {
9 return obj[key]; 8 return obj[key];
10 } 9 }
11 10
12 function mergeStaticProps(startPtr, endPtr, obj, fieldName) {
13 var result = obj;
14 var ptr = startPtr;
15 while (true) {
16 result = _.extend(result, _.result(ptr, fieldName));
17 if (ptr === endPtr) {
18 break;
19 }
20 ptr = ptr.__super__.constructor;
21 }
22 return result;
23 }
24
25 function mergeStaticSets(startPtr, endPtr, obj, fieldName) {
26 function arrayToMap(array) {
27 var result = {};
28 _.each(array, function(value) {
29 result[value] = true;
30 });
31 return result;
32 }
33 var set = arrayToMap(obj);
34 var ptr = startPtr;
35 while (true) {
36 set = _.extend(set, arrayToMap(_.result(ptr, fieldName)));
37 if (ptr === endPtr) {
38 break;
39 }
40 ptr = ptr.__super__.constructor;
41 }
42 return _.keys(set);
43 }
44
45 function getItemKeyAccessor(item) { 11 function getItemKeyAccessor(item) {
46 return item.get('key'); 12 return item.get('key');
47 } 13 }
48 var SolrFacets = (function() { 14
49 var Facets = NestedModels.mixin(Backbone.Model.extend({ 15 var Facets = NestedModels.mixin(Backbone.Model.extend({
50 initialize: function(data, options) { 16 initialize: function(data, options) {
51 this.url = function() { 17 this.url = function() {
52 return options.search.url(); 18 return options.search.url();
53 }; 19 };
54 _.each(this.keys(), _.bind(function(facetName) { 20 _.each(this.keys(), _.bind(function(facetName) {
55 var facet = this.get(facetName); 21 var facet = this.get(facetName);
56 _.each({ 22 _.each({
57 'item-change': 'item-change', 23 'item-change': 'item-change',
58 'change:query': 'child-query', 24 'change:query': 'child-query',
59 'change:queryMin': 'item-change', 25 'change:queryMin': 'item-change',
60 'change:queryMax': 'item-change', 26 'change:queryMax': 'item-change',
61 }, function(eventValue, eventKey) { 27 }, function(eventValue, eventKey) {
62 facet.on(eventKey, function(model, value, options) { 28 facet.on(eventKey, function(model, value, options) {
63 this.trigger(eventValue, facet, null, options); 29 this.trigger(eventValue, facet, null, options);
64 }, this);
65 }, this); 30 }, this);
66 }, this)); 31 }, this);
32 }, this));
67 33
68 this._doSuggestions = _.debounce(_.bind(function(childFacet) { 34 this._doSuggestions = _.debounce(_.bind(function(childFacet) {
69 if (childFacet.get('query')) { 35 if (childFacet.get('query')) {
70 this.fetch({ 36 this.fetch({
71 data: this.toJSON({childFacet: childFacet, facetSuggestions: true}), 37 data: this.toJSON({childFacet: childFacet, facetSuggestions: true}),
72 childFacet: childFacet, 38 childFacet: childFacet,
73 facetSuggestions: true, 39 facetSuggestions: true,
74 merge: true, 40 merge: true,
75 traditional: true, 41 traditional: true,
76 }); 42 });
77 } else { 43 } else {
78 childFacet.set('suggestions', []); 44 childFacet.set('suggestions', []);
79 } 45 }
80 }, this), 250); 46 }, this), 250);
81 this.on('child-query', this._doSuggestions, this); 47 this.on('child-query', this._doSuggestions, this);
82 48
83 return Facets.__super__.initialize.apply(this, arguments); 49 return Facets.__super__.initialize.apply(this, arguments);
84 }, 50 },
85 resetSearch: function(options) { 51 resetSearch: function(options) {
86 _.each(this.values(), function(facet) { 52 _.each(this.values(), function(facet) {
87 facet.get('items').reset(null, options); 53 facet.get('items').reset(null, options);
88 }); 54 });
89 }, 55 },
90 applyFacetResults: function(data, options) { 56 applyFacetResults: function(data, options) {
91 options = options || {}; 57 options = options || {};
92 var facetCounts = getField(data, 'facet_counts'); 58 var facetCounts = getField(data, 'facet_counts');
93 var facetIntervals = getField(facetCounts, 'facet_intervals'); 59 var facetIntervals = getField(facetCounts, 'facet_intervals');
94 var facetRanges = getField(facetCounts, 'facet_ranges'); 60 var facetRanges = getField(facetCounts, 'facet_ranges');
95 var facetFields = getField(facetCounts, 'facet_fields'); 61 var facetFields = getField(facetCounts, 'facet_fields');
96 var facetQueries = getField(facetCounts, 'facet_queries'); 62 var facetQueries = getField(facetCounts, 'facet_queries');
97 var statsFields = getField(data.stats, 'stats_fields'); 63 var statsFields = getField(data.stats, 'stats_fields');
98 function getFacetList(facetName, type) { 64 function getFacetList(facetName, type) {
99 switch (type) { 65 switch (type) {
100 case 'year': 66 case 'year':
101 case 'range': 67 case 'range':
102 return facetRanges[facetName] ? facetRanges[facetName].counts : null; 68 return facetRanges[facetName] ? facetRanges[facetName].counts : null;
103 case 'interval': 69 case 'interval':
104 return facetIntervals[facetName] ? facetIntervals[facetName].counts : null; 70 return facetIntervals[facetName] ? facetIntervals[facetName].counts : null;
105 case 'year-field': 71 case 'year-field':
106 case 'field': 72 case 'field':
107 return facetFields[facetName]; 73 return facetFields[facetName];
108 }
109 } 74 }
110 _.each(this.keys(), _.bind(function(facetName) { 75 }
111 var facet = this.get(facetName); 76 _.each(this.keys(), _.bind(function(facetName) {
112 var type = facet.facetType; 77 var facet = this.get(facetName);
78 var type = facet.facetType;
113 79
114 var suggestions = []; 80 var suggestions = [];
115 _.each(getFacetList('suggestions:' + facetName, type), function(value, index) { 81 _.each(getFacetList('suggestions:' + facetName, type), function(value, index) {
116 if (index % 2 === 0) { 82 if (index % 2 === 0) {
117 key = value; 83 key = value;
118 } else if (value > 0) { 84 } else if (value > 0) {
119 suggestions.push({key: key, value: value}); 85 suggestions.push({key: key, value: value});
120 }
121 });
122 facet.set('suggestions', suggestions);
123 if (!!options.facetSuggestions && options.childFacet === facet) {
124 return;
125 } 86 }
87 });
88 facet.set('suggestions', suggestions);
89 if (!!options.facetSuggestions && options.childFacet === facet) {
90 return;
91 }
126 92
127 var key; 93 var key;
128 var list = getFacetList(facetName, type); 94 var list = getFacetList(facetName, type);
129 var newItems = []; 95 var newItems = [];
130 var items = facet.get('items'); 96 var items = facet.get('items');
131 items.invoke('set', 'hidden', true); 97 items.invoke('set', 'hidden', true);
132 var valueOverrides = {}; 98 var valueOverrides = {};
133 var excludeValues = {}; 99 var excludeValues = {};
134 function recordIncludeValueIntoExclude(includeValue, key) { 100 function recordIncludeValueIntoExclude(includeValue, key) {
135 excludeValues[includeValue] = true; 101 excludeValues[includeValue] = true;
136 } 102 }
137 switch (type) { 103 switch (type) {
138 case 'year-field': 104 case 'year-field':
139 case 'field': 105 case 'field':
140 _.each(facet.bins, function(includeValues, key) { 106 _.each(facet.bins, function(includeValues, key) {
141 var tag = facetName + ':' + key; 107 var tag = facetName + ':' + key;
142 valueOverrides[key] = facetQueries[tag]; 108 valueOverrides[key] = facetQueries[tag];
143 _.each(includeValues, recordIncludeValueIntoExclude); 109 _.each(includeValues, recordIncludeValueIntoExclude);
144 }); 110 });
145 break; 111 break;
146 } 112 }
147 var addNewItem = _.bind(function addNewItem(key, value) { 113 var addNewItem = _.bind(function addNewItem(key, value) {
148 if (valueOverrides[key] !== undefined) { 114 if (valueOverrides[key] !== undefined) {
149 value = valueOverrides[key]; 115 value = valueOverrides[key];
150 if (!value) { 116 if (!value) {
151 return;
152 }
153 } else if (excludeValues[key]) {
154 return; 117 return;
155 } 118 }
156 var item = items.get({id: key}); 119 } else if (excludeValues[key]) {
157 if (item) { 120 return;
158 item.set({hidden: value === 0, value: value});
159 } else {
160 item = new Item({key: key, value: value});
161 item.on('change:checked', function() {
162 this.trigger('item-change');
163 facet.trigger('item-change');
164 }, this);
165 }
166 newItems.push(item);
167 }, this);
168
169 function checkOther(set, key) {
170 if (set.indexOf(facet.other) !== -1) {
171 addNewItem(key, facetRanges[facetName][key]);
172 }
173 }
174 checkOther(['all', 'before'], 'before');
175
176 _.each(list, function(value, index) {
177 if (index % 2 === 0) {
178 key = value;
179 } else if (value > 0) {
180 addNewItem(key, value);
181 }
182 });
183 switch (type) {
184 case 'year-field':
185 case 'field':
186 _.each(facet.bins, function(includeValues, key) {
187 var tag = facetName + ':' + key;
188 addNewItem(key, facetQueries[tag]);
189 });
190 break;
191 }
192 items.set(newItems, {remove: false});
193 if (facet.facetStats) {
194 var thisFacetStats = statsFields[facetName];
195 facet.set({minValue: thisFacetStats.min, maxValue: thisFacetStats.max});
196 }
197 }, this));
198 },
199 getFacetFormData: function(options) {
200 options = options || {};
201 var result = {
202 facet: true,
203 stats: true,
204 'facet.missing': true,
205 'facet.field': [],
206 'facet.range': [],
207 'facet.interval': [],
208 'facet.query': [],
209 'stats.field': [],
210 fq: [],
211 };
212 _.each(this.keys(), _.bind(function(facetName) {
213 var facet = this.get(facetName);
214 var queryField = facet.queryField;
215 var facetField = facet.facetField ? facet.facetField : queryField;
216 var type = facet.facetType;
217 var valueFormatter;
218 var facetOptions = {};
219 var quoteFormatter = function(value) {
220 return value.replace ? ('"' + value.replace(/"/, '\\"') + '"') : value;
221 };
222 var rangeFormatter = function(from, to) {
223 return '[' + quoteFormatter(from) + ' TO ' + quoteFormatter(to) + ']';
224 };
225 switch (type) {
226 case 'year':
227 type = 'range';
228 quoteFormatter = function(value) {
229 return value.toISOString();
230 };
231 valueFormatter = function(item) {
232 var key = item.get('key');
233 var fromDate, toDate;
234 if (key === 'before') {
235 return rangeFormatter('*', facet.rangeStart + '-1SECOND');
236 }
237 fromDate = new Date(key);
238 toDate = new Date(key);
239 toDate.setUTCFullYear(toDate.getUTCFullYear() + 1);
240 return rangeFormatter(fromDate, toDate);
241 };
242 if (facet.other) {
243 facetOptions['facet.range.other'] = facet.other;
244 }
245 break;
246 case 'range':
247 case 'interval':
248 valueFormatter = function(item) {
249 var key = parseInt(item.get('key'));
250 return rangeFormatter(key, key + facet.rangeGap);
251 };
252 break;
253 case 'year-field':
254 type = 'field';
255 quoteFormatter = function(value) {
256 return new Date(value).toISOString();
257 };
258 valueFormatter = facet.queryValue ? facet.queryValue : function(value) {
259 return quoteFormatter(getItemKeyAccessor(value));
260 };
261 _.each(facet.bins, function(includeValues, key) {
262 var query = _.map(includeValues, function(includeValue, index) {
263 return queryField + ':\'' + includeValue + '\'';
264 }).join(' OR ');
265 result['facet.query'].push('{!key=' + facetName + ':' + key + ' tag=' + facetName + ':' + key + '}(' + query + ')');
266 });
267 break;
268 case 'field':
269 valueFormatter = facet.queryValue ? facet.queryValue : function(value) {
270 return quoteFormatter(getItemKeyAccessor(value));
271 };
272 _.each(facet.bins, function(includeValues, key) {
273 var query = _.map(includeValues, function(includeValue, index) {
274 return queryField + ':' + includeValue;
275 }).join(' OR ');
276 result['facet.query'].push('{!key=' + facetName + ':' + key + ' tag=' + facetName + ':' + key + '}(' + query + ')');
277 });
278 break;
279 }
280 switch (type) {
281 case 'range':
282 facetOptions['facet.range.start'] = facet.rangeStart;
283 facetOptions['facet.range.end'] = facet.rangeEnd;
284 facetOptions['facet.range.gap'] = facet.rangeGap;
285 break;
286 case 'interval':
287 facetOptions['facet.interval.set'] = '[*,*]';
288 break;
289 }
290 var facetValues = [];
291 var queryMax = facet.get('queryMax');
292 var queryMin = facet.get('queryMin');
293 if (queryMax !== undefined && queryMin !== undefined) {
294 facetValues.push(rangeFormatter(queryMin, queryMax));
295 } 121 }
296 facet.get('items').each(function(item, index) { 122 var item = items.get({id: key});
297 if (!item.get('hidden') && item.get('checked')) { 123 if (item) {
298 facetValues.push(valueFormatter(item)); 124 item.set({hidden: value === 0, value: value});
299 }
300 });
301 facetOptions.key = facetName;
302 if (facetValues.length) {
303 facetOptions.ex = facetName;
304 result.fq.push('{!tag=' + facetName + '}' + facetField + ':(' + facetValues.join(' OR ') + ')');
305 } else { 125 } else {
306 facetOptions.tag = facetName; 126 item = new Item({key: key, value: value});
307 } 127 item.on('change:checked', function() {
308 if (!!options.facetSuggestions) { 128 this.trigger('item-change');
309 var childFacet = options.childFacet; 129 facet.trigger('item-change');
310 if (childFacet === facet) { 130 }, this);
311 facetOptions['facet.contains'] = facet.get('query');
312 facetOptions['facet.contains.ignoreCase'] = true;
313 facetOptions['facet.mincount'] = 1;
314 }
315 }
316 var facetOptionList = [];
317 _.each(facetOptions, function(value, key) {
318 if (value !== undefined) {
319 facetOptionList.push(key + '=' + value);
320 }
321 });
322 result['facet.' + type].push('{!' + facetOptionList.join(' ') + '}' + queryField);
323 if (facet.facetStats) {
324 result['stats.field'].push('{!' + facetOptionList.join(' ') + '}' + queryField);
325 }
326 var facetQuery = facet.get('query');
327 if (facetQuery !== null && facetQuery !== '') {
328 facetOptions.key = 'suggestions:' + facetName;
329 facetOptionList = [];
330 _.each(facetOptions, function(value, key) {
331 if (value !== undefined) {
332 facetOptionList.push(key + '=' + value);
333 }
334 });
335 result['facet.' + type].push('{!' + facetOptionList.join(' ') + '}' + queryField);
336 } 131 }
337 }, this)); 132 newItems.push(item);
338 return result; 133 }, this);
339 },
340
341
342 134
343 toJSON: function toJSON(options) { 135 function checkOther(set, key) {
344 if (!!options.facetSuggestions) { 136 if (set.indexOf(facet.other) !== -1) {
345 return _.extend({ 137 addNewItem(key, facetRanges[facetName][key]);
346 rows: 0,
347 facet: true,
348 wt: 'json',
349 q: '*:*',
350 }, this.getFacetFormData(options));
351 }
352 },
353 parse: function parse(data, options) {
354 if (!!options.facetSuggestions) {
355 this.applyFacetResults(data, options);
356 }
357 return null;
358 },
359 }));
360 var Sort = Facets.Sort = {
361 standard: function standardSort(a, b) {
362 if (a < b) {
363 return -1;
364 } else if (a > b) {
365 return 1;
366 } else {
367 return 0;
368 }
369 },
370 date: function dateSort(a, b) {
371 return Sort.standard.call(this, a, b);
372 },
373 number: function numberSort(a, b) {
374 return Sort.standard.call(this, parseInt(a), parseInt(b));
375 },
376 };
377 function wrapComparatorForAllBeforeAfter(comparator, doBefore, doAfter) {
378 if (!doBefore && !doAfter) {
379 return comparator;
380 }
381 return function beforeAfterComparator(a, b) {
382 if (doBefore) {
383 if (a === 'before') {
384 return -1;
385 } else if (b === 'before') {
386 return 1;
387 } 138 }
388 } 139 }
389 if (doAfter) { 140 checkOther(['all', 'before'], 'before');
390 if (a === 'after') { 141
391 return 1; 142 _.each(list, function(value, index) {
392 } else if (b === 'after') { 143 if (index % 2 === 0) {
393 return -1; 144 key = value;
145 } else if (value > 0) {
146 addNewItem(key, value);
394 } 147 }
148 });
149 switch (type) {
150 case 'year-field':
151 case 'field':
152 _.each(facet.bins, function(includeValues, key) {
153 var tag = facetName + ':' + key;
154 addNewItem(key, facetQueries[tag]);
155 });
156 break;
157 }
158 items.set(newItems, {remove: false});
159 if (facet.facetStats) {
160 var thisFacetStats = statsFields[facetName];
161 facet.set({minValue: thisFacetStats.min, maxValue: thisFacetStats.max});
395 } 162 }
396 return comparator.call(this, a, b); 163 }, this));
164 },
165 getFacetFormData: function(options) {
166 options = options || {};
167 var result = {
168 facet: true,
169 stats: true,
170 'facet.missing': true,
171 'facet.field': [],
172 'facet.range': [],
173 'facet.interval': [],
174 'facet.query': [],
175 'stats.field': [],
176 fq: [],
397 }; 177 };
398 } 178 _.each(this.keys(), _.bind(function(facetName) {
399 var Facet = Facets.Facet = Backbone.Model.extend({ 179 var facet = this.get(facetName);
400 defaults: function defaults() { 180 var queryField = facet.queryField;
401 return { 181 var facetField = facet.facetField ? facet.facetField : queryField;
402 formName: null, 182 var type = facet.facetType;
403 query: null, 183 var valueFormatter;
404 suggestions: [], 184 var facetOptions = {};
185 var quoteFormatter = function(value) {
186 return value.replace ? ('"' + value.replace(/"/, '\\"') + '"') : value;
405 }; 187 };
406 }, 188 var rangeFormatter = function(from, to) {
407 initialize: function(data, options) { 189 return '[' + quoteFormatter(from) + ' TO ' + quoteFormatter(to) + ']';
408 this.set('all', true); 190 };
409 this.on('change:queryMax change:queryMin', function() { 191 switch (type) {
410 this.set('hasQuery', this.get('queryMin') !== undefined && this.get('queryMax') !== undefined); 192 case 'year':
411 }, this); 193 type = 'range';
412 (function(self) { 194 quoteFormatter = function(value) {
413 var skipCallback; 195 return value.toISOString();
414 self.on('item-change change:queryMax change:queryMin', function() { 196 };
415 switch (skipCallback) { 197 valueFormatter = function(item) {
416 case 'change:checkedKeys': 198 var key = item.get('key');
417 case 'change:all': 199 var fromDate, toDate;
418 return; 200 if (key === 'before') {
419 } 201 return rangeFormatter('*', facet.rangeStart + '-1SECOND');
420 var checkedKeys = [];
421 this.get('items').each(function(facetItem) {
422 if (facetItem.get('checked')) {
423 checkedKeys.push(facetItem.get('key'));
424 } 202 }
425 }); 203 fromDate = new Date(key);
426 var checkedCount = checkedKeys.length; 204 toDate = new Date(key);
427 if (this.get('queryMin') !== undefined && this.get('queryMax') !== undefined) { 205 toDate.setUTCFullYear(toDate.getUTCFullYear() + 1);
428 checkedCount++; 206 return rangeFormatter(fromDate, toDate);
429 } 207 };
430 skipCallback = 'item-change'; 208 if (facet.other) {
431 try { 209 facetOptions['facet.range.other'] = facet.other;
432 this.set({
433 all: checkedCount === 0,
434 checkedKeys: checkedKeys,
435
436 });
437 } finally {
438 skipCallback = null;
439 }
440 }, self);
441 self.on('change:all', function() {
442 switch (skipCallback) {
443 case 'change:checkedKeys':
444 case 'item-change':
445 return;
446 }
447 skipCallback = 'change:all';
448 try {
449 this.get('items').invoke('set', {checked: false});
450 this.set({
451 checkedKeys: [],
452 queryMax: undefined,
453 queryMin: undefined,
454 });
455 } finally {
456 skipCallback = null;
457 }
458 }, self);
459 self.on('change:checkedKeys', function() {
460 switch (skipCallback) {
461 case 'item-change':
462 case 'change:all':
463 return;
464 }
465 var checkedKeys = {};
466 _.each(this.get('checkedKeys'), function(value, i) {
467 checkedKeys[value] = true;
468 });
469 skipCallback = 'change:checkedKeys';
470 var checkedCount = 0;
471 if (this.get('queryMin') !== undefined && this.get('queryMax') !== undefined) {
472 checkedCount++;
473 }
474 try {
475 this.get('items').each(function(facetItem) {
476 var newChecked = checkedKeys[facetItem.get('key')];
477 var currentChecked = facetItem.get('checked');
478 if (newChecked !== currentChecked) {
479 facetItem.set('checked', newChecked);
480 }
481 if (facetItem.get('checked')) {
482 checkedCount++;
483 }
484 });
485 this.set({
486 all: checkedCount === 0,
487 });
488 } finally {
489 skipCallback = null;
490 } 210 }
491 }, self); 211 break;
492 })(this);
493 var sortKeyExtractor = options.sortKeyExtractor;
494 if (!sortKeyExtractor) {
495 sortKeyExtractor = getItemKeyAccessor;
496 }
497 var comparator = options.comparator;
498 if (!comparator) {
499 comparator = Sort.standard;
500 } else if (typeof comparator === 'string') {
501 comparator = Sort[comparator];
502 }
503 switch (options.facetType) {
504 case 'year':
505 case 'range': 212 case 'range':
506 case 'interval': 213 case 'interval':
507 comparator = wrapComparatorForAllBeforeAfter(comparator, ['all', 'before'].indexOf(options.other) !== -1, ['all', 'after'].indexOf(options.other) !== -1); 214 valueFormatter = function(item) {
215 var key = parseInt(item.get('key'));
216 return rangeFormatter(key, key + facet.rangeGap);
217 };
508 break; 218 break;
509 } 219 case 'year-field':
510 var formatter = options.formatter; 220 type = 'field';
511 if (!formatter) { 221 quoteFormatter = function(value) {
512 var labelMap = options.labelMap; 222 return new Date(value).toISOString();
513 if (labelMap) {
514 formatter = function(item) {
515 var value = item.get('key');
516 var label = options.labelMap[value.toUpperCase()];
517 return label ? label : value;
518 }; 223 };
519 } else { 224 valueFormatter = facet.queryValue ? facet.queryValue : function(value) {
520 formatter = getItemKeyAccessor; 225 return quoteFormatter(getItemKeyAccessor(value));
521 } 226 };
522 } 227 _.each(facet.bins, function(includeValues, key) {
523 this.formatter = formatter; 228 var query = _.map(includeValues, function(includeValue, index) {
524 comparator = (function(comparator) { 229 return queryField + ':\'' + includeValue + '\'';
525 return function facetItemValue(a, b) { 230 }).join(' OR ');
526 var keyA = sortKeyExtractor(a); 231 result['facet.query'].push('{!key=' + facetName + ':' + key + ' tag=' + facetName + ':' + key + '}(' + query + ')');
527 var keyB = sortKeyExtractor(b); 232 });
528 switch (this.get('orderByDirection')) { 233 break;
529 case 'desc': 234 case 'field':
530 var tmp = keyA; 235 valueFormatter = facet.queryValue ? facet.queryValue : function(value) {
531 keyA = keyB; 236 return quoteFormatter(getItemKeyAccessor(value));
532 keyB = tmp; 237 };
533 break; 238 _.each(facet.bins, function(includeValues, key) {
534 } 239 var query = _.map(includeValues, function(includeValue, index) {
535 return comparator.call(this, sortKeyExtractor(a), sortKeyExtractor(b)); 240 return queryField + ':' + includeValue;
536 }; 241 }).join(' OR ');
537 })(comparator); 242 result['facet.query'].push('{!key=' + facetName + ':' + key + ' tag=' + facetName + ':' + key + '}(' + query + ')');
538 this.set('items', new ItemCollection([], {comparator: comparator})); 243 });
539 this.facetType = options.facetType;
540 this.facetStats = options.facetStats;
541 this.other = options.other;
542 this.facetField = options.facetField;
543 this.queryField = options.queryField;
544 this.queryValue = options.queryValue;
545 this.bins = options.bins;
546 switch (this.facetType) {
547 case 'year':
548 this.rangeStart = options.start;
549 this.rangeEnd = options.end;
550 this.rangeGap = '+' + (options.gap ? options.gap : 1) + 'YEAR';
551 break; 244 break;
245 }
246 switch (type) {
552 case 'range': 247 case 'range':
553 this.rangeStart = options.start; 248 facetOptions['facet.range.start'] = facet.rangeStart;
554 this.rangeEnd = options.end; 249 facetOptions['facet.range.end'] = facet.rangeEnd;
555 this.rangeGap = options.gap; 250 facetOptions['facet.range.gap'] = facet.rangeGap;
556 break; 251 break;
557 case 'field':
558 case 'year-field':
559 case 'interval': 252 case 'interval':
253 facetOptions['facet.interval.set'] = '[*,*]';
560 break; 254 break;
561 default:
562 var e = new Error('Unsupported facet type: ' + this.facetType);
563 e.facet = this;
564 throw e;
565 } 255 }
566 return Facet.__super__.initialize.apply(this, arguments); 256 var facetValues = [];
567 }, 257 var queryMax = facet.get('queryMax');
568 /* 258 var queryMin = facet.get('queryMin');
569 toJSON: function toJSON(options) { 259 if (queryMax !== undefined && queryMin !== undefined) {
570 if (!!options.facetSuggestions) { 260 facetValues.push(rangeFormatter(queryMin, queryMax));
571 return { 261 }
572 rows: 0, 262 facet.get('items').each(function(item, index) {
573 facet: true, 263 if (!item.get('hidden') && item.get('checked')) {
574 wt: 'json', 264 facetValues.push(valueFormatter(item));
575 q: '*:*', 265 }
576 'facet.contains': this.get('query'), 266 });
577 'facet.contains.ignoreCase': true, 267 facetOptions.key = facetName;
578 'facet.field': this.queryField, 268 if (facetValues.length) {
579 'facet.mincount': 1, 269 facetOptions.ex = facetName;
580 }; 270 result.fq.push('{!tag=' + facetName + '}' + facetField + ':(' + facetValues.join(' OR ') + ')');
271 } else {
272 facetOptions.tag = facetName;
581 } 273 }
582 },
583 parse: function parse(data, options) {
584 if (!!options.facetSuggestions) { 274 if (!!options.facetSuggestions) {
585 var suggestions = []; 275 var childFacet = options.childFacet;
586 var key; 276 if (childFacet === facet) {
587 _.each(data.facet_counts.facet_fields[this.queryField], function(value, index) { 277 facetOptions['facet.contains'] = facet.get('query');
588 if (index % 2 === 0) { 278 facetOptions['facet.contains.ignoreCase'] = true;
589 key = value; 279 facetOptions['facet.mincount'] = 1;
590 } else if (value > 0) { 280 }
591 suggestions.push({key: key, value: value}); 281 }
282 var facetOptionList = [];
283 _.each(facetOptions, function(value, key) {
284 if (value !== undefined) {
285 facetOptionList.push(key + '=' + value);
286 }
287 });
288 result['facet.' + type].push('{!' + facetOptionList.join(' ') + '}' + queryField);
289 if (facet.facetStats) {
290 result['stats.field'].push('{!' + facetOptionList.join(' ') + '}' + queryField);
291 }
292 var facetQuery = facet.get('query');
293 if (facetQuery !== null && facetQuery !== '') {
294 facetOptions.key = 'suggestions:' + facetName;
295 facetOptionList = [];
296 _.each(facetOptions, function(value, key) {
297 if (value !== undefined) {
298 facetOptionList.push(key + '=' + value);
592 } 299 }
593 }); 300 });
594 return { 301 result['facet.' + type].push('{!' + facetOptionList.join(' ') + '}' + queryField);
595 suggestions: suggestions,
596 };
597 } 302 }
598 return null; 303 }, this));
599 }, 304 return result;
600 */ 305 },
601 });
602 var ItemCollection = Backbone.Collection.extend({
603 remove: function() {
604 }
605 });
606 var Item = Facet.Item = Backbone.Model.extend({
607 idAttribute: 'key',
608 defaults: {
609 checked: false,
610 hidden: false,
611 },
612 _initialize: function(data, options) {
613 this.on('add', function(model, parent, options) {
614 // added to a collection
615 this.on('change:checked', function() {
616 parent.trigger('item-change');
617 });
618 }, this);
619 return Item.__super__.initialize.apply(this, arguments);
620 }
621 });
622 306
623 return Facets;
624 })();
625 var Pagination = (function() {
626 function stepFalse(e) {
627 e.preventDefault();
628 return false;
629 }
630 var Pagination = Backbone.Model.extend({
631 defaults: {
632 currentPage: 1,
633 hasNext: false,
634 hasNextJumpPage: false,
635 hasPrevious: false,
636 hasPreviousJumpPage: false,
637 nextJumpPage: undefined,
638 nextPage: undefined,
639 pages: [],
640 pageJump: 5,
641 pageSize: 10,
642 previousJumpPage: undefined,
643 previousPage: undefined,
644 totalCount: 0,
645 totalPages: 0,
646 },
647 initialize: function(data, options) {
648 var buildPages = function buildPages() {
649 var currentPage = parseInt(this.get('currentPage'));
650 var pageSize = parseInt(this.get('pageSize'));
651 var totalCount = parseInt(this.get('totalCount'));
652 if (!currentPage || !pageSize || !totalCount) {
653 return;
654 }
655 var pages = [];
656 var totalPages = Math.floor((totalCount + pageSize - 1) / pageSize);
657 function addPage(self, i) {
658 pages.push({
659 current: i === currentPage,
660 jump: function() {
661 self.set('currentPage', i);
662 return true;
663 },
664 number: i,
665 });
666 }
667 var startAt = currentPage - 4, endAt = currentPage + 5;
668 if (startAt < 1) {
669 endAt += (1 - startAt);
670 }
671 if (endAt > totalPages) {
672 startAt -= endAt - totalPages - 1;
673 endAt = totalPages + 1;
674 }
675 if (startAt < 1) {
676 startAt = 1;
677 }
678 if (endAt - startAt < 9) {
679 /* global console:false */
680 console.log('foo');
681 }
682 307
683 for (var i = startAt; i < endAt; i++) { 308
684 if (i > 0 && i <= totalPages) { 309 toJSON: function toJSON(options) {
685 addPage(this, i); 310 if (!!options.facetSuggestions) {
686 } 311 return _.extend({
687 } 312 rows: 0,
688 var hasPrevious = currentPage > 1; 313 facet: true,
689 var hasNext = currentPage < totalPages; 314 wt: 'json',
690 var pageJump = this.get('pageJump'); 315 q: '*:*',
691 var nextJumpPage, previousJumpPage, hasNextJump, hasPreviousJump; 316 }, this.getFacetFormData(options));
692 if (pageJump) {
693 nextJumpPage = currentPage + pageJump;
694 previousJumpPage = currentPage - pageJump;
695 hasNextJump = nextJumpPage < totalPages;
696 hasPreviousJump = previousJumpPage > 0;
697 } else {
698 hasNextJump = false;
699 hasPreviousJump = false;
700 }
701 this.set({
702 hasNext: hasNext,
703 hasNextJump: hasNextJump,
704 hasPrevious: hasPrevious,
705 hasPreviousJump: hasPreviousJump,
706 nextJumpPage: hasNextJump ? nextJumpPage : undefined,
707 nextPage: hasNext ? currentPage + 1 : undefined,
708 pages: pages,
709 previousJumpPage: hasNextJump ? previousJumpPage : undefined,
710 previousPage: hasPrevious ? currentPage - 1 : undefined,
711 totalPages: totalPages,
712 });
713 };
714 this.on('change:pageSize change:currentPage change:totalCount', buildPages, this);
715 var installActions = _.bind(function installActions(eventName, nextName, hasNextName, previousName, hasPreviousName, pageCount) {
716 this.on(eventName, function() {
717 var hasNext = this.get(hasNextName);
718 var hasPrevious = this.get(hasPreviousName);
719 var next, previous;
720 if (hasNext) {
721 next = _.bind(function(e) {
722 e.preventDefault();
723 this.set('currentPage', this.get('currentPage') + pageCount);
724 return false;
725 }, this);
726 } else {
727 next = stepFalse;
728 }
729 if (hasPrevious) {
730 previous = _.bind(function(e) {
731 e.preventDefault();
732 this.set('currentPage', this.get('currentPage') - pageCount);
733 return false;
734 }, this);
735 } else {
736 previous = stepFalse;
737 }
738 this.set(nextName, next);
739 this.set(previousName, previous);
740 }, this);
741 this[nextName] = _.bind(function() {
742 return this.get(nextName)();
743 }, this);
744 this[previousName] = _.bind(function() {
745 return this.get(previousName)();
746 }, this);
747 }, this);
748 this.on('change:pageJump', function() {
749 var pageJump = this.get('pageJump');
750 installActions('change:hasNextJump change:hasPreviousJump', 'nextJump', 'hasNextJump', 'previousJump', 'hasPreviousJump', pageJump);
751 }, this);
752 installActions('change:hasNext change:hasPrevious', 'next', 'hasNext', 'previous', 'hasPrevious', 1);
753 buildPages.apply(this);
754 this.trigger('change:pageJump');
755 return Pagination.__super__.initialize.apply(this, arguments);
756 } 317 }
757 });
758 return Pagination;
759 })();
760 var Ordering = Backbone.Model.extend({
761 defaults: {
762 value: null,
763 items: new Backbone.Collection(),
764 }, 318 },
765 initialize: function(data, options) { 319 parse: function parse(data, options) {
766 if (this.get('value') === null) { 320 if (!!options.facetSuggestions) {
767 var firstItem = this.get('items').at(0); 321 this.applyFacetResults(data, options);
768 if (firstItem) {
769 this.set('value', firstItem.get('value'));
770 }
771 } 322 }
323 return null;
772 }, 324 },
773 parse: function(data) { 325 }));
774 var result = _.clone(data); 326 var Sort = Facets.Sort = {
775 if (result.items && !(result.items instanceof Backbone.Collection)) { 327 standard: function standardSort(a, b) {
776 result.items = new Backbone.Collection(result.items, {parse: true}); 328 if (a < b) {
329 return -1;
330 } else if (a > b) {
331 return 1;
332 } else {
333 return 0;
777 } 334 }
778 return result;
779 }, 335 },
780 }); 336 date: function dateSort(a, b) {
781 var QueryTextField = Backbone.Model.extend({ 337 return Sort.standard.call(this, a, b);
782 defaults: {
783 formName: null,
784 name: null,
785 queries: [],
786 fields: null,
787 multi: false
788 }, 338 },
789 }); 339 number: function numberSort(a, b) {
790 var SolrSearch = Pagination.extend({ 340 return Sort.standard.call(this, parseInt(a), parseInt(b));
791 url: function url() {
792 return this.constructor.selectUrl;
793 }, 341 },
794 defaults: function defaults() { 342 };
795 var constructor = this.constructor; 343 function wrapComparatorForAllBeforeAfter(comparator, doBefore, doAfter) {
796 var formNameMap = {}; 344 if (!doBefore && !doAfter) {
797 var facets = new SolrFacets(this.constructor.facets, {search: this}); 345 return comparator;
798 _.each(facets.values(), function(facet) { 346 }
799 var formName = facet.get('formName'); 347 return function beforeAfterComparator(a, b) {
800 if (formName) { 348 if (doBefore) {
801 formNameMap[formName] = facet; 349 if (a === 'before') {
350 return -1;
351 } else if (b === 'before') {
352 return 1;
802 } 353 }
803 }); 354 }
804 var queryFields = new Backbone.Model(); 355 if (doAfter) {
805 _.each(constructor.queryTextFields, function(definition, queryName) { 356 if (a === 'after') {
806 var qtf = new QueryTextField({formName: definition.formName, name: queryName, queries: [], fields: definition.fields, multi: !!definition.multi}); 357 return 1;
807 var formName = qtf.get('formName'); 358 } else if (b === 'after') {
808 if (formName) { 359 return -1;
809 formNameMap[formName] = qtf;
810 } 360 }
811 queryFields.set(queryName, qtf);
812 }, this);
813 return _.extend(_.result(SolrSearch.__super__, 'defaults'), {
814 initializing: true,
815 initialized: false,
816 query: '',
817 formNameMap: formNameMap,
818 results: new Backbone.Collection(),
819 ordering: new Ordering({items: constructor.orderingItems}, {parse: true}),
820 facets: facets,
821 queryFields: queryFields,
822 });
823 },
824 applyQueryParameters: function() {
825 var skipOptions = {skipSearch: true};
826 var parts = document.location.href.match(/.*\?(.*)/);
827 var facets = this.get('facets');
828 facets.resetSearch();
829 _.each(this.get('queryFields').values(), function(qtf) {
830 qtf.set({
831 query: null,
832 queries: [],
833 }, skipOptions);
834 });
835
836 if (parts) {
837 var formNameMap = this.get('formNameMap');
838 var keyValueParts = parts[1].split('&');
839 _.each(keyValueParts, function(keyValuePart, i) {
840 var keyFieldValue = keyValuePart.match(/^([^.]+)(?:\.([^.]+))?=(.*)$/);
841 if (keyFieldValue) {
842 var key = keyFieldValue[1];
843 var field = keyFieldValue[2];
844 var value = keyFieldValue[3];
845 value = value.replace(/(\+|%20)/g, ' ');
846 var impl = formNameMap[key];
847 if (impl) {
848 if (impl instanceof QueryTextField) {
849 impl.set('query', value, skipOptions);
850 } else if (impl.facetType === 'field' && value) {
851 if (field === 'min') {
852 impl.set('queryMin', parseInt(value), skipOptions);
853 } else if (field === 'max') {
854 impl.set('queryMax', parseInt(value), skipOptions);
855 } else if (field === 'items') {
856 var items = impl.get('items');
857 var item = items.get(value);
858 if (!item) {
859 item = new SolrFacets.Facet.Item({key: value, value: 0});
860 items.add(item);
861 item.on('change:checked', function(model, value, options) {
862 this.trigger('item-change', null, null, options);
863 impl.trigger('item-change', null, null, options);
864 }, facets);
865 }
866 item.set('checked', true, skipOptions);
867 }
868 }
869 }
870 }
871 }, this);
872 } 361 }
362 return comparator.call(this, a, b);
363 };
364 }
365 var Facet = Facets.Facet = Backbone.Model.extend({
366 defaults: function defaults() {
367 return {
368 formName: null,
369 query: null,
370 suggestions: [],
371 };
873 }, 372 },
874 initialize: function(data, options) { 373 initialize: function(data, options) {
875 options = (options || {}); 374 this.set('all', true);
876 this.set('options', new Backbone.Model({ 375 this.on('change:queryMax change:queryMin', function() {
877 faceting: !!options.faceting, 376 this.set('hasQuery', this.get('queryMin') !== undefined && this.get('queryMax') !== undefined);
878 highlighting: !!options.highlighting,
879 showAll: !!options.showAll,
880 }));
881 this._doSearchImmediately = _.bind(function(options) {
882 this.fetch(_.extend({}, options, {
883 data: this.toJSON(),
884 merge: false,
885 traditional: true,
886 }));
887 }, this); 377 }, this);
888 this._doSearch = _.debounce(this._doSearchImmediately, 250); 378 (function(self) {
889 this._doSearch = (function(doSearch) { 379 var skipCallback;
890 return function(options) { 380 self.on('item-change change:queryMax change:queryMin', function() {
891 var skipSearch = this._skipSearch || (options || {}).skipSearch; 381 switch (skipCallback) {
892 if (!skipSearch) { 382 case 'change:checkedKeys':
893 doSearch(options); 383 case 'change:all':
384 return;
894 } 385 }
895 }; 386 var checkedKeys = [];
896 })(this._doSearch); 387 this.get('items').each(function(facetItem) {
897 _.each(this.get('queryFields').values(), function(subQueryModel) { 388 if (facetItem.get('checked')) {
898 subQueryModel.on('change:query change:queries', function(model, value, options) { 389 checkedKeys.push(facetItem.get('key'));
899 this.trigger('change:queryFields', null, null, options); 390 }
900 }, this); 391 });
901 }, this); 392 var checkedCount = checkedKeys.length;
902 this.on('change:queryFields', function(model, value, options) { 393 if (this.get('queryMin') !== undefined && this.get('queryMax') !== undefined) {
903 this.trigger('change:query', null, null, options); 394 checkedCount++;
904 }, this); 395 }
905 this.get('options').on('change:faceting change:highlighting', function(model, value, options) { 396 skipCallback = 'item-change';
906 this._doSearch(options); 397 try {
907 }, this); 398 this.set({
908 this.on('change:query', function(model, value, options) { 399 all: checkedCount === 0,
909 // this is silent, which causes the change events to not fire; 400 checkedKeys: checkedKeys,
910 // this is ok, because the next .on will also see the change
911 // event on query, and resend the search
912 this._doSearch(_.extend({}, options, {resetCurrentPage: true, resetFacets: true, resetOrdering: true}));
913 }, this);
914 this.on('change:pageSize', function(model, value, options) {
915 this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
916 }, this);
917 this.on('change:currentPage', function(model, value, options) {
918 this._doSearch(options);
919 }, this);
920 this.get('facets').on('item-change', function(model, value, options) {
921 this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
922 }, this);
923 this.get('ordering').on('change:value', function(model, value, options) {
924 this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
925 }, this);
926 return SolrSearch.__super__.initialize.apply(this, arguments);
927
928 },
929 parse: function parse(data, options) {
930 if (options.facetSuggestions) {
931 return null;
932 }
933 var skipOptions = _.extend({}, options, {skipSearch: true});
934 if (options.resetCurrentPage) {
935 this.set('currentPage', 1, skipOptions);
936 }
937 if (options.resetFacets) {
938 this.get('facets').resetSearch(skipOptions);
939 }
940 if (options.resetOrdering) {
941 this.get('ordering').set('value', this.get('ordering').get('items').at(0).get('value'), skipOptions);
942 }
943 var facets = this.get('facets');
944 if (this.get('options').get('faceting')) {
945 facets.applyFacetResults(data);
946 } else {
947 facets.resetSearch(options);
948 }
949 var list = [];
950 var highlighting = this.get('options').get('highlighting') ? data.highlighting : {};
951
952 var self = this;
953 _.each(data.response.docs, function(doc, index) {
954 var itemHighlighting = highlighting[doc.id];
955 if (!itemHighlighting) {
956 itemHighlighting = {};
957 }
958 list.push(self.searchParseDoc(doc, index, itemHighlighting));
959 });
960 401
961 return { 402 });
962 initializing: false, 403 } finally {
963 initialized: true, 404 skipCallback = null;
964 results: new Backbone.Collection(list), 405 }
965 facets: facets, 406 }, self);
966 options: this.get('options'), 407 self.on('change:all', function() {
967 totalCount: data.response.numFound, 408 switch (skipCallback) {
968 queryTime: (data.responseHeader.QTime / 1000).toFixed(2), 409 case 'change:checkedKeys':
969 hasResults: list.length > 0, 410 case 'item-change':
970 }; 411 return;
971 }, 412 }
972 searchParseDoc: function searchParseDoc(doc, index, itemHighlighting) { 413 skipCallback = 'change:all';
973 var fieldsToParse = mergeStaticProps(this.constructor, SolrSearch, {}, 'parsedFieldMap'); 414 try {
974 var result = {}; 415 this.get('items').invoke('set', {checked: false});
975 _.each(fieldsToParse, function(value, key) { 416 this.set({
976 var parsed; 417 checkedKeys: [],
977 if (_.isFunction(value)) { 418 queryMax: undefined,
978 parsed = value.call(this, doc, index, itemHighlighting, key); 419 queryMin: undefined,
979 } else if (_.isArray(value)) { 420 });
980 parsed = []; 421 } finally {
981 _.each(value, function(item, i) { 422 skipCallback = null;
982 var rawValue = doc[item.name]; 423 }
983 if (rawValue) { 424 }, self);
984 var parsedValue = item.parse.call(this, doc, index, itemHighlighting, rawValue, item.name); 425 self.on('change:checkedKeys', function() {
985 if (parsedValue) { 426 switch (skipCallback) {
986 parsed.push(parsedValue); 427 case 'item-change':
428 case 'change:all':
429 return;
430 }
431 var checkedKeys = {};
432 _.each(this.get('checkedKeys'), function(value, i) {
433 checkedKeys[value] = true;
434 });
435 skipCallback = 'change:checkedKeys';
436 var checkedCount = 0;
437 if (this.get('queryMin') !== undefined && this.get('queryMax') !== undefined) {
438 checkedCount++;
439 }
440 try {
441 this.get('items').each(function(facetItem) {
442 var newChecked = checkedKeys[facetItem.get('key')];
443 var currentChecked = facetItem.get('checked');
444 if (newChecked !== currentChecked) {
445 facetItem.set('checked', newChecked);
987 } 446 }
988 } 447 if (facetItem.get('checked')) {
989 }, this); 448 checkedCount++;
990 } else { 449 }
991 parsed = doc[value]; 450 });
992 } 451 this.set({
993 result[key] = parsed; 452 all: checkedCount === 0,
994 }, this); 453 });
995 return result; 454 } finally {
996 }, 455 skipCallback = null;
997 toJSON: function(options) { 456 }
998 if (!options) { 457 }, self);
999 options = {}; 458 })(this);
1000 } 459 var sortKeyExtractor = options.sortKeyExtractor;
1001 var facets = this.get('facets'); 460 if (!sortKeyExtractor) {
1002 var constructor = this.constructor; 461 sortKeyExtractor = getItemKeyAccessor;
1003 var result = {
1004 defType: 'edismax',
1005 qf: constructor.queryField,
1006 wt: 'json',
1007 };
1008 var ordering = this.get('ordering');
1009 var sort = ordering.get('value');
1010 if (options.resetOrdering) {
1011 sort = ordering.get('items').at(0).get('value');
1012 } 462 }
1013 var currentPage = this.get('currentPage'); 463 var comparator = options.comparator;
1014 var pageSize = this.get('pageSize'); 464 if (!comparator) {
1015 if (options.resetCurrentPage) { 465 comparator = Sort.standard;
1016 currentPage = 1; 466 } else if (typeof comparator === 'string') {
467 comparator = Sort[comparator];
1017 } 468 }
1018 result.sort = sort; 469 switch (options.facetType) {
1019 result.rows = pageSize; 470 case 'year':
1020 result.start = (currentPage - 1) * pageSize; 471 case 'range':
1021 result.fl = mergeStaticSets(constructor, SolrSearch, [], 'returnFields'); 472 case 'interval':
1022 if (this.get('options').get('highlighting')) { 473 comparator = wrapComparatorForAllBeforeAfter(comparator, ['all', 'before'].indexOf(options.other) !== -1, ['all', 'after'].indexOf(options.other) !== -1);
1023 result.hl = true; 474 break;
1024 result['hl.fl'] = ['content', 'title'].concat(constructor.extraHighlightFields);
1025 } 475 }
1026 476 var formatter = options.formatter;
1027 if (this.get('options').get('faceting')) { 477 if (!formatter) {
1028 var facetFormData = facets.getFacetFormData(); 478 var labelMap = options.labelMap;
1029 var fq = facetFormData.fq; 479 if (labelMap) {
1030 delete(facetFormData.fq); 480 formatter = function(item) {
1031 result = _.extend(result, facetFormData); 481 var value = item.get('key');
1032 if (fq.length) { 482 var label = options.labelMap[value.toUpperCase()];
1033 if (result.fq) { 483 return label ? label : value;
1034 result.fq = result.fq.concat(fq); 484 };
1035 } else { 485 } else {
1036 result.fq = fq; 486 formatter = getItemKeyAccessor;
1037 }
1038 } 487 }
1039 } 488 }
1040 489 this.formatter = formatter;
1041 var queryParts = []; 490 comparator = (function(comparator) {
1042 var queryFields = this.get('queryFields'); 491 return function facetItemValue(a, b) {
1043 _.each(queryFields.keys(), function(queryName) { 492 var keyA = sortKeyExtractor(a);
1044 var subQueryModel = queryFields.get(queryName); 493 var keyB = sortKeyExtractor(b);
1045 var fields = subQueryModel.get('fields'); 494 switch (this.get('orderByDirection')) {
1046 var queries = subQueryModel.get('queries'); 495 case 'desc':
1047 var query = subQueryModel.get('query'); 496 var tmp = keyA;
1048 if (query) { 497 keyA = keyB;
1049 queries = queries.concat([query]); 498 keyB = tmp;
1050 } 499 break;
1051 var subQuery = [];
1052 for (var i = 0; i < fields.length; i++) {
1053 var fieldName = fields[i];
1054 for (var j = 0; j < queries.length; j++) {
1055 // FIXME: quote the query
1056 subQuery.push(fieldName + ':\'' + queries[j] + '\'');
1057 } 500 }
1058 } 501 return comparator.call(this, sortKeyExtractor(a), sortKeyExtractor(b));
1059 if (subQuery.length) { 502 };
1060 queryParts.push(subQuery.join(' OR ')); 503 })(comparator);
1061 } 504 this.set('items', new ItemCollection([], {comparator: comparator}));
1062 }, this); 505 this.facetType = options.facetType;
1063 506 this.facetStats = options.facetStats;
1064 var query = queryParts.join(' AND '); 507 this.other = options.other;
1065 if (!query) { 508 this.facetField = options.facetField;
1066 query = '*:*'; 509 this.queryField = options.queryField;
1067 if (!this.get('options').get('showAll') && !result.fq) { 510 this.queryValue = options.queryValue;
1068 result.rows = 0; 511 this.bins = options.bins;
1069 } 512 switch (this.facetType) {
513 case 'year':
514 this.rangeStart = options.start;
515 this.rangeEnd = options.end;
516 this.rangeGap = '+' + (options.gap ? options.gap : 1) + 'YEAR';
517 break;
518 case 'range':
519 this.rangeStart = options.start;
520 this.rangeEnd = options.end;
521 this.rangeGap = options.gap;
522 break;
523 case 'field':
524 case 'year-field':
525 case 'interval':
526 break;
527 default:
528 var e = new Error('Unsupported facet type: ' + this.facetType);
529 e.facet = this;
530 throw e;
1070 } 531 }
1071 var dateBoostField = constructor.dateBoostField; 532 return Facet.__super__.initialize.apply(this, arguments);
1072 var popularityBoostField = constructor.popularityBoostField; 533 },
1073 if (result.rows !== 0 && (dateBoostField || popularityBoostField)) { 534 /*
1074 var boostSum = []; 535 toJSON: function toJSON(options) {
1075 if (dateBoostField) { 536 if (!!options.facetSuggestions) {
1076 result.dateBoost = 'recip(ms(NOW,' + dateBoostField + '),3.16e-11,1,1)'; 537 return {
1077 boostSum.push('$dateBoost'); 538 rows: 0,
1078 } 539 facet: true,
1079 if (popularityBoostField) { 540 wt: 'json',
1080 result.popularityBoost = 'def(' + popularityBoostField + ',0)'; 541 q: '*:*',
1081 boostSum.push('$popularityBoost'); 542 'facet.contains': this.get('query'),
1082 } 543 'facet.contains.ignoreCase': true,
1083 result.qq = query; 544 'facet.field': this.queryField,
1084 result.q = '{!boost b=sum(' + boostSum.join(',') + ') v=$qq defType=$defType}'; 545 'facet.mincount': 1,
1085 } else { 546 };
1086 result.q = query;
1087 } 547 }
1088 return result;
1089 }, 548 },
1090 }, { 549 parse: function parse(data, options) {
1091 cleanup: function cleanup(txt) { 550 if (!!options.facetSuggestions) {
1092 if (txt) { 551 var suggestions = [];
1093 txt = txt.replace(/&amp;/g, '&'); 552 var key;
1094 txt = txt.replace(/&#39;/g, '\''); 553 _.each(data.facet_counts.facet_fields[this.queryField], function(value, index) {
1095 txt = txt.replace(/[^a-zA-Z0-9 -\.,:;%<>\/'"|]/g, ' '); 554 if (index % 2 === 0) {
555 key = value;
556 } else if (value > 0) {
557 suggestions.push({key: key, value: value});
558 }
559 });
560 return {
561 suggestions: suggestions,
562 };
1096 } 563 }
1097 return txt; 564 return null;
1098 }, 565 },
1099 returnFields: [ 566 */
1100 'keywords', 567 });
1101 'last_modified', 568 var ItemCollection = Backbone.Collection.extend({
1102 'path', 569 remove: function() {
1103 'score', 570 }
1104 'title', 571 });
1105 ], 572 var Item = Facet.Item = Backbone.Model.extend({
1106 parsedFieldMap: { 573 idAttribute: 'key',
1107 content: function(doc, index, itemHighlighting) { 574 defaults: {
1108 var content = itemHighlighting.content; 575 checked: false,
1109 if (content && content.constructor === Array) { 576 hidden: false,
1110 content = content.join(' ');
1111 }
1112 if (!content) {
1113 content = '';
1114 }
1115 return this.constructor.cleanup(content);
1116 },
1117 keywords: 'keywords',
1118 lastModified: 'last_modified',
1119 path: 'url',
1120 score: 'score',
1121 title: function(doc, index, itemHighlighting) {
1122 var title;
1123 if (itemHighlighting.title) {
1124 title = itemHighlighting.title.join(' ');
1125 } else {
1126 title = doc.title[0];
1127 }
1128 return this.constructor.cleanup(title);
1129 },
1130 }, 577 },
578 _initialize: function(data, options) {
579 this.on('add', function(model, parent, options) {
580 // added to a collection
581 this.on('change:checked', function() {
582 parent.trigger('item-change');
583 });
584 }, this);
585 return Item.__super__.initialize.apply(this, arguments);
586 }
1131 }); 587 });
1132 SolrSearch.Facets = SolrFacets; 588
1133 SolrSearch.Pagination = Pagination; 589 return Facets;
1134 return SolrSearch;
1135 }); 590 });
......
1 define(function(require) {
2 'use strict';
3 var Backbone = require('backbone');
4
5 var Ordering = Backbone.Model.extend({
6 defaults: {
7 value: null,
8 items: new Backbone.Collection(),
9 },
10 initialize: function(data, options) {
11 if (this.get('value') === null) {
12 var firstItem = this.get('items').at(0);
13 if (firstItem) {
14 this.set('value', firstItem.get('value'));
15 }
16 }
17 },
18 parse: function(data) {
19 var result = _.clone(data);
20 if (result.items && !(result.items instanceof Backbone.Collection)) {
21 result.items = new Backbone.Collection(result.items, {parse: true});
22 }
23 return result;
24 },
25 });
26 return Ordering;
27 });
1 define(function(require) {
2 'use strict';
3 var Backbone = require('backbone');
4
5 function stepFalse(e) {
6 e.preventDefault();
7 return false;
8 }
9 var Pagination = Backbone.Model.extend({
10 defaults: {
11 currentPage: 1,
12 hasNext: false,
13 hasNextJumpPage: false,
14 hasPrevious: false,
15 hasPreviousJumpPage: false,
16 nextJumpPage: undefined,
17 nextPage: undefined,
18 pages: [],
19 pageJump: 5,
20 pageSize: 10,
21 previousJumpPage: undefined,
22 previousPage: undefined,
23 totalCount: 0,
24 totalPages: 0,
25 },
26 initialize: function(data, options) {
27 var buildPages = function buildPages() {
28 var currentPage = parseInt(this.get('currentPage'));
29 var pageSize = parseInt(this.get('pageSize'));
30 var totalCount = parseInt(this.get('totalCount'));
31 if (!currentPage || !pageSize || !totalCount) {
32 return;
33 }
34 var pages = [];
35 var totalPages = Math.floor((totalCount + pageSize - 1) / pageSize);
36 function addPage(self, i) {
37 pages.push({
38 current: i === currentPage,
39 jump: function() {
40 self.set('currentPage', i);
41 return true;
42 },
43 number: i,
44 });
45 }
46 var startAt = currentPage - 4, endAt = currentPage + 5;
47 if (startAt < 1) {
48 endAt += (1 - startAt);
49 }
50 if (endAt > totalPages) {
51 startAt -= endAt - totalPages - 1;
52 endAt = totalPages + 1;
53 }
54 if (startAt < 1) {
55 startAt = 1;
56 }
57 if (endAt - startAt < 9) {
58 /* global console:false */
59 console.log('foo');
60 }
61
62 for (var i = startAt; i < endAt; i++) {
63 if (i > 0 && i <= totalPages) {
64 addPage(this, i);
65 }
66 }
67 var hasPrevious = currentPage > 1;
68 var hasNext = currentPage < totalPages;
69 var pageJump = this.get('pageJump');
70 var nextJumpPage, previousJumpPage, hasNextJump, hasPreviousJump;
71 if (pageJump) {
72 nextJumpPage = currentPage + pageJump;
73 previousJumpPage = currentPage - pageJump;
74 hasNextJump = nextJumpPage < totalPages;
75 hasPreviousJump = previousJumpPage > 0;
76 } else {
77 hasNextJump = false;
78 hasPreviousJump = false;
79 }
80 this.set({
81 hasNext: hasNext,
82 hasNextJump: hasNextJump,
83 hasPrevious: hasPrevious,
84 hasPreviousJump: hasPreviousJump,
85 nextJumpPage: hasNextJump ? nextJumpPage : undefined,
86 nextPage: hasNext ? currentPage + 1 : undefined,
87 pages: pages,
88 previousJumpPage: hasNextJump ? previousJumpPage : undefined,
89 previousPage: hasPrevious ? currentPage - 1 : undefined,
90 totalPages: totalPages,
91 });
92 };
93 this.on('change:pageSize change:currentPage change:totalCount', buildPages, this);
94 var installActions = _.bind(function installActions(eventName, nextName, hasNextName, previousName, hasPreviousName, pageCount) {
95 this.on(eventName, function() {
96 var hasNext = this.get(hasNextName);
97 var hasPrevious = this.get(hasPreviousName);
98 var next, previous;
99 if (hasNext) {
100 next = _.bind(function(e) {
101 e.preventDefault();
102 this.set('currentPage', this.get('currentPage') + pageCount);
103 return false;
104 }, this);
105 } else {
106 next = stepFalse;
107 }
108 if (hasPrevious) {
109 previous = _.bind(function(e) {
110 e.preventDefault();
111 this.set('currentPage', this.get('currentPage') - pageCount);
112 return false;
113 }, this);
114 } else {
115 previous = stepFalse;
116 }
117 this.set(nextName, next);
118 this.set(previousName, previous);
119 }, this);
120 this[nextName] = _.bind(function() {
121 return this.get(nextName)();
122 }, this);
123 this[previousName] = _.bind(function() {
124 return this.get(previousName)();
125 }, this);
126 }, this);
127 this.on('change:pageJump', function() {
128 var pageJump = this.get('pageJump');
129 installActions('change:hasNextJump change:hasPreviousJump', 'nextJump', 'hasNextJump', 'previousJump', 'hasPreviousJump', pageJump);
130 }, this);
131 installActions('change:hasNext change:hasPrevious', 'next', 'hasNext', 'previous', 'hasPrevious', 1);
132 buildPages.apply(this);
133 this.trigger('change:pageJump');
134 return Pagination.__super__.initialize.apply(this, arguments);
135 }
136 });
137 return Pagination;
138 });
1 define(function(require) {
2 'use strict';
3 var Backbone = require('backbone');
4
5 var QueryTextField = Backbone.Model.extend({
6 defaults: {
7 formName: null,
8 name: null,
9 queries: [],
10 fields: null,
11 multi: false
12 },
13 });
14 return QueryTextField;
15 });
1 define(function(require) {
2 'use strict';
3 var _ = require('underscore');
4 var Backbone = require('backbone');
5
6 var Facets = require('solr/model/Facets');
7 var Ordering = require('solr/model/Ordering');
8 var Pagination = require('solr/model/Pagination');
9 var QueryTextField = require('solr/model/QueryTextField');
10
11 //var module = require('module');
12
13 function mergeStatic(startPtr, endPtr, obj, fieldName, filterFunc) {
14 var result = filterFunc(obj);
15 var ptr = startPtr;
16 while (true) {
17 result = _.extend(result, filterFunc(_.result(ptr, fieldName)));
18 if (ptr === endPtr) {
19 break;
20 }
21 ptr = ptr.__super__.constructor;
22 }
23 return result;
24 }
25
26 function mergeStaticProps(startPtr, endPtr, obj, fieldName) {
27 return mergeStaticFunc(startPtr, endPtr, obj, fieldName, _.identity);
28 }
29
30 function mergeStaticSets(startPtr, endPtr, obj, fieldName) {
31 return _.keys(mergeStaticFunc(startPtr, endPtr, obj, fieldName, function arrayToMap(array) {
32 var result = {};
33 _.each(array, function(value) {
34 result[value] = true;
35 });
36 return result;
37 }));
38 }
39
40 function getItemKeyAccessor(item) {
41 return item.get('key');
42 }
43 var Solr = Pagination.extend({
44 url: function url() {
45 return this.constructor.selectUrl;
46 },
47 defaults: function defaults() {
48 var constructor = this.constructor;
49 var formNameMap = {};
50 var facets = new Facets(this.constructor.facets, {search: this});
51 _.each(facets.values(), function(facet) {
52 var formName = facet.get('formName');
53 if (formName) {
54 formNameMap[formName] = facet;
55 }
56 });
57 var queryFields = new Backbone.Model();
58 _.each(constructor.queryTextFields, function(definition, queryName) {
59 var qtf = new QueryTextField({formName: definition.formName, name: queryName, queries: [], fields: definition.fields, multi: !!definition.multi});
60 var formName = qtf.get('formName');
61 if (formName) {
62 formNameMap[formName] = qtf;
63 }
64 queryFields.set(queryName, qtf);
65 }, this);
66 return _.extend(_.result(Solr.__super__, 'defaults'), {
67 initializing: true,
68 initialized: false,
69 query: '',
70 formNameMap: formNameMap,
71 results: new Backbone.Collection(),
72 ordering: new Ordering({items: constructor.orderingItems}, {parse: true}),
73 facets: facets,
74 queryFields: queryFields,
75 });
76 },
77 applyQueryParameters: function() {
78 var skipOptions = {skipSearch: true};
79 var parts = document.location.href.match(/.*\?(.*)/);
80 var facets = this.get('facets');
81 facets.resetSearch();
82 _.each(this.get('queryFields').values(), function(qtf) {
83 qtf.set({
84 query: null,
85 queries: [],
86 }, skipOptions);
87 });
88
89 if (parts) {
90 var formNameMap = this.get('formNameMap');
91 var keyValueParts = parts[1].split('&');
92 _.each(keyValueParts, function(keyValuePart, i) {
93 var keyFieldValue = keyValuePart.match(/^([^.]+)(?:\.([^.]+))?=(.*)$/);
94 if (keyFieldValue) {
95 var key = keyFieldValue[1];
96 var field = keyFieldValue[2];
97 var value = keyFieldValue[3];
98 value = value.replace(/(\+|%20)/g, ' ');
99 var impl = formNameMap[key];
100 if (impl) {
101 if (impl instanceof QueryTextField) {
102 impl.set('query', value, skipOptions);
103 } else if (impl.facetType === 'field' && value) {
104 if (field === 'min') {
105 impl.set('queryMin', parseInt(value), skipOptions);
106 } else if (field === 'max') {
107 impl.set('queryMax', parseInt(value), skipOptions);
108 } else if (field === 'items') {
109 var items = impl.get('items');
110 var item = items.get(value);
111 if (!item) {
112 item = new Facets.Facet.Item({key: value, value: 0});
113 items.add(item);
114 item.on('change:checked', function(model, value, options) {
115 this.trigger('item-change', null, null, options);
116 impl.trigger('item-change', null, null, options);
117 }, facets);
118 }
119 item.set('checked', true, skipOptions);
120 }
121 }
122 }
123 }
124 }, this);
125 }
126 },
127 initialize: function(data, options) {
128 options = (options || {});
129 this.set('options', new Backbone.Model({
130 faceting: !!options.faceting,
131 highlighting: !!options.highlighting,
132 showAll: !!options.showAll,
133 }));
134 this._doSearchImmediately = _.bind(function(options) {
135 this.fetch(_.extend({}, options, {
136 data: this.toJSON(),
137 merge: false,
138 traditional: true,
139 }));
140 }, this);
141 this._doSearch = _.debounce(this._doSearchImmediately, 250);
142 this._doSearch = (function(doSearch) {
143 return function(options) {
144 var skipSearch = this._skipSearch || (options || {}).skipSearch;
145 if (!skipSearch) {
146 doSearch(options);
147 }
148 };
149 })(this._doSearch);
150 _.each(this.get('queryFields').values(), function(subQueryModel) {
151 subQueryModel.on('change:query change:queries', function(model, value, options) {
152 this.trigger('change:queryFields', null, null, options);
153 }, this);
154 }, this);
155 this.on('change:queryFields', function(model, value, options) {
156 this.trigger('change:query', null, null, options);
157 }, this);
158 this.get('options').on('change:faceting change:highlighting', function(model, value, options) {
159 this._doSearch(options);
160 }, this);
161 this.on('change:query', function(model, value, options) {
162 // this is silent, which causes the change events to not fire;
163 // this is ok, because the next .on will also see the change
164 // event on query, and resend the search
165 this._doSearch(_.extend({}, options, {resetCurrentPage: true, resetFacets: true, resetOrdering: true}));
166 }, this);
167 this.on('change:pageSize', function(model, value, options) {
168 this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
169 }, this);
170 this.on('change:currentPage', function(model, value, options) {
171 this._doSearch(options);
172 }, this);
173 this.get('facets').on('item-change', function(model, value, options) {
174 this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
175 }, this);
176 this.get('ordering').on('change:value', function(model, value, options) {
177 this._doSearch(_.extend({}, options, {resetCurrentPage: true}));
178 }, this);
179 return Solr.__super__.initialize.apply(this, arguments);
180
181 },
182 parse: function parse(data, options) {
183 if (options.facetSuggestions) {
184 return null;
185 }
186 var skipOptions = _.extend({}, options, {skipSearch: true});
187 if (options.resetCurrentPage) {
188 this.set('currentPage', 1, skipOptions);
189 }
190 if (options.resetFacets) {
191 this.get('facets').resetSearch(skipOptions);
192 }
193 if (options.resetOrdering) {
194 this.get('ordering').set('value', this.get('ordering').get('items').at(0).get('value'), skipOptions);
195 }
196 var facets = this.get('facets');
197 if (this.get('options').get('faceting')) {
198 facets.applyFacetResults(data);
199 } else {
200 facets.resetSearch(options);
201 }
202 var list = [];
203 var highlighting = this.get('options').get('highlighting') ? data.highlighting : {};
204
205 var self = this;
206 _.each(data.response.docs, function(doc, index) {
207 var itemHighlighting = highlighting[doc.id];
208 if (!itemHighlighting) {
209 itemHighlighting = {};
210 }
211 list.push(self.searchParseDoc(doc, index, itemHighlighting));
212 });
213
214 return {
215 initializing: false,
216 initialized: true,
217 results: new Backbone.Collection(list),
218 facets: facets,
219 options: this.get('options'),
220 totalCount: data.response.numFound,
221 queryTime: (data.responseHeader.QTime / 1000).toFixed(2),
222 hasResults: list.length > 0,
223 };
224 },
225 searchParseDoc: function searchParseDoc(doc, index, itemHighlighting) {
226 var fieldsToParse = mergeStaticProps(this.constructor, Solr, {}, 'parsedFieldMap');
227 var result = {};
228 _.each(fieldsToParse, function(value, key) {
229 var parsed;
230 if (_.isFunction(value)) {
231 parsed = value.call(this, doc, index, itemHighlighting, key);
232 } else if (_.isArray(value)) {
233 parsed = [];
234 _.each(value, function(item, i) {
235 var rawValue = doc[item.name];
236 if (rawValue) {
237 var parsedValue = item.parse.call(this, doc, index, itemHighlighting, rawValue, item.name);
238 if (parsedValue) {
239 parsed.push(parsedValue);
240 }
241 }
242 }, this);
243 } else {
244 parsed = doc[value];
245 }
246 result[key] = parsed;
247 }, this);
248 return result;
249 },
250 toJSON: function(options) {
251 if (!options) {
252 options = {};
253 }
254 var facets = this.get('facets');
255 var constructor = this.constructor;
256 var result = {
257 defType: 'edismax',
258 qf: constructor.queryField,
259 wt: 'json',
260 };
261 var ordering = this.get('ordering');
262 var sort = ordering.get('value');
263 if (options.resetOrdering) {
264 sort = ordering.get('items').at(0).get('value');
265 }
266 var currentPage = this.get('currentPage');
267 var pageSize = this.get('pageSize');
268 if (options.resetCurrentPage) {
269 currentPage = 1;
270 }
271 result.sort = sort;
272 result.rows = pageSize;
273 result.start = (currentPage - 1) * pageSize;
274 result.fl = mergeStaticSets(constructor, Solr, [], 'returnFields');
275 if (this.get('options').get('highlighting')) {
276 result.hl = true;
277 result['hl.fl'] = ['content', 'title'].concat(constructor.extraHighlightFields);
278 }
279
280 if (this.get('options').get('faceting')) {
281 var facetFormData = facets.getFacetFormData();
282 var fq = facetFormData.fq;
283 delete(facetFormData.fq);
284 result = _.extend(result, facetFormData);
285 if (fq.length) {
286 if (result.fq) {
287 result.fq = result.fq.concat(fq);
288 } else {
289 result.fq = fq;
290 }
291 }
292 }
293
294 var queryParts = [];
295 var queryFields = this.get('queryFields');
296 _.each(queryFields.keys(), function(queryName) {
297 var subQueryModel = queryFields.get(queryName);
298 var fields = subQueryModel.get('fields');
299 var queries = subQueryModel.get('queries');
300 var query = subQueryModel.get('query');
301 if (query) {
302 queries = queries.concat([query]);
303 }
304 var subQuery = [];
305 for (var i = 0; i < fields.length; i++) {
306 var fieldName = fields[i];
307 for (var j = 0; j < queries.length; j++) {
308 // FIXME: quote the query
309 subQuery.push(fieldName + ':\'' + queries[j] + '\'');
310 }
311 }
312 if (subQuery.length) {
313 queryParts.push(subQuery.join(' OR '));
314 }
315 }, this);
316
317 var query = queryParts.join(' AND ');
318 if (!query) {
319 query = '*:*';
320 if (!this.get('options').get('showAll') && !result.fq) {
321 result.rows = 0;
322 }
323 }
324 var dateBoostField = constructor.dateBoostField;
325 var popularityBoostField = constructor.popularityBoostField;
326 if (result.rows !== 0 && (dateBoostField || popularityBoostField)) {
327 var boostSum = [];
328 if (dateBoostField) {
329 result.dateBoost = 'recip(ms(NOW,' + dateBoostField + '),3.16e-11,1,1)';
330 boostSum.push('$dateBoost');
331 }
332 if (popularityBoostField) {
333 result.popularityBoost = 'def(' + popularityBoostField + ',0)';
334 boostSum.push('$popularityBoost');
335 }
336 result.qq = query;
337 result.q = '{!boost b=sum(' + boostSum.join(',') + ') v=$qq defType=$defType}';
338 } else {
339 result.q = query;
340 }
341 return result;
342 },
343 }, {
344 cleanup: function cleanup(txt) {
345 if (txt) {
346 txt = txt.replace(/&amp;/g, '&');
347 txt = txt.replace(/&#39;/g, '\'');
348 txt = txt.replace(/[^a-zA-Z0-9 -\.,:;%<>\/'"|]/g, ' ');
349 }
350 return txt;
351 },
352 returnFields: [
353 'keywords',
354 'last_modified',
355 'path',
356 'score',
357 'title',
358 ],
359 parsedFieldMap: {
360 content: function(doc, index, itemHighlighting) {
361 var content = itemHighlighting.content;
362 if (content && content.constructor === Array) {
363 content = content.join(' ');
364 }
365 if (!content) {
366 content = '';
367 }
368 return this.constructor.cleanup(content);
369 },
370 keywords: 'keywords',
371 lastModified: 'last_modified',
372 path: 'url',
373 score: 'score',
374 title: function(doc, index, itemHighlighting) {
375 var title;
376 if (itemHighlighting.title) {
377 title = itemHighlighting.title.join(' ');
378 } else {
379 title = doc.title[0];
380 }
381 return this.constructor.cleanup(title);
382 },
383 },
384 });
385 Solr.Facets = Facets;
386 Solr.Pagination = Pagination;
387 return Solr;
388 });