forked from eikes/facetedsearch
-
Notifications
You must be signed in to change notification settings - Fork 0
/
facetedsearch.js
349 lines (330 loc) · 12 KB
/
facetedsearch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
;(function(){
/**
* Please note that when passing in custom templates for
* listItemTemplate and orderByTemplate to keep the classes as
* they are used in the code at other locations as well.
*/
var defaults = {
items : [{a:2,b:1,c:2},{a:2,b:2,c:1},{a:1,b:1,c:1},{a:3,b:3,c:1}],
facets : {'a': 'Title A', 'b': 'Title B', 'c': 'Title C'},
resultSelector : '#results',
facetSelector : '#facets',
facetContainer : '<div class=facetsearch id=<%= id %> ></div>',
facetTitleTemplate : '<h3 class=facettitle><%= title %></h3>',
facetListContainer : '<div class=facetlist></div>',
listItemTemplate : '<div class=facetitem id="<%= id %>"><%= name %> <span class=facetitemcount>(<%= count %>)</span></div>',
bottomContainer : '<div class=bottomline></div>',
orderByTemplate : '<div class=orderby><span class="orderby-title">Sort by: </span><ul><% _.each(options, function(value, key) { %>'+
'<li class=orderbyitem id=orderby_<%= key %>>'+
'<%= value %> </li> <% }); %></ul></div>',
countTemplate : '<div class=facettotalcount><%= count %> Results</div>',
deselectTemplate : '<div class=deselectstartover>Deselect all filters</div>',
resultTemplate : '<div class=facetresultbox><%= name %></div>',
noResults : '<div class=results>Sorry, but no items match these criteria</div>',
orderByOptions : {'a': 'by A', 'b': 'by B', 'RANDOM': 'by random'},
state : {
orderBy : false,
filters : {}
},
showMoreTemplate : '<a id=showmorebutton>Show more</a>',
enablePagination : true,
paginationCount : 20
}
/**
* This is the first function / variable that gets exported into the
* jQuery namespace. Pass in your own settings (see above) to initialize
* the faceted search
*/
var settings = {};
jQuery.facetelize = function(usersettings) {
$.extend(settings, defaults, usersettings);
settings.currentResults = [];
settings.facetStore = {};
$(settings.facetSelector).data("settings", settings);
initFacetCount();
filter();
order();
createFacetUI();
updateResults();
}
/**
* This is the second function / variable that gets exported into the
* jQuery namespace. Use it to update everything if you messed with
* the settings object
*/
jQuery.facetUpdate = function() {
filter();
order();
updateFacetUI();
updateResults();
}
/**
* The following section contains the logic of the faceted search
*/
/**
* initializes all facets and their individual filters
*/
function initFacetCount() {
_.each(settings.facets, function(facettitle, facet) {
settings.facetStore[facet] = {};
});
_.each(settings.items, function(item) {
// intialize the count to be zero
_.each(settings.facets, function(facettitle, facet) {
if ($.isArray(item[facet])) {
_.each(item[facet], function(facetitem) {
settings.facetStore[facet][facetitem] = settings.facetStore[facet][facetitem] || {count: 0, id: _.uniqueId("facet_")}
});
} else {
if (item[facet] !== undefined) {
settings.facetStore[facet][item[facet]] = settings.facetStore[facet][item[facet]] || {count: 0, id: _.uniqueId("facet_")}
}
}
});
});
// sort it:
_.each(settings.facetStore, function(facet, facettitle) {
var sorted = _.keys(settings.facetStore[facettitle]).sort();
if (settings.facetSortOption && settings.facetSortOption[facettitle]) {
sorted = _.union(settings.facetSortOption[facettitle], sorted);
}
var sortedstore = {};
_.each(sorted, function(el) {
sortedstore[el] = settings.facetStore[facettitle][el];
});
settings.facetStore[facettitle] = sortedstore;
});
}
/**
* resets the facet count
*/
function resetFacetCount() {
_.each(settings.facetStore, function(items, facetname) {
_.each(items, function(value, itemname) {
settings.facetStore[facetname][itemname].count = 0;
});
});
}
/**
* Filters all items from the settings according to the currently
* set filters and stores the results in the settings.currentResults.
* The number of items in each filter from each facet is also updated
*/
function filter() {
// first apply the filters to the items
settings.currentResults = _.select(settings.items, function(item) {
var filtersApply = true;
_.each(settings.state.filters, function(filter, facet) {
if ($.isArray(item[facet])) {
var inters = _.intersect(item[facet], filter);
if (inters.length == 0) {
filtersApply = false;
}
} else {
if (filter.length && _.indexOf(filter, item[facet]) == -1) {
filtersApply = false;
}
}
});
return filtersApply;
});
// Update the count for each facet and item:
// intialize the count to be zero
resetFacetCount();
// then reduce the items to get the current count for each facet
_.each(settings.facets, function(facettitle, facet) {
_.each(settings.currentResults, function(item) {
if ($.isArray(item[facet])) {
_.each(item[facet], function(facetitem) {
settings.facetStore[facet][facetitem].count += 1;
});
} else {
if (item[facet] !== undefined) {
settings.facetStore[facet][item[facet]].count += 1;
}
}
});
});
// remove confusing 0 from facets where a filter has been set
_.each(settings.state.filters, function(filters, facettitle) {
_.each(settings.facetStore[facettitle], function(facet) {
if (facet.count == 0 && settings.state.filters[facettitle].length) facet.count = "+";
});
});
settings.state.shownResults = 0;
}
/**
* Orders the currentResults according to the settings.state.orderBy variable
*/
function order() {
if (settings.state.orderBy) {
$(".activeorderby").removeClass("activeorderby");
$('#orderby_'+settings.state.orderBy).addClass("activeorderby");
settings.currentResults = _.sortBy(settings.currentResults, function(item) {
if (settings.state.orderBy == 'RANDOM') {
return Math.random()*10000;
} else {
return item[settings.state.orderBy];
}
});
}
}
/**
* The given facetname and filtername are activated or deactivated
* depending on what they were beforehand. This causes the items to
* be filtered again and the UI is updated accordingly.
*/
function toggleFilter(key, value) {
settings.state.filters[key] = settings.state.filters[key] || [] ;
if (_.indexOf(settings.state.filters[key], value) == -1) {
settings.state.filters[key].push(value);
} else {
settings.state.filters[key] = _.without(settings.state.filters[key], value);
if (settings.state.filters[key].length == 0) {
delete settings.state.filters[key];
}
}
filter();
}
/**
* The following section contains the presentation of the faceted search
*/
/**
* This function is only called once, it creates the facets ui.
*/
function createFacetUI() {
var itemtemplate = _.template(settings.listItemTemplate);
var titletemplate = _.template(settings.facetTitleTemplate);
var containertemplate = _.template(settings.facetContainer);
$(settings.facetSelector).html("");
_.each(settings.facets, function(facettitle, facet) {
var facetHtml = $(containertemplate({id: facet}));
var facetItem = {title: facettitle};
var facetItemHtml = $(titletemplate(facetItem));
facetHtml.append(facetItemHtml);
var facetlist = $(settings.facetListContainer);
_.each(settings.facetStore[facet], function(filter, filtername){
var item = {id: filter.id, name: filtername, count: filter.count};
var filteritem = $(itemtemplate(item));
if (_.indexOf(settings.state.filters[facet], filtername) >= 0) {
filteritem.addClass("activefacet");
}
facetlist.append(filteritem);
});
facetHtml.append(facetlist);
$(settings.facetSelector).append(facetHtml);
});
// add the click event handler to each facet item:
$('.facetitem').click(function(event){
var filter = getFilterById(this.id);
toggleFilter(filter.facetname, filter.filtername);
$(settings.facetSelector).trigger("facetedsearchfacetclick", filter);
order();
updateFacetUI();
updateResults();
});
// Append total result count
var bottom = $(settings.bottomContainer);
countHtml = _.template(settings.countTemplate, {count: settings.currentResults.length});
$(bottom).append(countHtml);
// generate the "order by" options:
var ordertemplate = _.template(settings.orderByTemplate);
var itemHtml = $(ordertemplate({'options': settings.orderByOptions}));
$(bottom).append(itemHtml);
$(settings.facetSelector).append(bottom);
$('.orderbyitem').each(function(){
var id = this.id.substr(8);
if (settings.state.orderBy == id) {
$(this).addClass("activeorderby");
}
});
// add the click event handler to each "order by" item:
$('.orderbyitem').click(function(event){
var id = this.id.substr(8);
settings.state.orderBy = id;
$(settings.facetSelector).trigger("facetedsearchorderby", id);
settings.state.shownResults = 0;
order();
updateResults();
});
// Append deselect filters button
var deselect = $(settings.deselectTemplate).click(function(event){
settings.state.filters = {};
jQuery.facetUpdate();
});
$(bottom).append(deselect);
$(settings.facetSelector).trigger("facetuicreated");
}
/**
* get a facetname and filtername by the unique id that is created in the beginning
*/
function getFilterById(id) {
var result = false;
_.each(settings.facetStore, function(facet, facetname) {
_.each(facet, function(filter, filtername){
if (filter.id == id) {
result = {'facetname': facetname, 'filtername': filtername};
}
});
});
return result;
}
/**
* This function is only called whenever a filter has been added or removed
* It adds a class to the active filters and shows the correct number for each
*/
function updateFacetUI() {
var itemtemplate = _.template(settings.listItemTemplate);
_.each(settings.facetStore, function(facet, facetname) {
_.each(facet, function(filter, filtername){
var item = {id: filter.id, name: filtername, count: filter.count};
var filteritem = $(itemtemplate(item)).html();
$("#"+filter.id).html(filteritem);
if (settings.state.filters[facetname] && _.indexOf(settings.state.filters[facetname], filtername) >= 0) {
$("#"+filter.id).addClass("activefacet");
} else {
$("#"+filter.id).removeClass("activefacet");
}
});
});
countHtml = _.template(settings.countTemplate, {count: settings.currentResults.length});
$(settings.facetSelector + ' .facettotalcount').replaceWith(countHtml);
}
/**
* Updates the the list of results according to the filters that have been set
*/
function updateResults() {
$(settings.resultSelector).html(settings.currentResults.length == 0 ? settings.noResults : "");
showMoreResults();
}
var moreButton;
function showMoreResults() {
var showNowCount =
settings.enablePagination ?
Math.min(settings.currentResults.length - settings.state.shownResults, settings.paginationCount) :
settings.currentResults.length;
var itemHtml = "";
var template = _.template(settings.resultTemplate);
for (var i = settings.state.shownResults; i < settings.state.shownResults + showNowCount; i++) {
var item = $.extend(settings.currentResults[i], {
totalItemNr : i,
batchItemNr : i - settings.state.shownResults,
batchItemCount : showNowCount
});
var itemHtml = itemHtml + template(item);
}
$(settings.resultSelector).append(itemHtml);
if (!moreButton) {
moreButton = $(settings.showMoreTemplate).click(showMoreResults);
$(settings.resultSelector).after(moreButton);
}
if (settings.state.shownResults == 0) {
moreButton.show();
}
settings.state.shownResults += showNowCount;
if (settings.state.shownResults == settings.currentResults.length) {
$(moreButton).hide();
}
$(settings.resultSelector).trigger("facetedsearchresultupdate");
}
})();