');
215 |
216 | _.each(this.datasets, destroyDataset);
217 |
218 | function destroyDataset(dataset) { dataset.destroy(); }
219 | }
220 | });
221 |
222 | return Menu;
223 | })();
224 |
--------------------------------------------------------------------------------
/src/typeahead/highlight.js:
--------------------------------------------------------------------------------
1 | /*
2 | * typeahead.js
3 | * https://github.com/twitter/typeahead.js
4 | * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
5 | */
6 |
7 | // inspired by https://github.com/jharding/bearhug
8 |
9 | var highlight = (function(doc) {
10 | 'use strict';
11 |
12 | var defaults = {
13 | node: null,
14 | pattern: null,
15 | tagName: 'strong',
16 | className: null,
17 | wordsOnly: false,
18 | caseSensitive: false,
19 | diacriticInsensitive: false
20 | };
21 |
22 | // used for diacritic insensitivity
23 | var accented = {
24 | 'A': '[Aa\xaa\xc0-\xc5\xe0-\xe5\u0100-\u0105\u01cd\u01ce\u0200-\u0203\u0226\u0227\u1d2c\u1d43\u1e00\u1e01\u1e9a\u1ea0-\u1ea3\u2090\u2100\u2101\u213b\u249c\u24b6\u24d0\u3371-\u3374\u3380-\u3384\u3388\u3389\u33a9-\u33af\u33c2\u33ca\u33df\u33ff\uff21\uff41]',
25 | 'B': '[Bb\u1d2e\u1d47\u1e02-\u1e07\u212c\u249d\u24b7\u24d1\u3374\u3385-\u3387\u33c3\u33c8\u33d4\u33dd\uff22\uff42]',
26 | 'C': '[Cc\xc7\xe7\u0106-\u010d\u1d9c\u2100\u2102\u2103\u2105\u2106\u212d\u216d\u217d\u249e\u24b8\u24d2\u3376\u3388\u3389\u339d\u33a0\u33a4\u33c4-\u33c7\uff23\uff43]',
27 | 'D': '[Dd\u010e\u010f\u01c4-\u01c6\u01f1-\u01f3\u1d30\u1d48\u1e0a-\u1e13\u2145\u2146\u216e\u217e\u249f\u24b9\u24d3\u32cf\u3372\u3377-\u3379\u3397\u33ad-\u33af\u33c5\u33c8\uff24\uff44]',
28 | 'E': '[Ee\xc8-\xcb\xe8-\xeb\u0112-\u011b\u0204-\u0207\u0228\u0229\u1d31\u1d49\u1e18-\u1e1b\u1eb8-\u1ebd\u2091\u2121\u212f\u2130\u2147\u24a0\u24ba\u24d4\u3250\u32cd\u32ce\uff25\uff45]',
29 | 'F': '[Ff\u1da0\u1e1e\u1e1f\u2109\u2131\u213b\u24a1\u24bb\u24d5\u338a-\u338c\u3399\ufb00-\ufb04\uff26\uff46]',
30 | 'G': '[Gg\u011c-\u0123\u01e6\u01e7\u01f4\u01f5\u1d33\u1d4d\u1e20\u1e21\u210a\u24a2\u24bc\u24d6\u32cc\u32cd\u3387\u338d-\u338f\u3393\u33ac\u33c6\u33c9\u33d2\u33ff\uff27\uff47]',
31 | 'H': '[Hh\u0124\u0125\u021e\u021f\u02b0\u1d34\u1e22-\u1e2b\u1e96\u210b-\u210e\u24a3\u24bd\u24d7\u32cc\u3371\u3390-\u3394\u33ca\u33cb\u33d7\uff28\uff48]',
32 | 'I': '[Ii\xcc-\xcf\xec-\xef\u0128-\u0130\u0132\u0133\u01cf\u01d0\u0208-\u020b\u1d35\u1d62\u1e2c\u1e2d\u1ec8-\u1ecb\u2071\u2110\u2111\u2139\u2148\u2160-\u2163\u2165-\u2168\u216a\u216b\u2170-\u2173\u2175-\u2178\u217a\u217b\u24a4\u24be\u24d8\u337a\u33cc\u33d5\ufb01\ufb03\uff29\uff49]',
33 | 'J': '[Jj\u0132-\u0135\u01c7-\u01cc\u01f0\u02b2\u1d36\u2149\u24a5\u24bf\u24d9\u2c7c\uff2a\uff4a]',
34 | 'K': '[Kk\u0136\u0137\u01e8\u01e9\u1d37\u1d4f\u1e30-\u1e35\u212a\u24a6\u24c0\u24da\u3384\u3385\u3389\u338f\u3391\u3398\u339e\u33a2\u33a6\u33aa\u33b8\u33be\u33c0\u33c6\u33cd-\u33cf\uff2b\uff4b]',
35 | 'L': '[Ll\u0139-\u0140\u01c7-\u01c9\u02e1\u1d38\u1e36\u1e37\u1e3a-\u1e3d\u2112\u2113\u2121\u216c\u217c\u24a7\u24c1\u24db\u32cf\u3388\u3389\u33d0-\u33d3\u33d5\u33d6\u33ff\ufb02\ufb04\uff2c\uff4c]',
36 | 'M': '[Mm\u1d39\u1d50\u1e3e-\u1e43\u2120\u2122\u2133\u216f\u217f\u24a8\u24c2\u24dc\u3377-\u3379\u3383\u3386\u338e\u3392\u3396\u3399-\u33a8\u33ab\u33b3\u33b7\u33b9\u33bd\u33bf\u33c1\u33c2\u33ce\u33d0\u33d4-\u33d6\u33d8\u33d9\u33de\u33df\uff2d\uff4d]',
37 | 'N': '[Nn\xd1\xf1\u0143-\u0149\u01ca-\u01cc\u01f8\u01f9\u1d3a\u1e44-\u1e4b\u207f\u2115\u2116\u24a9\u24c3\u24dd\u3381\u338b\u339a\u33b1\u33b5\u33bb\u33cc\u33d1\uff2e\uff4e]',
38 | 'O': '[Oo\xba\xd2-\xd6\xf2-\xf6\u014c-\u0151\u01a0\u01a1\u01d1\u01d2\u01ea\u01eb\u020c-\u020f\u022e\u022f\u1d3c\u1d52\u1ecc-\u1ecf\u2092\u2105\u2116\u2134\u24aa\u24c4\u24de\u3375\u33c7\u33d2\u33d6\uff2f\uff4f]',
39 | 'P': '[Pp\u1d3e\u1d56\u1e54-\u1e57\u2119\u24ab\u24c5\u24df\u3250\u3371\u3376\u3380\u338a\u33a9-\u33ac\u33b0\u33b4\u33ba\u33cb\u33d7-\u33da\uff30\uff50]',
40 | 'Q': '[Qq\u211a\u24ac\u24c6\u24e0\u33c3\uff31\uff51]',
41 | 'R': '[Rr\u0154-\u0159\u0210-\u0213\u02b3\u1d3f\u1d63\u1e58-\u1e5b\u1e5e\u1e5f\u20a8\u211b-\u211d\u24ad\u24c7\u24e1\u32cd\u3374\u33ad-\u33af\u33da\u33db\uff32\uff52]',
42 | 'S': '[Ss\u015a-\u0161\u017f\u0218\u0219\u02e2\u1e60-\u1e63\u20a8\u2101\u2120\u24ae\u24c8\u24e2\u33a7\u33a8\u33ae-\u33b3\u33db\u33dc\ufb06\uff33\uff53]',
43 | 'T': '[Tt\u0162-\u0165\u021a\u021b\u1d40\u1d57\u1e6a-\u1e71\u1e97\u2121\u2122\u24af\u24c9\u24e3\u3250\u32cf\u3394\u33cf\ufb05\ufb06\uff34\uff54]',
44 | 'U': '[Uu\xd9-\xdc\xf9-\xfc\u0168-\u0173\u01af\u01b0\u01d3\u01d4\u0214-\u0217\u1d41\u1d58\u1d64\u1e72-\u1e77\u1ee4-\u1ee7\u2106\u24b0\u24ca\u24e4\u3373\u337a\uff35\uff55]',
45 | 'V': '[Vv\u1d5b\u1d65\u1e7c-\u1e7f\u2163-\u2167\u2173-\u2177\u24b1\u24cb\u24e5\u2c7d\u32ce\u3375\u33b4-\u33b9\u33dc\u33de\uff36\uff56]',
46 | 'W': '[Ww\u0174\u0175\u02b7\u1d42\u1e80-\u1e89\u1e98\u24b2\u24cc\u24e6\u33ba-\u33bf\u33dd\uff37\uff57]',
47 | 'X': '[Xx\u02e3\u1e8a-\u1e8d\u2093\u213b\u2168-\u216b\u2178-\u217b\u24b3\u24cd\u24e7\u33d3\uff38\uff58]',
48 | 'Y': '[Yy\xdd\xfd\xff\u0176-\u0178\u0232\u0233\u02b8\u1e8e\u1e8f\u1e99\u1ef2-\u1ef9\u24b4\u24ce\u24e8\u33c9\uff39\uff59]',
49 | 'Z': '[Zz\u0179-\u017e\u01f1-\u01f3\u1dbb\u1e90-\u1e95\u2124\u2128\u24b5\u24cf\u24e9\u3390-\u3394\uff3a\uff5a]'
50 | };
51 |
52 | return function hightlight(o) {
53 | var regex;
54 |
55 | o = _.mixin({}, defaults, o);
56 |
57 | if (!o.node || !o.pattern) {
58 | // fail silently
59 | return;
60 | }
61 |
62 | // support wrapping multiple patterns
63 | o.pattern = _.isArray(o.pattern) ? o.pattern : [o.pattern];
64 |
65 | regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive);
66 | traverse(o.node, hightlightTextNode);
67 |
68 | function hightlightTextNode(textNode) {
69 | var match, patternNode, wrapperNode;
70 |
71 | if (match = regex.exec(textNode.data)) {
72 | wrapperNode = doc.createElement(o.tagName);
73 | o.className && (wrapperNode.className = o.className);
74 |
75 | patternNode = textNode.splitText(match.index);
76 | patternNode.splitText(match[0].length);
77 | wrapperNode.appendChild(patternNode.cloneNode(true));
78 |
79 | textNode.parentNode.replaceChild(wrapperNode, patternNode);
80 | }
81 |
82 | return !!match;
83 | }
84 |
85 | function traverse(el, hightlightTextNode) {
86 | var childNode, TEXT_NODE_TYPE = 3;
87 |
88 | for (var i = 0; i < el.childNodes.length; i++) {
89 | childNode = el.childNodes[i];
90 |
91 | if (childNode.nodeType === TEXT_NODE_TYPE) {
92 | i += hightlightTextNode(childNode) ? 1 : 0;
93 | }
94 |
95 | else {
96 | traverse(childNode, hightlightTextNode);
97 | }
98 | }
99 | }
100 | };
101 |
102 | // replace characters by their compositors
103 | // custom for diacritic insensitivity
104 | function accent_replacer(chr) {
105 | return accented[chr.toUpperCase()] || chr;
106 | }
107 | function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) {
108 | var escapedPatterns = [], regexStr;
109 | for (var i = 0, len = patterns.length; i < len; i++) {
110 | var escapedWord = _.escapeRegExChars(patterns[i]);
111 | // added for diacritic insensitivity
112 | if(diacriticInsensitive){
113 | escapedWord = escapedWord.replace(/\S/g,accent_replacer);
114 | }
115 | escapedPatterns.push(escapedWord);
116 | }
117 | regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
118 | return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
119 | }
120 | })(window.document);
121 |
--------------------------------------------------------------------------------
/doc/migration/0.10.0.md:
--------------------------------------------------------------------------------
1 | Migrating to typeahead.js v0.10.0
2 | =================================
3 |
4 | Preamble
5 | --------
6 |
7 | v0.10.0 of typeahead.js ended up being almost a complete rewrite. Many things
8 | stayed the same, but there were a handful of changes you need to be aware of
9 | if you plan on upgrading from an older version. This document aims to call out
10 | those changes and explain what you need to do in order to have an painless
11 | upgrade.
12 |
13 | Notable Changes
14 | ----------------
15 |
16 | ### First Argument to the jQuery Plugin
17 |
18 | In v0.10.0, the first argument to `jQuery#typeahead` is an options hash that
19 | can be used to configure the behavior of the typeahead. This is in contrast
20 | to previous versions where `jQuery#typeahead` expected just a series of datasets
21 | to be passed to it:
22 |
23 | ```javascript
24 | // pre-v0.10.0
25 | $('.typeahead').typeahead(myDataset);
26 |
27 | // v0.10.0
28 | $('.typeahead').typeahead({
29 | highlight: true,
30 | hint: false
31 | }, myDataset);
32 | ```
33 |
34 | If you're fine with the default configuration, you can just pass `null` as the
35 | first argument:
36 |
37 | ```javascript
38 | $('.typeahead').typeahead(null, myDataset);
39 | ```
40 |
41 | ### Bloodhound Suggestion Engine
42 |
43 | The most notable change in v0.10.0 is that typeahead.js has been decomposed into
44 | a suggestion engine and a UI view. As part of this change, the way you configure
45 | datasets has changed. Previously, a dataset config would have looked like:
46 |
47 | ```javascript
48 | {
49 | valueKey: 'num',
50 | local: [{ num: 'one' }, { num: 'two' }, { num: 'three' }],
51 | prefetch: '/prefetch',
52 | remote: '/remote?q=%QUERY'
53 | }
54 | ```
55 |
56 | In v0.10.0, an equivalent dataset config would look like:
57 |
58 | ```javascript
59 | {
60 | displayKey: 'num',
61 | source: mySource
62 | }
63 | ```
64 |
65 | As you can see, `local`, `prefetch`, and `remote` are no longer defined at the
66 | dataset level. Instead, all you set in a dataset config is `source`. `source` is
67 | expected to be a function with the signature `function(query, callback)`. When a
68 | typeahead's query changes, suggestions will be requested from `source`. It's
69 | expected `source` will compute the suggestion set and invoke `callback` with an array
70 | of suggestion objects. The typeahead will then go on to render those suggestions.
71 |
72 | If you're wondering if you can still configure `local`, `prefetch`, and
73 | `remote`, don't worry, that's where the Bloodhound suggestion engine comes in.
74 | Here's how you would define `mySource` which was referenced in the previous
75 | code snippet:
76 |
77 | ```
78 | var mySource = new Bloodhound({
79 | datumTokenizer: function(d) {
80 | return Bloodhound.tokenizers.whitespace(d.num);
81 | },
82 | queryTokenizer: Bloodhound.tokenizers.whitespace,
83 | local: [{ num: 'one' }, { num: 'two' }, { num: 'three' }],
84 | prefetch: '/prefetch',
85 | remote: '/remote?q=%QUERY'
86 | });
87 |
88 | // this kicks off the loading and processing of local and prefetch data
89 | // the suggestion engine will be useless until it is initialized
90 | mySource.initialize();
91 | ```
92 |
93 | In the above snippet, a Bloodhound suggestion engine is initialized and that's
94 | what will be used as the source of your dataset. There's still one last thing
95 | that needs to be done before you can use a Bloodhound suggestion engine as the
96 | source of a dataset. Because datasets expect `source` to be function, the
97 | Bloodhound instance needs to be wrapped in an adapter so it can meet that
98 | expectation.
99 |
100 | ```
101 | mySource = mySource.ttAdapter();
102 | ```
103 |
104 | Put it all together:
105 |
106 | ```javascript
107 | var mySource = new Bloodhound({
108 | datumTokenizer: function(d) {
109 | return Bloodhound.tokenizers.whitespace(d.num);
110 | },
111 | queryTokenizer: Bloodhound.tokenizers.whitespace,
112 | local: [{ num: 'one' }, { num: 'two' }, { num: 'three' }],
113 | prefetch: '/prefetch',
114 | remote: '/remote?q=%QUERY'
115 | });
116 |
117 | mySource.initialize();
118 |
119 | $('.typeahead').typeahead(null, {
120 | displayKey: 'num',
121 | source: mySource.ttAdapter()
122 | });
123 | ```
124 |
125 | ### Tokenization Methods Must Be Provided
126 |
127 | The Bloodhound suggestion engine is token-based, so how datums and queries are
128 | tokenized plays a vital role in the quality of search results. Pre-v0.10.0,
129 | it was not possible to configure the tokenization method. Starting in v0.10.0,
130 | you **must** specify how you want datums and queries tokenized.
131 |
132 | The most common tokenization methods split a given string on whitespace or
133 | non-word characters. Bloodhound provides implementations for those methods
134 | out of the box:
135 |
136 | ```javascript
137 | // returns ['one', 'two', 'twenty-five']
138 | Bloodhound.tokenizers.whitespace(' one two twenty-five');
139 |
140 | // returns ['one', 'two', 'twenty', 'five']
141 | Bloodhound.tokenizers.nonword(' one two twenty-five');
142 | ```
143 |
144 | For query tokenization, you'll probably want to use one of the above methods.
145 | For datum tokenization, this is where you may want to do something a tad bit
146 | more advanced.
147 |
148 | For datums, sometimes you want tokens to be dervied from more than one property.
149 | For example, if you were building a search engine for GitHub repositories, it'd
150 | probably be wise to have tokens derived from the repo's name, owner, and
151 | primary language:
152 |
153 | ```javascript
154 | var repos = [
155 | { name: 'example', owner: 'John Doe', language: 'JavaScript' },
156 | { name: 'another example', owner: 'Joe Doe', language: 'Scala' }
157 | ];
158 |
159 | function customTokenizer(datum) {
160 | var nameTokens = Bloodhound.tokenizers.whitespace(datum.name);
161 | var ownerTokens = Bloodhound.tokenizers.whitespace(datum.owner);
162 | var languageTokens = Bloodhound.tokenizers.whitespace(datum.language);
163 |
164 | return nameTokens.concat(ownerTokens).concat(languageTokens);
165 | }
166 | ```
167 |
168 | There may also be the scenario where you want datum tokenization to be performed
169 | on the backend. The best way to do that is to just add a property to your datums
170 | that contains those tokens. You can then provide a tokenizer that just returns
171 | the already existing tokens:
172 |
173 | ```javascript
174 | var sports = [
175 | { value: 'football', tokens: ['football', 'pigskin'] },
176 | { value: 'basketball', tokens: ['basketball', 'bball'] }
177 | ];
178 |
179 | function customTokenizer(datum) { return datum.tokens; }
180 | ```
181 |
182 | There are plenty of other ways you could go about tokenizing datums, it really
183 | just depends on what you are trying to accomplish.
184 |
185 | ### String Datums Are No Longer Supported
186 |
187 | Dropping support for string datums was a difficult choice, but in the end it
188 | made sense for a number of reasons. If you still want to hydrate the suggestion
189 | engine with string datums, you'll need to use the `filter` function:
190 |
191 | ```javascript
192 | var engine = new Bloodhound({
193 | prefetch: {
194 | url: '/data',
195 | filter: function(data) {
196 | // assume data is an array of strings e.g. ['one', 'two', 'three']
197 | return $.map(data, function(str) { return { value: str }; });
198 | },
199 | datumTokenizer: function(d) {
200 | return Bloodhound.tokenizers.whitespace(d.value);
201 | },
202 | queryTokenizer: Bloodhound.tokenizers.whitespace
203 | }
204 | });
205 | ```
206 |
207 | ### Precompiled Templates Are Now Required
208 |
209 | In previous versions of typeahead.js, you could specify a string template along
210 | with the templating engine that should be used to compile/render it. In
211 | v0.10.0, you can no longer specify templating engines; instead you must provide
212 | precompiled templates. Precompiled templates are functions that take one
213 | argument: the context the template should be rendered with.
214 |
215 | Most of the popular templating engines allow for the creation of precompiled
216 | templates. For example, you can generate one using Handlebars by doing the
217 | following:
218 |
219 | ```javascript
220 | var precompiledTemplate = Handlebars.compile('
').attr('id', _.guid()).text(displayFn(context));
332 | }
333 | }
334 |
335 | function isValidName(str) {
336 | // dashes, underscores, letters, and numbers
337 | return (/^[_a-zA-Z0-9-]+$/).test(str);
338 | }
339 | })();
340 |
--------------------------------------------------------------------------------
/src/typeahead/input.js:
--------------------------------------------------------------------------------
1 | /*
2 | * typeahead.js
3 | * https://github.com/twitter/typeahead.js
4 | * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
5 | */
6 |
7 | var Input = (function() {
8 | 'use strict';
9 |
10 | var specialKeyCodeMap;
11 |
12 | specialKeyCodeMap = {
13 | 9: 'tab',
14 | 27: 'esc',
15 | 37: 'left',
16 | 39: 'right',
17 | 13: 'enter',
18 | 38: 'up',
19 | 40: 'down'
20 | };
21 |
22 | // constructor
23 | // -----------
24 |
25 | function Input(o, www) {
26 | var id;
27 | o = o || {};
28 |
29 | if (!o.input) {
30 | $.error('input is missing');
31 | }
32 |
33 | www.mixin(this);
34 |
35 | this.$hint = $(o.hint);
36 | this.$input = $(o.input);
37 | this.$menu = $(o.menu);
38 |
39 | // this id is used for aria-owns and aria-controls
40 | id = this.$input.attr('id') || _.guid();
41 |
42 | this.$menu.attr('id', id + '_listbox');
43 |
44 | this.$hint.attr({
45 | 'aria-hidden': true
46 | });
47 |
48 | this.$input.attr({
49 | 'aria-owns': id + '_listbox',
50 | 'aria-controls': id + '_listbox',
51 | role: 'combobox',
52 | 'aria-autocomplete': 'list',
53 | 'aria-expanded': false
54 | });
55 |
56 | // the query defaults to whatever the value of the input is
57 | // on initialization, it'll most likely be an empty string
58 | this.query = this.$input.val();
59 |
60 | // for tracking when a change event should be triggered
61 | this.queryWhenFocused = this.hasFocus() ? this.query : null;
62 |
63 | // helps with calculating the width of the input's value
64 | this.$overflowHelper = buildOverflowHelper(this.$input);
65 |
66 | // detect the initial lang direction
67 | this._checkLanguageDirection();
68 |
69 | // if no hint, noop all the hint related functions
70 | if (this.$hint.length === 0) {
71 | this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid =
72 | _.noop;
73 | }
74 |
75 | this.onSync('cursorchange', this._updateDescendent);
76 | }
77 |
78 | // static methods
79 | // --------------
80 |
81 | Input.normalizeQuery = function(str) {
82 | // strips leading whitespace and condenses all whitespace
83 | return _.toStr(str)
84 | .replace(/^\s*/g, '')
85 | .replace(/\s{2,}/g, ' ');
86 | };
87 |
88 | // instance methods
89 | // ----------------
90 |
91 | _.mixin(Input.prototype, EventEmitter, {
92 | // ### event handlers
93 |
94 | _onBlur: function onBlur() {
95 | this.resetInputValue();
96 | this.trigger('blurred');
97 | },
98 |
99 | _onFocus: function onFocus() {
100 | this.queryWhenFocused = this.query;
101 | this.trigger('focused');
102 | },
103 |
104 | _onKeydown: function onKeydown($e) {
105 | // which is normalized and consistent (but not for ie)
106 | var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
107 |
108 | this._managePreventDefault(keyName, $e);
109 | if (keyName && this._shouldTrigger(keyName, $e)) {
110 | this.trigger(keyName + 'Keyed', $e);
111 | }
112 | },
113 |
114 | _onInput: function onInput() {
115 | this._setQuery(this.getInputValue());
116 | this.clearHintIfInvalid();
117 | this._checkLanguageDirection();
118 | },
119 |
120 | // ### private
121 |
122 | _managePreventDefault: function managePreventDefault(keyName, $e) {
123 | var preventDefault;
124 |
125 | switch (keyName) {
126 | case 'up':
127 | case 'down':
128 | preventDefault = !withModifier($e);
129 | break;
130 |
131 | default:
132 | preventDefault = false;
133 | }
134 |
135 | preventDefault && $e.preventDefault();
136 | },
137 |
138 | _shouldTrigger: function shouldTrigger(keyName, $e) {
139 | var trigger;
140 |
141 | switch (keyName) {
142 | case 'tab':
143 | trigger = !withModifier($e);
144 | break;
145 |
146 | default:
147 | trigger = true;
148 | }
149 |
150 | return trigger;
151 | },
152 |
153 | _checkLanguageDirection: function checkLanguageDirection() {
154 | var dir = (this.$input.css('direction') || 'ltr').toLowerCase();
155 |
156 | if (this.dir !== dir) {
157 | this.dir = dir;
158 | this.$hint.attr('dir', dir);
159 | this.trigger('langDirChanged', dir);
160 | }
161 | },
162 |
163 | _setQuery: function setQuery(val, silent) {
164 | var areEquivalent, hasDifferentWhitespace;
165 |
166 | areEquivalent = areQueriesEquivalent(val, this.query);
167 | hasDifferentWhitespace = areEquivalent
168 | ? this.query.length !== val.length
169 | : false;
170 |
171 | this.query = val;
172 |
173 | if (!silent && !areEquivalent) {
174 | this.trigger('queryChanged', this.query);
175 | } else if (!silent && hasDifferentWhitespace) {
176 | this.trigger('whitespaceChanged', this.query);
177 | }
178 | },
179 |
180 | _updateDescendent: function updateDescendent(event, id) {
181 | this.$input.attr('aria-activedescendant', id);
182 | },
183 |
184 | // ### public
185 |
186 | bind: function() {
187 | var that = this,
188 | onBlur,
189 | onFocus,
190 | onKeydown,
191 | onInput;
192 |
193 | // bound functions
194 | onBlur = _.bind(this._onBlur, this);
195 | onFocus = _.bind(this._onFocus, this);
196 | onKeydown = _.bind(this._onKeydown, this);
197 | onInput = _.bind(this._onInput, this);
198 |
199 | this.$input
200 | .on('blur.tt', onBlur)
201 | .on('focus.tt', onFocus)
202 | .on('keydown.tt', onKeydown);
203 |
204 | // ie8 don't support the input event
205 | // ie9 doesn't fire the input event when characters are removed
206 | if (!_.isMsie() || _.isMsie() > 9) {
207 | this.$input.on('input.tt', onInput);
208 | } else {
209 | this.$input.on('keydown.tt keypress.tt cut.tt paste.tt', function($e) {
210 | // if a special key triggered this, ignore it
211 | if (specialKeyCodeMap[$e.which || $e.keyCode]) {
212 | return;
213 | }
214 |
215 | // give the browser a chance to update the value of the input
216 | // before checking to see if the query changed
217 | _.defer(_.bind(that._onInput, that, $e));
218 | });
219 | }
220 |
221 | return this;
222 | },
223 |
224 | focus: function focus() {
225 | this.$input.focus();
226 | },
227 |
228 | blur: function blur() {
229 | this.$input.blur();
230 | },
231 |
232 | getLangDir: function getLangDir() {
233 | return this.dir;
234 | },
235 |
236 | getQuery: function getQuery() {
237 | return this.query || '';
238 | },
239 |
240 | setQuery: function setQuery(val, silent) {
241 | this.setInputValue(val);
242 | this._setQuery(val, silent);
243 | },
244 |
245 | hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
246 | return this.query !== this.queryWhenFocused;
247 | },
248 |
249 | getInputValue: function getInputValue() {
250 | return this.$input.val();
251 | },
252 |
253 | setInputValue: function setInputValue(value) {
254 | this.$input.val(value);
255 | this.clearHintIfInvalid();
256 | this._checkLanguageDirection();
257 | },
258 |
259 | resetInputValue: function resetInputValue() {
260 | this.setInputValue(this.query);
261 | },
262 |
263 | getHint: function getHint() {
264 | return this.$hint.val();
265 | },
266 |
267 | setHint: function setHint(value) {
268 | this.$hint.val(value);
269 | },
270 |
271 | clearHint: function clearHint() {
272 | this.setHint('');
273 | },
274 |
275 | clearHintIfInvalid: function clearHintIfInvalid() {
276 | var val, hint, valIsPrefixOfHint, isValid;
277 |
278 | val = this.getInputValue();
279 | hint = this.getHint();
280 | valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
281 | isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow();
282 |
283 | !isValid && this.clearHint();
284 | },
285 |
286 | hasFocus: function hasFocus() {
287 | return this.$input.is(':focus');
288 | },
289 |
290 | hasOverflow: function hasOverflow() {
291 | // 2 is arbitrary, just picking a small number to handle edge cases
292 | var constraint = this.$input.width() - 2;
293 |
294 | this.$overflowHelper.text(this.getInputValue());
295 |
296 | return this.$overflowHelper.width() >= constraint;
297 | },
298 |
299 | isCursorAtEnd: function() {
300 | var valueLength, selectionStart, range;
301 |
302 | valueLength = this.$input.val().length;
303 | selectionStart = this.$input[0].selectionStart;
304 |
305 | if (_.isNumber(selectionStart)) {
306 | return selectionStart === valueLength;
307 | } else if (document.selection) {
308 | // NOTE: this won't work unless the input has focus, the good news
309 | // is this code should only get called when the input has focus
310 | range = document.selection.createRange();
311 | range.moveStart('character', -valueLength);
312 |
313 | return valueLength === range.text.length;
314 | }
315 |
316 | return true;
317 | },
318 |
319 | destroy: function destroy() {
320 | this.$hint.off('.tt');
321 | this.$input.off('.tt');
322 | this.$overflowHelper.remove();
323 |
324 | // #970
325 | this.$hint = this.$input = this.$overflowHelper = $('
');
326 | },
327 | setAriaExpanded: function setAriaExpanded(value) {
328 | this.$input.attr('aria-expanded', value);
329 | }
330 | });
331 |
332 | return Input;
333 |
334 | // helper functions
335 | // ----------------
336 |
337 | function buildOverflowHelper($input) {
338 | return $('')
339 | .css({
340 | // position helper off-screen
341 | position: 'absolute',
342 | visibility: 'hidden',
343 | // avoid line breaks and whitespace collapsing
344 | whiteSpace: 'pre',
345 | // use same font css as input to calculate accurate width
346 | fontFamily: $input.css('font-family'),
347 | fontSize: $input.css('font-size'),
348 | fontStyle: $input.css('font-style'),
349 | fontVariant: $input.css('font-variant'),
350 | fontWeight: $input.css('font-weight'),
351 | wordSpacing: $input.css('word-spacing'),
352 | letterSpacing: $input.css('letter-spacing'),
353 | textIndent: $input.css('text-indent'),
354 | textRendering: $input.css('text-rendering'),
355 | textTransform: $input.css('text-transform')
356 | })
357 | .insertAfter($input);
358 | }
359 |
360 | function areQueriesEquivalent(a, b) {
361 | return Input.normalizeQuery(a) === Input.normalizeQuery(b);
362 | }
363 |
364 | function withModifier($e) {
365 | return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
366 | }
367 | })();
368 |
--------------------------------------------------------------------------------
/doc/jquery_typeahead.md:
--------------------------------------------------------------------------------
1 | jQuery#typeahead
2 | ----------------
3 |
4 | The UI component of typeahead.js is available as a jQuery plugin. It's
5 | responsible for rendering suggestions and handling DOM interactions.
6 |
7 | Table of Contents
8 | -----------------
9 |
10 | * [Features](#features)
11 | * [Usage](#usage)
12 | * [API](#api)
13 | * [Options](#options)
14 | * [Datasets](#datasets)
15 | * [Custom Events](#custom-events)
16 | * [Class Names](#class-names)
17 |
18 | Features
19 | --------
20 |
21 | * Displays suggestions to end-users as they type
22 | * Shows top suggestion as a hint (i.e. background text)
23 | * Supports custom templates to allow for UI flexibility
24 | * Works well with RTL languages and input method editors
25 | * Highlights query matches within the suggestion
26 | * Triggers custom events to encourage extensibility
27 |
28 | Usage
29 | -----
30 |
31 | ### API
32 |
33 | * [`jQuery#typeahead(options, [*datasets])`](#jquerytypeaheadoptions-datasets)
34 | * [`jQuery#typeahead('val')`](#jquerytypeaheadval)
35 | * [`jQuery#typeahead('val', val)`](#jquerytypeaheadval-val)
36 | * [`jQuery#typeahead('destroy')`](#jquerytypeaheaddestroy)
37 | * [`jQuery.fn.typeahead.noConflict()`](#jqueryfntypeaheadnoconflict)
38 |
39 | #### jQuery#typeahead(options, [\*datasets])
40 |
41 | For a given `input[type="text"]`, enables typeahead functionality. `options`
42 | is an options hash that's used for configuration. Refer to [Options](#options)
43 | for more info regarding the available configs. Subsequent arguments
44 | (`*datasets`), are individual option hashes for datasets. For more details
45 | regarding datasets, refer to [Datasets](#datasets).
46 |
47 | ```javascript
48 | $('.typeahead').typeahead({
49 | minLength: 3,
50 | highlight: true
51 | },
52 | {
53 | name: 'my-dataset',
54 | source: mySource
55 | });
56 | ```
57 |
58 | #### jQuery#typeahead('val')
59 |
60 | Returns the current value of the typeahead. The value is the text the user has
61 | entered into the `input` element.
62 |
63 | ```javascript
64 | var myVal = $('.typeahead').typeahead('val');
65 | ```
66 |
67 | #### jQuery#typeahead('val', val)
68 |
69 | Sets the value of the typeahead. This should be used in place of `jQuery#val`.
70 |
71 | ```javascript
72 | $('.typeahead').typeahead('val', myVal);
73 | ```
74 |
75 | #### jQuery#typeahead('open')
76 |
77 | Opens the suggestion menu.
78 |
79 | ```javascript
80 | $('.typeahead').typeahead('open');
81 | ```
82 |
83 | #### jQuery#typeahead('close')
84 |
85 | Closes the suggestion menu.
86 |
87 | ```javascript
88 | $('.typeahead').typeahead('close');
89 | ```
90 |
91 | #### jQuery#typeahead('destroy')
92 |
93 | Removes typeahead functionality and reverts the `input` element back to its
94 | original state.
95 |
96 | ```javascript
97 | $('.typeahead').typeahead('destroy');
98 | ```
99 |
100 | #### jQuery.fn.typeahead.noConflict()
101 |
102 | Returns a reference to the typeahead plugin and reverts `jQuery.fn.typeahead`
103 | to its previous value. Can be used to avoid naming collisions.
104 |
105 | ```javascript
106 | var typeahead = jQuery.fn.typeahead.noConflict();
107 | jQuery.fn._typeahead = typeahead;
108 | ```
109 |
110 | ### Options
111 |
112 | When initializing a typeahead, there are a number of options you can configure.
113 |
114 | * `highlight` – If `true`, when suggestions are rendered, pattern matches
115 | for the current query in text nodes will be wrapped in a `strong` element with
116 | its class set to `{{classNames.highlight}}`. Defaults to `false`.
117 |
118 | * `hint` – If `false`, the typeahead will not show a hint. Defaults to `true`.
119 |
120 | * `autoselect` – If `true`, the first suggestion will be selected when pressing the Enter key.
121 |
122 | * `minLength` – The minimum character length needed before suggestions start
123 | getting rendered. Defaults to `1`.
124 |
125 | * `classNames` – For overriding the default class names used. See
126 | [Class Names](#class-names) for more details.
127 |
128 | ### Datasets
129 |
130 | A typeahead is composed of one or more datasets. When an end-user modifies the
131 | value of a typeahead, each dataset will attempt to render suggestions for the
132 | new value.
133 |
134 | For most use cases, one dataset should suffice. It's only in the scenario where
135 | you want rendered suggestions to be grouped based on some sort of categorical
136 | relationship that you'd need to use multiple datasets. For example, on
137 | twitter.com, the search typeahead groups results into recent searches, trends,
138 | and accounts – that would be a great use case for using multiple datasets.
139 |
140 | Datasets can be configured using the following options.
141 |
142 | * `source` – The backing data source for suggestions. Expected to be a function
143 | with the signature `(query, syncResults, asyncResults)`. `syncResults` should
144 | be called with suggestions computed synchronously and `asyncResults` should be
145 | called with suggestions computed asynchronously (e.g. suggestions that come
146 | for an AJAX request). `source` can also be a Bloodhound instance.
147 | **Required**.
148 |
149 | * `async` – Lets the dataset know if async suggestions should be expected. If
150 | not set, this information is inferred from the signature of `source` i.e.
151 | if the `source` function expects 3 arguments, `async` will be set to `true`.
152 |
153 | * `name` – The name of the dataset. This will be appended to
154 | `{{classNames.dataset}}-` to form the class name of the containing DOM
155 | element. Must only consist of underscores, dashes, letters (`a-z`), and
156 | numbers. Defaults to a random number.
157 |
158 | * `limit` – The max number of suggestions to be displayed. Defaults to `5`.
159 |
160 | * `display` | `displayKey` – For a given suggestion, determines the string representation
161 | of it. This will be used when setting the value of the input control after a
162 | suggestion is selected. Can be either a key string or a function that
163 | transforms a suggestion object into a string. Defaults to stringifying the
164 | suggestion.
165 |
166 | * `templates` – A hash of templates to be used when rendering the dataset. Note
167 | a precompiled template is a function that takes a JavaScript object as its
168 | first argument and returns a HTML string.
169 |
170 | * `notFound` – Rendered when `0` suggestions are available for the given
171 | query. Can be either a HTML string or a precompiled template. If it's a
172 | precompiled template, the passed in context will contain `query`.
173 |
174 | * `pending` - Rendered when `0` synchronous suggestions are available but
175 | asynchronous suggestions are expected. Can be either a HTML string or a
176 | precompiled template. If it's a precompiled template, the passed in context
177 | will contain `query`.
178 |
179 | * `header`– Rendered at the top of the dataset when suggestions are present.
180 | Can be either a HTML string or a precompiled template. If it's a precompiled
181 | template, the passed in context will contain `query` and `suggestions`.
182 |
183 | * `footer`– Rendered at the bottom of the dataset when suggestions are
184 | present. Can be either a HTML string or a precompiled template. If it's a
185 | precompiled template, the passed in context will contain `query` and
186 | `suggestions`.
187 |
188 | * `suggestion` – Used to render a single suggestion. If set, this has to be a
189 | precompiled template. The associated suggestion object will serve as the
190 | context. Defaults to the value of `display` wrapped in a `div` tag i.e.
191 | `
{{value}}
`.
192 |
193 | ### Custom Events
194 |
195 | The following events get triggered on the input element during the life-cycle of
196 | a typeahead.
197 |
198 | * `typeahead:active` – Fired when the typeahead moves to active state.
199 |
200 | * `typeahead:idle` – Fired when the typeahead moves to idle state.
201 |
202 | * `typeahead:open` – Fired when the results container is opened.
203 |
204 | * `typeahead:close` – Fired when the results container is closed.
205 |
206 | * `typeahead:change` – Normalized version of the native [`change` event].
207 | Fired when input loses focus and the value has changed since it originally
208 | received focus.
209 |
210 | * `typeahead:render` – Fired when suggestions are rendered for a dataset. The
211 | event handler will be invoked with 4 arguments: the jQuery event object, the
212 | suggestions that were rendered, a flag indicating whether the suggestions
213 | were fetched asynchronously, and the name of the dataset the rendering
214 | occurred in.
215 |
216 | * `typeahead:select` – Fired when a suggestion is selected. The event handler
217 | will be invoked with 3 arguments: the jQuery event object, the suggestion
218 | object that was selected, and the name of the dataset the suggestion belongs
219 | to.
220 |
221 | * `typeahead:autocomplete` – Fired when a autocompletion occurs. The event
222 | handler will be invoked with 3 arguments: the jQuery event object, the
223 | suggestion object that was used for autocompletion, and the name of the
224 | dataset the suggestion belongs to.
225 |
226 | * `typeahead:cursorchange` – Fired when the results container cursor moves. The
227 | event handler will be invoked with 3 arguments: the jQuery event object, the
228 | suggestion object that was moved to, and the name of the dataset the
229 | suggestion belongs to.
230 |
231 | * `typeahead:asyncrequest` – Fired when an async request for suggestions is
232 | sent. The event handler will be invoked with 3 arguments: the jQuery event
233 | object, the current query, and the name of the dataset the async request
234 | belongs to.
235 |
236 | * `typeahead:asynccancel` – Fired when an async request is cancelled. The event
237 | handler will be invoked with 3 arguments: the jQuery event object, the current
238 | query, and the name of the dataset the async request belonged to.
239 |
240 | * `typeahead:asyncreceive` – Fired when an async request completes. The event
241 | handler will be invoked with 3 arguments: the jQuery event object, the current
242 | query, and the name of the dataset the async request belongs to.
243 |
244 | Example usage:
245 |
246 | ```
247 | $('.typeahead').bind('typeahead:select', function(ev, suggestion) {
248 | console.log('Selection: ', suggestion);
249 | });
250 | ```
251 |
252 | **NOTE**: Every event does not supply the same arguments. See the event
253 | descriptions above for details on each event's argument list.
254 |
255 |
256 |
257 | [`change` event]: https://developer.mozilla.org/en-US/docs/Web/Events/change
258 |
259 | ### Class Names
260 |
261 | * `input` - Added to input that's initialized into a typeahead. Defaults to
262 | `tt-input`.
263 |
264 | * `hint` - Added to hint input. Defaults to `tt-hint`.
265 |
266 | * `menu` - Added to menu element. Defaults to `tt-menu`.
267 |
268 | * `dataset` - Added to dataset elements. to Defaults to `tt-dataset`.
269 |
270 | * `suggestion` - Added to suggestion elements. Defaults to `tt-suggestion`.
271 |
272 | * `empty` - Added to menu element when it contains no content. Defaults to
273 | `tt-empty`.
274 |
275 | * `open` - Added to menu element when it is opened. Defaults to `tt-open`.
276 |
277 | * `cursor` - Added to suggestion element when menu cursor moves to said
278 | suggestion. Defaults to `tt-cursor`.
279 |
280 | * `highlight` - Added to the element that wraps highlighted text. Defaults to
281 | `tt-highlight`.
282 |
283 | To override any of these defaults, you can use the `classNames` option:
284 |
285 | ```javascript
286 | $('.typeahead').typeahead({
287 | classNames: {
288 | input: 'Typeahead-input',
289 | hint: 'Typeahead-hint',
290 | selectable: 'Typeahead-selectable'
291 | }
292 | });
293 | ```
294 |
--------------------------------------------------------------------------------