3
* https://github.com/twitter/typeahead.js
4
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
12
return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
14
isBlankString: function(str) {
15
return !str || /^\s*$/.test(str);
17
escapeRegExChars: function(str) {
18
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
20
isString: function(obj) {
21
return typeof obj === "string";
23
isNumber: function(obj) {
24
return typeof obj === "number";
27
isFunction: $.isFunction,
28
isObject: $.isPlainObject,
29
isUndefined: function(obj) {
30
return typeof obj === "undefined";
32
toStr: function toStr(s) {
33
return _.isUndefined(s) || s === null ? "" : s + "";
36
each: function(collection, cb) {
37
$.each(collection, reverseArgs);
38
function reverseArgs(index, value) {
39
return cb(value, index);
44
every: function(obj, test) {
49
$.each(obj, function(key, val) {
50
if (!(result = test.call(null, val, key, obj))) {
56
some: function(obj, test) {
61
$.each(obj, function(key, val) {
62
if (result = test.call(null, val, key, obj)) {
69
getUniqueId: function() {
75
templatify: function templatify(obj) {
76
return $.isFunction(obj) ? obj : template;
84
debounce: function(func, wait, immediate) {
87
var context = this, args = arguments, later, callNow;
91
result = func.apply(context, args);
94
callNow = immediate && !timeout;
95
clearTimeout(timeout);
96
timeout = setTimeout(later, wait);
98
result = func.apply(context, args);
103
throttle: function(func, wait) {
104
var context, args, timeout, result, previous, later;
107
previous = new Date();
109
result = func.apply(context, args);
112
var now = new Date(), remaining = wait - (now - previous);
115
if (remaining <= 0) {
116
clearTimeout(timeout);
119
result = func.apply(context, args);
120
} else if (!timeout) {
121
timeout = setTimeout(later, remaining);
129
var html = function() {
131
wrapper: '<span class="twitter-typeahead"></span>',
132
dropdown: '<span class="tt-dropdown-menu"></span>',
133
dataset: '<div class="tt-dataset-%CLASS%"></div>',
134
suggestions: '<span class="tt-suggestions"></span>',
135
suggestion: '<div class="tt-suggestion"></div>'
138
var css = function() {
142
position: "relative",
143
display: "inline-block"
146
position: "absolute",
149
borderColor: "transparent",
154
position: "relative",
155
verticalAlign: "top",
156
backgroundColor: "transparent"
159
position: "relative",
163
position: "absolute",
173
whiteSpace: "nowrap",
190
backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
193
if (_.isMsie() && _.isMsie() <= 7) {
200
var EventBus = function() {
202
var namespace = "typeahead:";
203
function EventBus(o) {
205
$.error("EventBus initialized without el");
209
_.mixin(EventBus.prototype, {
210
trigger: function(type) {
211
var args = [].slice.call(arguments, 1);
212
this.$el.trigger(namespace + type, args);
217
var EventEmitter = function() {
219
var splitter = /\s+/, nextTick = getNextTick();
226
function on(method, types, cb, context) {
231
types = types.split(splitter);
232
cb = context ? bindContext(cb, context) : cb;
233
this._callbacks = this._callbacks || {};
234
while (type = types.shift()) {
235
this._callbacks[type] = this._callbacks[type] || {
239
this._callbacks[type][method].push(cb);
243
function onAsync(types, cb, context) {
244
return on.call(this, "async", types, cb, context);
246
function onSync(types, cb, context) {
247
return on.call(this, "sync", types, cb, context);
249
function off(types) {
251
if (!this._callbacks) {
254
types = types.split(splitter);
255
while (type = types.shift()) {
256
delete this._callbacks[type];
260
function trigger(types) {
261
var type, callbacks, args, syncFlush, asyncFlush;
262
if (!this._callbacks) {
265
types = types.split(splitter);
266
args = [].slice.call(arguments, 1);
267
while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
268
syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
269
asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
270
syncFlush() && nextTick(asyncFlush);
274
function getFlush(callbacks, context, args) {
278
for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
279
cancelled = callbacks[i].apply(context, args) === false;
284
function getNextTick() {
286
if (window.setImmediate) {
287
nextTickFn = function nextTickSetImmediate(fn) {
288
setImmediate(function() {
293
nextTickFn = function nextTickSetTimeout(fn) {
294
setTimeout(function() {
301
function bindContext(fn, context) {
302
return fn.bind ? fn.bind(context) : function() {
303
fn.apply(context, [].slice.call(arguments, 0));
307
var highlight = function(doc) {
317
return function hightlight(o) {
319
o = _.mixin({}, defaults, o);
320
if (!o.node || !o.pattern) {
323
o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
324
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
325
traverse(o.node, hightlightTextNode);
326
function hightlightTextNode(textNode) {
327
var match, patternNode, wrapperNode;
328
if (match = regex.exec(textNode.data)) {
329
wrapperNode = doc.createElement(o.tagName);
330
o.className && (wrapperNode.className = o.className);
331
patternNode = textNode.splitText(match.index);
332
patternNode.splitText(match[0].length);
333
wrapperNode.appendChild(patternNode.cloneNode(true));
334
textNode.parentNode.replaceChild(wrapperNode, patternNode);
338
function traverse(el, hightlightTextNode) {
339
var childNode, TEXT_NODE_TYPE = 3;
340
for (var i = 0; i < el.childNodes.length; i++) {
341
childNode = el.childNodes[i];
342
if (childNode.nodeType === TEXT_NODE_TYPE) {
343
i += hightlightTextNode(childNode) ? 1 : 0;
345
traverse(childNode, hightlightTextNode);
350
function getRegex(patterns, caseSensitive, wordsOnly) {
351
var escapedPatterns = [], regexStr;
352
for (var i = 0, len = patterns.length; i < len; i++) {
353
escapedPatterns.push(_.escapeRegExChars(patterns[i]));
355
regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
356
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
359
var Input = function() {
361
var specialKeyCodeMap;
362
specialKeyCodeMap = {
372
var that = this, onBlur, onFocus, onKeydown, onInput;
375
$.error("input is missing");
377
onBlur = _.bind(this._onBlur, this);
378
onFocus = _.bind(this._onFocus, this);
379
onKeydown = _.bind(this._onKeydown, this);
380
onInput = _.bind(this._onInput, this);
381
this.$hint = $(o.hint);
382
this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
383
if (this.$hint.length === 0) {
384
this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
387
this.$input.on("input.tt", onInput);
389
this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
390
if (specialKeyCodeMap[$e.which || $e.keyCode]) {
393
_.defer(_.bind(that._onInput, that, $e));
396
this.query = this.$input.val();
397
this.$overflowHelper = buildOverflowHelper(this.$input);
399
Input.normalizeQuery = function(str) {
400
return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
402
_.mixin(Input.prototype, EventEmitter, {
403
_onBlur: function onBlur() {
404
this.resetInputValue();
405
this.trigger("blurred");
407
_onFocus: function onFocus() {
408
this.trigger("focused");
410
_onKeydown: function onKeydown($e) {
411
var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
412
this._managePreventDefault(keyName, $e);
413
if (keyName && this._shouldTrigger(keyName, $e)) {
414
this.trigger(keyName + "Keyed", $e);
417
_onInput: function onInput() {
418
this._checkInputValue();
420
_managePreventDefault: function managePreventDefault(keyName, $e) {
421
var preventDefault, hintValue, inputValue;
424
hintValue = this.getHint();
425
inputValue = this.getInputValue();
426
preventDefault = hintValue && hintValue !== inputValue && !withModifier($e);
431
preventDefault = !withModifier($e);
435
preventDefault = false;
437
preventDefault && $e.preventDefault();
439
_shouldTrigger: function shouldTrigger(keyName, $e) {
443
trigger = !withModifier($e);
451
_checkInputValue: function checkInputValue() {
452
var inputValue, areEquivalent, hasDifferentWhitespace;
453
inputValue = this.getInputValue();
454
areEquivalent = areQueriesEquivalent(inputValue, this.query);
455
hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false;
456
this.query = inputValue;
457
if (!areEquivalent) {
458
this.trigger("queryChanged", this.query);
459
} else if (hasDifferentWhitespace) {
460
this.trigger("whitespaceChanged", this.query);
463
focus: function focus() {
466
blur: function blur() {
469
getQuery: function getQuery() {
472
setQuery: function setQuery(query) {
475
getInputValue: function getInputValue() {
476
return this.$input.val();
478
setInputValue: function setInputValue(value, silent) {
479
this.$input.val(value);
480
silent ? this.clearHint() : this._checkInputValue();
482
resetInputValue: function resetInputValue() {
483
this.setInputValue(this.query, true);
485
getHint: function getHint() {
486
return this.$hint.val();
488
setHint: function setHint(value) {
489
this.$hint.val(value);
491
clearHint: function clearHint() {
494
clearHintIfInvalid: function clearHintIfInvalid() {
495
var val, hint, valIsPrefixOfHint, isValid;
496
val = this.getInputValue();
497
hint = this.getHint();
498
valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
499
isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
500
!isValid && this.clearHint();
502
getLanguageDirection: function getLanguageDirection() {
503
return (this.$input.css("direction") || "ltr").toLowerCase();
505
hasOverflow: function hasOverflow() {
506
var constraint = this.$input.width() - 2;
507
this.$overflowHelper.text(this.getInputValue());
508
return this.$overflowHelper.width() >= constraint;
510
isCursorAtEnd: function() {
511
var valueLength, selectionStart, range;
512
valueLength = this.$input.val().length;
513
selectionStart = this.$input[0].selectionStart;
514
if (_.isNumber(selectionStart)) {
515
return selectionStart === valueLength;
516
} else if (document.selection) {
517
range = document.selection.createRange();
518
range.moveStart("character", -valueLength);
519
return valueLength === range.text.length;
523
destroy: function destroy() {
524
this.$hint.off(".tt");
525
this.$input.off(".tt");
526
this.$hint = this.$input = this.$overflowHelper = null;
530
function buildOverflowHelper($input) {
531
return $('<pre aria-hidden="true"></pre>').css({
532
position: "absolute",
533
visibility: "hidden",
535
fontFamily: $input.css("font-family"),
536
fontSize: $input.css("font-size"),
537
fontStyle: $input.css("font-style"),
538
fontVariant: $input.css("font-variant"),
539
fontWeight: $input.css("font-weight"),
540
wordSpacing: $input.css("word-spacing"),
541
letterSpacing: $input.css("letter-spacing"),
542
textIndent: $input.css("text-indent"),
543
textRendering: $input.css("text-rendering"),
544
textTransform: $input.css("text-transform")
545
}).insertAfter($input);
547
function areQueriesEquivalent(a, b) {
548
return Input.normalizeQuery(a) === Input.normalizeQuery(b);
550
function withModifier($e) {
551
return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
554
var Dataset = function() {
556
var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum";
557
function Dataset(o) {
559
o.templates = o.templates || {};
561
$.error("missing source");
563
if (o.name && !isValidName(o.name)) {
564
$.error("invalid dataset name: " + o.name);
567
this.highlight = !!o.highlight;
568
this.name = o.name || _.getUniqueId();
569
this.source = o.source;
570
this.displayFn = getDisplayFn(o.display || o.displayKey);
571
this.templates = getTemplates(o.templates, this.displayFn);
572
this.$el = $(html.dataset.replace("%CLASS%", this.name));
574
Dataset.extractDatasetName = function extractDatasetName(el) {
575
return $(el).data(datasetKey);
577
Dataset.extractValue = function extractDatum(el) {
578
return $(el).data(valueKey);
580
Dataset.extractDatum = function extractDatum(el) {
581
return $(el).data(datumKey);
583
_.mixin(Dataset.prototype, EventEmitter, {
584
_render: function render(query, suggestions) {
588
var that = this, hasSuggestions;
590
hasSuggestions = suggestions && suggestions.length;
591
if (!hasSuggestions && this.templates.empty) {
592
this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
593
} else if (hasSuggestions) {
594
this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null);
596
this.trigger("rendered");
597
function getEmptyHtml() {
598
return that.templates.empty({
603
function getSuggestionsHtml() {
604
var $suggestions, nodes;
605
$suggestions = $(html.suggestions).css(css.suggestions);
606
nodes = _.map(suggestions, getSuggestionNode);
607
$suggestions.append.apply($suggestions, nodes);
608
that.highlight && highlight({
609
className: "tt-highlight",
610
node: $suggestions[0],
614
function getSuggestionNode(suggestion) {
616
$el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion);
617
$el.children().each(function() {
618
$(this).css(css.suggestionChild);
623
function getHeaderHtml() {
624
return that.templates.header({
626
isEmpty: !hasSuggestions
629
function getFooterHtml() {
630
return that.templates.footer({
632
isEmpty: !hasSuggestions
636
getRoot: function getRoot() {
639
update: function update(query) {
642
this.canceled = false;
643
this.source(query, render);
644
function render(suggestions) {
645
if (!that.canceled && query === that.query) {
646
that._render(query, suggestions);
650
cancel: function cancel() {
651
this.canceled = true;
653
clear: function clear() {
656
this.trigger("rendered");
658
isEmpty: function isEmpty() {
659
return this.$el.is(":empty");
661
destroy: function destroy() {
666
function getDisplayFn(display) {
667
display = display || "value";
668
return _.isFunction(display) ? display : displayFn;
669
function displayFn(obj) {
673
function getTemplates(templates, displayFn) {
675
empty: templates.empty && _.templatify(templates.empty),
676
header: templates.header && _.templatify(templates.header),
677
footer: templates.footer && _.templatify(templates.footer),
678
suggestion: templates.suggestion || suggestionTemplate
680
function suggestionTemplate(context) {
681
return "<p>" + displayFn(context) + "</p>";
684
function isValidName(str) {
685
return /^[_a-zA-Z0-9-]+$/.test(str);
688
var Dropdown = function() {
690
function Dropdown(o) {
691
var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave;
694
$.error("menu is required");
698
this.datasets = _.map(o.datasets, initializeDataset);
699
onSuggestionClick = _.bind(this._onSuggestionClick, this);
700
onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this);
701
onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this);
702
this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave);
703
_.each(this.datasets, function(dataset) {
704
that.$menu.append(dataset.getRoot());
705
dataset.onSync("rendered", that._onRendered, that);
708
_.mixin(Dropdown.prototype, EventEmitter, {
709
_onSuggestionClick: function onSuggestionClick($e) {
710
this.trigger("suggestionClicked", $($e.currentTarget));
712
_onSuggestionMouseEnter: function onSuggestionMouseEnter($e) {
713
this._removeCursor();
714
this._setCursor($($e.currentTarget), true);
716
_onSuggestionMouseLeave: function onSuggestionMouseLeave() {
717
this._removeCursor();
719
_onRendered: function onRendered() {
720
this.isEmpty = _.every(this.datasets, isDatasetEmpty);
721
this.isEmpty ? this._hide() : this.isOpen && this._show();
722
this.trigger("datasetRendered");
723
function isDatasetEmpty(dataset) {
724
return dataset.isEmpty();
731
this.$menu.css("display", "block");
733
_getSuggestions: function getSuggestions() {
734
return this.$menu.find(".tt-suggestion");
736
_getCursor: function getCursor() {
737
return this.$menu.find(".tt-cursor").first();
739
_setCursor: function setCursor($el, silent) {
740
$el.first().addClass("tt-cursor");
741
!silent && this.trigger("cursorMoved");
743
_removeCursor: function removeCursor() {
744
this._getCursor().removeClass("tt-cursor");
746
_moveCursor: function moveCursor(increment) {
747
var $suggestions, $oldCursor, newCursorIndex, $newCursor;
751
$oldCursor = this._getCursor();
752
$suggestions = this._getSuggestions();
753
this._removeCursor();
754
newCursorIndex = $suggestions.index($oldCursor) + increment;
755
newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1;
756
if (newCursorIndex === -1) {
757
this.trigger("cursorRemoved");
759
} else if (newCursorIndex < -1) {
760
newCursorIndex = $suggestions.length - 1;
762
this._setCursor($newCursor = $suggestions.eq(newCursorIndex));
763
this._ensureVisible($newCursor);
765
_ensureVisible: function ensureVisible($el) {
766
var elTop, elBottom, menuScrollTop, menuHeight;
767
elTop = $el.position().top;
768
elBottom = elTop + $el.outerHeight(true);
769
menuScrollTop = this.$menu.scrollTop();
770
menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10);
772
this.$menu.scrollTop(menuScrollTop + elTop);
773
} else if (menuHeight < elBottom) {
774
this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
777
close: function close() {
780
this._removeCursor();
782
this.trigger("closed");
785
open: function open() {
788
!this.isEmpty && this._show();
789
this.trigger("opened");
792
setLanguageDirection: function setLanguageDirection(dir) {
793
this.$menu.css(dir === "ltr" ? css.ltr : css.rtl);
795
moveCursorUp: function moveCursorUp() {
796
this._moveCursor(-1);
798
moveCursorDown: function moveCursorDown() {
799
this._moveCursor(+1);
801
getDatumForSuggestion: function getDatumForSuggestion($el) {
805
raw: Dataset.extractDatum($el),
806
value: Dataset.extractValue($el),
807
datasetName: Dataset.extractDatasetName($el)
812
getDatumForCursor: function getDatumForCursor() {
813
return this.getDatumForSuggestion(this._getCursor().first());
815
getDatumForTopSuggestion: function getDatumForTopSuggestion() {
816
return this.getDatumForSuggestion(this._getSuggestions().first());
818
update: function update(query) {
819
_.each(this.datasets, updateDataset);
820
function updateDataset(dataset) {
821
dataset.update(query);
824
empty: function empty() {
825
_.each(this.datasets, clearDataset);
827
function clearDataset(dataset) {
831
isVisible: function isVisible() {
832
return this.isOpen && !this.isEmpty;
834
destroy: function destroy() {
835
this.$menu.off(".tt");
837
_.each(this.datasets, destroyDataset);
838
function destroyDataset(dataset) {
844
function initializeDataset(oDataset) {
845
return new Dataset(oDataset);
848
var Typeahead = function() {
850
var attrsKey = "ttAttrs";
851
function Typeahead(o) {
852
var $menu, $input, $hint;
855
$.error("missing input");
857
this.isActivated = false;
858
this.autoselect = !!o.autoselect;
859
this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
860
this.$node = buildDom(o.input, o.withHint);
861
$menu = this.$node.find(".tt-dropdown-menu");
862
$input = this.$node.find(".tt-input");
863
$hint = this.$node.find(".tt-hint");
864
$input.on("blur.tt", function($e) {
865
var active, isActive, hasActive;
866
active = document.activeElement;
867
isActive = $menu.is(active);
868
hasActive = $menu.has(active).length > 0;
869
if (_.isMsie() && (isActive || hasActive)) {
871
$e.stopImmediatePropagation();
877
$menu.on("mousedown.tt", function($e) {
880
this.eventBus = o.eventBus || new EventBus({
883
this.dropdown = new Dropdown({
886
}).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this);
887
this.input = new Input({
890
}).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this);
891
this._setLanguageDirection();
893
_.mixin(Typeahead.prototype, {
894
_onSuggestionClicked: function onSuggestionClicked(type, $el) {
896
if (datum = this.dropdown.getDatumForSuggestion($el)) {
900
_onCursorMoved: function onCursorMoved() {
901
var datum = this.dropdown.getDatumForCursor();
902
this.input.setInputValue(datum.value, true);
903
this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName);
905
_onCursorRemoved: function onCursorRemoved() {
906
this.input.resetInputValue();
909
_onDatasetRendered: function onDatasetRendered() {
912
_onOpened: function onOpened() {
914
this.eventBus.trigger("opened");
916
_onClosed: function onClosed() {
917
this.input.clearHint();
918
this.eventBus.trigger("closed");
920
_onFocused: function onFocused() {
921
this.isActivated = true;
922
this.dropdown.open();
924
_onBlurred: function onBlurred() {
925
this.isActivated = false;
926
this.dropdown.empty();
927
this.dropdown.close();
929
_onEnterKeyed: function onEnterKeyed(type, $e) {
930
var cursorDatum, topSuggestionDatum;
931
cursorDatum = this.dropdown.getDatumForCursor();
932
topSuggestionDatum = this.dropdown.getDatumForTopSuggestion();
934
this._select(cursorDatum);
936
} else if (this.autoselect && topSuggestionDatum) {
937
this._select(topSuggestionDatum);
941
_onTabKeyed: function onTabKeyed(type, $e) {
943
if (datum = this.dropdown.getDatumForCursor()) {
947
this._autocomplete(true);
950
_onEscKeyed: function onEscKeyed() {
951
this.dropdown.close();
952
this.input.resetInputValue();
954
_onUpKeyed: function onUpKeyed() {
955
var query = this.input.getQuery();
956
this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp();
957
this.dropdown.open();
959
_onDownKeyed: function onDownKeyed() {
960
var query = this.input.getQuery();
961
this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown();
962
this.dropdown.open();
964
_onLeftKeyed: function onLeftKeyed() {
965
this.dir === "rtl" && this._autocomplete();
967
_onRightKeyed: function onRightKeyed() {
968
this.dir === "ltr" && this._autocomplete();
970
_onQueryChanged: function onQueryChanged(e, query) {
971
this.input.clearHintIfInvalid();
972
query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty();
973
this.dropdown.open();
974
this._setLanguageDirection();
976
_onWhitespaceChanged: function onWhitespaceChanged() {
978
this.dropdown.open();
980
_setLanguageDirection: function setLanguageDirection() {
982
if (this.dir !== (dir = this.input.getLanguageDirection())) {
984
this.$node.css("direction", dir);
985
this.dropdown.setLanguageDirection(dir);
988
_updateHint: function updateHint() {
989
var datum, val, query, escapedQuery, frontMatchRegEx, match;
990
datum = this.dropdown.getDatumForTopSuggestion();
991
if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) {
992
val = this.input.getInputValue();
993
query = Input.normalizeQuery(val);
994
escapedQuery = _.escapeRegExChars(query);
995
frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
996
match = frontMatchRegEx.exec(datum.value);
997
match ? this.input.setHint(val + match[1]) : this.input.clearHint();
999
this.input.clearHint();
1002
_autocomplete: function autocomplete(laxCursor) {
1003
var hint, query, isCursorAtEnd, datum;
1004
hint = this.input.getHint();
1005
query = this.input.getQuery();
1006
isCursorAtEnd = laxCursor || this.input.isCursorAtEnd();
1007
if (hint && query !== hint && isCursorAtEnd) {
1008
datum = this.dropdown.getDatumForTopSuggestion();
1009
datum && this.input.setInputValue(datum.value);
1010
this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName);
1013
_select: function select(datum) {
1014
this.input.setQuery(datum.value);
1015
this.input.setInputValue(datum.value, true);
1016
this._setLanguageDirection();
1017
this.eventBus.trigger("selected", datum.raw, datum.datasetName);
1018
this.dropdown.close();
1019
_.defer(_.bind(this.dropdown.empty, this.dropdown));
1021
open: function open() {
1022
this.dropdown.open();
1024
close: function close() {
1025
this.dropdown.close();
1027
setVal: function setVal(val) {
1029
if (this.isActivated) {
1030
this.input.setInputValue(val);
1032
this.input.setQuery(val);
1033
this.input.setInputValue(val, true);
1035
this._setLanguageDirection();
1037
getVal: function getVal() {
1038
return this.input.getQuery();
1040
destroy: function destroy() {
1041
this.input.destroy();
1042
this.dropdown.destroy();
1043
destroyDomStructure(this.$node);
1048
function buildDom(input, withHint) {
1049
var $input, $wrapper, $dropdown, $hint;
1051
$wrapper = $(html.wrapper).css(css.wrapper);
1052
$dropdown = $(html.dropdown).css(css.dropdown);
1053
$hint = $input.clone().css(css.hint).css(getBackgroundStyles($input));
1054
$hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder required").prop("readonly", true).attr({
1055
autocomplete: "off",
1056
spellcheck: "false",
1059
$input.data(attrsKey, {
1060
dir: $input.attr("dir"),
1061
autocomplete: $input.attr("autocomplete"),
1062
spellcheck: $input.attr("spellcheck"),
1063
style: $input.attr("style")
1065
$input.addClass("tt-input").attr({
1066
autocomplete: "off",
1068
}).css(withHint ? css.input : css.inputWithNoHint);
1070
!$input.attr("dir") && $input.attr("dir", "auto");
1072
return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown);
1074
function getBackgroundStyles($el) {
1076
backgroundAttachment: $el.css("background-attachment"),
1077
backgroundClip: $el.css("background-clip"),
1078
backgroundColor: $el.css("background-color"),
1079
backgroundImage: $el.css("background-image"),
1080
backgroundOrigin: $el.css("background-origin"),
1081
backgroundPosition: $el.css("background-position"),
1082
backgroundRepeat: $el.css("background-repeat"),
1083
backgroundSize: $el.css("background-size")
1086
function destroyDomStructure($node) {
1087
var $input = $node.find(".tt-input");
1088
_.each($input.data(attrsKey), function(val, key) {
1089
_.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
1091
$input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node);
1097
var old, typeaheadKey, methods;
1098
old = $.fn.typeahead;
1099
typeaheadKey = "ttTypeahead";
1101
initialize: function initialize(o, datasets) {
1102
datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
1104
return this.each(attach);
1106
var $input = $(this), eventBus, typeahead;
1107
_.each(datasets, function(d) {
1108
d.highlight = !!o.highlight;
1110
typeahead = new Typeahead({
1112
eventBus: eventBus = new EventBus({
1115
withHint: _.isUndefined(o.hint) ? true : !!o.hint,
1116
minLength: o.minLength,
1117
autoselect: o.autoselect,
1120
$input.data(typeaheadKey, typeahead);
1123
open: function open() {
1124
return this.each(openTypeahead);
1125
function openTypeahead() {
1126
var $input = $(this), typeahead;
1127
if (typeahead = $input.data(typeaheadKey)) {
1132
close: function close() {
1133
return this.each(closeTypeahead);
1134
function closeTypeahead() {
1135
var $input = $(this), typeahead;
1136
if (typeahead = $input.data(typeaheadKey)) {
1141
val: function val(newVal) {
1142
return !arguments.length ? getVal(this.first()) : this.each(setVal);
1144
var $input = $(this), typeahead;
1145
if (typeahead = $input.data(typeaheadKey)) {
1146
typeahead.setVal(newVal);
1149
function getVal($input) {
1150
var typeahead, query;
1151
if (typeahead = $input.data(typeaheadKey)) {
1152
query = typeahead.getVal();
1157
destroy: function destroy() {
1158
return this.each(unattach);
1159
function unattach() {
1160
var $input = $(this), typeahead;
1161
if (typeahead = $input.data(typeaheadKey)) {
1162
typeahead.destroy();
1163
$input.removeData(typeaheadKey);
1168
$.fn.typeahead = function(method) {
1170
if (methods[method] && method !== "initialize") {
1171
tts = this.filter(function() {
1172
return !!$(this).data(typeaheadKey);
1174
return methods[method].apply(tts, [].slice.call(arguments, 1));
1176
return methods.initialize.apply(this, arguments);
1179
$.fn.typeahead.noConflict = function noConflict() {
1180
$.fn.typeahead = old;
b'\\ No newline at end of file'