'
206 | ].join('\n'),
207 | suggestion: function(value) {
208 | if (value.post_topic_name != undefined) {
209 | return `
210 |
211 |
212 |
213 |
214 | ${value.post_topic_name}
215 |
216 |
217 | ${value.post_topic_view}
218 |
219 |
220 |
228 |
229 |
${value.post_author}:
${value.post_pre}
230 |
231 |
232 |
`
233 | }else{
234 | return '
'
235 |
236 | }
237 | }
238 | }
239 | },{
240 | name: 'users-tags',
241 | displayKey: 'value',
242 | source: discourseUsers,
243 | async: true,
244 | templates: {
245 | empty: "",
246 | suggestion: function(value) {
247 | if (value.user_username != undefined){
248 | return `
249 |
`
263 | }else{
264 | return '
'
265 | }
266 | }
267 | }
268 |
269 | },{
270 | name: 'emp',
271 | displayKey: 'value',
272 | source: discourseTags,
273 | async: true,
274 | templates: {
275 | empty: "",
276 | suggestion: function(value) {
277 | if (value.tag_name != undefined) {
278 | return `
279 |
`
287 | }else{
288 | return '
'
289 | }
290 | }
291 | }
292 |
293 | }).on('typeahead:selected', function(event, datum) {
294 | window.location = datum.url;
295 | }).on('typeahead:asyncrequest', function() {
296 | $('.Typeahead-spinner').show();
297 | }).on('typeahead:asynccancel typeahead:asyncreceive', function() {
298 | $('.Typeahead-spinner').hide();
299 | }).on({
300 | 'typeahead:render': function (event, datum) {
301 | console.log(Array.prototype.slice.call(arguments, 1)[0]);
302 |
303 | if (datum.length > 0 && Array.prototype.slice.call(arguments, 1)[2] == 'emp') {
304 | $('.tt-dataset-users-tags').delay(500).queue(function (next) {
305 | for (var _i = 0; _i < datum.length; _i++) {
306 | var tags = datum[_i];
307 | var $tags = $('
');
308 | // Append to user-tag list.
309 | $(this).append($tags);
310 | next();
311 | }
312 | });
313 | }
314 | $('.tt-dataset-emp').empty();
315 | }
316 | });
317 |
318 |
319 | $("#search-box").on('focus', function (event) {
320 | $(this).select();
321 | });
322 | }
323 | }
324 |
325 |
--------------------------------------------------------------------------------
/assets/javascripts/initializers/discourse-elasticsearch.js.es6:
--------------------------------------------------------------------------------
1 | import { h } from 'virtual-dom';
2 | import { on } from 'ember-addons/ember-computed-decorators';
3 | import DiscourseURL from 'discourse/lib/url';
4 | import { withPluginApi } from 'discourse/lib/plugin-api';
5 | import discourseAutocomplete from './discourse-autocomplete';
6 |
7 | function elasticsearch(api){
8 | const container = api.container;
9 | const siteSettings = container.lookup("site-settings:main");
10 | api.modifyClass('component:site-header', {
11 | @on("didInsertElement")
12 | initializeElk() {
13 | this._super();
14 | var elasticsearch_address = this.siteSettings.elasticsearch_server_ip + ":" + this.siteSettings.elasticsearch_server_port;
15 | if (this.siteSettings.elasticsearch_enabled) {
16 | $("body").addClass("elasticsearch-enabled");
17 | if (!this.siteSettings.elasticsearch_server_port) {
18 | elasticsearch_address = this.siteSettings.elasticsearch_server_ip;
19 | }
20 | setTimeout(() => {
21 | discourseAutocomplete._initialize({
22 | elasticsearch_address: elasticsearch_address
23 | });
24 | }, 100);
25 | }
26 | }
27 | });
28 |
29 | api.createWidget('es', {
30 | tagName: 'li.es-holder',
31 | html() {
32 | return [
33 | h('form', {
34 | action: '/search',
35 | method: 'GET'
36 | }, [
37 | h('input.es-input#search-box', {
38 | name: "q",
39 | placeholder: "Search the forum...",
40 | autocomplete: "off"
41 | }),
42 | h('img.Typeahead-spinner',{
43 | src: "https://hugelolcdn.com/comments/1225799.gif"
44 | })
45 | ])
46 | ];
47 | }
48 | });
49 |
50 | api.decorateWidget('header-icons:before', function(helper) {
51 | if (helper.widget.siteSettings.elasticsearch_enabled) {
52 | return helper.attach('es');
53 | }
54 | });
55 | }
56 |
57 | export default {
58 | name : "discourse-elasticsearch",
59 | initialize(container) {
60 | withPluginApi('0.8.8', api => elasticsearch(api, container));
61 |
62 | }
63 | }
--------------------------------------------------------------------------------
/assets/lib/typehead.bundle.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * typeahead.js 1.2.0
3 | * https://github.com/twitter/typeahead.js
4 | * Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
5 | */
6 |
7 | (function(root, factory) {
8 | if (typeof define === "function" && define.amd) {
9 | define([ "jquery" ], function(a0) {
10 | return root["Bloodhound"] = factory(a0);
11 | });
12 | } else if (typeof exports === "object") {
13 | module.exports = factory(require("jquery"));
14 | } else {
15 | root["Bloodhound"] = factory(root["jQuery"]);
16 | }
17 | })(this, function($) {
18 | var _ = function() {
19 | "use strict";
20 | return {
21 | isMsie: function() {
22 | return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
23 | },
24 | isBlankString: function(str) {
25 | return !str || /^\s*$/.test(str);
26 | },
27 | escapeRegExChars: function(str) {
28 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
29 | },
30 | isString: function(obj) {
31 | return typeof obj === "string";
32 | },
33 | isNumber: function(obj) {
34 | return typeof obj === "number";
35 | },
36 | isArray: $.isArray,
37 | isFunction: $.isFunction,
38 | isObject: $.isPlainObject,
39 | isUndefined: function(obj) {
40 | return typeof obj === "undefined";
41 | },
42 | isElement: function(obj) {
43 | return !!(obj && obj.nodeType === 1);
44 | },
45 | isJQuery: function(obj) {
46 | return obj instanceof $;
47 | },
48 | toStr: function toStr(s) {
49 | return _.isUndefined(s) || s === null ? "" : s + "";
50 | },
51 | bind: $.proxy,
52 | each: function(collection, cb) {
53 | $.each(collection, reverseArgs);
54 | function reverseArgs(index, value) {
55 | return cb(value, index);
56 | }
57 | },
58 | map: $.map,
59 | filter: $.grep,
60 | every: function(obj, test) {
61 | var result = true;
62 | if (!obj) {
63 | return result;
64 | }
65 | $.each(obj, function(key, val) {
66 | if (!(result = test.call(null, val, key, obj))) {
67 | return false;
68 | }
69 | });
70 | return !!result;
71 | },
72 | some: function(obj, test) {
73 | var result = false;
74 | if (!obj) {
75 | return result;
76 | }
77 | $.each(obj, function(key, val) {
78 | if (result = test.call(null, val, key, obj)) {
79 | return false;
80 | }
81 | });
82 | return !!result;
83 | },
84 | mixin: $.extend,
85 | identity: function(x) {
86 | return x;
87 | },
88 | clone: function(obj) {
89 | return $.extend(true, {}, obj);
90 | },
91 | getIdGenerator: function() {
92 | var counter = 0;
93 | return function() {
94 | return counter++;
95 | };
96 | },
97 | templatify: function templatify(obj) {
98 | return $.isFunction(obj) ? obj : template;
99 | function template() {
100 | return String(obj);
101 | }
102 | },
103 | defer: function(fn) {
104 | setTimeout(fn, 0);
105 | },
106 | debounce: function(func, wait, immediate) {
107 | var timeout, result;
108 | return function() {
109 | var context = this, args = arguments, later, callNow;
110 | later = function() {
111 | timeout = null;
112 | if (!immediate) {
113 | result = func.apply(context, args);
114 | }
115 | };
116 | callNow = immediate && !timeout;
117 | clearTimeout(timeout);
118 | timeout = setTimeout(later, wait);
119 | if (callNow) {
120 | result = func.apply(context, args);
121 | }
122 | return result;
123 | };
124 | },
125 | throttle: function(func, wait) {
126 | var context, args, timeout, result, previous, later;
127 | previous = 0;
128 | later = function() {
129 | previous = new Date();
130 | timeout = null;
131 | result = func.apply(context, args);
132 | };
133 | return function() {
134 | var now = new Date(), remaining = wait - (now - previous);
135 | context = this;
136 | args = arguments;
137 | if (remaining <= 0) {
138 | clearTimeout(timeout);
139 | timeout = null;
140 | previous = now;
141 | result = func.apply(context, args);
142 | } else if (!timeout) {
143 | timeout = setTimeout(later, remaining);
144 | }
145 | return result;
146 | };
147 | },
148 | stringify: function(val) {
149 | return _.isString(val) ? val : JSON.stringify(val);
150 | },
151 | guid: function() {
152 | function _p8(s) {
153 | var p = (Math.random().toString(16) + "000000000").substr(2, 8);
154 | return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
155 | }
156 | return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
157 | },
158 | noop: function() {}
159 | };
160 | }();
161 | var VERSION = "1.2.0";
162 | var tokenizers = function() {
163 | "use strict";
164 | return {
165 | nonword: nonword,
166 | whitespace: whitespace,
167 | ngram: ngram,
168 | obj: {
169 | nonword: getObjTokenizer(nonword),
170 | whitespace: getObjTokenizer(whitespace),
171 | ngram: getObjTokenizer(ngram)
172 | }
173 | };
174 | function whitespace(str) {
175 | str = _.toStr(str);
176 | return str ? str.split(/\s+/) : [];
177 | }
178 | function nonword(str) {
179 | str = _.toStr(str);
180 | return str ? str.split(/\W+/) : [];
181 | }
182 | function ngram(str) {
183 | str = _.toStr(str);
184 | var tokens = [], word = "";
185 | _.each(str.split(""), function(char) {
186 | if (char.match(/\s+/)) {
187 | word = "";
188 | } else {
189 | tokens.push(word + char);
190 | word += char;
191 | }
192 | });
193 | return tokens;
194 | }
195 | function getObjTokenizer(tokenizer) {
196 | return function setKey(keys) {
197 | keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
198 | return function tokenize(o) {
199 | var tokens = [];
200 | _.each(keys, function(k) {
201 | tokens = tokens.concat(tokenizer(_.toStr(o[k])));
202 | });
203 | return tokens;
204 | };
205 | };
206 | }
207 | }();
208 | var LruCache = function() {
209 | "use strict";
210 | function LruCache(maxSize) {
211 | this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
212 | this.reset();
213 | if (this.maxSize <= 0) {
214 | this.set = this.get = $.noop;
215 | }
216 | }
217 | _.mixin(LruCache.prototype, {
218 | set: function set(key, val) {
219 | var tailItem = this.list.tail, node;
220 | if (this.size >= this.maxSize) {
221 | this.list.remove(tailItem);
222 | delete this.hash[tailItem.key];
223 | this.size--;
224 | }
225 | if (node = this.hash[key]) {
226 | node.val = val;
227 | this.list.moveToFront(node);
228 | } else {
229 | node = new Node(key, val);
230 | this.list.add(node);
231 | this.hash[key] = node;
232 | this.size++;
233 | }
234 | },
235 | get: function get(key) {
236 | var node = this.hash[key];
237 | if (node) {
238 | this.list.moveToFront(node);
239 | return node.val;
240 | }
241 | },
242 | reset: function reset() {
243 | this.size = 0;
244 | this.hash = {};
245 | this.list = new List();
246 | }
247 | });
248 | function List() {
249 | this.head = this.tail = null;
250 | }
251 | _.mixin(List.prototype, {
252 | add: function add(node) {
253 | if (this.head) {
254 | node.next = this.head;
255 | this.head.prev = node;
256 | }
257 | this.head = node;
258 | this.tail = this.tail || node;
259 | },
260 | remove: function remove(node) {
261 | node.prev ? node.prev.next = node.next : this.head = node.next;
262 | node.next ? node.next.prev = node.prev : this.tail = node.prev;
263 | },
264 | moveToFront: function(node) {
265 | this.remove(node);
266 | this.add(node);
267 | }
268 | });
269 | function Node(key, val) {
270 | this.key = key;
271 | this.val = val;
272 | this.prev = this.next = null;
273 | }
274 | return LruCache;
275 | }();
276 | var PersistentStorage = function() {
277 | "use strict";
278 | var LOCAL_STORAGE;
279 | try {
280 | LOCAL_STORAGE = window.localStorage;
281 | LOCAL_STORAGE.setItem("~~~", "!");
282 | LOCAL_STORAGE.removeItem("~~~");
283 | } catch (err) {
284 | LOCAL_STORAGE = null;
285 | }
286 | function PersistentStorage(namespace, override) {
287 | this.prefix = [ "__", namespace, "__" ].join("");
288 | this.ttlKey = "__ttl__";
289 | this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
290 | this.ls = override || LOCAL_STORAGE;
291 | !this.ls && this._noop();
292 | }
293 | _.mixin(PersistentStorage.prototype, {
294 | _prefix: function(key) {
295 | return this.prefix + key;
296 | },
297 | _ttlKey: function(key) {
298 | return this._prefix(key) + this.ttlKey;
299 | },
300 | _noop: function() {
301 | this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
302 | },
303 | _safeSet: function(key, val) {
304 | try {
305 | this.ls.setItem(key, val);
306 | } catch (err) {
307 | if (err.name === "QuotaExceededError") {
308 | this.clear();
309 | this._noop();
310 | }
311 | }
312 | },
313 | get: function(key) {
314 | if (this.isExpired(key)) {
315 | this.remove(key);
316 | }
317 | return decode(this.ls.getItem(this._prefix(key)));
318 | },
319 | set: function(key, val, ttl) {
320 | if (_.isNumber(ttl)) {
321 | this._safeSet(this._ttlKey(key), encode(now() + ttl));
322 | } else {
323 | this.ls.removeItem(this._ttlKey(key));
324 | }
325 | return this._safeSet(this._prefix(key), encode(val));
326 | },
327 | remove: function(key) {
328 | this.ls.removeItem(this._ttlKey(key));
329 | this.ls.removeItem(this._prefix(key));
330 | return this;
331 | },
332 | clear: function() {
333 | var i, keys = gatherMatchingKeys(this.keyMatcher);
334 | for (i = keys.length; i--; ) {
335 | this.remove(keys[i]);
336 | }
337 | return this;
338 | },
339 | isExpired: function(key) {
340 | var ttl = decode(this.ls.getItem(this._ttlKey(key)));
341 | return _.isNumber(ttl) && now() > ttl ? true : false;
342 | }
343 | });
344 | return PersistentStorage;
345 | function now() {
346 | return new Date().getTime();
347 | }
348 | function encode(val) {
349 | return JSON.stringify(_.isUndefined(val) ? null : val);
350 | }
351 | function decode(val) {
352 | return $.parseJSON(val);
353 | }
354 | function gatherMatchingKeys(keyMatcher) {
355 | var i, key, keys = [], len = LOCAL_STORAGE.length;
356 | for (i = 0; i < len; i++) {
357 | if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
358 | keys.push(key.replace(keyMatcher, ""));
359 | }
360 | }
361 | return keys;
362 | }
363 | }();
364 | var Transport = function() {
365 | "use strict";
366 | var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
367 | function Transport(o) {
368 | o = o || {};
369 | this.maxPendingRequests = o.maxPendingRequests || 6;
370 | this.cancelled = false;
371 | this.lastReq = null;
372 | this._send = o.transport;
373 | this._get = o.limiter ? o.limiter(this._get) : this._get;
374 | this._cache = o.cache === false ? new LruCache(0) : sharedCache;
375 | }
376 | Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
377 | this.maxPendingRequests = num;
378 | };
379 | Transport.resetCache = function resetCache() {
380 | sharedCache.reset();
381 | };
382 | _.mixin(Transport.prototype, {
383 | _fingerprint: function fingerprint(o) {
384 | o = o || {};
385 | return o.url + o.type + $.param(o.data || {});
386 | },
387 | _get: function(o, cb) {
388 | var that = this, fingerprint, jqXhr;
389 | fingerprint = this._fingerprint(o);
390 | if (this.cancelled || fingerprint !== this.lastReq) {
391 | return;
392 | }
393 | if (jqXhr = pendingRequests[fingerprint]) {
394 | jqXhr.done(done).fail(fail);
395 | } else if (pendingRequestsCount < this.maxPendingRequests) {
396 | pendingRequestsCount++;
397 | pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
398 | } else {
399 | this.onDeckRequestArgs = [].slice.call(arguments, 0);
400 | }
401 | function done(resp) {
402 | cb(null, resp);
403 | that._cache.set(fingerprint, resp);
404 | }
405 | function fail() {
406 | cb(true);
407 | }
408 | function always() {
409 | pendingRequestsCount--;
410 | delete pendingRequests[fingerprint];
411 | if (that.onDeckRequestArgs) {
412 | that._get.apply(that, that.onDeckRequestArgs);
413 | that.onDeckRequestArgs = null;
414 | }
415 | }
416 | },
417 | get: function(o, cb) {
418 | var resp, fingerprint;
419 | cb = cb || $.noop;
420 | o = _.isString(o) ? {
421 | url: o
422 | } : o || {};
423 | fingerprint = this._fingerprint(o);
424 | this.cancelled = false;
425 | this.lastReq = fingerprint;
426 | if (resp = this._cache.get(fingerprint)) {
427 | cb(null, resp);
428 | } else {
429 | this._get(o, cb);
430 | }
431 | },
432 | cancel: function() {
433 | this.cancelled = true;
434 | }
435 | });
436 | return Transport;
437 | }();
438 | var SearchIndex = window.SearchIndex = function() {
439 | "use strict";
440 | var CHILDREN = "c", IDS = "i";
441 | function SearchIndex(o) {
442 | o = o || {};
443 | if (!o.datumTokenizer || !o.queryTokenizer) {
444 | $.error("datumTokenizer and queryTokenizer are both required");
445 | }
446 | this.identify = o.identify || _.stringify;
447 | this.datumTokenizer = o.datumTokenizer;
448 | this.queryTokenizer = o.queryTokenizer;
449 | this.matchAnyQueryToken = o.matchAnyQueryToken;
450 | this.reset();
451 | }
452 | _.mixin(SearchIndex.prototype, {
453 | bootstrap: function bootstrap(o) {
454 | this.datums = o.datums;
455 | this.trie = o.trie;
456 | },
457 | add: function(data) {
458 | var that = this;
459 | data = _.isArray(data) ? data : [ data ];
460 | _.each(data, function(datum) {
461 | var id, tokens;
462 | that.datums[id = that.identify(datum)] = datum;
463 | tokens = normalizeTokens(that.datumTokenizer(datum));
464 | _.each(tokens, function(token) {
465 | var node, chars, ch;
466 | node = that.trie;
467 | chars = token.split("");
468 | while (ch = chars.shift()) {
469 | node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
470 | node[IDS].push(id);
471 | }
472 | });
473 | });
474 | },
475 | get: function get(ids) {
476 | var that = this;
477 | return _.map(ids, function(id) {
478 | return that.datums[id];
479 | });
480 | },
481 | search: function search(query) {
482 | var that = this, tokens, matches;
483 | tokens = normalizeTokens(this.queryTokenizer(query));
484 | _.each(tokens, function(token) {
485 | var node, chars, ch, ids;
486 | if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
487 | return false;
488 | }
489 | node = that.trie;
490 | chars = token.split("");
491 | while (node && (ch = chars.shift())) {
492 | node = node[CHILDREN][ch];
493 | }
494 | if (node && chars.length === 0) {
495 | ids = node[IDS].slice(0);
496 | matches = matches ? getIntersection(matches, ids) : ids;
497 | } else {
498 | if (!that.matchAnyQueryToken) {
499 | matches = [];
500 | return false;
501 | }
502 | }
503 | });
504 | return matches ? _.map(unique(matches), function(id) {
505 | return that.datums[id];
506 | }) : [];
507 | },
508 | all: function all() {
509 | var values = [];
510 | for (var key in this.datums) {
511 | values.push(this.datums[key]);
512 | }
513 | return values;
514 | },
515 | reset: function reset() {
516 | this.datums = {};
517 | this.trie = newNode();
518 | },
519 | serialize: function serialize() {
520 | return {
521 | datums: this.datums,
522 | trie: this.trie
523 | };
524 | }
525 | });
526 | return SearchIndex;
527 | function normalizeTokens(tokens) {
528 | tokens = _.filter(tokens, function(token) {
529 | return !!token;
530 | });
531 | tokens = _.map(tokens, function(token) {
532 | return token.toLowerCase();
533 | });
534 | return tokens;
535 | }
536 | function newNode() {
537 | var node = {};
538 | node[IDS] = [];
539 | node[CHILDREN] = {};
540 | return node;
541 | }
542 | function unique(array) {
543 | var seen = {}, uniques = [];
544 | for (var i = 0, len = array.length; i < len; i++) {
545 | if (!seen[array[i]]) {
546 | seen[array[i]] = true;
547 | uniques.push(array[i]);
548 | }
549 | }
550 | return uniques;
551 | }
552 | function getIntersection(arrayA, arrayB) {
553 | var ai = 0, bi = 0, intersection = [];
554 | arrayA = arrayA.sort();
555 | arrayB = arrayB.sort();
556 | var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
557 | while (ai < lenArrayA && bi < lenArrayB) {
558 | if (arrayA[ai] < arrayB[bi]) {
559 | ai++;
560 | } else if (arrayA[ai] > arrayB[bi]) {
561 | bi++;
562 | } else {
563 | intersection.push(arrayA[ai]);
564 | ai++;
565 | bi++;
566 | }
567 | }
568 | return intersection;
569 | }
570 | }();
571 | var Prefetch = function() {
572 | "use strict";
573 | var keys;
574 | keys = {
575 | data: "data",
576 | protocol: "protocol",
577 | thumbprint: "thumbprint"
578 | };
579 | function Prefetch(o) {
580 | this.url = o.url;
581 | this.ttl = o.ttl;
582 | this.cache = o.cache;
583 | this.prepare = o.prepare;
584 | this.transform = o.transform;
585 | this.transport = o.transport;
586 | this.thumbprint = o.thumbprint;
587 | this.storage = new PersistentStorage(o.cacheKey);
588 | }
589 | _.mixin(Prefetch.prototype, {
590 | _settings: function settings() {
591 | return {
592 | url: this.url,
593 | type: "GET",
594 | dataType: "json"
595 | };
596 | },
597 | store: function store(data) {
598 | if (!this.cache) {
599 | return;
600 | }
601 | this.storage.set(keys.data, data, this.ttl);
602 | this.storage.set(keys.protocol, location.protocol, this.ttl);
603 | this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
604 | },
605 | fromCache: function fromCache() {
606 | var stored = {}, isExpired;
607 | if (!this.cache) {
608 | return null;
609 | }
610 | stored.data = this.storage.get(keys.data);
611 | stored.protocol = this.storage.get(keys.protocol);
612 | stored.thumbprint = this.storage.get(keys.thumbprint);
613 | isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
614 | return stored.data && !isExpired ? stored.data : null;
615 | },
616 | fromNetwork: function(cb) {
617 | var that = this, settings;
618 | if (!cb) {
619 | return;
620 | }
621 | settings = this.prepare(this._settings());
622 | this.transport(settings).fail(onError).done(onResponse);
623 | function onError() {
624 | cb(true);
625 | }
626 | function onResponse(resp) {
627 | cb(null, that.transform(resp));
628 | }
629 | },
630 | clear: function clear() {
631 | this.storage.clear();
632 | return this;
633 | }
634 | });
635 | return Prefetch;
636 | }();
637 | var Remote = function() {
638 | "use strict";
639 | function Remote(o) {
640 | this.url = o.url;
641 | this.prepare = o.prepare;
642 | this.transform = o.transform;
643 | this.indexResponse = o.indexResponse;
644 | this.transport = new Transport({
645 | cache: o.cache,
646 | limiter: o.limiter,
647 | transport: o.transport,
648 | maxPendingRequests: o.maxPendingRequests
649 | });
650 | }
651 | _.mixin(Remote.prototype, {
652 | _settings: function settings() {
653 | return {
654 | url: this.url,
655 | type: "GET",
656 | dataType: "json"
657 | };
658 | },
659 | get: function get(query, cb) {
660 | var that = this, settings;
661 | if (!cb) {
662 | return;
663 | }
664 | query = query || "";
665 | settings = this.prepare(query, this._settings());
666 | return this.transport.get(settings, onResponse);
667 | function onResponse(err, resp) {
668 | err ? cb([]) : cb(that.transform(resp));
669 | }
670 | },
671 | cancelLastRequest: function cancelLastRequest() {
672 | this.transport.cancel();
673 | }
674 | });
675 | return Remote;
676 | }();
677 | var oParser = function() {
678 | "use strict";
679 | return function parse(o) {
680 | var defaults, sorter;
681 | defaults = {
682 | initialize: true,
683 | identify: _.stringify,
684 | datumTokenizer: null,
685 | queryTokenizer: null,
686 | matchAnyQueryToken: false,
687 | sufficient: 5,
688 | indexRemote: false,
689 | sorter: null,
690 | local: [],
691 | prefetch: null,
692 | remote: null
693 | };
694 | o = _.mixin(defaults, o || {});
695 | !o.datumTokenizer && $.error("datumTokenizer is required");
696 | !o.queryTokenizer && $.error("queryTokenizer is required");
697 | sorter = o.sorter;
698 | o.sorter = sorter ? function(x) {
699 | return x.sort(sorter);
700 | } : _.identity;
701 | o.local = _.isFunction(o.local) ? o.local() : o.local;
702 | o.prefetch = parsePrefetch(o.prefetch);
703 | o.remote = parseRemote(o.remote);
704 | return o;
705 | };
706 | function parsePrefetch(o) {
707 | var defaults;
708 | if (!o) {
709 | return null;
710 | }
711 | defaults = {
712 | url: null,
713 | ttl: 24 * 60 * 60 * 1e3,
714 | cache: true,
715 | cacheKey: null,
716 | thumbprint: "",
717 | prepare: _.identity,
718 | transform: _.identity,
719 | transport: null
720 | };
721 | o = _.isString(o) ? {
722 | url: o
723 | } : o;
724 | o = _.mixin(defaults, o);
725 | !o.url && $.error("prefetch requires url to be set");
726 | o.transform = o.filter || o.transform;
727 | o.cacheKey = o.cacheKey || o.url;
728 | o.thumbprint = VERSION + o.thumbprint;
729 | o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
730 | return o;
731 | }
732 | function parseRemote(o) {
733 | var defaults;
734 | if (!o) {
735 | return;
736 | }
737 | defaults = {
738 | url: null,
739 | cache: true,
740 | prepare: null,
741 | replace: null,
742 | wildcard: null,
743 | limiter: null,
744 | rateLimitBy: "debounce",
745 | rateLimitWait: 300,
746 | transform: _.identity,
747 | transport: null
748 | };
749 | o = _.isString(o) ? {
750 | url: o
751 | } : o;
752 | o = _.mixin(defaults, o);
753 | !o.url && $.error("remote requires url to be set");
754 | o.transform = o.filter || o.transform;
755 | o.prepare = toRemotePrepare(o);
756 | o.limiter = toLimiter(o);
757 | o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
758 | delete o.replace;
759 | delete o.wildcard;
760 | delete o.rateLimitBy;
761 | delete o.rateLimitWait;
762 | return o;
763 | }
764 | function toRemotePrepare(o) {
765 | var prepare, replace, wildcard;
766 | prepare = o.prepare;
767 | replace = o.replace;
768 | wildcard = o.wildcard;
769 | if (prepare) {
770 | return prepare;
771 | }
772 | if (replace) {
773 | prepare = prepareByReplace;
774 | } else if (o.wildcard) {
775 | prepare = prepareByWildcard;
776 | } else {
777 | prepare = identityPrepare;
778 | }
779 | return prepare;
780 | function prepareByReplace(query, settings) {
781 | settings.url = replace(settings.url, query);
782 | return settings;
783 | }
784 | function prepareByWildcard(query, settings) {
785 | settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
786 | return settings;
787 | }
788 | function identityPrepare(query, settings) {
789 | return settings;
790 | }
791 | }
792 | function toLimiter(o) {
793 | var limiter, method, wait;
794 | limiter = o.limiter;
795 | method = o.rateLimitBy;
796 | wait = o.rateLimitWait;
797 | if (!limiter) {
798 | limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
799 | }
800 | return limiter;
801 | function debounce(wait) {
802 | return function debounce(fn) {
803 | return _.debounce(fn, wait);
804 | };
805 | }
806 | function throttle(wait) {
807 | return function throttle(fn) {
808 | return _.throttle(fn, wait);
809 | };
810 | }
811 | }
812 | function callbackToDeferred(fn) {
813 | return function wrapper(o) {
814 | var deferred = $.Deferred();
815 | fn(o, onSuccess, onError);
816 | return deferred;
817 | function onSuccess(resp) {
818 | _.defer(function() {
819 | deferred.resolve(resp);
820 | });
821 | }
822 | function onError(err) {
823 | _.defer(function() {
824 | deferred.reject(err);
825 | });
826 | }
827 | };
828 | }
829 | }();
830 | var Bloodhound = function() {
831 | "use strict";
832 | var old;
833 | old = window && window.Bloodhound;
834 | function Bloodhound(o) {
835 | o = oParser(o);
836 | this.sorter = o.sorter;
837 | this.identify = o.identify;
838 | this.sufficient = o.sufficient;
839 | this.indexRemote = o.indexRemote;
840 | this.local = o.local;
841 | this.remote = o.remote ? new Remote(o.remote) : null;
842 | this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
843 | this.index = new SearchIndex({
844 | identify: this.identify,
845 | datumTokenizer: o.datumTokenizer,
846 | queryTokenizer: o.queryTokenizer
847 | });
848 | o.initialize !== false && this.initialize();
849 | }
850 | Bloodhound.noConflict = function noConflict() {
851 | window && (window.Bloodhound = old);
852 | return Bloodhound;
853 | };
854 | Bloodhound.tokenizers = tokenizers;
855 | _.mixin(Bloodhound.prototype, {
856 | __ttAdapter: function ttAdapter() {
857 | var that = this;
858 | return this.remote ? withAsync : withoutAsync;
859 | function withAsync(query, sync, async) {
860 | return that.search(query, sync, async);
861 | }
862 | function withoutAsync(query, sync) {
863 | return that.search(query, sync);
864 | }
865 | },
866 | _loadPrefetch: function loadPrefetch() {
867 | var that = this, deferred, serialized;
868 | deferred = $.Deferred();
869 | if (!this.prefetch) {
870 | deferred.resolve();
871 | } else if (serialized = this.prefetch.fromCache()) {
872 | this.index.bootstrap(serialized);
873 | deferred.resolve();
874 | } else {
875 | this.prefetch.fromNetwork(done);
876 | }
877 | return deferred.promise();
878 | function done(err, data) {
879 | if (err) {
880 | return deferred.reject();
881 | }
882 | that.add(data);
883 | that.prefetch.store(that.index.serialize());
884 | deferred.resolve();
885 | }
886 | },
887 | _initialize: function initialize() {
888 | var that = this, deferred;
889 | this.clear();
890 | (this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
891 | return this.initPromise;
892 | function addLocalToIndex() {
893 | that.add(that.local);
894 | }
895 | },
896 | initialize: function initialize(force) {
897 | return !this.initPromise || force ? this._initialize() : this.initPromise;
898 | },
899 | add: function add(data) {
900 | this.index.add(data);
901 | return this;
902 | },
903 | get: function get(ids) {
904 | ids = _.isArray(ids) ? ids : [].slice.call(arguments);
905 | return this.index.get(ids);
906 | },
907 | search: function search(query, sync, async) {
908 | var that = this, local;
909 | sync = sync || _.noop;
910 | async = async || _.noop;
911 | local = this.sorter(this.index.search(query));
912 | sync(this.remote ? local.slice() : local);
913 | if (this.remote && local.length < this.sufficient) {
914 | this.remote.get(query, processRemote);
915 | } else if (this.remote) {
916 | this.remote.cancelLastRequest();
917 | }
918 | return this;
919 | function processRemote(remote) {
920 | var nonDuplicates = [];
921 | _.each(remote, function(r) {
922 | !_.some(local, function(l) {
923 | return that.identify(r) === that.identify(l);
924 | }) && nonDuplicates.push(r);
925 | });
926 | that.indexRemote && that.add(nonDuplicates);
927 | async(nonDuplicates);
928 | }
929 | },
930 | all: function all() {
931 | return this.index.all();
932 | },
933 | clear: function clear() {
934 | this.index.reset();
935 | return this;
936 | },
937 | clearPrefetchCache: function clearPrefetchCache() {
938 | this.prefetch && this.prefetch.clear();
939 | return this;
940 | },
941 | clearRemoteCache: function clearRemoteCache() {
942 | Transport.resetCache();
943 | return this;
944 | },
945 | ttAdapter: function ttAdapter() {
946 | return this.__ttAdapter();
947 | }
948 | });
949 | return Bloodhound;
950 | }();
951 | return Bloodhound;
952 | });
953 |
954 | (function(root, factory) {
955 | if (typeof define === "function" && define.amd) {
956 | define([ "jquery" ], function(a0) {
957 | return factory(a0);
958 | });
959 | } else if (typeof exports === "object") {
960 | module.exports = factory(require("jquery"));
961 | } else {
962 | factory(root["jQuery"]);
963 | }
964 | })(this, function($) {
965 | var _ = function() {
966 | "use strict";
967 | return {
968 | isMsie: function() {
969 | return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
970 | },
971 | isBlankString: function(str) {
972 | return !str || /^\s*$/.test(str);
973 | },
974 | escapeRegExChars: function(str) {
975 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
976 | },
977 | isString: function(obj) {
978 | return typeof obj === "string";
979 | },
980 | isNumber: function(obj) {
981 | return typeof obj === "number";
982 | },
983 | isArray: $.isArray,
984 | isFunction: $.isFunction,
985 | isObject: $.isPlainObject,
986 | isUndefined: function(obj) {
987 | return typeof obj === "undefined";
988 | },
989 | isElement: function(obj) {
990 | return !!(obj && obj.nodeType === 1);
991 | },
992 | isJQuery: function(obj) {
993 | return obj instanceof $;
994 | },
995 | toStr: function toStr(s) {
996 | return _.isUndefined(s) || s === null ? "" : s + "";
997 | },
998 | bind: $.proxy,
999 | each: function(collection, cb) {
1000 | $.each(collection, reverseArgs);
1001 | function reverseArgs(index, value) {
1002 | return cb(value, index);
1003 | }
1004 | },
1005 | map: $.map,
1006 | filter: $.grep,
1007 | every: function(obj, test) {
1008 | var result = true;
1009 | if (!obj) {
1010 | return result;
1011 | }
1012 | $.each(obj, function(key, val) {
1013 | if (!(result = test.call(null, val, key, obj))) {
1014 | return false;
1015 | }
1016 | });
1017 | return !!result;
1018 | },
1019 | some: function(obj, test) {
1020 | var result = false;
1021 | if (!obj) {
1022 | return result;
1023 | }
1024 | $.each(obj, function(key, val) {
1025 | if (result = test.call(null, val, key, obj)) {
1026 | return false;
1027 | }
1028 | });
1029 | return !!result;
1030 | },
1031 | mixin: $.extend,
1032 | identity: function(x) {
1033 | return x;
1034 | },
1035 | clone: function(obj) {
1036 | return $.extend(true, {}, obj);
1037 | },
1038 | getIdGenerator: function() {
1039 | var counter = 0;
1040 | return function() {
1041 | return counter++;
1042 | };
1043 | },
1044 | templatify: function templatify(obj) {
1045 | return $.isFunction(obj) ? obj : template;
1046 | function template() {
1047 | return String(obj);
1048 | }
1049 | },
1050 | defer: function(fn) {
1051 | setTimeout(fn, 0);
1052 | },
1053 | debounce: function(func, wait, immediate) {
1054 | var timeout, result;
1055 | return function() {
1056 | var context = this, args = arguments, later, callNow;
1057 | later = function() {
1058 | timeout = null;
1059 | if (!immediate) {
1060 | result = func.apply(context, args);
1061 | }
1062 | };
1063 | callNow = immediate && !timeout;
1064 | clearTimeout(timeout);
1065 | timeout = setTimeout(later, wait);
1066 | if (callNow) {
1067 | result = func.apply(context, args);
1068 | }
1069 | return result;
1070 | };
1071 | },
1072 | throttle: function(func, wait) {
1073 | var context, args, timeout, result, previous, later;
1074 | previous = 0;
1075 | later = function() {
1076 | previous = new Date();
1077 | timeout = null;
1078 | result = func.apply(context, args);
1079 | };
1080 | return function() {
1081 | var now = new Date(), remaining = wait - (now - previous);
1082 | context = this;
1083 | args = arguments;
1084 | if (remaining <= 0) {
1085 | clearTimeout(timeout);
1086 | timeout = null;
1087 | previous = now;
1088 | result = func.apply(context, args);
1089 | } else if (!timeout) {
1090 | timeout = setTimeout(later, remaining);
1091 | }
1092 | return result;
1093 | };
1094 | },
1095 | stringify: function(val) {
1096 | return _.isString(val) ? val : JSON.stringify(val);
1097 | },
1098 | guid: function() {
1099 | function _p8(s) {
1100 | var p = (Math.random().toString(16) + "000000000").substr(2, 8);
1101 | return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
1102 | }
1103 | return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
1104 | },
1105 | noop: function() {}
1106 | };
1107 | }();
1108 | var WWW = function() {
1109 | "use strict";
1110 | var defaultClassNames = {
1111 | wrapper: "twitter-typeahead",
1112 | input: "tt-input",
1113 | hint: "tt-hint",
1114 | menu: "tt-menu",
1115 | dataset: "tt-dataset",
1116 | suggestion: "tt-suggestion",
1117 | selectable: "tt-selectable",
1118 | empty: "tt-empty",
1119 | open: "tt-open",
1120 | cursor: "tt-cursor",
1121 | highlight: "tt-highlight"
1122 | };
1123 | return build;
1124 | function build(o) {
1125 | var www, classes;
1126 | classes = _.mixin({}, defaultClassNames, o);
1127 | www = {
1128 | css: buildCss(),
1129 | classes: classes,
1130 | html: buildHtml(classes),
1131 | selectors: buildSelectors(classes)
1132 | };
1133 | return {
1134 | css: www.css,
1135 | html: www.html,
1136 | classes: www.classes,
1137 | selectors: www.selectors,
1138 | mixin: function(o) {
1139 | _.mixin(o, www);
1140 | }
1141 | };
1142 | }
1143 | function buildHtml(c) {
1144 | return {
1145 | wrapper: '
',
1146 | menu: ''
1147 | };
1148 | }
1149 | function buildSelectors(classes) {
1150 | var selectors = {};
1151 | _.each(classes, function(v, k) {
1152 | selectors[k] = "." + v;
1153 | });
1154 | return selectors;
1155 | }
1156 | function buildCss() {
1157 | var css = {
1158 | wrapper: {
1159 | position: "relative",
1160 | display: "inline-block"
1161 | },
1162 | hint: {
1163 | position: "absolute",
1164 | top: "0",
1165 | left: "0",
1166 | borderColor: "transparent",
1167 | boxShadow: "none",
1168 | opacity: "1"
1169 | },
1170 | input: {
1171 | position: "relative",
1172 | verticalAlign: "top",
1173 | backgroundColor: "transparent"
1174 | },
1175 | inputWithNoHint: {
1176 | position: "relative",
1177 | verticalAlign: "top"
1178 | },
1179 | menu: {
1180 | position: "absolute",
1181 | top: "100%",
1182 | right: "0",
1183 | zIndex: "100",
1184 | display: "none"
1185 | },
1186 | ltr: {
1187 | left: "0",
1188 | right: "auto"
1189 | },
1190 | rtl: {
1191 | left: "auto",
1192 | right: " 0"
1193 | }
1194 | };
1195 | if (_.isMsie()) {
1196 | _.mixin(css.input, {
1197 | backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
1198 | });
1199 | }
1200 | return css;
1201 | }
1202 | }();
1203 | var EventBus = function() {
1204 | "use strict";
1205 | var namespace, deprecationMap;
1206 | namespace = "typeahead:";
1207 | deprecationMap = {
1208 | render: "rendered",
1209 | cursorchange: "cursorchanged",
1210 | select: "selected",
1211 | autocomplete: "autocompleted"
1212 | };
1213 | function EventBus(o) {
1214 | if (!o || !o.el) {
1215 | $.error("EventBus initialized without el");
1216 | }
1217 | this.$el = $(o.el);
1218 | }
1219 | _.mixin(EventBus.prototype, {
1220 | _trigger: function(type, args) {
1221 | var $e = $.Event(namespace + type);
1222 | this.$el.trigger.call(this.$el, $e, args || []);
1223 | return $e;
1224 | },
1225 | before: function(type) {
1226 | var args, $e;
1227 | args = [].slice.call(arguments, 1);
1228 | $e = this._trigger("before" + type, args);
1229 | return $e.isDefaultPrevented();
1230 | },
1231 | trigger: function(type) {
1232 | var deprecatedType;
1233 | this._trigger(type, [].slice.call(arguments, 1));
1234 | if (deprecatedType = deprecationMap[type]) {
1235 | this._trigger(deprecatedType, [].slice.call(arguments, 1));
1236 | }
1237 | }
1238 | });
1239 | return EventBus;
1240 | }();
1241 | var EventEmitter = function() {
1242 | "use strict";
1243 | var splitter = /\s+/, nextTick = getNextTick();
1244 | return {
1245 | onSync: onSync,
1246 | onAsync: onAsync,
1247 | off: off,
1248 | trigger: trigger
1249 | };
1250 | function on(method, types, cb, context) {
1251 | var type;
1252 | if (!cb) {
1253 | return this;
1254 | }
1255 | types = types.split(splitter);
1256 | cb = context ? bindContext(cb, context) : cb;
1257 | this._callbacks = this._callbacks || {};
1258 | while (type = types.shift()) {
1259 | this._callbacks[type] = this._callbacks[type] || {
1260 | sync: [],
1261 | async: []
1262 | };
1263 | this._callbacks[type][method].push(cb);
1264 | }
1265 | return this;
1266 | }
1267 | function onAsync(types, cb, context) {
1268 | return on.call(this, "async", types, cb, context);
1269 | }
1270 | function onSync(types, cb, context) {
1271 | return on.call(this, "sync", types, cb, context);
1272 | }
1273 | function off(types) {
1274 | var type;
1275 | if (!this._callbacks) {
1276 | return this;
1277 | }
1278 | types = types.split(splitter);
1279 | while (type = types.shift()) {
1280 | delete this._callbacks[type];
1281 | }
1282 | return this;
1283 | }
1284 | function trigger(types) {
1285 | var type, callbacks, args, syncFlush, asyncFlush;
1286 | if (!this._callbacks) {
1287 | return this;
1288 | }
1289 | types = types.split(splitter);
1290 | args = [].slice.call(arguments, 1);
1291 | while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
1292 | syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
1293 | asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
1294 | syncFlush() && nextTick(asyncFlush);
1295 | }
1296 | return this;
1297 | }
1298 | function getFlush(callbacks, context, args) {
1299 | return flush;
1300 | function flush() {
1301 | var cancelled;
1302 | for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
1303 | cancelled = callbacks[i].apply(context, args) === false;
1304 | }
1305 | return !cancelled;
1306 | }
1307 | }
1308 | function getNextTick() {
1309 | var nextTickFn;
1310 | if (window.setImmediate) {
1311 | nextTickFn = function nextTickSetImmediate(fn) {
1312 | setImmediate(function() {
1313 | fn();
1314 | });
1315 | };
1316 | } else {
1317 | nextTickFn = function nextTickSetTimeout(fn) {
1318 | setTimeout(function() {
1319 | fn();
1320 | }, 0);
1321 | };
1322 | }
1323 | return nextTickFn;
1324 | }
1325 | function bindContext(fn, context) {
1326 | return fn.bind ? fn.bind(context) : function() {
1327 | fn.apply(context, [].slice.call(arguments, 0));
1328 | };
1329 | }
1330 | }();
1331 | var highlight = function(doc) {
1332 | "use strict";
1333 | var defaults = {
1334 | node: null,
1335 | pattern: null,
1336 | tagName: "strong",
1337 | className: null,
1338 | wordsOnly: false,
1339 | caseSensitive: false,
1340 | diacriticInsensitive: false
1341 | };
1342 | var accented = {
1343 | A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]",
1344 | B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]",
1345 | C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]",
1346 | D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]",
1347 | E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]",
1348 | F: "[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]",
1349 | G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]",
1350 | H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]",
1351 | I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]",
1352 | J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]",
1353 | K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]",
1354 | L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]",
1355 | M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]",
1356 | N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]",
1357 | O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]",
1358 | P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]",
1359 | Q: "[Qqℚ⒬Ⓠⓠ㏃Qq]",
1360 | R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]",
1361 | S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]",
1362 | T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]",
1363 | U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]",
1364 | V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]",
1365 | W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]",
1366 | X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]",
1367 | Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]",
1368 | Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]"
1369 | };
1370 | return function hightlight(o) {
1371 | var regex;
1372 | o = _.mixin({}, defaults, o);
1373 | if (!o.node || !o.pattern) {
1374 | return;
1375 | }
1376 | o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
1377 | regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive);
1378 | traverse(o.node, hightlightTextNode);
1379 | function hightlightTextNode(textNode) {
1380 | var match, patternNode, wrapperNode;
1381 | if (match = regex.exec(textNode.data)) {
1382 | wrapperNode = doc.createElement(o.tagName);
1383 | o.className && (wrapperNode.className = o.className);
1384 | patternNode = textNode.splitText(match.index);
1385 | patternNode.splitText(match[0].length);
1386 | wrapperNode.appendChild(patternNode.cloneNode(true));
1387 | textNode.parentNode.replaceChild(wrapperNode, patternNode);
1388 | }
1389 | return !!match;
1390 | }
1391 | function traverse(el, hightlightTextNode) {
1392 | var childNode, TEXT_NODE_TYPE = 3;
1393 | for (var i = 0; i < el.childNodes.length; i++) {
1394 | childNode = el.childNodes[i];
1395 | if (childNode.nodeType === TEXT_NODE_TYPE) {
1396 | i += hightlightTextNode(childNode) ? 1 : 0;
1397 | } else {
1398 | traverse(childNode, hightlightTextNode);
1399 | }
1400 | }
1401 | }
1402 | };
1403 | function accent_replacer(chr) {
1404 | return accented[chr.toUpperCase()] || chr;
1405 | }
1406 | function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) {
1407 | var escapedPatterns = [], regexStr;
1408 | for (var i = 0, len = patterns.length; i < len; i++) {
1409 | var escapedWord = _.escapeRegExChars(patterns[i]);
1410 | if (diacriticInsensitive) {
1411 | escapedWord = escapedWord.replace(/\S/g, accent_replacer);
1412 | }
1413 | escapedPatterns.push(escapedWord);
1414 | }
1415 | regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
1416 | return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
1417 | }
1418 | }(window.document);
1419 | var Input = function() {
1420 | "use strict";
1421 | var specialKeyCodeMap;
1422 | specialKeyCodeMap = {
1423 | 9: "tab",
1424 | 27: "esc",
1425 | 37: "left",
1426 | 39: "right",
1427 | 13: "enter",
1428 | 38: "up",
1429 | 40: "down"
1430 | };
1431 | function Input(o, www) {
1432 | o = o || {};
1433 | if (!o.input) {
1434 | $.error("input is missing");
1435 | }
1436 | www.mixin(this);
1437 | this.$hint = $(o.hint);
1438 | this.$input = $(o.input);
1439 | this.$input.attr({
1440 | "aria-activedescendant": "",
1441 | "aria-owns": this.$input.attr("id") + "_listbox",
1442 | role: "combobox",
1443 | "aria-readonly": "true",
1444 | "aria-autocomplete": "list"
1445 | });
1446 | $(www.menu).attr("id", this.$input.attr("id") + "_listbox");
1447 | this.query = this.$input.val();
1448 | this.queryWhenFocused = this.hasFocus() ? this.query : null;
1449 | this.$overflowHelper = buildOverflowHelper(this.$input);
1450 | this._checkLanguageDirection();
1451 | if (this.$hint.length === 0) {
1452 | this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
1453 | }
1454 | this.onSync("cursorchange", this._updateDescendent);
1455 | }
1456 | Input.normalizeQuery = function(str) {
1457 | return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
1458 | };
1459 | _.mixin(Input.prototype, EventEmitter, {
1460 | _onBlur: function onBlur() {
1461 | this.resetInputValue();
1462 | this.trigger("blurred");
1463 | },
1464 | _onFocus: function onFocus() {
1465 | this.queryWhenFocused = this.query;
1466 | this.trigger("focused");
1467 | },
1468 | _onKeydown: function onKeydown($e) {
1469 | var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
1470 | this._managePreventDefault(keyName, $e);
1471 | if (keyName && this._shouldTrigger(keyName, $e)) {
1472 | this.trigger(keyName + "Keyed", $e);
1473 | }
1474 | },
1475 | _onInput: function onInput() {
1476 | this._setQuery(this.getInputValue());
1477 | this.clearHintIfInvalid();
1478 | this._checkLanguageDirection();
1479 | },
1480 | _managePreventDefault: function managePreventDefault(keyName, $e) {
1481 | var preventDefault;
1482 | switch (keyName) {
1483 | case "up":
1484 | case "down":
1485 | preventDefault = !withModifier($e);
1486 | break;
1487 |
1488 | default:
1489 | preventDefault = false;
1490 | }
1491 | preventDefault && $e.preventDefault();
1492 | },
1493 | _shouldTrigger: function shouldTrigger(keyName, $e) {
1494 | var trigger;
1495 | switch (keyName) {
1496 | case "tab":
1497 | trigger = !withModifier($e);
1498 | break;
1499 |
1500 | default:
1501 | trigger = true;
1502 | }
1503 | return trigger;
1504 | },
1505 | _checkLanguageDirection: function checkLanguageDirection() {
1506 | var dir = (this.$input.css("direction") || "ltr").toLowerCase();
1507 | if (this.dir !== dir) {
1508 | this.dir = dir;
1509 | this.$hint.attr("dir", dir);
1510 | this.trigger("langDirChanged", dir);
1511 | }
1512 | },
1513 | _setQuery: function setQuery(val, silent) {
1514 | var areEquivalent, hasDifferentWhitespace;
1515 | areEquivalent = areQueriesEquivalent(val, this.query);
1516 | hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false;
1517 | this.query = val;
1518 | if (!silent && !areEquivalent) {
1519 | this.trigger("queryChanged", this.query);
1520 | } else if (!silent && hasDifferentWhitespace) {
1521 | this.trigger("whitespaceChanged", this.query);
1522 | }
1523 | },
1524 | _updateDescendent: function updateDescendent(event, id) {
1525 | this.$input.attr("aria-activedescendant", id);
1526 | },
1527 | bind: function() {
1528 | var that = this, onBlur, onFocus, onKeydown, onInput;
1529 | onBlur = _.bind(this._onBlur, this);
1530 | onFocus = _.bind(this._onFocus, this);
1531 | onKeydown = _.bind(this._onKeydown, this);
1532 | onInput = _.bind(this._onInput, this);
1533 | this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
1534 | if (!_.isMsie() || _.isMsie() > 9) {
1535 | this.$input.on("input.tt", onInput);
1536 | } else {
1537 | this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
1538 | if (specialKeyCodeMap[$e.which || $e.keyCode]) {
1539 | return;
1540 | }
1541 | _.defer(_.bind(that._onInput, that, $e));
1542 | });
1543 | }
1544 | return this;
1545 | },
1546 | focus: function focus() {
1547 | this.$input.focus();
1548 | },
1549 | blur: function blur() {
1550 | this.$input.blur();
1551 | },
1552 | getLangDir: function getLangDir() {
1553 | return this.dir;
1554 | },
1555 | getQuery: function getQuery() {
1556 | return this.query || "";
1557 | },
1558 | setQuery: function setQuery(val, silent) {
1559 | this.setInputValue(val);
1560 | this._setQuery(val, silent);
1561 | },
1562 | hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
1563 | return this.query !== this.queryWhenFocused;
1564 | },
1565 | getInputValue: function getInputValue() {
1566 | return this.$input.val();
1567 | },
1568 | setInputValue: function setInputValue(value) {
1569 | this.$input.val(value);
1570 | this.clearHintIfInvalid();
1571 | this._checkLanguageDirection();
1572 | },
1573 | resetInputValue: function resetInputValue() {
1574 | this.setInputValue(this.query);
1575 | },
1576 | getHint: function getHint() {
1577 | return this.$hint.val();
1578 | },
1579 | setHint: function setHint(value) {
1580 | this.$hint.val(value);
1581 | },
1582 | clearHint: function clearHint() {
1583 | this.setHint("");
1584 | },
1585 | clearHintIfInvalid: function clearHintIfInvalid() {
1586 | var val, hint, valIsPrefixOfHint, isValid;
1587 | val = this.getInputValue();
1588 | hint = this.getHint();
1589 | valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
1590 | isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
1591 | !isValid && this.clearHint();
1592 | },
1593 | hasFocus: function hasFocus() {
1594 | return this.$input.is(":focus");
1595 | },
1596 | hasOverflow: function hasOverflow() {
1597 | var constraint = this.$input.width() - 2;
1598 | this.$overflowHelper.text(this.getInputValue());
1599 | return this.$overflowHelper.width() >= constraint;
1600 | },
1601 | isCursorAtEnd: function() {
1602 | var valueLength, selectionStart, range;
1603 | valueLength = this.$input.val().length;
1604 | selectionStart = this.$input[0].selectionStart;
1605 | if (_.isNumber(selectionStart)) {
1606 | return selectionStart === valueLength;
1607 | } else if (document.selection) {
1608 | range = document.selection.createRange();
1609 | range.moveStart("character", -valueLength);
1610 | return valueLength === range.text.length;
1611 | }
1612 | return true;
1613 | },
1614 | destroy: function destroy() {
1615 | this.$hint.off(".tt");
1616 | this.$input.off(".tt");
1617 | this.$overflowHelper.remove();
1618 | this.$hint = this.$input = this.$overflowHelper = $("
");
1619 | }
1620 | });
1621 | return Input;
1622 | function buildOverflowHelper($input) {
1623 | return $('
').css({
1624 | position: "absolute",
1625 | visibility: "hidden",
1626 | whiteSpace: "pre",
1627 | fontFamily: $input.css("font-family"),
1628 | fontSize: $input.css("font-size"),
1629 | fontStyle: $input.css("font-style"),
1630 | fontVariant: $input.css("font-variant"),
1631 | fontWeight: $input.css("font-weight"),
1632 | wordSpacing: $input.css("word-spacing"),
1633 | letterSpacing: $input.css("letter-spacing"),
1634 | textIndent: $input.css("text-indent"),
1635 | textRendering: $input.css("text-rendering"),
1636 | textTransform: $input.css("text-transform")
1637 | }).insertAfter($input);
1638 | }
1639 | function areQueriesEquivalent(a, b) {
1640 | return Input.normalizeQuery(a) === Input.normalizeQuery(b);
1641 | }
1642 | function withModifier($e) {
1643 | return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
1644 | }
1645 | }();
1646 | var Dataset = function() {
1647 | "use strict";
1648 | var keys, nameGenerator;
1649 | keys = {
1650 | dataset: "tt-selectable-dataset",
1651 | val: "tt-selectable-display",
1652 | obj: "tt-selectable-object"
1653 | };
1654 | nameGenerator = _.getIdGenerator();
1655 | function Dataset(o, www) {
1656 | o = o || {};
1657 | o.templates = o.templates || {};
1658 | o.templates.notFound = o.templates.notFound || o.templates.empty;
1659 | if (!o.source) {
1660 | $.error("missing source");
1661 | }
1662 | if (!o.node) {
1663 | $.error("missing node");
1664 | }
1665 | if (o.name && !isValidName(o.name)) {
1666 | $.error("invalid dataset name: " + o.name);
1667 | }
1668 | www.mixin(this);
1669 | this.highlight = !!o.highlight;
1670 | this.name = _.toStr(o.name || nameGenerator());
1671 | this.limit = o.limit || 5;
1672 | this.displayFn = getDisplayFn(o.display || o.displayKey);
1673 | this.templates = getTemplates(o.templates, this.displayFn);
1674 | this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
1675 | this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
1676 | this._resetLastSuggestion();
1677 | this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
1678 | }
1679 | Dataset.extractData = function extractData(el) {
1680 | var $el = $(el);
1681 | if ($el.data(keys.obj)) {
1682 | return {
1683 | dataset: $el.data(keys.dataset) || "",
1684 | val: $el.data(keys.val) || "",
1685 | obj: $el.data(keys.obj) || null
1686 | };
1687 | }
1688 | return null;
1689 | };
1690 | _.mixin(Dataset.prototype, EventEmitter, {
1691 | _overwrite: function overwrite(query, suggestions) {
1692 | suggestions = suggestions || [];
1693 | if (suggestions.length) {
1694 | this._renderSuggestions(query, suggestions);
1695 | } else if (this.async && this.templates.pending) {
1696 | this._renderPending(query);
1697 | } else if (!this.async && this.templates.notFound) {
1698 | this._renderNotFound(query);
1699 | } else {
1700 | this._empty();
1701 | }
1702 | this.trigger("rendered", suggestions, false, this.name);
1703 | },
1704 | _append: function append(query, suggestions) {
1705 | suggestions = suggestions || [];
1706 | if (suggestions.length && this.$lastSuggestion.length) {
1707 | this._appendSuggestions(query, suggestions);
1708 | } else if (suggestions.length) {
1709 | this._renderSuggestions(query, suggestions);
1710 | } else if (!this.$lastSuggestion.length && this.templates.notFound) {
1711 | this._renderNotFound(query);
1712 | }
1713 | this.trigger("rendered", suggestions, true, this.name);
1714 | },
1715 | _renderSuggestions: function renderSuggestions(query, suggestions) {
1716 | var $fragment;
1717 | $fragment = this._getSuggestionsFragment(query, suggestions);
1718 | this.$lastSuggestion = $fragment.children().last();
1719 | this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions));
1720 | },
1721 | _appendSuggestions: function appendSuggestions(query, suggestions) {
1722 | var $fragment, $lastSuggestion;
1723 | $fragment = this._getSuggestionsFragment(query, suggestions);
1724 | $lastSuggestion = $fragment.children().last();
1725 | this.$lastSuggestion.after($fragment);
1726 | this.$lastSuggestion = $lastSuggestion;
1727 | },
1728 | _renderPending: function renderPending(query) {
1729 | var template = this.templates.pending;
1730 | this._resetLastSuggestion();
1731 | template && this.$el.html(template({
1732 | query: query,
1733 | dataset: this.name
1734 | }));
1735 | },
1736 | _renderNotFound: function renderNotFound(query) {
1737 | var template = this.templates.notFound;
1738 | this._resetLastSuggestion();
1739 | template && this.$el.html(template({
1740 | query: query,
1741 | dataset: this.name
1742 | }));
1743 | },
1744 | _empty: function empty() {
1745 | this.$el.empty();
1746 | this._resetLastSuggestion();
1747 | },
1748 | _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
1749 | var that = this, fragment;
1750 | fragment = document.createDocumentFragment();
1751 | _.each(suggestions, function getSuggestionNode(suggestion) {
1752 | var $el, context;
1753 | context = that._injectQuery(query, suggestion);
1754 | $el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
1755 | fragment.appendChild($el[0]);
1756 | });
1757 | this.highlight && highlight({
1758 | className: this.classes.highlight,
1759 | node: fragment,
1760 | pattern: query
1761 | });
1762 | return $(fragment);
1763 | },
1764 | _getFooter: function getFooter(query, suggestions) {
1765 | return this.templates.footer ? this.templates.footer({
1766 | query: query,
1767 | suggestions: suggestions,
1768 | dataset: this.name
1769 | }) : null;
1770 | },
1771 | _getHeader: function getHeader(query, suggestions) {
1772 | return this.templates.header ? this.templates.header({
1773 | query: query,
1774 | suggestions: suggestions,
1775 | dataset: this.name
1776 | }) : null;
1777 | },
1778 | _resetLastSuggestion: function resetLastSuggestion() {
1779 | this.$lastSuggestion = $();
1780 | },
1781 | _injectQuery: function injectQuery(query, obj) {
1782 | return _.isObject(obj) ? _.mixin({
1783 | _query: query
1784 | }, obj) : obj;
1785 | },
1786 | update: function update(query) {
1787 | var that = this, canceled = false, syncCalled = false, rendered = 0;
1788 | this.cancel();
1789 | this.cancel = function cancel() {
1790 | canceled = true;
1791 | that.cancel = $.noop;
1792 | that.async && that.trigger("asyncCanceled", query, that.name);
1793 | };
1794 | this.source(query, sync, async);
1795 | !syncCalled && sync([]);
1796 | function sync(suggestions) {
1797 | if (syncCalled) {
1798 | return;
1799 | }
1800 | syncCalled = true;
1801 | suggestions = (suggestions || []).slice(0, that.limit);
1802 | rendered = suggestions.length;
1803 | that._overwrite(query, suggestions);
1804 | if (rendered < that.limit && that.async) {
1805 | that.trigger("asyncRequested", query, that.name);
1806 | }
1807 | }
1808 | function async(suggestions) {
1809 | suggestions = suggestions || [];
1810 | if (!canceled && rendered < that.limit) {
1811 | that.cancel = $.noop;
1812 | var idx = Math.abs(rendered - that.limit);
1813 | rendered += idx;
1814 | that._append(query, suggestions.slice(0, idx));
1815 | that.async && that.trigger("asyncReceived", query, that.name);
1816 | }
1817 | }
1818 | },
1819 | cancel: $.noop,
1820 | clear: function clear() {
1821 | this._empty();
1822 | this.cancel();
1823 | this.trigger("cleared");
1824 | },
1825 | isEmpty: function isEmpty() {
1826 | return this.$el.is(":empty");
1827 | },
1828 | destroy: function destroy() {
1829 | this.$el = $("
");
1830 | }
1831 | });
1832 | return Dataset;
1833 | function getDisplayFn(display) {
1834 | display = display || _.stringify;
1835 | return _.isFunction(display) ? display : displayFn;
1836 | function displayFn(obj) {
1837 | return obj[display];
1838 | }
1839 | }
1840 | function getTemplates(templates, displayFn) {
1841 | return {
1842 | notFound: templates.notFound && _.templatify(templates.notFound),
1843 | pending: templates.pending && _.templatify(templates.pending),
1844 | header: templates.header && _.templatify(templates.header),
1845 | footer: templates.footer && _.templatify(templates.footer),
1846 | suggestion: templates.suggestion || suggestionTemplate
1847 | };
1848 | function suggestionTemplate(context) {
1849 | return $('
').attr("id", _.guid()).text(displayFn(context));
1850 | }
1851 | }
1852 | function isValidName(str) {
1853 | return /^[_a-zA-Z0-9-]+$/.test(str);
1854 | }
1855 | }();
1856 | var Menu = function() {
1857 | "use strict";
1858 | function Menu(o, www) {
1859 | var that = this;
1860 | o = o || {};
1861 | if (!o.node) {
1862 | $.error("node is required");
1863 | }
1864 | www.mixin(this);
1865 | this.$node = $(o.node);
1866 | this.query = null;
1867 | this.datasets = _.map(o.datasets, initializeDataset);
1868 | function initializeDataset(oDataset) {
1869 | var node = that.$node.find(oDataset.node).first();
1870 | oDataset.node = node.length ? node : $("
").appendTo(that.$node);
1871 | return new Dataset(oDataset, www);
1872 | }
1873 | }
1874 | _.mixin(Menu.prototype, EventEmitter, {
1875 | _onSelectableClick: function onSelectableClick($e) {
1876 | this.trigger("selectableClicked", $($e.currentTarget));
1877 | },
1878 | _onRendered: function onRendered(type, dataset, suggestions, async) {
1879 | this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
1880 | this.trigger("datasetRendered", dataset, suggestions, async);
1881 | },
1882 | _onCleared: function onCleared() {
1883 | this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
1884 | this.trigger("datasetCleared");
1885 | },
1886 | _propagate: function propagate() {
1887 | this.trigger.apply(this, arguments);
1888 | },
1889 | _allDatasetsEmpty: function allDatasetsEmpty() {
1890 | return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) {
1891 | var isEmpty = dataset.isEmpty();
1892 | this.$node.attr("aria-expanded", !isEmpty);
1893 | return isEmpty;
1894 | }, this));
1895 | },
1896 | _getSelectables: function getSelectables() {
1897 | return this.$node.find(this.selectors.selectable);
1898 | },
1899 | _removeCursor: function _removeCursor() {
1900 | var $selectable = this.getActiveSelectable();
1901 | $selectable && $selectable.removeClass(this.classes.cursor);
1902 | },
1903 | _ensureVisible: function ensureVisible($el) {
1904 | var elTop, elBottom, nodeScrollTop, nodeHeight;
1905 | elTop = $el.position().top;
1906 | elBottom = elTop + $el.outerHeight(true);
1907 | nodeScrollTop = this.$node.scrollTop();
1908 | nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10);
1909 | if (elTop < 0) {
1910 | this.$node.scrollTop(nodeScrollTop + elTop);
1911 | } else if (nodeHeight < elBottom) {
1912 | this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
1913 | }
1914 | },
1915 | bind: function() {
1916 | var that = this, onSelectableClick;
1917 | onSelectableClick = _.bind(this._onSelectableClick, this);
1918 | this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
1919 | this.$node.on("mouseover", this.selectors.selectable, function() {
1920 | that.setCursor($(this));
1921 | });
1922 | this.$node.on("mouseleave", function() {
1923 | that._removeCursor();
1924 | });
1925 | _.each(this.datasets, function(dataset) {
1926 | dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
1927 | });
1928 | return this;
1929 | },
1930 | isOpen: function isOpen() {
1931 | return this.$node.hasClass(this.classes.open);
1932 | },
1933 | open: function open() {
1934 | this.$node.scrollTop(0);
1935 | this.$node.addClass(this.classes.open);
1936 | },
1937 | close: function close() {
1938 | this.$node.attr("aria-expanded", false);
1939 | this.$node.removeClass(this.classes.open);
1940 | this._removeCursor();
1941 | },
1942 | setLanguageDirection: function setLanguageDirection(dir) {
1943 | this.$node.attr("dir", dir);
1944 | },
1945 | selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
1946 | var $selectables, $oldCursor, oldIndex, newIndex;
1947 | $oldCursor = this.getActiveSelectable();
1948 | $selectables = this._getSelectables();
1949 | oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
1950 | newIndex = oldIndex + delta;
1951 | newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
1952 | newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
1953 | return newIndex === -1 ? null : $selectables.eq(newIndex);
1954 | },
1955 | setCursor: function setCursor($selectable) {
1956 | this._removeCursor();
1957 | if ($selectable = $selectable && $selectable.first()) {
1958 | $selectable.addClass(this.classes.cursor);
1959 | this._ensureVisible($selectable);
1960 | }
1961 | },
1962 | getSelectableData: function getSelectableData($el) {
1963 | return $el && $el.length ? Dataset.extractData($el) : null;
1964 | },
1965 | getActiveSelectable: function getActiveSelectable() {
1966 | var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
1967 | return $selectable.length ? $selectable : null;
1968 | },
1969 | getTopSelectable: function getTopSelectable() {
1970 | var $selectable = this._getSelectables().first();
1971 | return $selectable.length ? $selectable : null;
1972 | },
1973 | update: function update(query) {
1974 | var isValidUpdate = query !== this.query;
1975 | if (isValidUpdate) {
1976 | this.query = query;
1977 | _.each(this.datasets, updateDataset);
1978 | }
1979 | return isValidUpdate;
1980 | function updateDataset(dataset) {
1981 | dataset.update(query);
1982 | }
1983 | },
1984 | empty: function empty() {
1985 | _.each(this.datasets, clearDataset);
1986 | this.query = null;
1987 | this.$node.addClass(this.classes.empty);
1988 | function clearDataset(dataset) {
1989 | dataset.clear();
1990 | }
1991 | },
1992 | destroy: function destroy() {
1993 | this.$node.off(".tt");
1994 | this.$node = $("
");
1995 | _.each(this.datasets, destroyDataset);
1996 | function destroyDataset(dataset) {
1997 | dataset.destroy();
1998 | }
1999 | }
2000 | });
2001 | return Menu;
2002 | }();
2003 | var Status = function() {
2004 | "use strict";
2005 | function Status(options) {
2006 | this.$el = $("
", {
2007 | role: "status",
2008 | "aria-live": "polite"
2009 | }).css({
2010 | position: "absolute",
2011 | padding: "0",
2012 | border: "0",
2013 | height: "1px",
2014 | width: "1px",
2015 | "margin-bottom": "-1px",
2016 | "margin-right": "-1px",
2017 | overflow: "hidden",
2018 | clip: "rect(0 0 0 0)",
2019 | "white-space": "nowrap"
2020 | });
2021 | options.$input.after(this.$el);
2022 | _.each(options.menu.datasets, _.bind(function(dataset) {
2023 | if (dataset.onSync) {
2024 | dataset.onSync("rendered", _.bind(this.update, this));
2025 | dataset.onSync("cleared", _.bind(this.cleared, this));
2026 | }
2027 | }, this));
2028 | }
2029 | _.mixin(Status.prototype, {
2030 | update: function update(event, suggestions) {
2031 | var length = suggestions.length;
2032 | var words;
2033 | if (length === 1) {
2034 | words = {
2035 | result: "result",
2036 | is: "is"
2037 | };
2038 | } else {
2039 | words = {
2040 | result: "results",
2041 | is: "are"
2042 | };
2043 | }
2044 | this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate.");
2045 | },
2046 | cleared: function() {
2047 | this.$el.text("");
2048 | }
2049 | });
2050 | return Status;
2051 | }();
2052 | var DefaultMenu = function() {
2053 | "use strict";
2054 | var s = Menu.prototype;
2055 | function DefaultMenu() {
2056 | Menu.apply(this, [].slice.call(arguments, 0));
2057 | }
2058 | _.mixin(DefaultMenu.prototype, Menu.prototype, {
2059 | open: function open() {
2060 | !this._allDatasetsEmpty() && this._show();
2061 | return s.open.apply(this, [].slice.call(arguments, 0));
2062 | },
2063 | close: function close() {
2064 | this._hide();
2065 | return s.close.apply(this, [].slice.call(arguments, 0));
2066 | },
2067 | _onRendered: function onRendered() {
2068 | if (this._allDatasetsEmpty()) {
2069 | this._hide();
2070 | } else {
2071 | this.isOpen() && this._show();
2072 | }
2073 | return s._onRendered.apply(this, [].slice.call(arguments, 0));
2074 | },
2075 | _onCleared: function onCleared() {
2076 | if (this._allDatasetsEmpty()) {
2077 | this._hide();
2078 | } else {
2079 | this.isOpen() && this._show();
2080 | }
2081 | return s._onCleared.apply(this, [].slice.call(arguments, 0));
2082 | },
2083 | setLanguageDirection: function setLanguageDirection(dir) {
2084 | this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl);
2085 | return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0));
2086 | },
2087 | _hide: function hide() {
2088 | this.$node.hide();
2089 | },
2090 | _show: function show() {
2091 | this.$node.css("display", "flex");
2092 | }
2093 | });
2094 | return DefaultMenu;
2095 | }();
2096 | var Typeahead = function() {
2097 | "use strict";
2098 | function Typeahead(o, www) {
2099 | var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged;
2100 | o = o || {};
2101 | if (!o.input) {
2102 | $.error("missing input");
2103 | }
2104 | if (!o.menu) {
2105 | $.error("missing menu");
2106 | }
2107 | if (!o.eventBus) {
2108 | $.error("missing event bus");
2109 | }
2110 | www.mixin(this);
2111 | this.eventBus = o.eventBus;
2112 | this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
2113 | this.input = o.input;
2114 | this.menu = o.menu;
2115 | this.enabled = true;
2116 | this.autoselect = !!o.autoselect;
2117 | this.active = false;
2118 | this.input.hasFocus() && this.activate();
2119 | this.dir = this.input.getLangDir();
2120 | this._hacks();
2121 | this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this);
2122 | onFocused = c(this, "activate", "open", "_onFocused");
2123 | onBlurred = c(this, "deactivate", "_onBlurred");
2124 | onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed");
2125 | onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed");
2126 | onEscKeyed = c(this, "isActive", "_onEscKeyed");
2127 | onUpKeyed = c(this, "isActive", "open", "_onUpKeyed");
2128 | onDownKeyed = c(this, "isActive", "open", "_onDownKeyed");
2129 | onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed");
2130 | onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed");
2131 | onQueryChanged = c(this, "_openIfActive", "_onQueryChanged");
2132 | onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged");
2133 | this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this);
2134 | }
2135 | _.mixin(Typeahead.prototype, {
2136 | _hacks: function hacks() {
2137 | var $input, $menu;
2138 | $input = this.input.$input || $("
");
2139 | $menu = this.menu.$node || $("
");
2140 | $input.on("blur.tt", function($e) {
2141 | var active, isActive, hasActive;
2142 | active = document.activeElement;
2143 | isActive = $menu.is(active);
2144 | hasActive = $menu.has(active).length > 0;
2145 | if (_.isMsie() && (isActive || hasActive)) {
2146 | $e.preventDefault();
2147 | $e.stopImmediatePropagation();
2148 | _.defer(function() {
2149 | $input.focus();
2150 | });
2151 | }
2152 | });
2153 | $menu.on("mousedown.tt", function($e) {
2154 | $e.preventDefault();
2155 | });
2156 | },
2157 | _onSelectableClicked: function onSelectableClicked(type, $el) {
2158 | this.select($el);
2159 | },
2160 | _onDatasetCleared: function onDatasetCleared() {
2161 | this._updateHint();
2162 | },
2163 | _onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) {
2164 | this._updateHint();
2165 | if (this.autoselect) {
2166 | var cursorClass = this.selectors.cursor.substr(1);
2167 | this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass);
2168 | }
2169 | this.eventBus.trigger("render", suggestions, async, dataset);
2170 | },
2171 | _onAsyncRequested: function onAsyncRequested(type, dataset, query) {
2172 | this.eventBus.trigger("asyncrequest", query, dataset);
2173 | },
2174 | _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
2175 | this.eventBus.trigger("asynccancel", query, dataset);
2176 | },
2177 | _onAsyncReceived: function onAsyncReceived(type, dataset, query) {
2178 | this.eventBus.trigger("asyncreceive", query, dataset);
2179 | },
2180 | _onFocused: function onFocused() {
2181 | this._minLengthMet() && this.menu.update(this.input.getQuery());
2182 | },
2183 | _onBlurred: function onBlurred() {
2184 | if (this.input.hasQueryChangedSinceLastFocus()) {
2185 | this.eventBus.trigger("change", this.input.getQuery());
2186 | }
2187 | },
2188 | _onEnterKeyed: function onEnterKeyed(type, $e) {
2189 | var $selectable;
2190 | if ($selectable = this.menu.getActiveSelectable()) {
2191 | if (this.select($selectable)) {
2192 | $e.preventDefault();
2193 | $e.stopPropagation();
2194 | }
2195 | } else if (this.autoselect) {
2196 | if (this.select(this.menu.getTopSelectable())) {
2197 | $e.preventDefault();
2198 | $e.stopPropagation();
2199 | }
2200 | }
2201 | },
2202 | _onTabKeyed: function onTabKeyed(type, $e) {
2203 | var $selectable;
2204 | if ($selectable = this.menu.getActiveSelectable()) {
2205 | this.select($selectable) && $e.preventDefault();
2206 | } else if ($selectable = this.menu.getTopSelectable()) {
2207 | this.autocomplete($selectable) && $e.preventDefault();
2208 | }
2209 | },
2210 | _onEscKeyed: function onEscKeyed() {
2211 | this.close();
2212 | },
2213 | _onUpKeyed: function onUpKeyed() {
2214 | this.moveCursor(-1);
2215 | },
2216 | _onDownKeyed: function onDownKeyed() {
2217 | this.moveCursor(+1);
2218 | },
2219 | _onLeftKeyed: function onLeftKeyed() {
2220 | if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
2221 | this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
2222 | }
2223 | },
2224 | _onRightKeyed: function onRightKeyed() {
2225 | if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
2226 | this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
2227 | }
2228 | },
2229 | _onQueryChanged: function onQueryChanged(e, query) {
2230 | this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
2231 | },
2232 | _onWhitespaceChanged: function onWhitespaceChanged() {
2233 | this._updateHint();
2234 | },
2235 | _onLangDirChanged: function onLangDirChanged(e, dir) {
2236 | if (this.dir !== dir) {
2237 | this.dir = dir;
2238 | this.menu.setLanguageDirection(dir);
2239 | }
2240 | },
2241 | _openIfActive: function openIfActive() {
2242 | this.isActive() && this.open();
2243 | },
2244 | _minLengthMet: function minLengthMet(query) {
2245 | query = _.isString(query) ? query : this.input.getQuery() || "";
2246 | return query.length >= this.minLength;
2247 | },
2248 | _updateHint: function updateHint() {
2249 | var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
2250 | $selectable = this.menu.getTopSelectable();
2251 | data = this.menu.getSelectableData($selectable);
2252 | val = this.input.getInputValue();
2253 | if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
2254 | query = Input.normalizeQuery(val);
2255 | escapedQuery = _.escapeRegExChars(query);
2256 | frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
2257 | match = frontMatchRegEx.exec(data.val);
2258 | match && this.input.setHint(val + match[1]);
2259 | } else {
2260 | this.input.clearHint();
2261 | }
2262 | },
2263 | isEnabled: function isEnabled() {
2264 | return this.enabled;
2265 | },
2266 | enable: function enable() {
2267 | this.enabled = true;
2268 | },
2269 | disable: function disable() {
2270 | this.enabled = false;
2271 | },
2272 | isActive: function isActive() {
2273 | return this.active;
2274 | },
2275 | activate: function activate() {
2276 | if (this.isActive()) {
2277 | return true;
2278 | } else if (!this.isEnabled() || this.eventBus.before("active")) {
2279 | return false;
2280 | } else {
2281 | this.active = true;
2282 | this.eventBus.trigger("active");
2283 | return true;
2284 | }
2285 | },
2286 | deactivate: function deactivate() {
2287 | if (!this.isActive()) {
2288 | return true;
2289 | } else if (this.eventBus.before("idle")) {
2290 | return false;
2291 | } else {
2292 | this.active = false;
2293 | this.close();
2294 | this.eventBus.trigger("idle");
2295 | return true;
2296 | }
2297 | },
2298 | isOpen: function isOpen() {
2299 | return this.menu.isOpen();
2300 | },
2301 | open: function open() {
2302 | if (!this.isOpen() && !this.eventBus.before("open")) {
2303 | this.menu.open();
2304 | this._updateHint();
2305 | this.eventBus.trigger("open");
2306 | }
2307 | return this.isOpen();
2308 | },
2309 | close: function close() {
2310 | if (this.isOpen() && !this.eventBus.before("close")) {
2311 | this.menu.close();
2312 | this.input.clearHint();
2313 | this.input.resetInputValue();
2314 | this.eventBus.trigger("close");
2315 | }
2316 | return !this.isOpen();
2317 | },
2318 | setVal: function setVal(val) {
2319 | this.input.setQuery(_.toStr(val));
2320 | },
2321 | getVal: function getVal() {
2322 | return this.input.getQuery();
2323 | },
2324 | select: function select($selectable) {
2325 | var data = this.menu.getSelectableData($selectable);
2326 | if (data && !this.eventBus.before("select", data.obj, data.dataset)) {
2327 | this.input.setQuery(data.val, true);
2328 | this.eventBus.trigger("select", data.obj, data.dataset);
2329 | this.close();
2330 | return true;
2331 | }
2332 | return false;
2333 | },
2334 | autocomplete: function autocomplete($selectable) {
2335 | var query, data, isValid;
2336 | query = this.input.getQuery();
2337 | data = this.menu.getSelectableData($selectable);
2338 | isValid = data && query !== data.val;
2339 | if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) {
2340 | this.input.setQuery(data.val);
2341 | this.eventBus.trigger("autocomplete", data.obj, data.dataset);
2342 | return true;
2343 | }
2344 | return false;
2345 | },
2346 | moveCursor: function moveCursor(delta) {
2347 | var query, $candidate, data, suggestion, datasetName, cancelMove, id;
2348 | query = this.input.getQuery();
2349 | $candidate = this.menu.selectableRelativeToCursor(delta);
2350 | data = this.menu.getSelectableData($candidate);
2351 | suggestion = data ? data.obj : null;
2352 | datasetName = data ? data.dataset : null;
2353 | id = $candidate ? $candidate.attr("id") : null;
2354 | this.input.trigger("cursorchange", id);
2355 | cancelMove = this._minLengthMet() && this.menu.update(query);
2356 | if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) {
2357 | this.menu.setCursor($candidate);
2358 | if (data) {
2359 | this.input.setInputValue(data.val);
2360 | } else {
2361 | this.input.resetInputValue();
2362 | this._updateHint();
2363 | }
2364 | this.eventBus.trigger("cursorchange", suggestion, datasetName);
2365 | return true;
2366 | }
2367 | return false;
2368 | },
2369 | destroy: function destroy() {
2370 | this.input.destroy();
2371 | this.menu.destroy();
2372 | }
2373 | });
2374 | return Typeahead;
2375 | function c(ctx) {
2376 | var methods = [].slice.call(arguments, 1);
2377 | return function() {
2378 | var args = [].slice.call(arguments);
2379 | _.each(methods, function(method) {
2380 | return ctx[method].apply(ctx, args);
2381 | });
2382 | };
2383 | }
2384 | }();
2385 | (function() {
2386 | "use strict";
2387 | var old, keys, methods;
2388 | old = $.fn.typeahead;
2389 | keys = {
2390 | www: "tt-www",
2391 | attrs: "tt-attrs",
2392 | typeahead: "tt-typeahead"
2393 | };
2394 | methods = {
2395 | initialize: function initialize(o, datasets) {
2396 | var www;
2397 | datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
2398 | o = o || {};
2399 | www = WWW(o.classNames);
2400 | return this.each(attach);
2401 | function attach() {
2402 | var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor;
2403 | _.each(datasets, function(d) {
2404 | d.highlight = !!o.highlight;
2405 | });
2406 | $input = $(this);
2407 | $wrapper = $(www.html.wrapper);
2408 | $hint = $elOrNull(o.hint);
2409 | $menu = $elOrNull(o.menu);
2410 | defaultHint = o.hint !== false && !$hint;
2411 | defaultMenu = o.menu !== false && !$menu;
2412 | defaultHint && ($hint = buildHintFromInput($input, www));
2413 | defaultMenu && ($menu = $(www.html.menu).css(www.css.menu));
2414 | $hint && $hint.val("");
2415 | $input = prepInput($input, www);
2416 | if (defaultHint || defaultMenu) {
2417 | $wrapper.css(www.css.wrapper);
2418 | $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint);
2419 | $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null);
2420 | }
2421 | MenuConstructor = defaultMenu ? DefaultMenu : Menu;
2422 | eventBus = new EventBus({
2423 | el: $input
2424 | });
2425 | input = new Input({
2426 | hint: $hint,
2427 | input: $input
2428 | }, www);
2429 | menu = new MenuConstructor({
2430 | node: $menu,
2431 | datasets: datasets
2432 | }, www);
2433 | status = new Status({
2434 | $input: $input,
2435 | menu: menu
2436 | });
2437 | typeahead = new Typeahead({
2438 | input: input,
2439 | menu: menu,
2440 | eventBus: eventBus,
2441 | minLength: o.minLength,
2442 | autoselect: o.autoselect
2443 | }, www);
2444 | $input.data(keys.www, www);
2445 | $input.data(keys.typeahead, typeahead);
2446 | }
2447 | },
2448 | isEnabled: function isEnabled() {
2449 | var enabled;
2450 | ttEach(this.first(), function(t) {
2451 | enabled = t.isEnabled();
2452 | });
2453 | return enabled;
2454 | },
2455 | enable: function enable() {
2456 | ttEach(this, function(t) {
2457 | t.enable();
2458 | });
2459 | return this;
2460 | },
2461 | disable: function disable() {
2462 | ttEach(this, function(t) {
2463 | t.disable();
2464 | });
2465 | return this;
2466 | },
2467 | isActive: function isActive() {
2468 | var active;
2469 | ttEach(this.first(), function(t) {
2470 | active = t.isActive();
2471 | });
2472 | return active;
2473 | },
2474 | activate: function activate() {
2475 | ttEach(this, function(t) {
2476 | t.activate();
2477 | });
2478 | return this;
2479 | },
2480 | deactivate: function deactivate() {
2481 | ttEach(this, function(t) {
2482 | t.deactivate();
2483 | });
2484 | return this;
2485 | },
2486 | isOpen: function isOpen() {
2487 | var open;
2488 | ttEach(this.first(), function(t) {
2489 | open = t.isOpen();
2490 | });
2491 | return open;
2492 | },
2493 | open: function open() {
2494 | ttEach(this, function(t) {
2495 | t.open();
2496 | });
2497 | return this;
2498 | },
2499 | close: function close() {
2500 | ttEach(this, function(t) {
2501 | t.close();
2502 | });
2503 | return this;
2504 | },
2505 | select: function select(el) {
2506 | var success = false, $el = $(el);
2507 | ttEach(this.first(), function(t) {
2508 | success = t.select($el);
2509 | });
2510 | return success;
2511 | },
2512 | autocomplete: function autocomplete(el) {
2513 | var success = false, $el = $(el);
2514 | ttEach(this.first(), function(t) {
2515 | success = t.autocomplete($el);
2516 | });
2517 | return success;
2518 | },
2519 | moveCursor: function moveCursoe(delta) {
2520 | var success = false;
2521 | ttEach(this.first(), function(t) {
2522 | success = t.moveCursor(delta);
2523 | });
2524 | return success;
2525 | },
2526 | val: function val(newVal) {
2527 | var query;
2528 | if (!arguments.length) {
2529 | ttEach(this.first(), function(t) {
2530 | query = t.getVal();
2531 | });
2532 | return query;
2533 | } else {
2534 | ttEach(this, function(t) {
2535 | t.setVal(_.toStr(newVal));
2536 | });
2537 | return this;
2538 | }
2539 | },
2540 | destroy: function destroy() {
2541 | ttEach(this, function(typeahead, $input) {
2542 | revert($input);
2543 | typeahead.destroy();
2544 | });
2545 | return this;
2546 | }
2547 | };
2548 | $.fn.typeahead = function(method) {
2549 | if (methods[method]) {
2550 | return methods[method].apply(this, [].slice.call(arguments, 1));
2551 | } else {
2552 | return methods.initialize.apply(this, arguments);
2553 | }
2554 | };
2555 | $.fn.typeahead.noConflict = function noConflict() {
2556 | $.fn.typeahead = old;
2557 | return this;
2558 | };
2559 | function ttEach($els, fn) {
2560 | $els.each(function() {
2561 | var $input = $(this), typeahead;
2562 | (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input);
2563 | });
2564 | }
2565 | function buildHintFromInput($input, www) {
2566 | return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({
2567 | readonly: true,
2568 | required: false
2569 | }).removeAttr("id name placeholder").removeClass("required").attr({
2570 | spellcheck: "false",
2571 | tabindex: -1
2572 | });
2573 | }
2574 | function prepInput($input, www) {
2575 | $input.data(keys.attrs, {
2576 | dir: $input.attr("dir"),
2577 | autocomplete: $input.attr("autocomplete"),
2578 | spellcheck: $input.attr("spellcheck"),
2579 | style: $input.attr("style")
2580 | });
2581 | $input.addClass(www.classes.input).attr({
2582 | spellcheck: false
2583 | });
2584 | try {
2585 | !$input.attr("dir") && $input.attr("dir", "auto");
2586 | } catch (e) {}
2587 | return $input;
2588 | }
2589 | function getBackgroundStyles($el) {
2590 | return {
2591 | backgroundAttachment: $el.css("background-attachment"),
2592 | backgroundClip: $el.css("background-clip"),
2593 | backgroundColor: $el.css("background-color"),
2594 | backgroundImage: $el.css("background-image"),
2595 | backgroundOrigin: $el.css("background-origin"),
2596 | backgroundPosition: $el.css("background-position"),
2597 | backgroundRepeat: $el.css("background-repeat"),
2598 | backgroundSize: $el.css("background-size")
2599 | };
2600 | }
2601 | function revert($input) {
2602 | var www, $wrapper;
2603 | www = $input.data(keys.www);
2604 | $wrapper = $input.parent().filter(www.selectors.wrapper);
2605 | _.each($input.data(keys.attrs), function(val, key) {
2606 | _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
2607 | });
2608 | $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input);
2609 | if ($wrapper.length) {
2610 | $input.detach().insertAfter($wrapper);
2611 | $wrapper.remove();
2612 | }
2613 | }
2614 | function $elOrNull(obj) {
2615 | var isValid, $el;
2616 | isValid = _.isJQuery(obj) || _.isElement(obj);
2617 | $el = isValid ? $(obj).first() : [];
2618 | return $el.length ? $el : null;
2619 | }
2620 | })();
2621 | });
--------------------------------------------------------------------------------
/assets/stylesheets/elasticsearch-base.scss:
--------------------------------------------------------------------------------
1 | /* make changes to discourse styling outside the affected widgets
2 | only when the plugin has been enabled */
3 | body.elasticsearch-enabled {
4 |
5 | /* don't let topic titles run into the search bar */
6 | .extra-info-wrapper {
7 | max-width: 300px;
8 | }
9 |
10 | /* hide the default search icon */
11 | header .icons > li:nth-child(2){
12 | display: none;
13 | }
14 |
15 | .es-holder {
16 | padding: 3px 15px 0 0;
17 | text-align: left;
18 | }
19 |
20 | .es-holder .es-input {
21 | color: #919191;
22 | font-family: Open Sans, Arial, sans-serif;
23 | border: solid 1px rgba(137,149,199,0.2);
24 | border-radius: 6px;
25 | font-size: 13px;
26 | line-height: 13px;
27 | padding: 8px 10px 8px 35px;
28 | height: auto;
29 | width: 250px;
30 | outline: 0;
31 | background-image: url("data:image/svg+xml;utf8,
");
32 | background-size: 14px 14px;
33 | background-repeat: no-repeat;
34 | background-position: 10px center;
35 |
36 | }
37 |
38 | @media (max-width: 768px) {
39 | .es-holder .es-input {
40 | display: none;
41 | }
42 | }
43 |
44 | .es-autocomplete .es-dropdown-menu {
45 | left: -295px !important;
46 | margin-top: 8px;
47 | border-radius: 3px;
48 | box-shadow: 0 1px 0 0 rgba(0,0,0,0.2),0 2px 3px 0 rgba(0,0,0,0.1);
49 | }
50 |
51 | .empty-message {
52 | background-color: #fff;
53 | padding: 5px;
54 | border: 1px solid #e3e5ec;
55 | border-radius: 5px;
56 | width: 285px;
57 | right: 0;
58 | position: absolute;
59 | }
60 | .es-holder form{
61 | margin: 0!important;
62 | }
63 | }
--------------------------------------------------------------------------------
/assets/stylesheets/elasticsearch-layout.scss:
--------------------------------------------------------------------------------
1 | .tt-dataset {
2 | /*width: $es-dropdown-width;*/
3 | .tt-dataset-users-tags, .tt-dataset-posts {
4 | display: table-cell;
5 | vertical-align: top;
6 | }
7 | .tt-dataset-users-tags {
8 | width: $es-dropdown-right-panel-width;
9 | }
10 | .tt-dataset-posts {
11 | width: $es-dropdown-left-panel-width;
12 | }
13 | }
14 |
15 | .tt-dataset {
16 | border-right: 1px solid $es-border-color;
17 | border-bottom: 1px solid $es-border-color;
18 | background-color: #fff;
19 | .tt-dataset-posts {
20 | background-color: $es-background-color;
21 | }
22 | .tt-dataset-users-tags {
23 | border-left: 1px solid $es-border-color;
24 | background-color: #f8f9fc;
25 | }
26 | }
27 | .tt-dataset:first-child{
28 | border-left: 1px solid $es-border-color;
29 | flex-grow: 2;
30 | }
31 |
32 | /* suggestions */
33 | .hit-post-category-name:hover {
34 | text-decoration: underline;
35 | }
36 |
37 | .hit-post-tag:hover {
38 | text-decoration: underline;
39 | }
40 |
41 | .hit-post-username:hover {
42 | text-decoration: underline;
43 | }
44 |
45 | .tt-dataset .tt-suggestion {
46 | cursor: pointer;
47 | }
48 |
49 | .tt-dataset .tt-suggestion:focus,.tt-dataset .tt-suggestion:hover,.tt-dataset .tt-suggestion:active {
50 | background-color: $es-cursor-color;
51 | }
52 |
53 | .tt-dataset .tt-suggestion em {
54 | background-color: $es-highlight-background-color;
55 | font-weight: bold;
56 | font-style: normal;
57 | font-weight: inherit;
58 | }
59 |
60 | .es-empty {
61 | padding: 12px;
62 | color: $es-light-font-color;
63 | }
64 |
65 | .es-dataset-users {
66 | padding: 8px 12px;
67 | .hit-user-left, .hit-user-right {
68 | display: table-cell;
69 | vertical-align: middle;
70 | }
71 | .hit-user-right {
72 | padding-left: 8px;
73 | }
74 | .hit-user-username-holder {
75 | margin-bottom: 3px;
76 | }
77 | .hit-user-username {
78 | font-weight: bold;
79 | }
80 | .hit-user-name {
81 | font-size: $es-small-font;
82 | color: $es-light-font-color;
83 | }
84 | .hit-user-name, .hit-user-username-holder {
85 | min-width: 100px;
86 | overflow: hidden;
87 | }
88 | .hit-user-avatar {
89 | width: 45px;
90 | height: 45px;
91 | vertical-align: middle;
92 | border-radius: 50%;
93 | }
94 | .hit-user-custom-ranking {
95 | color: $es-light-font-color;
96 | font-size: $es-small-font;
97 | }
98 | .hit-user-like-heart {
99 | color: $es-heart-color;
100 | }
101 | }
102 |
103 | .es-dataset-tags {
104 | padding: 8px 12px;
105 | .tt-suggestion {
106 | border-top: 1px solid $es-border-color;
107 | }
108 | .hit-tag:focus, .hit-tag:active{
109 | background-color: #f8f9fc;
110 | }
111 | .hit-tag-name {
112 | font-weight: bold;
113 | }
114 | .hit-tag-topic_count {
115 | color: $es-light-font-color;
116 | font-size: $es-small-font;
117 | }
118 | }
119 |
120 | .es-dataset-posts {
121 | padding: 12px;
122 | &:last-child {
123 | border-bottom: none;
124 | }
125 | .hit-post {
126 | .hit-post-topic-title {
127 | font-size: $es-large-font;
128 | font-weight: bold;
129 | }
130 | .hit-post-topic-views {
131 | font-size: $es-small-font;
132 | color: $es-light-font-color;
133 | }
134 | .hit-post-category-tags {
135 | margin: 2px 0 4px;
136 | }
137 | .hit-post-tag {
138 | color: $es-light-font-color;
139 | font-size: $es-small-font;
140 | padding: 0 3px;
141 | }
142 | .hit-post-category-name {
143 | color: $es-dark-font-color;
144 | font-size: $es-normal-font;
145 | font-weight: normal;
146 | }
147 | .hit-post-content-holder {
148 | color: $es-light-font-color;
149 | font-size: $es-small-font;
150 | }
151 | .hit-post-content {
152 | word-break: break-word;
153 | font-size: $es-small-font;
154 | }
155 | }
156 | }
157 | .tt-dataset-posts > .es-dataset-posts:nth-last-of-type(2){
158 | margin-bottom: 20px;
159 | }
160 | .tt-dataset-users-tags {
161 | background-color: #f8f9fc;
162 | min-width: 170px;
163 | }
164 | .tt-dataset-posts .show-more {
165 | padding: 5px;
166 | border-bottom: 1px solid $es-border-color;
167 | width: 50%;
168 | position: absolute;
169 | bottom: 0;
170 | }
171 | .tt-menu{
172 | right: 0!important;
173 | width: 600px;
174 | }
175 | .tt-highlight{
176 | background-color: #CCEFFF;
177 | }
178 | .Typeahead-spinner{
179 | position: absolute;
180 | left: 280px;
181 | top: 10px;
182 | display: none;
183 | }
--------------------------------------------------------------------------------
/assets/stylesheets/variables.scss:
--------------------------------------------------------------------------------
1 | $es-blue: rgb(0,174,255);
2 | $titan-white: rgb(248,250,255);
3 | $athens-gray: rgb(238, 240, 247);
4 | $ghost: rgb(196, 200, 216);
5 |
6 | $es-large-font: 15px;
7 | $es-normal-font: 13px;
8 | $es-small-font: 11.5px;
9 |
10 | $es-dark-font-color: #222;
11 | $es-light-font-color: #919191;
12 | $es-heart-color: #fa6c8d;
13 |
14 | $es-dropdown-left-panel-width: 450px;
15 | $es-dropdown-right-panel-width: 225px;
16 | $es-dropdown-width: 675px;
17 |
18 | $es-cursor-color: $athens-gray;
19 | $es-background-color: lighten($titan-white, 1%);
20 | $es-panel-background-color: lighten($athens-gray, 3%);
21 | $es-border-color: lighten($ghost, 10%);
22 | $es-highlight-background-color: lighten($es-blue, 40%);
23 |
--------------------------------------------------------------------------------
/config/locales/server.en.yml:
--------------------------------------------------------------------------------
1 | en:
2 | site_settings:
3 | elasticsearch_enabled: "Enable discourse-elasticsearch plugin"
4 | elasticsearch_autocomplete_enabled: "auto complete js"
5 | elasticsearch_server_ip: "elasticsearch server ip address"
6 | elasticsearch_server_port: "elasticsearch server port"
7 | elasticsearch_discourse_username: "discourse username"
8 |
--------------------------------------------------------------------------------
/config/locales/server.zh_CN.yml:
--------------------------------------------------------------------------------
1 | zh_CN:
2 | site_settings:
3 | elasticsearch_enabled: "启用 discourse-elasticsearch 插件"
4 | elasticsearch_autocomplete_enabled: "启用自动补全搜索结果"
5 | elasticsearch_server_ip: "elasticsearch 服务器 ip 地址"
6 | elasticsearch_server_port: "elasticsearch 服务器端口"
7 | elasticsearch_discourse_username: "discourse 用户名"
8 |
--------------------------------------------------------------------------------
/config/settings.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | elasticsearch_enabled:
3 | default: true
4 | client: true
5 | elasticsearch_autocomplete_enabled:
6 | default: true
7 | client: true
8 | elasticsearch_server_ip:
9 | default: 'localhost'
10 | client: true
11 | elasticsearch_server_port:
12 | default: '9200'
13 | client: true
14 | elasticsearch_discourse_username:
15 | default: 'system'
16 | client: false
17 |
--------------------------------------------------------------------------------
/lib/discourse_elasticsearch/elasticsearch_helper.rb:
--------------------------------------------------------------------------------
1 | require 'elasticsearch'
2 | module DiscourseElasticsearch
3 | class ElasticsearchHelper
4 |
5 | USERS_INDEX = "discourse-users".freeze
6 | POSTS_INDEX = "discourse-posts".freeze
7 | TAGS_INDEX = "discourse-tags".freeze
8 |
9 | # rank fragments with just a few words lower than others
10 | # usually they contain less substance
11 | WORDINESS_THRESHOLD = 5
12 |
13 | # detect salutations to avoid indexing with these common words
14 | SKIP_WORDS = ['thanks']
15 |
16 | def self.index_user(user_id, discourse_event)
17 | user = User.find_by(id: user_id)
18 | return if user.blank? || !guardian.can_see?(user)
19 |
20 | user_record = to_user_record(user)
21 | add_elasticsearch_users(USERS_INDEX, user_record, user_id)
22 | end
23 |
24 | def self.to_user_record(user)
25 | {
26 | objectID: user.id,
27 | url: "/users/#{user.username}",
28 | name: user.name,
29 | username: user.username,
30 | avatar_template: user.avatar_template,
31 | bio_raw: user.user_profile.bio_raw,
32 | post_count: user.post_count,
33 | badge_count: user.badge_count,
34 | likes_given: user.user_stat.likes_given,
35 | likes_received: user.user_stat.likes_received,
36 | days_visited: user.user_stat.days_visited,
37 | topic_count: user.user_stat.topic_count,
38 | posts_read: user.user_stat.posts_read_count,
39 | time_read: user.user_stat.time_read,
40 | created_at: user.created_at.to_i,
41 | updated_at: user.updated_at.to_i,
42 | last_seen_at: user.last_seen_at
43 | }
44 | end
45 |
46 | def self.index_topic(topic_id, discourse_event)
47 | end
48 |
49 | def self.index_post(post_id, discourse_event)
50 | post = Post.find_by(id: post_id)
51 | if should_index_post?(post)
52 | post_records = to_post_records(post)
53 | add_elasticsearch_posts(POSTS_INDEX, post_records)
54 | end
55 | end
56 |
57 | def self.should_index_post?(post)
58 | return false if post.blank? || post.post_type != Post.types[:regular] || !guardian.can_see?(post)
59 | topic = post.topic
60 | return false if topic.blank? || topic.archetype == Archetype.private_message
61 | return true
62 | end
63 |
64 | def self.to_post_records(post)
65 |
66 | post_records = []
67 |
68 | doc = Nokogiri::HTML(post.cooked)
69 | parts = doc.text.split(/\n/)
70 |
71 | parts.reject! do |content|
72 | content.strip.empty?
73 | end
74 |
75 | # for debugging, print the skips after the loop
76 | # to see what was excluded from indexing
77 | skips = []
78 |
79 | parts.each_with_index do |content, index|
80 |
81 | # skip anything without any alpha characters
82 | # commonly formatted code lines with only symbols
83 | unless content =~ /\w/
84 | skips.push(content)
85 | next
86 | end
87 |
88 | words = content.split(/\s+/)
89 |
90 | # don't index short lines that are probably saluations
91 | words.map! do |word|
92 | word.downcase.gsub(/[^0-9a-z]/i, '')
93 | end
94 | if words.length <= WORDINESS_THRESHOLD && (SKIP_WORDS & words).length > 0
95 | skips.push(content)
96 | next
97 | end
98 |
99 | record = {
100 | objectID: "#{post.id}-#{index}",
101 | url: "/t/#{post.topic.slug}/#{post.topic.id}/#{post.post_number}",
102 | post_id: post.id,
103 | part_number: index,
104 | post_number: post.post_number,
105 | created_at: post.created_at.to_i,
106 | updated_at: post.updated_at.to_i,
107 | reads: post.reads,
108 | like_count: post.like_count,
109 | image_url: post.image_url,
110 | word_count: words.length,
111 | is_wordy: words.length >= WORDINESS_THRESHOLD,
112 | content: content[0..8000]
113 | }
114 |
115 | user = post.user
116 | record[:user] = {
117 | id: user.id,
118 | url: "/users/#{user.username}",
119 | name: user.name,
120 | username: user.username,
121 | avatar_template: user.avatar_template
122 | }
123 |
124 | topic = post.topic
125 | if topic
126 | clean_title = topic.title
127 | record[:topic] = {
128 | id: topic.id,
129 | url: "/t/#{topic.slug}/#{topic.id}",
130 | title: clean_title,
131 | views: topic.views,
132 | slug: topic.slug,
133 | like_count: topic.like_count,
134 | tags: topic.tags.map(&:name)
135 | }
136 |
137 | category = topic.category
138 | if category
139 | record[:category] = {
140 | id: category.id,
141 | url: "/c/#{category.slug}",
142 | name: category.name,
143 | color: category.color,
144 | slug: category.slug
145 | }
146 | end
147 | end
148 |
149 | post_records << record
150 | end
151 |
152 | post_records
153 |
154 | end
155 |
156 | def self.to_tag_record(tag)
157 | {
158 | objectID: tag.id,
159 | url: "/tags/#{tag.name}",
160 | name: tag.name,
161 | topic_count: tag.topic_count
162 | }
163 | end
164 |
165 | def self.index_tags(tag_names, discourse_event)
166 | tag_names.each do |tag_name|
167 | tag = Tag.find_by_name(tag_name)
168 | if tag && should_index_tag?(tag)
169 | add_elasticsearch_users(TAGS_INDEX, to_tag_record(tag), tag.id)
170 | end
171 | end
172 | end
173 |
174 | def self.should_index_tag?(tag)
175 | tag.topic_count > 0
176 | end
177 |
178 | def self.add_elasticsearch_users(index_name, record, user_id)
179 | client = elasticsearch_index
180 | client.index index: index_name, id: user_id, body: record
181 | end
182 |
183 | def self.add_elasticsearch_posts(index_name, posts)
184 | client = elasticsearch_index
185 | posts.each do |post|
186 | client.index index: index_name, body: post
187 | end
188 | end
189 |
190 | def self.add_elasticsearch_tags(index_name, tags)
191 | client = elasticsearch_index
192 | tags.each do |tag|
193 | client.index index: index_name, body: tag
194 | end
195 | end
196 |
197 | def self.elasticsearch_index
198 | server_ip = SiteSetting.elasticsearch_server_ip
199 | server_port = SiteSetting.elasticsearch_server_port
200 | client = Elasticsearch::Client.new url: "#{server_ip}:#{server_port}", log: true
201 | return client
202 | end
203 |
204 | def self.clean_indices(index_name)
205 | client = elasticsearch_index
206 | if client.indices.exists? index: index_name
207 | client.indices.delete index: index_name
208 | else
209 | puts "Indices #{index_name} doesn't exist..."
210 | end
211 | end
212 |
213 | def self.create_mapping
214 | client = elasticsearch_index
215 | client.indices.create index: 'discourse-users',
216 | body: {
217 | mappings: {
218 | properties: {
219 | name: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" },
220 | url: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" },
221 | username: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" }
222 | }
223 | }
224 | }
225 |
226 | client.indices.create index: 'discourse-posts',
227 | body: {
228 | mappings: {
229 | properties: {
230 | topic: {
231 | properties: {
232 | title: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" }
233 | }
234 | },
235 | content: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" }
236 | }
237 | }
238 | }
239 |
240 | client.indices.create index: 'discourse-tags',
241 | body: {
242 | mappings: {
243 | properties: {
244 | name: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" },
245 | url: { type: 'text', analyzer: 'ik_max_word', search_analyzer: "ik_smart" }
246 | }
247 | }
248 | }
249 | end
250 |
251 | def self.guardian
252 | Guardian.new(User.find_by(username: SiteSetting.elasticsearch_discourse_username))
253 | end
254 | end
255 | end
256 |
--------------------------------------------------------------------------------
/lib/tasks/discourse_elasticsearch.rake:
--------------------------------------------------------------------------------
1 | desc "configure indices and upload data"
2 | task "elasticsearch:initialize" => :environment do
3 | Rake::Task["elasticsearch:configure"].invoke
4 | Rake::Task["elasticsearch:reindex"].invoke
5 | end
6 |
7 | desc "configure elasticsearch index settings"
8 | task "elasticsearch:configure" => :environment do
9 | elasticsearch_configure_users
10 | elasticsearch_configure_posts
11 | elasticsearch_configure_tags
12 | elasticsearch_configure_map
13 | end
14 |
15 | desc "reindex everything to elasticsearch"
16 | task "elasticsearch:reindex" => :environment do
17 | elasticsearch_reindex_users
18 | elasticsearch_reindex_posts
19 | elasticsearch_reindex_tags
20 | end
21 |
22 | desc "reindex users in elasticsearch"
23 | task "elasticsearch:reindex_users" => :environment do
24 | elasticsearch_reindex_users
25 | end
26 |
27 | desc "reindex posts in elasticsearch"
28 | task "elasticsearch:reindex_posts" => :environment do
29 | elasticsearch_reindex_posts
30 | end
31 |
32 | desc "reindex tags in elasticsearch"
33 | task "elasticsearch:reindex_tags" => :environment do
34 | elasticsearch_reindex_tags
35 | end
36 |
37 | def elasticsearch_configure_users
38 | puts "[Starting] Cleaning users index to Elasticsearch"
39 | DiscourseElasticsearch::ElasticsearchHelper.clean_indices(DiscourseElasticsearch::ElasticsearchHelper::USERS_INDEX)
40 | puts "[Finished] Successfully configured users index in Elasticsearch"
41 | end
42 |
43 | def elasticsearch_configure_posts
44 | puts "[Starting] Cleaning posts index to Elasticsearch"
45 | DiscourseElasticsearch::ElasticsearchHelper.clean_indices(DiscourseElasticsearch::ElasticsearchHelper::POSTS_INDEX)
46 | puts "[Finished] Successfully configured posts index in Elasticsearch"
47 | end
48 |
49 | def elasticsearch_configure_tags
50 | puts "[Starting] Cleaning tags index to Elasticsearch"
51 | DiscourseElasticsearch::ElasticsearchHelper.clean_indices(DiscourseElasticsearch::ElasticsearchHelper::TAGS_INDEX)
52 | puts "[Finished] Successfully configured tags index in Elasticsearch"
53 | end
54 |
55 | def elasticsearch_configure_map
56 | puts "[Starting] Creating mapping to Elasticsearch"
57 | DiscourseElasticsearch::ElasticsearchHelper.create_mapping
58 | end
59 |
60 | def elasticsearch_reindex_users
61 |
62 | puts "[Starting] Pushing users to Elasticsearch"
63 | User.all.each do |user|
64 | #user_records << DiscourseElasticsearch::ElasticsearchHelper.to_user_record(user)
65 | puts user.id
66 | user_record = DiscourseElasticsearch::ElasticsearchHelper.index_user(user.id, '')
67 | puts user_record
68 | end
69 | end
70 |
71 | def elasticsearch_reindex_posts
72 | puts "[Starting] Pushing posts to Elasticsearch"
73 | post_records = []
74 | Post.all.includes(:user, :topic).each do |post|
75 | if DiscourseElasticsearch::ElasticsearchHelper.should_index_post?(post)
76 | post_records << DiscourseElasticsearch::ElasticsearchHelper.to_post_records(post)
77 | end
78 | end
79 | post_records.flatten!
80 | puts "[Progress] Gathered posts from Discourse"
81 | post_records.each_slice(100) do |slice|
82 | DiscourseElasticsearch::ElasticsearchHelper.add_elasticsearch_posts(
83 | DiscourseElasticsearch::ElasticsearchHelper::POSTS_INDEX, slice.flatten)
84 | puts "[Progress] Pushed #{slice.length} post records to Elasticsearch"
85 | end
86 | puts "[Finished] Successfully pushed #{post_records.length} posts to Elasticsearch"
87 | end
88 |
89 |
90 | def elasticsearch_reindex_tags
91 | puts "[Starting] Pushing tags to Elasticsearch"
92 | tag_records = []
93 | Tag.all.each do |tag|
94 | if DiscourseElasticsearch::ElasticsearchHelper.should_index_tag?(tag)
95 | tag_records << DiscourseElasticsearch::ElasticsearchHelper.to_tag_record(tag)
96 | end
97 | end
98 | puts "[Progress] Gathered tags from Discourse"
99 | DiscourseElasticsearch::ElasticsearchHelper.add_elasticsearch_tags(
100 | DiscourseElasticsearch::ElasticsearchHelper::TAGS_INDEX, tag_records)
101 | puts "[Finished] Successfully pushed #{tag_records.length} tags to Elasticsearch"
102 | end
103 |
--------------------------------------------------------------------------------
/plugin.rb:
--------------------------------------------------------------------------------
1 | # name: discourse-elasticsearch
2 | # about:
3 | # version: 0.2
4 | # authors: imMMX
5 | # url: https://github.com/imMMX
6 |
7 |
8 | gem 'json', '2.2.0'
9 | gem 'httpclient', '2.8.3'
10 | gem 'elasticsearch-transport', '7.2.0'
11 | gem 'elasticsearch-api', '7.2.0'
12 | gem 'elasticsearch', '7.2.0'
13 |
14 |
15 | register_asset 'stylesheets/variables.scss'
16 | register_asset 'stylesheets/elasticsearch-base.scss'
17 | register_asset 'stylesheets/elasticsearch-layout.scss'
18 | register_asset 'lib/typehead.bundle.js'
19 |
20 | enabled_site_setting :elasticsearch_enabled
21 |
22 | PLUGIN_NAME ||= "discourse-elasticsearch".freeze
23 |
24 | after_initialize do
25 | load File.expand_path('../lib/discourse_elasticsearch/elasticsearch_helper.rb', __FILE__)
26 |
27 | # see lib/plugin/instance.rb for the methods available in this context
28 |
29 |
30 | module ::DiscourseElasticsearch
31 | class Engine < ::Rails::Engine
32 | engine_name PLUGIN_NAME
33 | isolate_namespace DiscourseElasticsearch
34 | end
35 | end
36 |
37 | require_dependency File.expand_path('../app/jobs/regular/update_elasticsearch_post.rb', __FILE__)
38 | require_dependency File.expand_path('../app/jobs/regular/update_elasticsearch_user.rb', __FILE__)
39 | require_dependency File.expand_path('../app/jobs/regular/update_elasticsearch_topic.rb', __FILE__)
40 | require_dependency File.expand_path('../app/jobs/regular/update_elasticsearch_tag.rb', __FILE__)
41 | require_dependency 'discourse_event'
42 |
43 | require_dependency "application_controller"
44 | class DiscourseElasticsearch::ActionsController < ::ApplicationController
45 | requires_plugin PLUGIN_NAME
46 |
47 | before_action :ensure_logged_in
48 |
49 | def list
50 | render json: success_json
51 | end
52 | end
53 |
54 | DiscourseElasticsearch::Engine.routes.draw do
55 | get "/list" => "actions#list"
56 | end
57 |
58 | Discourse::Application.routes.append do
59 | mount ::DiscourseElasticsearch::Engine, at: "/discourse-elasticsearch"
60 | end
61 |
62 | [:user_created, :user_updated].each do |discourse_event|
63 | DiscourseEvent.on(discourse_event) do |user|
64 | if SiteSetting.elasticsearch_enabled?
65 | Jobs.enqueue_in(0,
66 | :update_elasticsearch_user,
67 | user_id: user.id,
68 | discourse_event: discourse_event
69 | )
70 | end
71 | end
72 | end
73 |
74 | [:topic_created, :topic_edited, :topic_destroyed, :topic_recovered].each do |discourse_event|
75 | DiscourseEvent.on(discourse_event) do |topic|
76 | if SiteSetting.elasticsearch_enabled?
77 | Jobs.enqueue_in(0,
78 | :update_elasticsearch_topic,
79 | topic_id: topic.id,
80 | discourse_event: discourse_event
81 | )
82 | Jobs.enqueue_in(0,
83 | :update_elasticsearch_tag,
84 | tags: topic.tags.map(&:name),
85 | discourse_event: discourse_event
86 | )
87 | end
88 | end
89 | end
90 |
91 | [:post_created, :post_edited, :post_destroyed, :post_recovered].each do |discourse_event|
92 | DiscourseEvent.on(discourse_event) do |post|
93 | if SiteSetting.elasticsearch_enabled?
94 | Jobs.enqueue_in(0,
95 | :update_elasticsearch_post,
96 | post_id: post.id,
97 | discourse_event: discourse_event
98 | )
99 | if post.topic
100 | Jobs.enqueue_in(0,
101 | :update_elasticsearch_tag,
102 | tags: post.topic.tags.map(&:name),
103 | discourse_event: discourse_event
104 | )
105 | end
106 | end
107 | end
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/spec/requests/actions_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe discourse-elasticsearch::ActionsController do
4 | before do
5 | Jobs.run_immediately!
6 | end
7 |
8 | it 'can list' do
9 | sign_in(Fabricate(:user))
10 | get "/discourse-elasticsearch/list.json"
11 | expect(response.status).to eq(200)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test/javascripts/acceptance/discourse-elasticsearch-test.js.es6:
--------------------------------------------------------------------------------
1 | import { acceptance } from "helpers/qunit-helpers";
2 |
3 | acceptance("discourse-elasticsearch", { loggedIn: true });
4 |
5 | test("discourse-elasticsearch works", async assert => {
6 | await visit("/admin/plugins/discourse-elasticsearch");
7 |
8 | assert.ok(false, "it shows the discourse-elasticsearch button");
9 | });
10 |
--------------------------------------------------------------------------------