2
* @fileOverview Magic Search JS
7
// Allow the module to be pre-defined with additional dependencies
9
angular.module('MagicSearch');
11
angular.module('MagicSearch', []);
14
angular.module('MagicSearch')
15
.directive('magicSearch', function($compile) {
19
facets_param: '@facets',
20
filter_keys: '=filterKeys',
23
templateUrl: function (scope, elem) {
26
controller: function ($scope, $timeout) {
27
$scope.promptString = $scope.strings['prompt'];
28
$scope.currentSearch = [];
29
$scope.initSearch = function() {
30
if (typeof $scope.facets_param === 'string') {
31
// Parse facets JSON and convert to a list of facets.
32
var tmp = $scope.facets_param.replace(/__apos__/g, "\'").replace(/__dquote__/g, '\\"').replace(/__bslash__/g, "\\");
33
$scope.facetsObj = JSON.parse(tmp);
36
// Assume this is a usable javascript object
37
$scope.facetsObj = $scope.facets_param;
39
$scope.facetsSave = $scope.copyFacets($scope.facetsObj);
42
$scope.initFacets = function() {
43
// set facets selected and remove them from facetsObj
44
var initialFacets = window.location.search;
45
if (initialFacets.indexOf('?') === 0) {
46
initialFacets = initialFacets.slice(1);
48
initialFacets = initialFacets.split('&');
49
if (initialFacets.length > 1 || initialFacets[0].length > 0) {
51
$scope.strings['prompt'] = '';
54
angular.forEach(initialFacets, function(facet, idx) {
55
var facetParts = facet.split('=');
56
angular.forEach($scope.facetsObj, function(value, idx) {
57
if (value.name == facetParts[0]) {
58
if (value.options === undefined) {
59
$scope.currentSearch.push({'name':facet, 'label':[value.label, facetParts[1]]});
60
// allow free-form facets to remain
63
angular.forEach(value.options, function(option, idx) {
64
if (option.key == facetParts[1]) {
65
$scope.currentSearch.push({'name':facet, 'label':[value.label, option.label]});
66
if (value.singleton === true) {
67
$scope.deleteFacetEntirely(facetParts);
70
$scope.deleteFacetSelection(facetParts);
78
if ($scope.textSearch !== undefined) {
79
$scope.currentSearch.push({'name':'text='+$scope.textSearch, 'label':[$scope.strings['text'], $scope.textSearch]});
81
$scope.filteredObj = $scope.facetsObj;
83
$scope.copyFacets = function(facets) {
85
for (var i=0; i<facets.length; i++) {
86
var facet = Object.create(facets[i]);
87
if (facets[i].options !== undefined) {
89
for (var j=0; j<facets[i].options.length; j++) {
90
facet.options.push(Object.create(facets[i].options[j]));
97
// removes a facet from the menu
98
$scope.deleteFacetSelection = function(facetParts) {
99
angular.forEach($scope.facetsObj.slice(), function(facet, idx) {
100
if (facet.name == facetParts[0]) {
101
if (facet.options === undefined) {
102
return; // allow free-form facets to remain
104
for (var i=0; i<facet.options.length; i++) {
105
var option = facet.options[i];
106
if (option.key == facetParts[1]) {
107
$scope.facetsObj[idx].options.splice($scope.facetsObj[idx].options.indexOf(option), 1);
110
if (facet.options.length === 0) {
111
$scope.facetsObj.splice($scope.facetsObj.indexOf(facet), 1);
116
$scope.deleteFacetEntirely = function(facetParts) {
117
// remove entire facet
118
angular.forEach($scope.facetsObj.slice(), function(facet, idx) {
119
if (facet.name == facetParts[0]) {
120
$scope.facetsObj.splice($scope.facetsObj.indexOf(facet), 1);
124
$('.search-input').on('keydown', function($event) { // handle ctrl-char input
125
var key = $event.keyCode || $event.charCode;
126
if (key == 9) { // prevent default when we can.
127
$event.preventDefault();
130
$('.search-input').on('keyup', function($event) { // handle ctrl-char input
131
if ($event.metaKey == true) {
134
var searchVal = $('.search-input').val();
135
var key = $event.keyCode || $event.charCode;
136
if (key == 9) { // tab, so select facet if narrowed down to 1
137
if ($scope.facetSelected === undefined) {
138
if ($scope.filteredObj.length != 1) return;
139
$scope.facetClicked(0, '', $scope.filteredObj[0].name);
142
if ($scope.filteredOptions === undefined || $scope.filteredOptions.length != 1) return;
143
$scope.optionClicked(0, '', $scope.filteredOptions[0].key);
146
$timeout(function() {
147
$('.search-input').val('');
151
if (key == 27) { // esc, so cancel and reset everthing
152
$timeout(function() {
154
$('.search-input').val('');
157
var textFilter = $scope.textSearch;
158
if (textFilter === undefined) {
161
$scope.$emit('textSearch', textFilter, $scope.filter_keys);
164
if (key == 13) { // enter, so accept value
165
// if tag search, treat as regular facet
166
if ($scope.facetSelected && $scope.facetSelected.options === undefined) {
167
var curr = $scope.facetSelected;
168
curr.name = curr.name + '=' + searchVal;
169
curr.label[1] = searchVal;
170
$scope.currentSearch.push(curr);
175
// if text search treat as search
177
for (i=0; i<$scope.currentSearch.length; i++) {
178
if ($scope.currentSearch[i].name.indexOf('text') === 0) {
179
$scope.currentSearch.splice(i, 1);
182
$scope.currentSearch.push({'name':'text='+searchVal, 'label':[$scope.strings['text'], searchVal]});
185
$('.search-input').val('');
186
$scope.$emit('textSearch', searchVal, $scope.filter_keys);
187
$scope.textSearch = searchVal;
189
$scope.filteredObj = $scope.facetsObj;
192
if (searchVal === '') {
193
$scope.filteredObj = $scope.facetsObj;
194
$scope.$emit('textSearch', '', $scope.filter_keys);
197
$scope.filterFacets(searchVal);
201
$('.search-input').on('keypress', function($event) { // handle character input
202
var searchVal = $('.search-input').val();
203
var key = $event.which || $event.keyCode || $event.charCode;
204
if (key != 8 && key != 46 && key != 13 && key != 9 && key != 27) {
205
searchVal = searchVal + String.fromCharCode(key).toLowerCase();
207
if (searchVal == ' ') { // space and field is empty, show menu
209
$timeout(function() {
210
$('.search-input').val('');
214
if (searchVal === '') {
215
$scope.filteredObj = $scope.facetsObj;
216
$scope.$emit('textSearch', '', $scope.filter_keys);
219
if (key != 8 && key != 46) {
220
$scope.filterFacets(searchVal);
223
$scope.filterFacets = function(searchVal) {
224
// try filtering facets/options.. if no facets match, do text search
227
if ($scope.facetSelected === undefined) {
228
$scope.filteredObj = $scope.facetsObj;
229
for (i=0; i<$scope.filteredObj.length; i++) {
230
var facet = $scope.filteredObj[i];
231
idx = facet.label.toLowerCase().indexOf(searchVal);
233
label = [facet.label.substring(0, idx), facet.label.substring(idx, idx + searchVal.length), facet.label.substring(idx + searchVal.length)];
234
filtered.push({'name':facet.name, 'label':label, 'options':facet.options});
237
if (filtered.length > 0) {
239
$timeout(function() {
240
$scope.filteredObj = filtered;
244
$scope.$emit('textSearch', searchVal, $scope.filter_keys);
248
else { // assume option search
249
$scope.filteredOptions = $scope.facetOptions;
250
if ($scope.facetOptions === undefined) { // no options, assume free form text facet
253
for (i=0; i<$scope.filteredOptions.length; i++) {
254
var option = $scope.filteredOptions[i];
255
idx = option.label.toLowerCase().indexOf(searchVal);
257
label = [option.label.substring(0, idx), option.label.substring(idx, idx + searchVal.length), option.label.substring(idx + searchVal.length)];
258
filtered.push({'key':option.key, 'label':label});
261
if (filtered.length > 0) {
263
$timeout(function() {
264
$scope.filteredOptions = filtered;
269
// enable text entry when mouse clicked anywhere in search box
270
$('.search-main-area').on("click", function($event) {
271
$('.search-input').trigger("focus");
272
if ($scope.facetSelected === undefined) {
276
// when facet clicked, add 1st part of facet and set up options
277
$scope.facetClicked = function($index, $event, name) {
279
var facet = $scope.filteredObj[$index];
280
var label = facet.label;
281
if (Array.isArray(label)) {
282
label = label.join('');
284
$scope.facetSelected = {'name':facet.name, 'label':[label, '']};
285
if (facet.options !== undefined) {
286
$scope.filteredOptions = $scope.facetOptions = facet.options;
289
$timeout(function() {
290
$('.search-input').val('');
292
$scope.strings['prompt'] = '';
293
$timeout(function() {
294
$('.search-input').focus();
297
// when option clicked, complete facet and send event
298
$scope.optionClicked = function($index, $event, name) {
300
var curr = $scope.facetSelected;
301
curr.name = curr.name + '=' + name;
302
curr.label[1] = $scope.filteredOptions[$index].label;
303
if (Array.isArray(curr.label[1])) {
304
curr.label[1] = curr.label[1].join('');
306
$scope.currentSearch.push(curr);
311
// send event with new query string
312
$scope.emitQuery = function(removed) {
314
for (var i=0; i<$scope.currentSearch.length; i++) {
315
if ($scope.currentSearch[i].name.indexOf('text') !== 0) {
316
if (query.length > 0) query = query + "&";
317
query = query + $scope.currentSearch[i].name;
320
if (removed !== undefined && removed.indexOf('text') === 0) {
321
$scope.$emit('textSearch', '', $scope.filter_keys);
322
$scope.textSearch = undefined
325
$scope.$emit('searchUpdated', query);
326
if ($scope.currentSearch.length > 0) {
327
var newFacet = $scope.currentSearch[$scope.currentSearch.length-1].name;
328
var facetParts = newFacet.split('=');
329
angular.forEach($scope.facetsSave, function(facet, idx) {
330
if (facet.name == facetParts[0]) {
331
if (facet.singleton === true) {
332
$scope.deleteFacetEntirely(facetParts);
335
$scope.deleteFacetSelection(facetParts);
342
// remove facet and either update filter or search
343
$scope.removeFacet = function($index, $event) {
344
var removed = $scope.currentSearch[$index].name;
345
$scope.currentSearch.splice($index, 1);
346
if ($scope.facetSelected === undefined) {
347
$scope.emitQuery(removed);
351
$('.search-input').val('');
353
if ($scope.currentSearch.length == 0) {
354
$scope.strings['prompt'] = $scope.promptString;
356
// re-init to restore facets cleanly
357
$scope.facetsObj = $scope.copyFacets($scope.facetsSave);
358
$scope.currentSearch = [];
361
// clear entire searchbar
362
$scope.clearSearch = function() {
363
if ($scope.currentSearch.length > 0) {
364
$scope.currentSearch = [];
365
$scope.facetsObj = $scope.copyFacets($scope.facetsSave);
367
$scope.$emit('searchUpdated', '');
368
$scope.$emit('textSearch', '', $scope.filter_keys);
369
$scope.strings['prompt'] = $scope.promptString;
372
$scope.isMatchLabel = function(label) {
373
return Array.isArray(label);
375
$scope.resetState = function() {
376
$('.search-input').val('');
377
$scope.filteredObj = $scope.facetsObj;
378
$scope.facetSelected = undefined;
379
$scope.facetOptions = undefined;
380
$scope.filteredOptions = undefined
382
// showMenu and hideMenu depend on foundation's dropdown. They need
383
// to be modified to work with another dropdown implemenation (i.e. bootstrap)
384
$scope.showMenu = function() {
385
$timeout(function() {
386
if ($('#facet-drop').hasClass('open') === false) {
387
$('.search-input').trigger('click');
391
$scope.hideMenu = function() {
392
$(document).foundation('dropdown', 'closeall');