rocketjob_mission_control 1.2.2 → 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,470 @@
1
+ /**
2
+ * sifter.js
3
+ * Copyright (c) 2013 Brian Reavis & contributors
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
6
+ * file except in compliance with the License. You may obtain a copy of the License at:
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under
10
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ * ANY KIND, either express or implied. See the License for the specific language
12
+ * governing permissions and limitations under the License.
13
+ *
14
+ * @author Brian Reavis <brian@thirdroute.com>
15
+ */
16
+
17
+ (function(root, factory) {
18
+ if (typeof define === 'function' && define.amd) {
19
+ define(factory);
20
+ } else if (typeof exports === 'object') {
21
+ module.exports = factory();
22
+ } else {
23
+ root.Sifter = factory();
24
+ }
25
+ }(this, function() {
26
+
27
+ /**
28
+ * Textually searches arrays and hashes of objects
29
+ * by property (or multiple properties). Designed
30
+ * specifically for autocomplete.
31
+ *
32
+ * @constructor
33
+ * @param {array|object} items
34
+ * @param {object} items
35
+ */
36
+ var Sifter = function(items, settings) {
37
+ this.items = items;
38
+ this.settings = settings || {diacritics: true};
39
+ };
40
+
41
+ /**
42
+ * Splits a search string into an array of individual
43
+ * regexps to be used to match results.
44
+ *
45
+ * @param {string} query
46
+ * @returns {array}
47
+ */
48
+ Sifter.prototype.tokenize = function(query) {
49
+ query = trim(String(query || '').toLowerCase());
50
+ if (!query || !query.length) return [];
51
+
52
+ var i, n, regex, letter;
53
+ var tokens = [];
54
+ var words = query.split(/ +/);
55
+
56
+ for (i = 0, n = words.length; i < n; i++) {
57
+ regex = escape_regex(words[i]);
58
+ if (this.settings.diacritics) {
59
+ for (letter in DIACRITICS) {
60
+ if (DIACRITICS.hasOwnProperty(letter)) {
61
+ regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]);
62
+ }
63
+ }
64
+ }
65
+ tokens.push({
66
+ string : words[i],
67
+ regex : new RegExp(regex, 'i')
68
+ });
69
+ }
70
+
71
+ return tokens;
72
+ };
73
+
74
+ /**
75
+ * Iterates over arrays and hashes.
76
+ *
77
+ * ```
78
+ * this.iterator(this.items, function(item, id) {
79
+ * // invoked for each item
80
+ * });
81
+ * ```
82
+ *
83
+ * @param {array|object} object
84
+ */
85
+ Sifter.prototype.iterator = function(object, callback) {
86
+ var iterator;
87
+ if (is_array(object)) {
88
+ iterator = Array.prototype.forEach || function(callback) {
89
+ for (var i = 0, n = this.length; i < n; i++) {
90
+ callback(this[i], i, this);
91
+ }
92
+ };
93
+ } else {
94
+ iterator = function(callback) {
95
+ for (var key in this) {
96
+ if (this.hasOwnProperty(key)) {
97
+ callback(this[key], key, this);
98
+ }
99
+ }
100
+ };
101
+ }
102
+
103
+ iterator.apply(object, [callback]);
104
+ };
105
+
106
+ /**
107
+ * Returns a function to be used to score individual results.
108
+ *
109
+ * Good matches will have a higher score than poor matches.
110
+ * If an item is not a match, 0 will be returned by the function.
111
+ *
112
+ * @param {object|string} search
113
+ * @param {object} options (optional)
114
+ * @returns {function}
115
+ */
116
+ Sifter.prototype.getScoreFunction = function(search, options) {
117
+ var self, fields, tokens, token_count;
118
+
119
+ self = this;
120
+ search = self.prepareSearch(search, options);
121
+ tokens = search.tokens;
122
+ fields = search.options.fields;
123
+ token_count = tokens.length;
124
+
125
+ /**
126
+ * Calculates how close of a match the
127
+ * given value is against a search token.
128
+ *
129
+ * @param {mixed} value
130
+ * @param {object} token
131
+ * @return {number}
132
+ */
133
+ var scoreValue = function(value, token) {
134
+ var score, pos;
135
+
136
+ if (!value) return 0;
137
+ value = String(value || '');
138
+ pos = value.search(token.regex);
139
+ if (pos === -1) return 0;
140
+ score = token.string.length / value.length;
141
+ if (pos === 0) score += 0.5;
142
+ return score;
143
+ };
144
+
145
+ /**
146
+ * Calculates the score of an object
147
+ * against the search query.
148
+ *
149
+ * @param {object} token
150
+ * @param {object} data
151
+ * @return {number}
152
+ */
153
+ var scoreObject = (function() {
154
+ var field_count = fields.length;
155
+ if (!field_count) {
156
+ return function() { return 0; };
157
+ }
158
+ if (field_count === 1) {
159
+ return function(token, data) {
160
+ return scoreValue(data[fields[0]], token);
161
+ };
162
+ }
163
+ return function(token, data) {
164
+ for (var i = 0, sum = 0; i < field_count; i++) {
165
+ sum += scoreValue(data[fields[i]], token);
166
+ }
167
+ return sum / field_count;
168
+ };
169
+ })();
170
+
171
+ if (!token_count) {
172
+ return function() { return 0; };
173
+ }
174
+ if (token_count === 1) {
175
+ return function(data) {
176
+ return scoreObject(tokens[0], data);
177
+ };
178
+ }
179
+
180
+ if (search.options.conjunction === 'and') {
181
+ return function(data) {
182
+ var score;
183
+ for (var i = 0, sum = 0; i < token_count; i++) {
184
+ score = scoreObject(tokens[i], data);
185
+ if (score <= 0) return 0;
186
+ sum += score;
187
+ }
188
+ return sum / token_count;
189
+ };
190
+ } else {
191
+ return function(data) {
192
+ for (var i = 0, sum = 0; i < token_count; i++) {
193
+ sum += scoreObject(tokens[i], data);
194
+ }
195
+ return sum / token_count;
196
+ };
197
+ }
198
+ };
199
+
200
+ /**
201
+ * Returns a function that can be used to compare two
202
+ * results, for sorting purposes. If no sorting should
203
+ * be performed, `null` will be returned.
204
+ *
205
+ * @param {string|object} search
206
+ * @param {object} options
207
+ * @return function(a,b)
208
+ */
209
+ Sifter.prototype.getSortFunction = function(search, options) {
210
+ var i, n, self, field, fields, fields_count, multiplier, multipliers, get_field, implicit_score, sort;
211
+
212
+ self = this;
213
+ search = self.prepareSearch(search, options);
214
+ sort = (!search.query && options.sort_empty) || options.sort;
215
+
216
+ /**
217
+ * Fetches the specified sort field value
218
+ * from a search result item.
219
+ *
220
+ * @param {string} name
221
+ * @param {object} result
222
+ * @return {mixed}
223
+ */
224
+ get_field = function(name, result) {
225
+ if (name === '$score') return result.score;
226
+ return self.items[result.id][name];
227
+ };
228
+
229
+ // parse options
230
+ fields = [];
231
+ if (sort) {
232
+ for (i = 0, n = sort.length; i < n; i++) {
233
+ if (search.query || sort[i].field !== '$score') {
234
+ fields.push(sort[i]);
235
+ }
236
+ }
237
+ }
238
+
239
+ // the "$score" field is implied to be the primary
240
+ // sort field, unless it's manually specified
241
+ if (search.query) {
242
+ implicit_score = true;
243
+ for (i = 0, n = fields.length; i < n; i++) {
244
+ if (fields[i].field === '$score') {
245
+ implicit_score = false;
246
+ break;
247
+ }
248
+ }
249
+ if (implicit_score) {
250
+ fields.unshift({field: '$score', direction: 'desc'});
251
+ }
252
+ } else {
253
+ for (i = 0, n = fields.length; i < n; i++) {
254
+ if (fields[i].field === '$score') {
255
+ fields.splice(i, 1);
256
+ break;
257
+ }
258
+ }
259
+ }
260
+
261
+ multipliers = [];
262
+ for (i = 0, n = fields.length; i < n; i++) {
263
+ multipliers.push(fields[i].direction === 'desc' ? -1 : 1);
264
+ }
265
+
266
+ // build function
267
+ fields_count = fields.length;
268
+ if (!fields_count) {
269
+ return null;
270
+ } else if (fields_count === 1) {
271
+ field = fields[0].field;
272
+ multiplier = multipliers[0];
273
+ return function(a, b) {
274
+ return multiplier * cmp(
275
+ get_field(field, a),
276
+ get_field(field, b)
277
+ );
278
+ };
279
+ } else {
280
+ return function(a, b) {
281
+ var i, result, a_value, b_value, field;
282
+ for (i = 0; i < fields_count; i++) {
283
+ field = fields[i].field;
284
+ result = multipliers[i] * cmp(
285
+ get_field(field, a),
286
+ get_field(field, b)
287
+ );
288
+ if (result) return result;
289
+ }
290
+ return 0;
291
+ };
292
+ }
293
+ };
294
+
295
+ /**
296
+ * Parses a search query and returns an object
297
+ * with tokens and fields ready to be populated
298
+ * with results.
299
+ *
300
+ * @param {string} query
301
+ * @param {object} options
302
+ * @returns {object}
303
+ */
304
+ Sifter.prototype.prepareSearch = function(query, options) {
305
+ if (typeof query === 'object') return query;
306
+
307
+ options = extend({}, options);
308
+
309
+ var option_fields = options.fields;
310
+ var option_sort = options.sort;
311
+ var option_sort_empty = options.sort_empty;
312
+
313
+ if (option_fields && !is_array(option_fields)) options.fields = [option_fields];
314
+ if (option_sort && !is_array(option_sort)) options.sort = [option_sort];
315
+ if (option_sort_empty && !is_array(option_sort_empty)) options.sort_empty = [option_sort_empty];
316
+
317
+ return {
318
+ options : options,
319
+ query : String(query || '').toLowerCase(),
320
+ tokens : this.tokenize(query),
321
+ total : 0,
322
+ items : []
323
+ };
324
+ };
325
+
326
+ /**
327
+ * Searches through all items and returns a sorted array of matches.
328
+ *
329
+ * The `options` parameter can contain:
330
+ *
331
+ * - fields {string|array}
332
+ * - sort {array}
333
+ * - score {function}
334
+ * - filter {bool}
335
+ * - limit {integer}
336
+ *
337
+ * Returns an object containing:
338
+ *
339
+ * - options {object}
340
+ * - query {string}
341
+ * - tokens {array}
342
+ * - total {int}
343
+ * - items {array}
344
+ *
345
+ * @param {string} query
346
+ * @param {object} options
347
+ * @returns {object}
348
+ */
349
+ Sifter.prototype.search = function(query, options) {
350
+ var self = this, value, score, search, calculateScore;
351
+ var fn_sort;
352
+ var fn_score;
353
+
354
+ search = this.prepareSearch(query, options);
355
+ options = search.options;
356
+ query = search.query;
357
+
358
+ // generate result scoring function
359
+ fn_score = options.score || self.getScoreFunction(search);
360
+
361
+ // perform search and sort
362
+ if (query.length) {
363
+ self.iterator(self.items, function(item, id) {
364
+ score = fn_score(item);
365
+ if (options.filter === false || score > 0) {
366
+ search.items.push({'score': score, 'id': id});
367
+ }
368
+ });
369
+ } else {
370
+ self.iterator(self.items, function(item, id) {
371
+ search.items.push({'score': 1, 'id': id});
372
+ });
373
+ }
374
+
375
+ fn_sort = self.getSortFunction(search, options);
376
+ if (fn_sort) search.items.sort(fn_sort);
377
+
378
+ // apply limits
379
+ search.total = search.items.length;
380
+ if (typeof options.limit === 'number') {
381
+ search.items = search.items.slice(0, options.limit);
382
+ }
383
+
384
+ return search;
385
+ };
386
+
387
+ // utilities
388
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
389
+
390
+ var cmp = function(a, b) {
391
+ if (typeof a === 'number' && typeof b === 'number') {
392
+ return a > b ? 1 : (a < b ? -1 : 0);
393
+ }
394
+ a = asciifold(String(a || ''));
395
+ b = asciifold(String(b || ''));
396
+ if (a > b) return 1;
397
+ if (b > a) return -1;
398
+ return 0;
399
+ };
400
+
401
+ var extend = function(a, b) {
402
+ var i, n, k, object;
403
+ for (i = 1, n = arguments.length; i < n; i++) {
404
+ object = arguments[i];
405
+ if (!object) continue;
406
+ for (k in object) {
407
+ if (object.hasOwnProperty(k)) {
408
+ a[k] = object[k];
409
+ }
410
+ }
411
+ }
412
+ return a;
413
+ };
414
+
415
+ var trim = function(str) {
416
+ return (str + '').replace(/^\s+|\s+$|/g, '');
417
+ };
418
+
419
+ var escape_regex = function(str) {
420
+ return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
421
+ };
422
+
423
+ var is_array = Array.isArray || ($ && $.isArray) || function(object) {
424
+ return Object.prototype.toString.call(object) === '[object Array]';
425
+ };
426
+
427
+ var DIACRITICS = {
428
+ 'a': '[aÀÁÂÃÄÅàáâãäåĀāąĄ]',
429
+ 'c': '[cÇçćĆčČ]',
430
+ 'd': '[dđĐďĎð]',
431
+ 'e': '[eÈÉÊËèéêëěĚĒēęĘ]',
432
+ 'i': '[iÌÍÎÏìíîïĪī]',
433
+ 'l': '[lłŁ]',
434
+ 'n': '[nÑñňŇńŃ]',
435
+ 'o': '[oÒÓÔÕÕÖØòóôõöøŌō]',
436
+ 'r': '[rřŘ]',
437
+ 's': '[sŠšśŚ]',
438
+ 't': '[tťŤ]',
439
+ 'u': '[uÙÚÛÜùúûüůŮŪū]',
440
+ 'y': '[yŸÿýÝ]',
441
+ 'z': '[zŽžżŻźŹ]'
442
+ };
443
+
444
+ var asciifold = (function() {
445
+ var i, n, k, chunk;
446
+ var foreignletters = '';
447
+ var lookup = {};
448
+ for (k in DIACRITICS) {
449
+ if (DIACRITICS.hasOwnProperty(k)) {
450
+ chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1);
451
+ foreignletters += chunk;
452
+ for (i = 0, n = chunk.length; i < n; i++) {
453
+ lookup[chunk.charAt(i)] = k;
454
+ }
455
+ }
456
+ }
457
+ var regexp = new RegExp('[' + foreignletters + ']', 'g');
458
+ return function(str) {
459
+ return str.replace(regexp, function(foreignletter) {
460
+ return lookup[foreignletter];
461
+ }).toLowerCase();
462
+ };
463
+ })();
464
+
465
+
466
+ // export
467
+ // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
468
+
469
+ return Sifter;
470
+ }));