├── index.js ├── .gitignore ├── src ├── bloodhound │ ├── version.js │ ├── remote.js │ ├── tokenizers.js │ ├── lru_cache.js │ ├── prefetch.js │ ├── transport.js │ ├── persistent_storage.js │ ├── search_index.js │ ├── options_parser.js │ └── bloodhound.js ├── typeahead │ ├── status.js │ ├── event_bus.js │ ├── default_menu.js │ ├── www.js │ ├── event_emitter.js │ ├── menu.js │ ├── highlight.js │ ├── plugin.js │ ├── dataset.js │ └── input.js └── common │ └── utils.js ├── .jshintrc ├── .npmignore ├── bower.json ├── test ├── common │ └── utils_spec.js ├── ci ├── fixtures │ ├── html.js │ ├── ajax_responses.js │ └── data.js ├── typeahead │ ├── event_bus_spec.js │ ├── status_spec.js │ ├── default_results_spec.js │ ├── event_emitter_spec.js │ ├── highlight_spec.js │ └── plugin_spec.js ├── bloodhound │ ├── lru_cache_spec.js │ ├── remote_spec.js │ ├── search_index_spec.js │ ├── tokenizers_spec.js │ ├── prefetch_spec.js │ ├── transport_spec.js │ ├── persistent_storage_spec.js │ └── options_parser_spec.js ├── helpers │ └── typeahead_mocks.js ├── integration │ └── test.html └── playground.html ├── composer.json ├── license ├── .travis.yml ├── karma.conf.js ├── package.json ├── contributing.md ├── readme.md ├── doc ├── migration │ └── 0.10.0.md └── jquery_typeahead.md └── Gruntfile.js /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | "Bloodhound": require("./dist/bloodhound.js"), 5 | "loadjQueryPlugin": function() {require("./dist/typeahead.bundle.js");} 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | 4 | .grunt 5 | _SpecRunner.html 6 | test/coverage 7 | 8 | dist_temp 9 | 10 | node_modules 11 | npm-debug.log 12 | 13 | bower_components 14 | 15 | *.iml 16 | .idea 17 | -------------------------------------------------------------------------------- /src/bloodhound/version.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 VERSION = '%VERSION%'; 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "newcap": true, 4 | "noarg": true, 5 | "quotmark": "single", 6 | "regexp": true, 7 | "trailing": true, 8 | 9 | "boss": true, 10 | "eqnull": true, 11 | "expr": true, 12 | "validthis": true, 13 | 14 | "browser": true, 15 | "jquery": true 16 | } 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | .grunt 4 | _SpecRunner.html 5 | test/coverage 6 | dist_temp 7 | node_modules 8 | npm-debug.log 9 | bower_components 10 | test 11 | *.iml 12 | .idea 13 | .jshintrc 14 | .npmignore 15 | .travis.yml 16 | composer.json 17 | contributing.md 18 | Gruntfile.js 19 | karma.conf.js 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "corejs-typeahead", 3 | "version": "1.0.1", 4 | "main": "dist/typeahead.bundle.js", 5 | "dependencies": { 6 | "jquery": ">=1.11" 7 | }, 8 | "devDependencies": { 9 | "jquery": "~1.11", 10 | "jasmine-ajax": "~1.3.1", 11 | "jasmine-jquery": "~1.7.0" 12 | }, 13 | "resolutions": { 14 | "jquery": "1.11.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/common/utils_spec.js: -------------------------------------------------------------------------------- 1 | describe('utils', function() { 2 | 3 | describe('guid', function() { 4 | 5 | it('should return unique strings', function() { 6 | var a = _.guid(); 7 | var b = _.guid(); 8 | 9 | expect(typeof a).toEqual('string'); 10 | expect(typeof b).toEqual('string'); 11 | expect(a).toNotEqual(b); 12 | }); 13 | 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /test/ci: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | if [ "$TEST_SUITE" == "unit" ]; then 4 | bower install 5 | ./node_modules/karma/bin/karma start --single-run --browsers PhantomJS 6 | elif [ "$TRAVIS_SECURE_ENV_VARS" == "true" -a "$TEST_SUITE" == "integration" ]; then 7 | bower install 8 | ./node_modules/.bin/static -p 8888 & 9 | sleep 3 10 | # integration tests are flaky, don't let them fail the build 11 | ./node_modules/mocha/bin/mocha --harmony -R spec ./test/integration/test.js || true 12 | else 13 | echo "Not running any tests" 14 | fi 15 | -------------------------------------------------------------------------------- /test/fixtures/html.js: -------------------------------------------------------------------------------- 1 | var fixtures = fixtures || {}; 2 | 3 | fixtures.html = { 4 | input: '', 5 | hint: '', 6 | dataset: [ 7 | '
', 8 | '

one

', 9 | '

two

', 10 | '

three

', 11 | '
' 12 | ].join('') 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/ajax_responses.js: -------------------------------------------------------------------------------- 1 | var fixtures = fixtures || {}; 2 | 3 | fixtures.ajaxResps = { 4 | ok: { 5 | status: 200, 6 | responseText: '[{ "value": "big" }, { "value": "bigger" }, { "value": "biggest" }, { "value": "small" }, { "value": "smaller" }, { "value": "smallest" }]' 7 | }, 8 | ok1: { 9 | status: 200, 10 | responseText: '["dog", "cat", "moose"]' 11 | }, 12 | err: { 13 | status: 500 14 | } 15 | }; 16 | 17 | $.each(fixtures.ajaxResps, function(i, resp) { 18 | resp.responseText && (resp.parsed = $.parseJSON(resp.responseText)); 19 | }); 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "corejavascript/typeahead.js", 3 | "description": "fast and fully-featured autocomplete library", 4 | "keywords": ["typeahead", "autocomplete"], 5 | "homepage": "https://typeahead.js.org/", 6 | "authors": [ 7 | { 8 | "name": "Twitter Inc.", 9 | "homepage": "https://twitter.com/twitteross" 10 | }, 11 | { 12 | "name": "Jake Harding", 13 | "homepage": "https://github.com/jharding" 14 | } 15 | ], 16 | "support": { 17 | "issues": "https://github.com/corejavascript/typeahead.js/issues" 18 | }, 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Twitter, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/typeahead/event_bus_spec.js: -------------------------------------------------------------------------------- 1 | describe('EventBus', function() { 2 | 3 | beforeEach(function() { 4 | var $fixture; 5 | 6 | setFixtures(fixtures.html.input); 7 | 8 | $fixture = $('#jasmine-fixtures'); 9 | this.$el = $fixture.find('.tt-input'); 10 | 11 | this.eventBus = new EventBus({ el: this.$el }); 12 | }); 13 | 14 | it('#trigger should trigger event', function() { 15 | var spy = jasmine.createSpy(); 16 | 17 | this.$el.on('typeahead:fiz', spy); 18 | 19 | this.eventBus.trigger('fiz', 'foo', 'bar'); 20 | expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'foo', 'bar'); 21 | }); 22 | 23 | it('#before should return false if default was not prevented', function() { 24 | var spy = jasmine.createSpy(); 25 | 26 | this.$el.on('typeahead:beforefiz', spy); 27 | 28 | expect(this.eventBus.before('fiz')).toBe(false); 29 | expect(spy).toHaveBeenCalled(); 30 | }); 31 | 32 | it('#before should return true if default was prevented', function() { 33 | var spy = jasmine.createSpy().andCallFake(prevent); 34 | 35 | this.$el.on('typeahead:beforefiz', spy); 36 | 37 | expect(this.eventBus.before('fiz')).toBe(true); 38 | expect(spy).toHaveBeenCalled(); 39 | 40 | function prevent($e) { $e.preventDefault(); } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | env: 3 | matrix: 4 | - TEST_SUITE=unit 5 | - TEST_SUITE=integration BROWSER='firefox' 6 | - TEST_SUITE=integration BROWSER='firefox:3.5' 7 | - TEST_SUITE=integration BROWSER='firefox:3.6' 8 | - TEST_SUITE=integration BROWSER='safari:5' 9 | - TEST_SUITE=integration BROWSER='safari:6' 10 | - TEST_SUITE=integration BROWSER='safari:7' 11 | - TEST_SUITE=integration BROWSER='internet explorer:8' 12 | - TEST_SUITE=integration BROWSER='internet explorer:9' 13 | - TEST_SUITE=integration BROWSER='internet explorer:10' 14 | - TEST_SUITE=integration BROWSER='internet explorer:11' 15 | - TEST_SUITE=integration BROWSER='chrome' 16 | global: 17 | - secure: VY4J2ERfrMEin++f4+UDDtTMWLuE3jaYAVchRxfO2c6PQUYgR+SW4SMekz855U/BuptMtiVMR2UUoNGMgOSKIFkIXpPfHhx47G5a541v0WNjXfQ2qzivXAWaXNK3l3C58z4dKxgPWsFY9JtMVCddJd2vQieAILto8D8G09p7bpo= 18 | - secure: kehbNCoYUG2gLnhmCH/oKhlJG6LoxgcOPMCtY7KOI4ropG8qlypb+O2b/19+BWeO3aIuMB0JajNh3p2NL0UKgLmUK7EYBA9fQz+vesFReRk0V/KqMTSxHJuseM4aLOWA2Wr9US843VGltfODVvDN5sNrfY7RcoRx2cTK/k1CXa8= 19 | node_js: 20 | - "10.18.0" 21 | cache: 22 | directories: 23 | - node_modules 24 | - bower_components 25 | before_install: 26 | - npm install -g grunt-cli@0.1.13 27 | - npm install -g bower@1.3.8 28 | install: 29 | - npm install 30 | before_script: 31 | - grunt build 32 | script: test/ci 33 | addons: 34 | sauce_connect: true 35 | -------------------------------------------------------------------------------- /test/bloodhound/lru_cache_spec.js: -------------------------------------------------------------------------------- 1 | describe('LruCache', function() { 2 | 3 | beforeEach(function() { 4 | this.cache = new LruCache(3); 5 | }); 6 | 7 | it('should make entries retrievable by their keys', function() { 8 | var key = 'key', val = 42; 9 | 10 | this.cache.set(key, val); 11 | expect(this.cache.get(key)).toBe(val); 12 | }); 13 | 14 | it('should return undefined if key has not been set', function() { 15 | expect(this.cache.get('wat?')).toBeUndefined(); 16 | }); 17 | 18 | it('should hold up to maxSize entries', function() { 19 | this.cache.set('one', 1); 20 | this.cache.set('two', 2); 21 | this.cache.set('three', 3); 22 | this.cache.set('four', 4); 23 | 24 | expect(this.cache.get('one')).toBeUndefined(); 25 | expect(this.cache.get('two')).toBe(2); 26 | expect(this.cache.get('three')).toBe(3); 27 | expect(this.cache.get('four')).toBe(4); 28 | }); 29 | 30 | it('should evict lru entry if cache is full', function() { 31 | this.cache.set('one', 1); 32 | this.cache.set('two', 2); 33 | this.cache.set('three', 3); 34 | this.cache.get('one'); 35 | this.cache.set('four', 4); 36 | 37 | expect(this.cache.get('one')).toBe(1); 38 | expect(this.cache.get('two')).toBeUndefined(); 39 | expect(this.cache.get('three')).toBe(3); 40 | expect(this.cache.get('four')).toBe(4); 41 | expect(this.cache.size).toBe(3); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/bloodhound/remote.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 Remote = (function() { 8 | 'use strict'; 9 | 10 | // constructor 11 | // ----------- 12 | 13 | function Remote(o) { 14 | this.url = o.url; 15 | this.prepare = o.prepare; 16 | this.transform = o.transform; 17 | this.indexResponse = o.indexResponse; 18 | 19 | this.transport = new Transport({ 20 | cache: o.cache, 21 | limiter: o.limiter, 22 | transport: o.transport, 23 | maxPendingRequests: o.maxPendingRequests 24 | }); 25 | } 26 | 27 | // instance methods 28 | // ---------------- 29 | 30 | _.mixin(Remote.prototype, { 31 | // ### private 32 | 33 | _settings: function settings() { 34 | return { url: this.url, type: 'GET', dataType: 'json' }; 35 | }, 36 | 37 | // ### public 38 | 39 | get: function get(query, cb) { 40 | var that = this, settings; 41 | 42 | if (!cb) { return; } 43 | 44 | query = query || ''; 45 | settings = this.prepare(query, this._settings()); 46 | 47 | return this.transport.get(settings, onResponse); 48 | 49 | function onResponse(err, resp) { 50 | err ? cb([]) : cb(that.transform(resp)); 51 | } 52 | }, 53 | 54 | cancelLastRequest: function cancelLastRequest() { 55 | this.transport.cancel(); 56 | } 57 | }); 58 | 59 | return Remote; 60 | })(); 61 | -------------------------------------------------------------------------------- /src/bloodhound/tokenizers.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 tokenizers = (function() { 8 | 'use strict'; 9 | 10 | return { 11 | nonword: nonword, 12 | whitespace: whitespace, 13 | ngram: ngram, 14 | obj: { 15 | nonword: getObjTokenizer(nonword), 16 | whitespace: getObjTokenizer(whitespace), 17 | ngram: getObjTokenizer(ngram) 18 | } 19 | }; 20 | 21 | function whitespace(str) { 22 | str = _.toStr(str); 23 | return str ? str.split(/\s+/) : []; 24 | } 25 | 26 | function nonword(str) { 27 | str = _.toStr(str); 28 | return str ? str.split(/\W+/) : []; 29 | } 30 | 31 | function ngram(str) { 32 | str = _.toStr(str); 33 | 34 | var tokens = [], 35 | word = ''; 36 | 37 | _.each(str.split(''), function(char) { 38 | if (char.match(/\s+/)) { 39 | word = ''; 40 | } else { 41 | tokens.push(word+char); 42 | word += char; 43 | } 44 | }); 45 | 46 | return tokens; 47 | } 48 | 49 | function getObjTokenizer(tokenizer) { 50 | return function setKey(keys) { 51 | keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); 52 | 53 | return function tokenize(o) { 54 | var tokens = []; 55 | 56 | _.each(keys, function(k) { 57 | tokens = tokens.concat(tokenizer(_.toStr(o[k]))); 58 | }); 59 | 60 | return tokens; 61 | }; 62 | }; 63 | } 64 | })(); 65 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | 5 | preprocessors: { 6 | 'src/**/*.js': 'coverage' 7 | }, 8 | 9 | reporters: ['progress', 'coverage'], 10 | 11 | browsers: ['Chrome'], 12 | 13 | frameworks: ['jasmine'], 14 | 15 | coverageReporter: { 16 | type: 'html', 17 | dir: 'test/coverage/' 18 | }, 19 | 20 | files: [ 21 | 'bower_components/jquery/dist/jquery.js', 22 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 23 | 'src/common/utils.js', 24 | 'src/bloodhound/version.js', 25 | 'src/bloodhound/tokenizers.js', 26 | 'src/bloodhound/lru_cache.js', 27 | 'src/bloodhound/persistent_storage.js', 28 | 'src/bloodhound/transport.js', 29 | 'src/bloodhound/remote.js', 30 | 'src/bloodhound/prefetch.js', 31 | 'src/bloodhound/search_index.js', 32 | 'src/bloodhound/options_parser.js', 33 | 'src/bloodhound/bloodhound.js', 34 | 'src/typeahead/www.js', 35 | 'src/typeahead/event_bus.js', 36 | 'src/typeahead/event_emitter.js', 37 | 'src/typeahead/highlight.js', 38 | 'src/typeahead/input.js', 39 | 'src/typeahead/dataset.js', 40 | 'src/typeahead/menu.js', 41 | 'src/typeahead/status.js', 42 | 'src/typeahead/default_menu.js', 43 | 'src/typeahead/typeahead.js', 44 | 'src/typeahead/plugin.js', 45 | 'test/fixtures/**/*', 46 | 'bower_components/jasmine-jquery/lib/jasmine-jquery.js', 47 | 'bower_components/jasmine-ajax/lib/mock-ajax.js', 48 | 'test/helpers/**/*', 49 | 'test/**/*_spec.js' 50 | ] 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/typeahead/status.js: -------------------------------------------------------------------------------- 1 | var Status = (function () { 2 | 'use strict'; 3 | 4 | function Status(options) { 5 | this.$el = $('', { 6 | 'role': 'status', 7 | 'aria-live': 'polite', 8 | }).css({ 9 | // This `.visuallyhidden` style is inspired by HTML5 Boilerplate 10 | // https://github.com/h5bp/html5-boilerplate/blob/fea7f22/src/css/main.css#L128 11 | 'position': 'absolute', 12 | 'padding': '0', 13 | 'border': '0', 14 | 'height': '1px', 15 | 'width': '1px', 16 | 'margin-bottom': '-1px', 17 | 'margin-right': '-1px', 18 | 'overflow': 'hidden', 19 | 'clip': 'rect(0 0 0 0)', 20 | 'white-space': 'nowrap', 21 | }); 22 | options.$input.after(this.$el); 23 | _.each(options.menu.datasets, _.bind(function (dataset) { 24 | if (dataset.onSync) { 25 | dataset.onSync('rendered', _.bind(this.update, this)); 26 | dataset.onSync('cleared', _.bind(this.cleared, this)); 27 | } 28 | }, this)); 29 | } 30 | _.mixin(Status.prototype, { 31 | update: function update(event, suggestions) { 32 | var length = suggestions.length; 33 | var words; 34 | if (length === 1) { 35 | words = { 36 | result: 'result', 37 | is: 'is' 38 | }; 39 | } else { 40 | words = { 41 | result: 'results', 42 | is: 'are' 43 | }; 44 | }; 45 | this.$el.text(length + ' ' + words.result + ' ' + words.is + ' available, use up and down arrow keys to navigate.'); 46 | }, 47 | cleared: function () { 48 | this.$el.text(''); 49 | } 50 | }); 51 | 52 | return Status; 53 | })(); 54 | -------------------------------------------------------------------------------- /src/typeahead/event_bus.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 EventBus = (function() { 8 | 'use strict'; 9 | 10 | var namespace, deprecationMap; 11 | 12 | namespace = 'typeahead:'; 13 | 14 | // DEPRECATED: will be remove in v1 15 | // 16 | // NOTE: there is no deprecation plan for the opened and closed event 17 | // as their behavior has changed enough that it wouldn't make sense 18 | deprecationMap = { 19 | render: 'rendered', 20 | cursorchange: 'cursorchanged', 21 | select: 'selected', 22 | autocomplete: 'autocompleted' 23 | }; 24 | 25 | // constructor 26 | // ----------- 27 | 28 | function EventBus(o) { 29 | if (!o || !o.el) { 30 | $.error('EventBus initialized without el'); 31 | } 32 | 33 | this.$el = $(o.el); 34 | } 35 | 36 | // instance methods 37 | // ---------------- 38 | 39 | _.mixin(EventBus.prototype, { 40 | 41 | // ### private 42 | 43 | _trigger: function(type, args) { 44 | var $e = $.Event(namespace + type); 45 | 46 | this.$el.trigger.call(this.$el, $e, args || []); 47 | 48 | return $e; 49 | }, 50 | 51 | // ### public 52 | 53 | before: function(type) { 54 | var args, $e; 55 | 56 | args = [].slice.call(arguments, 1); 57 | $e = this._trigger('before' + type, args); 58 | 59 | return $e.isDefaultPrevented(); 60 | }, 61 | 62 | trigger: function(type) { 63 | var deprecatedType; 64 | 65 | this._trigger(type, [].slice.call(arguments, 1)); 66 | 67 | // TODO: remove in v1 68 | if (deprecatedType = deprecationMap[type]) { 69 | this._trigger(deprecatedType, [].slice.call(arguments, 1)); 70 | } 71 | } 72 | }); 73 | 74 | return EventBus; 75 | })(); 76 | -------------------------------------------------------------------------------- /src/typeahead/default_menu.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 DefaultMenu = (function() { 8 | 'use strict'; 9 | 10 | var s = Menu.prototype; 11 | 12 | function DefaultMenu() { 13 | Menu.apply(this, [].slice.call(arguments, 0)); 14 | } 15 | 16 | _.mixin(DefaultMenu.prototype, Menu.prototype, { 17 | // overrides 18 | // --------- 19 | 20 | open: function open() { 21 | // only display the menu when there's something to be shown 22 | !this._allDatasetsEmpty() && this._show(); 23 | return s.open.apply(this, [].slice.call(arguments, 0)); 24 | }, 25 | 26 | close: function close() { 27 | this._hide(); 28 | return s.close.apply(this, [].slice.call(arguments, 0)); 29 | }, 30 | 31 | _onRendered: function onRendered() { 32 | if (this._allDatasetsEmpty()) { 33 | this._hide(); 34 | } 35 | 36 | else { 37 | this.isOpen() && this._show(); 38 | } 39 | 40 | return s._onRendered.apply(this, [].slice.call(arguments, 0)); 41 | }, 42 | 43 | _onCleared: function onCleared() { 44 | if (this._allDatasetsEmpty()) { 45 | this._hide(); 46 | } 47 | 48 | else { 49 | this.isOpen() && this._show(); 50 | } 51 | 52 | return s._onCleared.apply(this, [].slice.call(arguments, 0)); 53 | }, 54 | 55 | setLanguageDirection: function setLanguageDirection(dir) { 56 | this.$node.css(dir === 'ltr' ? this.css.ltr : this.css.rtl); 57 | return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); 58 | }, 59 | 60 | // private 61 | // --------- 62 | 63 | _hide: function hide() { 64 | this.$node.hide(); 65 | }, 66 | 67 | _show: function show() { 68 | // can't use jQuery#show because $node is a span element we want 69 | // display: block; not display: inline; 70 | this.$node.css('display', 'block'); 71 | } 72 | }); 73 | 74 | return DefaultMenu; 75 | })(); 76 | -------------------------------------------------------------------------------- /test/helpers/typeahead_mocks.js: -------------------------------------------------------------------------------- 1 | (function(root) { 2 | var components; 3 | 4 | components = [ 5 | 'Bloodhound', 6 | 'Prefetch', 7 | 'Remote', 8 | 'PersistentStorage', 9 | 'Transport', 10 | 'SearchIndex', 11 | 'Input', 12 | 'Dataset', 13 | 'Menu' 14 | ]; 15 | 16 | for (var i = 0; i < components.length; i++) { 17 | makeMockable(components[i]); 18 | } 19 | 20 | function makeMockable(component) { 21 | var Original, Mock; 22 | 23 | Original = root[component]; 24 | Mock = mock(Original); 25 | 26 | jasmine[component] = { useMock: useMock, uninstallMock: uninstallMock }; 27 | 28 | function useMock() { 29 | root[component] = Mock; 30 | jasmine.getEnv().currentSpec.after(uninstallMock); 31 | } 32 | 33 | function uninstallMock() { 34 | root[component] = Original; 35 | } 36 | } 37 | 38 | function mock(Constructor) { 39 | var constructorSpy; 40 | 41 | Mock.prototype = Constructor.prototype; 42 | constructorSpy = jasmine.createSpy('mock constructor').andCallFake(Mock); 43 | 44 | // copy instance methods 45 | for (var key in Constructor) { 46 | if (typeof Constructor[key] === 'function') { 47 | constructorSpy[key] = Constructor[key]; 48 | } 49 | } 50 | 51 | return constructorSpy; 52 | 53 | function Mock() { 54 | var instance = _.mixin({}, Constructor.prototype); 55 | 56 | for (var key in instance) { 57 | if (typeof instance[key] === 'function') { 58 | spyOn(instance, key); 59 | 60 | // special case for some components 61 | if (key === 'bind') { 62 | instance[key].andCallFake(function() { return this; }); 63 | } 64 | } 65 | } 66 | 67 | // have the event emitter methods call through 68 | instance.onSync && instance.onSync.andCallThrough(); 69 | instance.onAsync && instance.onAsync.andCallThrough(); 70 | instance.off && instance.off.andCallThrough(); 71 | instance.trigger && instance.trigger.andCallThrough(); 72 | 73 | instance.constructor = Constructor; 74 | 75 | return instance; 76 | } 77 | } 78 | })(this); 79 | -------------------------------------------------------------------------------- /test/bloodhound/remote_spec.js: -------------------------------------------------------------------------------- 1 | describe('Remote', function() { 2 | 3 | beforeEach(function() { 4 | jasmine.Transport.useMock(); 5 | 6 | this.remote = new Remote({ 7 | url: '/test?q=%QUERY', 8 | prepare: function(x) { return x; }, 9 | transform: function(x) { return x; } 10 | }); 11 | 12 | this.transport = this.remote.transport; 13 | }); 14 | 15 | describe('#cancelLastRequest', function() { 16 | it('should cancel last request', function() { 17 | this.remote.cancelLastRequest(); 18 | expect(this.transport.cancel).toHaveBeenCalled(); 19 | }); 20 | }); 21 | 22 | describe('#get', function() { 23 | it('should have sensible default request settings', function() { 24 | var spy; 25 | 26 | spy = jasmine.createSpy(); 27 | spyOn(this.remote, 'prepare'); 28 | 29 | this.remote.get('foo', spy); 30 | 31 | expect(this.remote.prepare).toHaveBeenCalledWith('foo', { 32 | url: '/test?q=%QUERY', 33 | type: 'GET', 34 | dataType: 'json' 35 | }); 36 | }); 37 | 38 | it('should transform request settings with prepare', function() { 39 | var spy; 40 | 41 | spy = jasmine.createSpy(); 42 | spyOn(this.remote, 'prepare').andReturn([{ foo: 'bar' }]); 43 | 44 | this.remote.get('foo', spy); 45 | 46 | expect(this.transport.get) 47 | .toHaveBeenCalledWith([{ foo: 'bar' }], jasmine.any(Function)); 48 | }); 49 | 50 | it('should transform response with transform', function() { 51 | var spy; 52 | 53 | spy = jasmine.createSpy(); 54 | spyOn(this.remote, 'transform').andReturn([{ foo: 'bar' }]); 55 | this.transport.get.andCallFake(function(_, cb) { cb(null, {}); }); 56 | 57 | this.remote.get('foo', spy); 58 | 59 | expect(spy).toHaveBeenCalledWith([{ foo: 'bar' }]); 60 | }); 61 | 62 | it('should return empty array on error', function() { 63 | var spy; 64 | 65 | spy = jasmine.createSpy(); 66 | this.transport.get.andCallFake(function(_, cb) { cb(true); }); 67 | 68 | this.remote.get('foo', spy); 69 | 70 | expect(spy).toHaveBeenCalledWith([]); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "corejs-typeahead", 3 | "description": "fast and fully-featured autocomplete library", 4 | "keywords": [ 5 | "typeahead", 6 | "autocomplete", 7 | "aria" 8 | ], 9 | "homepage": "http://corejavascript.github.io/typeahead.js/", 10 | "bugs": "https://github.com/corejavascript/typeahead.js/issues", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/corejavascript/typeahead.js.git" 14 | }, 15 | "author": { 16 | "name": "Twitter, Inc.", 17 | "url": "https://twitter.com/twitteross" 18 | }, 19 | "contributors": [ 20 | { 21 | "name": "Jake Harding", 22 | "url": "https://twitter.com/JakeHarding" 23 | }, 24 | { 25 | "name": "Tim Trueman", 26 | "url": "https://twitter.com/timtrueman" 27 | }, 28 | { 29 | "name": "Veljko Skarich", 30 | "url": "https://twitter.com/vskarich" 31 | }, 32 | { 33 | "name": "Jeremy Booker", 34 | "url": "https://twitter.com/jbook3r" 35 | } 36 | ], 37 | "license": "MIT", 38 | "dependencies": { 39 | "jquery": ">=1.11" 40 | }, 41 | "devDependencies": { 42 | "chai": "^3.5.0", 43 | "colors": "^1.1.2", 44 | "grunt": "~1.0.1", 45 | "grunt-concurrent": "^2.0.3", 46 | "grunt-contrib-clean": "^1.0.0", 47 | "grunt-contrib-concat": "^1.0.1", 48 | "grunt-contrib-connect": "^1.0.2", 49 | "grunt-contrib-jshint": "^1.1.0", 50 | "grunt-contrib-uglify": "^2.0.0", 51 | "grunt-contrib-watch": "^1.0.0", 52 | "grunt-exec": "~1.0.1", 53 | "grunt-replace": "^1.0.1", 54 | "grunt-step": "~1.0.0", 55 | "grunt-umd": "^2.3.3", 56 | "karma": "^5.0.1", 57 | "karma-chrome-launcher": "^2.0.0", 58 | "karma-coverage": "^1.1.1", 59 | "karma-firefox-launcher": "^1.0.0", 60 | "karma-jasmine": "^0.1.6", 61 | "karma-opera-launcher": "^1.0.0", 62 | "karma-phantomjs-launcher": "^1.0.2", 63 | "karma-safari-launcher": "^1.0.0", 64 | "mocha": "^7.1.1", 65 | "node-static": "^0.7.7", 66 | "phantomjs": "^2.1.7", 67 | "semver": "^5.0.3", 68 | "underscore": "^1.6.0", 69 | "yiewd": "^0.6.0" 70 | }, 71 | "scripts": { 72 | "test": "bower install && node ./node_modules/karma/bin/karma start --single-run --browsers PhantomJS" 73 | }, 74 | "version": "1.3.2", 75 | "main": "dist/typeahead.bundle.js" 76 | } 77 | -------------------------------------------------------------------------------- /test/typeahead/status_spec.js: -------------------------------------------------------------------------------- 1 | describe('Status', function() { 2 | var status; 3 | var menu; 4 | 5 | beforeEach(function () { 6 | 7 | var $fixture; 8 | menu = { 9 | datasets: [ 10 | EventEmitter, 11 | EventEmitter 12 | ] 13 | }; 14 | 15 | setFixtures(''); 16 | 17 | $fixture = $('#jasmine-fixtures'); 18 | this.$input = $fixture.find('input'); 19 | 20 | status = new Status({ 21 | $input: this.$input, 22 | menu: menu 23 | }); 24 | 25 | }); 26 | 27 | it('renders a status element after the input', function() { 28 | expect(status.$el.attr('role')).toEqual('status'); 29 | expect(status.$el.attr('aria-live')).toEqual('polite'); 30 | expect(status.$el.prev()).toEqual(this.$input); 31 | }); 32 | 33 | it('renders a status element that is visible to screen readers', function () { 34 | expect(status.$el.attr('aria-hidden')).not.toEqual('true'); 35 | expect(status.$el.css('display')).not.toEqual('none'); 36 | expect(status.$el.css('visibility')).not.toEqual('hidden'); 37 | expect(status.$el.height()).not.toEqual(0); 38 | expect(status.$el.width()).not.toEqual(0); 39 | }); 40 | 41 | it('renders a status element that is hidden on displays', function () { 42 | expect(status.$el.outerHeight(true)).toEqual(0); 43 | expect(status.$el.outerWidth(true)).toEqual(0); 44 | }); 45 | 46 | describe('when rendered is triggered on the datasets', function() { 47 | 48 | it('should update the status text based on number of suggestion', function() { 49 | expect(status.$el.text()).toEqual(''); 50 | 51 | menu.datasets[0].trigger('rendered', [1, 2, 3]); 52 | 53 | expect(status.$el.text()).toEqual('3 results are available, use up and down arrow keys to navigate.'); 54 | }); 55 | 56 | it('should use singular conjugations if only one suggestion', function() { 57 | expect(status.$el.text()).toEqual(''); 58 | 59 | menu.datasets[0].trigger('rendered', [1]); 60 | 61 | expect(status.$el.text()).toEqual('1 result is available, use up and down arrow keys to navigate.'); 62 | }); 63 | 64 | }); 65 | 66 | describe('when cleared is triggered on the datasets', function() { 67 | it('should clear the status text on the suggestion', function() { 68 | status.$el.text('Text that will be cleared'); 69 | 70 | menu.datasets[0].trigger('cleared'); 71 | 72 | expect(status.$el.text()).toEqual(''); 73 | }); 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /src/bloodhound/lru_cache.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/lru-cache 8 | 9 | var LruCache = (function() { 10 | 'use strict'; 11 | 12 | function LruCache(maxSize) { 13 | this.maxSize = _.isNumber(maxSize) ? maxSize : 100; 14 | this.reset(); 15 | 16 | // if max size is less than 0, provide a noop cache 17 | if (this.maxSize <= 0) { 18 | this.set = this.get = $.noop; 19 | } 20 | } 21 | 22 | _.mixin(LruCache.prototype, { 23 | set: function set(key, val) { 24 | var tailItem = this.list.tail, node; 25 | 26 | // at capacity 27 | if (this.size >= this.maxSize) { 28 | this.list.remove(tailItem); 29 | delete this.hash[tailItem.key]; 30 | 31 | this.size--; 32 | } 33 | 34 | // writing over existing key 35 | if (node = this.hash[key]) { 36 | node.val = val; 37 | this.list.moveToFront(node); 38 | } 39 | 40 | // new key 41 | else { 42 | node = new Node(key, val); 43 | 44 | this.list.add(node); 45 | this.hash[key] = node; 46 | 47 | this.size++; 48 | } 49 | }, 50 | 51 | get: function get(key) { 52 | var node = this.hash[key]; 53 | 54 | if (node) { 55 | this.list.moveToFront(node); 56 | return node.val; 57 | } 58 | }, 59 | 60 | reset: function reset() { 61 | this.size = 0; 62 | this.hash = {}; 63 | this.list = new List(); 64 | } 65 | }); 66 | 67 | function List() { 68 | this.head = this.tail = null; 69 | } 70 | 71 | _.mixin(List.prototype, { 72 | add: function add(node) { 73 | if (this.head) { 74 | node.next = this.head; 75 | this.head.prev = node; 76 | } 77 | 78 | this.head = node; 79 | this.tail = this.tail || node; 80 | }, 81 | 82 | remove: function remove(node) { 83 | node.prev ? node.prev.next = node.next : this.head = node.next; 84 | node.next ? node.next.prev = node.prev : this.tail = node.prev; 85 | }, 86 | 87 | moveToFront: function(node) { 88 | this.remove(node); 89 | this.add(node); 90 | } 91 | }); 92 | 93 | function Node(key, val) { 94 | this.key = key; 95 | this.val = val; 96 | this.prev = this.next = null; 97 | } 98 | 99 | return LruCache; 100 | 101 | })(); 102 | -------------------------------------------------------------------------------- /src/bloodhound/prefetch.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 Prefetch = (function() { 8 | 'use strict'; 9 | 10 | var keys; 11 | 12 | keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' }; 13 | 14 | // constructor 15 | // ----------- 16 | 17 | // defaults for options are handled in options_parser 18 | function Prefetch(o) { 19 | this.url = o.url; 20 | this.ttl = o.ttl; 21 | this.cache = o.cache; 22 | this.prepare = o.prepare; 23 | this.transform = o.transform; 24 | this.transport = o.transport; 25 | this.thumbprint = o.thumbprint; 26 | 27 | this.storage = new PersistentStorage(o.cacheKey); 28 | } 29 | 30 | // instance methods 31 | // ---------------- 32 | 33 | _.mixin(Prefetch.prototype, { 34 | 35 | // ### private 36 | 37 | _settings: function settings() { 38 | return { url: this.url, type: 'GET', dataType: 'json' }; 39 | }, 40 | 41 | // ### public 42 | 43 | store: function store(data) { 44 | if (!this.cache) { return; } 45 | 46 | this.storage.set(keys.data, data, this.ttl); 47 | this.storage.set(keys.protocol, location.protocol, this.ttl); 48 | this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); 49 | }, 50 | 51 | fromCache: function fromCache() { 52 | var stored = {}, isExpired; 53 | 54 | if (!this.cache) { return null; } 55 | 56 | stored.data = this.storage.get(keys.data); 57 | stored.protocol = this.storage.get(keys.protocol); 58 | stored.thumbprint = this.storage.get(keys.thumbprint); 59 | 60 | // the stored data is considered expired if the thumbprints 61 | // don't match or if the protocol it was originally stored under 62 | // has changed 63 | isExpired = 64 | stored.thumbprint !== this.thumbprint || 65 | stored.protocol !== location.protocol; 66 | 67 | // TODO: if expired, remove from local storage 68 | 69 | return stored.data && !isExpired ? stored.data : null; 70 | }, 71 | 72 | fromNetwork: function(cb) { 73 | var that = this, settings; 74 | 75 | if (!cb) { return; } 76 | 77 | settings = this.prepare(this._settings()); 78 | this.transport(settings).fail(onError).done(onResponse); 79 | 80 | function onError() { cb(true); } 81 | function onResponse(resp) { cb(null, that.transform(resp)); } 82 | }, 83 | 84 | clear: function clear() { 85 | this.storage.clear(); 86 | return this; 87 | } 88 | }); 89 | 90 | return Prefetch; 91 | })(); 92 | -------------------------------------------------------------------------------- /test/bloodhound/search_index_spec.js: -------------------------------------------------------------------------------- 1 | describe('SearchIndex', function() { 2 | 3 | function build(o) { 4 | return new SearchIndex(_.mixin({ 5 | datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), 6 | queryTokenizer: Bloodhound.tokenizers.whitespace 7 | }, o || {})); 8 | } 9 | 10 | beforeEach(function() { 11 | this.index = build(); 12 | this.index.add(fixtures.data.simple); 13 | }); 14 | 15 | it('should support serialization/deserialization', function() { 16 | var serialized = this.index.serialize(); 17 | 18 | this.index.bootstrap(serialized); 19 | 20 | expect(this.index.search('smaller')).toEqual([{ value: 'smaller' }]); 21 | }); 22 | 23 | it('should be able to add data on the fly', function() { 24 | this.index.add({ value: 'new' }); 25 | 26 | expect(this.index.search('new')).toEqual([{ value: 'new' }]); 27 | }); 28 | 29 | it('#get should return datums by id', function() { 30 | this.index = build({ identify: function(d) { return d.value; } }); 31 | this.index.add(fixtures.data.simple); 32 | 33 | expect(this.index.get(['big', 'bigger'])).toEqual([ 34 | { value: 'big' }, 35 | { value: 'bigger' } 36 | ]); 37 | }); 38 | 39 | it('#search should return datums that match the given query', function() { 40 | expect(this.index.search('big')).toEqual([ 41 | { value: 'big' }, 42 | { value: 'bigger' }, 43 | { value: 'biggest' } 44 | ]); 45 | 46 | expect(this.index.search('small')).toEqual([ 47 | { value: 'small' }, 48 | { value: 'smaller' }, 49 | { value: 'smallest' } 50 | ]); 51 | }); 52 | 53 | it('#search should return an empty array of there are no matches', function() { 54 | expect(this.index.search('wtf')).toEqual([]); 55 | }); 56 | 57 | it('#search should handle multi-token queries', function() { 58 | this.index.add({ value: 'foo bar' }); 59 | expect(this.index.search('foo b')).toEqual([{ value: 'foo bar' }]); 60 | }); 61 | 62 | it('#search should return results that match ANY query-token when options.matchAnyQueryToken', function() { 63 | this.index = build({matchAnyQueryToken:true}); 64 | this.index.add({ value: 'foo bar' }); 65 | expect(this.index.search('blah bar')).toEqual([{ value: 'foo bar' }]); 66 | expect(this.index.search('food bark')).toEqual([]); 67 | }); 68 | 69 | it('#all should return all datums', function() { 70 | expect(this.index.all()).toEqual(fixtures.data.simple); 71 | }); 72 | 73 | it('#reset should empty the search index', function() { 74 | this.index.reset(); 75 | expect(this.index.datums).toEqual([]); 76 | expect(this.index.trie.i).toEqual([]); 77 | expect(this.index.trie.c).toEqual({}); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/typeahead/www.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 WWW = (function() { 8 | 'use strict'; 9 | 10 | var defaultClassNames = { 11 | wrapper: 'twitter-typeahead', 12 | input: 'tt-input', 13 | hint: 'tt-hint', 14 | menu: 'tt-menu', 15 | dataset: 'tt-dataset', 16 | suggestion: 'tt-suggestion', 17 | selectable: 'tt-selectable', 18 | empty: 'tt-empty', 19 | open: 'tt-open', 20 | cursor: 'tt-cursor', 21 | highlight: 'tt-highlight' 22 | }; 23 | 24 | return build; 25 | 26 | function build(o) { 27 | var www, classes; 28 | 29 | classes = _.mixin({}, defaultClassNames, o); 30 | 31 | www = { 32 | css: buildCss(), 33 | classes: classes, 34 | html: buildHtml(classes), 35 | selectors: buildSelectors(classes) 36 | }; 37 | 38 | return { 39 | css: www.css, 40 | html: www.html, 41 | classes: www.classes, 42 | selectors: www.selectors, 43 | mixin: function(o) { _.mixin(o, www); } 44 | }; 45 | } 46 | 47 | function buildHtml(c) { 48 | return { 49 | wrapper: '', 50 | menu: '
' 51 | }; 52 | } 53 | 54 | function buildSelectors(classes) { 55 | var selectors = {}; 56 | _.each(classes, function(v, k) { selectors[k] = '.' + v; }); 57 | 58 | return selectors; 59 | } 60 | 61 | function buildCss() { 62 | var css = { 63 | wrapper: { 64 | position: 'relative', 65 | display: 'inline-block' 66 | }, 67 | hint: { 68 | position: 'absolute', 69 | top: '0', 70 | left: '0', 71 | borderColor: 'transparent', 72 | boxShadow: 'none', 73 | // #741: fix hint opacity issue on iOS 74 | opacity: '1' 75 | }, 76 | input: { 77 | position: 'relative', 78 | verticalAlign: 'top', 79 | backgroundColor: 'transparent' 80 | }, 81 | inputWithNoHint: { 82 | position: 'relative', 83 | verticalAlign: 'top' 84 | }, 85 | menu: { 86 | position: 'absolute', 87 | top: '100%', 88 | left: '0', 89 | zIndex: '100', 90 | display: 'none' 91 | }, 92 | ltr: { 93 | left: '0', 94 | right: 'auto' 95 | }, 96 | rtl: { 97 | left: 'auto', 98 | right:' 0' 99 | } 100 | }; 101 | 102 | // ie specific styling 103 | if (_.isMsie()) { 104 | // ie6-8 (and 9?) doesn't fire hover and click events for elements with 105 | // transparent backgrounds, for a workaround, use 1x1 transparent gif 106 | _.mixin(css.input, { 107 | backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)' 108 | }); 109 | } 110 | 111 | return css; 112 | } 113 | })(); 114 | -------------------------------------------------------------------------------- /test/typeahead/default_results_spec.js: -------------------------------------------------------------------------------- 1 | describe('DefaultMenu', function() { 2 | var www = WWW(); 3 | 4 | beforeEach(function() { 5 | var $fixture; 6 | 7 | jasmine.Dataset.useMock(); 8 | 9 | setFixtures(''); 10 | 11 | $fixture = $('#jasmine-fixtures'); 12 | this.$node = $fixture.find('#menu-fixture'); 13 | this.$node.html(fixtures.html.dataset); 14 | 15 | this.view = new DefaultMenu({ node: this.$node, datasets: [{}] }, www).bind(); 16 | this.dataset = this.view.datasets[0]; 17 | }); 18 | 19 | describe('when rendered is triggered on a dataset', function() { 20 | it('should hide menu if empty', function() { 21 | this.dataset.isEmpty.andReturn(true); 22 | 23 | this.view._show(); 24 | this.dataset.trigger('rendered'); 25 | 26 | expect(this.$node).not.toBeVisible(); 27 | }); 28 | 29 | it('should not show menu if not open', function() { 30 | this.dataset.isEmpty.andReturn(false); 31 | 32 | this.view._hide(); 33 | this.dataset.trigger('rendered'); 34 | 35 | expect(this.$node).not.toBeVisible(); 36 | }); 37 | 38 | it('should show menu if not empty and open', function() { 39 | this.dataset.isEmpty.andReturn(false); 40 | 41 | this.view._hide(); 42 | this.view.open(); 43 | this.dataset.trigger('rendered'); 44 | 45 | expect(this.$node).toBeVisible(); 46 | }); 47 | }); 48 | 49 | describe('when cleared is triggered on a dataset', function() { 50 | it('should hide menu if empty', function() { 51 | this.dataset.isEmpty.andReturn(true); 52 | 53 | this.view._show(); 54 | this.dataset.trigger('cleared'); 55 | 56 | expect(this.$node).not.toBeVisible(); 57 | }); 58 | 59 | it('should not show menu if not open', function() { 60 | this.dataset.isEmpty.andReturn(false); 61 | 62 | this.view._hide(); 63 | this.dataset.trigger('cleared'); 64 | 65 | expect(this.$node).not.toBeVisible(); 66 | }); 67 | 68 | it('should show menu if not empty and open', function() { 69 | this.dataset.isEmpty.andReturn(false); 70 | 71 | this.view._hide(); 72 | this.view.open(); 73 | this.dataset.trigger('cleared'); 74 | 75 | expect(this.$node).toBeVisible(); 76 | }); 77 | }); 78 | 79 | describe('#open', function() { 80 | it('should show menu if not empty', function() { 81 | spyOn(this.view, '_allDatasetsEmpty').andReturn(false); 82 | this.view.open(); 83 | 84 | expect(this.$node[0].getAttribute('style')).toMatch(/display: block/); 85 | }); 86 | 87 | it('should not show menu if empty', function() { 88 | spyOn(this.view, '_allDatasetsEmpty').andReturn(true); 89 | this.view.open(); 90 | 91 | expect(this.$node).not.toHaveAttr('style', 'display: block;'); 92 | }); 93 | }); 94 | 95 | describe('#close', function() { 96 | it('should hide menu', function() { 97 | this.view._show(); 98 | this.view.close(); 99 | 100 | expect(this.$node).not.toBeVisible(); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/typeahead/event_emitter.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/boomerang 8 | 9 | var EventEmitter = (function() { 10 | 'use strict'; 11 | 12 | var splitter = /\s+/, nextTick = getNextTick(); 13 | 14 | return { 15 | onSync: onSync, 16 | onAsync: onAsync, 17 | off: off, 18 | trigger: trigger 19 | }; 20 | 21 | function on(method, types, cb, context) { 22 | var type; 23 | 24 | if (!cb) { return this; } 25 | 26 | types = types.split(splitter); 27 | cb = context ? bindContext(cb, context) : cb; 28 | 29 | this._callbacks = this._callbacks || {}; 30 | 31 | while (type = types.shift()) { 32 | this._callbacks[type] = this._callbacks[type] || { sync: [], async: [] }; 33 | this._callbacks[type][method].push(cb); 34 | } 35 | 36 | return this; 37 | } 38 | 39 | function onAsync(types, cb, context) { 40 | return on.call(this, 'async', types, cb, context); 41 | } 42 | 43 | function onSync(types, cb, context) { 44 | return on.call(this, 'sync', types, cb, context); 45 | } 46 | 47 | function off(types) { 48 | var type; 49 | 50 | if (!this._callbacks) { return this; } 51 | 52 | types = types.split(splitter); 53 | 54 | while (type = types.shift()) { 55 | delete this._callbacks[type]; 56 | } 57 | 58 | return this; 59 | } 60 | 61 | function trigger(types) { 62 | var type, callbacks, args, syncFlush, asyncFlush; 63 | 64 | if (!this._callbacks) { return this; } 65 | 66 | types = types.split(splitter); 67 | args = [].slice.call(arguments, 1); 68 | 69 | while ((type = types.shift()) && (callbacks = this._callbacks[type])) { 70 | syncFlush = getFlush(callbacks.sync, this, [type].concat(args)); 71 | asyncFlush = getFlush(callbacks.async, this, [type].concat(args)); 72 | 73 | syncFlush() && nextTick(asyncFlush); 74 | } 75 | 76 | return this; 77 | } 78 | 79 | function getFlush(callbacks, context, args) { 80 | return flush; 81 | 82 | function flush() { 83 | var cancelled; 84 | 85 | for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { 86 | // only cancel if the callback explicitly returns false 87 | cancelled = callbacks[i].apply(context, args) === false; 88 | } 89 | 90 | return !cancelled; 91 | } 92 | } 93 | 94 | function getNextTick() { 95 | var nextTickFn; 96 | 97 | // IE10+ 98 | if (window.setImmediate) { 99 | nextTickFn = function nextTickSetImmediate(fn) { 100 | setImmediate(function() { fn(); }); 101 | }; 102 | } 103 | 104 | // old browsers 105 | else { 106 | nextTickFn = function nextTickSetTimeout(fn) { 107 | setTimeout(function() { fn(); }, 0); 108 | }; 109 | } 110 | 111 | return nextTickFn; 112 | } 113 | 114 | function bindContext(fn, context) { 115 | return fn.bind ? 116 | fn.bind(context) : 117 | function() { fn.apply(context, [].slice.call(arguments, 0)); }; 118 | } 119 | })(); 120 | -------------------------------------------------------------------------------- /test/integration/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | 40 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/bloodhound/transport.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 Transport = (function() { 8 | 'use strict'; 9 | 10 | var pendingRequestsCount = 0, 11 | pendingRequests = {}, 12 | sharedCache = new LruCache(10); 13 | 14 | // constructor 15 | // ----------- 16 | 17 | function Transport(o) { 18 | o = o || {}; 19 | 20 | this.maxPendingRequests = o.maxPendingRequests || 6; 21 | this.cancelled = false; 22 | this.lastReq = null; 23 | 24 | this._send = o.transport; 25 | this._get = o.limiter ? o.limiter(this._get) : this._get; 26 | 27 | this._cache = o.cache === false ? new LruCache(0) : sharedCache; 28 | } 29 | 30 | // static methods 31 | // -------------- 32 | 33 | Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { 34 | this.maxPendingRequests = num; 35 | }; 36 | 37 | Transport.resetCache = function resetCache() { 38 | sharedCache.reset(); 39 | }; 40 | 41 | // instance methods 42 | // ---------------- 43 | 44 | _.mixin(Transport.prototype, { 45 | 46 | // ### private 47 | 48 | _fingerprint: function fingerprint(o) { 49 | o = o || {}; 50 | return o.url + o.type + $.param(o.data || {}); 51 | }, 52 | 53 | _get: function(o, cb) { 54 | var that = this, fingerprint, jqXhr; 55 | 56 | fingerprint = this._fingerprint(o); 57 | 58 | // #149: don't make a network request if there has been a cancellation 59 | // or if the url doesn't match the last url Transport#get was invoked with 60 | if (this.cancelled || fingerprint !== this.lastReq) { return; } 61 | 62 | // a request is already in progress, piggyback off of it 63 | if (jqXhr = pendingRequests[fingerprint]) { 64 | jqXhr.done(done).fail(fail); 65 | } 66 | 67 | // under the pending request threshold, so fire off a request 68 | else if (pendingRequestsCount < this.maxPendingRequests) { 69 | pendingRequestsCount++; 70 | pendingRequests[fingerprint] = 71 | this._send(o).done(done).fail(fail).always(always); 72 | } 73 | 74 | // at the pending request threshold, so hang out in the on deck circle 75 | else { 76 | this.onDeckRequestArgs = [].slice.call(arguments, 0); 77 | } 78 | 79 | function done(resp) { 80 | cb(null, resp); 81 | that._cache.set(fingerprint, resp); 82 | } 83 | 84 | function fail() { 85 | cb(true); 86 | } 87 | 88 | function always() { 89 | pendingRequestsCount--; 90 | delete pendingRequests[fingerprint]; 91 | 92 | // ensures request is always made for the last query 93 | if (that.onDeckRequestArgs) { 94 | that._get.apply(that, that.onDeckRequestArgs); 95 | that.onDeckRequestArgs = null; 96 | } 97 | } 98 | }, 99 | 100 | // ### public 101 | 102 | get: function(o, cb) { 103 | var resp, fingerprint; 104 | 105 | cb = cb || $.noop; 106 | o = _.isString(o) ? { url: o } : (o || {}); 107 | 108 | fingerprint = this._fingerprint(o); 109 | 110 | this.cancelled = false; 111 | this.lastReq = fingerprint; 112 | 113 | // in-memory cache hit 114 | if (resp = this._cache.get(fingerprint)) { 115 | cb(null, resp); 116 | } 117 | 118 | // go to network 119 | else { 120 | this._get(o, cb); 121 | } 122 | }, 123 | 124 | cancel: function() { 125 | this.cancelled = true; 126 | } 127 | }); 128 | 129 | return Transport; 130 | })(); 131 | -------------------------------------------------------------------------------- /test/typeahead/event_emitter_spec.js: -------------------------------------------------------------------------------- 1 | describe('EventEmitter', function() { 2 | 3 | beforeEach(function() { 4 | this.spy = jasmine.createSpy(); 5 | this.target = _.mixin({}, EventEmitter); 6 | }); 7 | 8 | it('methods should be chainable', function() { 9 | expect(this.target.onSync()).toEqual(this.target); 10 | expect(this.target.onAsync()).toEqual(this.target); 11 | expect(this.target.off()).toEqual(this.target); 12 | expect(this.target.trigger()).toEqual(this.target); 13 | }); 14 | 15 | it('#on should take the context a callback should be called in', function() { 16 | var context = { val: 3 }, cbContext; 17 | 18 | this.target.onSync('xevent', setCbContext, context).trigger('xevent'); 19 | 20 | waitsFor(assertCbContext, 'callback was called in the wrong context'); 21 | 22 | function setCbContext() { cbContext = this; } 23 | function assertCbContext() { return cbContext === context; } 24 | }); 25 | 26 | it('#onAsync callbacks should be invoked asynchronously', function() { 27 | this.target.onAsync('event', this.spy).trigger('event'); 28 | 29 | expect(this.spy.callCount).toBe(0); 30 | waitsFor(assertCallCount(this.spy, 1), 'the callback was not invoked'); 31 | }); 32 | 33 | it('#onSync callbacks should be invoked synchronously', function() { 34 | this.target.onSync('event', this.spy).trigger('event'); 35 | 36 | expect(this.spy.callCount).toBe(1); 37 | }); 38 | 39 | it('#off should remove callbacks', function() { 40 | this.target 41 | .onSync('event1 event2', this.spy) 42 | .onAsync('event1 event2', this.spy) 43 | .off('event1 event2') 44 | .trigger('event1 event2'); 45 | 46 | waits(100); 47 | runs(assertCallCount(this.spy, 0)); 48 | }); 49 | 50 | it('methods should accept multiple event types', function() { 51 | this.target 52 | .onSync('event1 event2', this.spy) 53 | .onAsync('event1 event2', this.spy) 54 | .trigger('event1 event2'); 55 | 56 | expect(this.spy.callCount).toBe(2); 57 | waitsFor(assertCallCount(this.spy, 4), 'the callback was not invoked'); 58 | }); 59 | 60 | it('the event type should be passed to the callback', function() { 61 | this.target 62 | .onSync('sync', this.spy) 63 | .onAsync('async', this.spy) 64 | .trigger('sync async'); 65 | 66 | waitsFor(assertArgs(this.spy, 0, ['sync']), 'bad args'); 67 | waitsFor(assertArgs(this.spy, 1, ['async']), 'bad args'); 68 | }); 69 | 70 | it('arbitrary args should be passed to the callback', function() { 71 | this.target 72 | .onSync('event', this.spy) 73 | .onAsync('event', this.spy) 74 | .trigger('event', 1, 2); 75 | 76 | waitsFor(assertArgs(this.spy, 0, ['event', 1, 2]), 'bad args'); 77 | waitsFor(assertArgs(this.spy, 1, ['event', 1, 2]), 'bad args'); 78 | }); 79 | 80 | it('callback execution should be cancellable', function() { 81 | var cancelSpy = jasmine.createSpy().andCallFake(cancel); 82 | 83 | this.target 84 | .onSync('one', cancelSpy) 85 | .onSync('one', this.spy) 86 | .onAsync('two', cancelSpy) 87 | .onAsync('two', this.spy) 88 | .onSync('three', cancelSpy) 89 | .onAsync('three', this.spy) 90 | .trigger('one two three'); 91 | 92 | waitsFor(assertCallCount(cancelSpy, 3)); 93 | waitsFor(assertCallCount(this.spy, 0)); 94 | 95 | function cancel() { return false; } 96 | }); 97 | 98 | function assertCallCount(spy, expected) { 99 | return function() { return spy.callCount === expected; }; 100 | } 101 | 102 | function assertArgs(spy, call, expected) { 103 | return function() { 104 | var env = jasmine.getEnv(), 105 | actual = spy.calls[call] ? spy.calls[call].args : undefined; 106 | 107 | return env.equals_(actual, expected); 108 | }; 109 | } 110 | 111 | }); 112 | -------------------------------------------------------------------------------- /src/bloodhound/persistent_storage.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 PersistentStorage = (function() { 8 | 'use strict'; 9 | 10 | var LOCAL_STORAGE; 11 | 12 | try { 13 | LOCAL_STORAGE = window.localStorage; 14 | 15 | // while in private browsing mode, some browsers make 16 | // localStorage available, but throw an error when used 17 | LOCAL_STORAGE.setItem('~~~', '!'); 18 | LOCAL_STORAGE.removeItem('~~~'); 19 | } catch (err) { 20 | LOCAL_STORAGE = null; 21 | } 22 | 23 | // constructor 24 | // ----------- 25 | 26 | function PersistentStorage(namespace, override) { 27 | this.prefix = ['__', namespace, '__'].join(''); 28 | this.ttlKey = '__ttl__'; 29 | this.keyMatcher = new RegExp('^' + _.escapeRegExChars(this.prefix)); 30 | 31 | // for testing purpose 32 | this.ls = override || LOCAL_STORAGE; 33 | 34 | // if local storage isn't available, everything becomes a noop 35 | !this.ls && this._noop(); 36 | } 37 | 38 | // instance methods 39 | // ---------------- 40 | 41 | _.mixin(PersistentStorage.prototype, { 42 | // ### private 43 | 44 | _prefix: function(key) { 45 | return this.prefix + key; 46 | }, 47 | 48 | _ttlKey: function(key) { 49 | return this._prefix(key) + this.ttlKey; 50 | }, 51 | 52 | _noop: function() { 53 | this.get = 54 | this.set = 55 | this.remove = 56 | this.clear = 57 | this.isExpired = _.noop; 58 | }, 59 | 60 | _safeSet: function(key, val) { 61 | try { 62 | this.ls.setItem(key, val); 63 | } catch (err) { 64 | // hit the localstorage limit so clean up and better luck next time 65 | if (err.name === 'QuotaExceededError') { 66 | this.clear(); 67 | this._noop(); 68 | } 69 | } 70 | }, 71 | 72 | // ### public 73 | 74 | get: function(key) { 75 | if (this.isExpired(key)) { 76 | this.remove(key); 77 | } 78 | 79 | return decode(this.ls.getItem(this._prefix(key))); 80 | }, 81 | 82 | set: function(key, val, ttl) { 83 | if (_.isNumber(ttl)) { 84 | this._safeSet(this._ttlKey(key), encode(now() + ttl)); 85 | } 86 | 87 | else { 88 | this.ls.removeItem(this._ttlKey(key)); 89 | } 90 | 91 | return this._safeSet(this._prefix(key), encode(val)); 92 | }, 93 | 94 | remove: function(key) { 95 | this.ls.removeItem(this._ttlKey(key)); 96 | this.ls.removeItem(this._prefix(key)); 97 | 98 | return this; 99 | }, 100 | 101 | clear: function() { 102 | var i, keys = gatherMatchingKeys(this.keyMatcher); 103 | 104 | for (i = keys.length; i--;) { 105 | this.remove(keys[i]); 106 | } 107 | 108 | return this; 109 | }, 110 | 111 | isExpired: function(key) { 112 | var ttl = decode(this.ls.getItem(this._ttlKey(key))); 113 | 114 | return _.isNumber(ttl) && now() > ttl ? true : false; 115 | } 116 | }); 117 | 118 | return PersistentStorage; 119 | 120 | // helper functions 121 | // ---------------- 122 | 123 | function now() { 124 | return new Date().getTime(); 125 | } 126 | 127 | function encode(val) { 128 | // convert undefined to null to avoid issues with JSON.parse 129 | return JSON.stringify(_.isUndefined(val) ? null : val); 130 | } 131 | 132 | function decode(val) { 133 | return $.parseJSON(val); 134 | } 135 | 136 | function gatherMatchingKeys(keyMatcher) { 137 | var i, key, keys = [], len = LOCAL_STORAGE.length; 138 | 139 | for (i = 0; i < len; i++) { 140 | if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { 141 | keys.push(key.replace(keyMatcher, '')); 142 | } 143 | } 144 | 145 | return keys; 146 | } 147 | })(); 148 | -------------------------------------------------------------------------------- /test/bloodhound/tokenizers_spec.js: -------------------------------------------------------------------------------- 1 | describe('tokenizers', function() { 2 | 3 | it('.whitespace should tokenize on whitespace', function() { 4 | var tokens = tokenizers.whitespace('big-deal ok'); 5 | expect(tokens).toEqual(['big-deal', 'ok']); 6 | }); 7 | 8 | it('.whitespace should treat null as empty string', function() { 9 | var tokens = tokenizers.whitespace(null); 10 | expect(tokens).toEqual([]); 11 | }); 12 | 13 | it('.whitespace should treat undefined as empty string', function() { 14 | var tokens = tokenizers.whitespace(undefined); 15 | expect(tokens).toEqual([]); 16 | }); 17 | 18 | it('.nonword should tokenize on non-word characters', function() { 19 | var tokens = tokenizers.nonword('big-deal ok'); 20 | expect(tokens).toEqual(['big', 'deal', 'ok']); 21 | }); 22 | 23 | it('.nonword should treat null as empty string', function() { 24 | var tokens = tokenizers.nonword(null); 25 | expect(tokens).toEqual([]); 26 | }); 27 | 28 | it('.nonword should treat undefined as empty string', function() { 29 | var tokens = tokenizers.nonword(undefined); 30 | expect(tokens).toEqual([]); 31 | }); 32 | 33 | it('.ngram should treat null as empty string', function() { 34 | var tokens = tokenizers.ngram(null); 35 | expect(tokens).toEqual([]); 36 | }); 37 | 38 | it('.ngram should treat undefined as empty string', function() { 39 | var tokens = tokenizers.ngram(undefined); 40 | expect(tokens).toEqual([]); 41 | }); 42 | 43 | it('.ngram should tokenize to edge ngrams', function() { 44 | var tokens = tokenizers.ngram('foo bar'); 45 | expect(tokens).toEqual(['f', 'fo', 'foo', 'b', 'ba', 'bar']); 46 | }); 47 | 48 | it('.obj.whitespace should tokenize on whitespace', function() { 49 | var t = tokenizers.obj.whitespace('val'); 50 | var tokens = t({ val: 'big-deal ok' }); 51 | 52 | expect(tokens).toEqual(['big-deal', 'ok']); 53 | }); 54 | 55 | it('.obj.whitespace should accept multiple properties', function() { 56 | var t = tokenizers.obj.whitespace('one', 'two'); 57 | var tokens = t({ one: 'big-deal ok', two: 'buzz' }); 58 | 59 | expect(tokens).toEqual(['big-deal', 'ok', 'buzz']); 60 | }); 61 | 62 | it('.obj.whitespace should accept array', function() { 63 | var t = tokenizers.obj.whitespace(['one', 'two']); 64 | var tokens = t({ one: 'big-deal ok', two: 'buzz' }); 65 | 66 | expect(tokens).toEqual(['big-deal', 'ok', 'buzz']); 67 | }); 68 | 69 | it('.obj.nonword should tokenize on non-word characters', function() { 70 | var t = tokenizers.obj.nonword('val'); 71 | var tokens = t({ val: 'big-deal ok' }); 72 | 73 | expect(tokens).toEqual(['big', 'deal', 'ok']); 74 | }); 75 | 76 | it('.obj.nonword should accept multiple properties', function() { 77 | var t = tokenizers.obj.nonword('one', 'two'); 78 | var tokens = t({ one: 'big-deal ok', two: 'buzz' }); 79 | 80 | expect(tokens).toEqual(['big', 'deal', 'ok', 'buzz']); 81 | }); 82 | 83 | it('.obj.nonword should accept array', function() { 84 | var t = tokenizers.obj.nonword(['one', 'two']); 85 | var tokens = t({ one: 'big-deal ok', two: 'buzz' }); 86 | 87 | expect(tokens).toEqual(['big', 'deal', 'ok', 'buzz']); 88 | }); 89 | 90 | it('.obj.ngram should tokenize to edge ngrams', function() { 91 | var t = tokenizers.obj.ngram('val'); 92 | var tokens = t({ val: 'foo bar' }); 93 | 94 | expect(tokens).toEqual(['f', 'fo', 'foo', 'b', 'ba', 'bar']); 95 | }); 96 | 97 | it('.obj.ngram should accept multiple properties', function() { 98 | var t = tokenizers.obj.ngram('one', 'two'); 99 | var tokens = t({ one: 'foo bar', two: 'baz' }); 100 | 101 | expect(tokens).toEqual(['f', 'fo', 'foo', 'b', 'ba', 'bar', 'b', 'ba', 'baz']); 102 | }); 103 | 104 | it('.obj.ngram should accept array', function() { 105 | var t = tokenizers.obj.ngram(['one', 'two']); 106 | var tokens = t({ one: 'foo bar', two: 'baz' }); 107 | 108 | expect(tokens).toEqual(['f', 'fo', 'foo', 'b', 'ba', 'bar', 'b', 'ba', 'baz']); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | Contributing to typeahead.js 2 | ============================ 3 | 4 | *These contributing guidelines were proudly stolen from the 5 | [Flight](https://github.com/flightjs/flight) project* 6 | 7 | Project Status 8 | -------------- 9 | The maintainers of this project welcome Pull Requests but, due to a lack of 10 | time, new major feature requests are unlikely to be taken on. 11 | 12 | Looking to contribute something to typeahead.js? Here's how you can help. 13 | 14 | Bugs Reports 15 | ------------ 16 | 17 | A bug is a _demonstrable problem_ that is caused by the code in the 18 | repository. Good bug reports are extremely helpful – thank you! 19 | 20 | Guidelines for bug reports: 21 | 22 | 1. **Use the GitHub issue search** — check if the issue has already been 23 | reported. 24 | 25 | 2. **Check if the issue has been fixed** — try to reproduce it using the 26 | latest `master` or integration branch in the repository. 27 | 28 | 3. **Isolate the problem** — ideally create a reduced test 29 | case and a live example. 30 | 31 | 4. Please try to be as detailed as possible in your report. Include specific 32 | information about the environment – operating system and version, browser 33 | and version, version of typeahead.js – and steps required to reproduce the 34 | issue. 35 | 36 | Feature Requests & Contribution Enquiries 37 | ----------------------------------------- 38 | 39 | Feature requests are welcome. But take a moment to find out whether your idea 40 | fits with the scope and aims of the project. It's up to *you* to make a strong 41 | case for the inclusion of your feature. Please provide as much detail and 42 | context as possible. 43 | 44 | Contribution enquiries should take place before any significant pull request, 45 | otherwise you risk spending a lot of time working on something that we might 46 | have good reasons for rejecting. 47 | 48 | Pull Requests 49 | ------------- 50 | 51 | Good pull requests – patches, improvements, new features – are a fantastic 52 | help. They should remain focused in scope and avoid containing unrelated 53 | commits. 54 | 55 | Make sure to adhere to the coding conventions used throughout the codebase 56 | (indentation, accurate comments, etc.) and any other requirements (such as test 57 | coverage). 58 | 59 | Please follow this process; it's the best way to get your work included in the 60 | project: 61 | 62 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 63 | and configure the remotes: 64 | 65 | ```bash 66 | # Clone your fork of the repo into the current directory 67 | git clone https://github.com//typeahead.js 68 | # Navigate to the newly cloned directory 69 | cd 70 | # Assign the original repo to a remote called "upstream" 71 | git remote add upstream git://github.com/twitter/typeahead.js 72 | ``` 73 | 74 | 2. If you cloned a while ago, get the latest changes from upstream: 75 | 76 | ```bash 77 | git checkout master 78 | git pull upstream master 79 | ``` 80 | 81 | 3. Install the dependencies (you must have Node.js and [Bower](http://bower.io) 82 | installed), and create a new topic branch (off the main project development 83 | branch) to contain your feature, change, or fix: 84 | 85 | ```bash 86 | npm install 87 | bower install 88 | git checkout -b 89 | ``` 90 | 91 | 4. Make sure to update, or add to the tests when appropriate. Patches and 92 | features will not be accepted without tests. Run `npm test` to check that 93 | all tests pass after you've made changes. 94 | 95 | 5. Commit your changes in logical chunks. Provide clear and explanatory commit 96 | messages. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up 97 | your commits before making them public. 98 | 99 | 6. Locally merge (or rebase) the upstream development branch into your topic branch: 100 | 101 | ```bash 102 | git pull [--rebase] upstream master 103 | ``` 104 | 105 | 7. Push your topic branch up to your fork: 106 | 107 | ```bash 108 | git push origin 109 | ``` 110 | 111 | 8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 112 | with a clear title and description. 113 | 114 | 9. If you are asked to amend your changes before they can be merged in, please 115 | use `git commit --amend` (or rebasing for multi-commit Pull Requests) and 116 | force push to your remote feature branch. You may also be asked to squash 117 | commits. 118 | 119 | License 120 | ------- 121 | 122 | By contributing your code, 123 | 124 | You agree to license your contribution under the terms of the MIT License 125 | https://github.com/twitter/typeahead.js/blob/master/LICENSE 126 | -------------------------------------------------------------------------------- /src/common/utils.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 _ = (function() { 8 | 'use strict'; 9 | 10 | return { 11 | isMsie: function() { 12 | // from https://github.com/ded/bowser/blob/master/bowser.js 13 | return (/(msie|trident)/i).test(navigator.userAgent) ? 14 | navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; 15 | }, 16 | 17 | isBlankString: function(str) { return !str || /^\s*$/.test(str); }, 18 | 19 | // http://stackoverflow.com/a/6969486 20 | escapeRegExChars: function(str) { 21 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 22 | }, 23 | 24 | isString: function(obj) { return typeof obj === 'string'; }, 25 | 26 | isNumber: function(obj) { return typeof obj === 'number'; }, 27 | 28 | isArray: $.isArray, 29 | 30 | isFunction: $.isFunction, 31 | 32 | isObject: $.isPlainObject, 33 | 34 | isUndefined: function(obj) { return typeof obj === 'undefined'; }, 35 | 36 | isElement: function(obj) { return !!(obj && obj.nodeType === 1); }, 37 | 38 | isJQuery: function(obj) { return obj instanceof $; }, 39 | 40 | toStr: function toStr(s) { 41 | return (_.isUndefined(s) || s === null) ? '' : s + ''; 42 | }, 43 | 44 | bind: $.proxy, 45 | 46 | each: function(collection, cb) { 47 | // stupid argument order for jQuery.each 48 | $.each(collection, reverseArgs); 49 | 50 | function reverseArgs(index, value) { return cb(value, index); } 51 | }, 52 | 53 | map: $.map, 54 | 55 | filter: $.grep, 56 | 57 | every: function(obj, test) { 58 | var result = true; 59 | 60 | if (!obj) { return result; } 61 | 62 | $.each(obj, function(key, val) { 63 | if (!(result = test.call(null, val, key, obj))) { 64 | return false; 65 | } 66 | }); 67 | 68 | return !!result; 69 | }, 70 | 71 | some: function(obj, test) { 72 | var result = false; 73 | 74 | if (!obj) { return result; } 75 | 76 | $.each(obj, function(key, val) { 77 | if (result = test.call(null, val, key, obj)) { 78 | return false; 79 | } 80 | }); 81 | 82 | return !!result; 83 | }, 84 | 85 | mixin: $.extend, 86 | 87 | identity: function(x) { return x; }, 88 | 89 | clone: function(obj) { return $.extend(true, {}, obj); }, 90 | 91 | getIdGenerator: function() { 92 | var counter = 0; 93 | return function() { return counter++; }; 94 | }, 95 | 96 | templatify: function templatify(obj) { 97 | return $.isFunction(obj) ? obj : template; 98 | 99 | function template() { return String(obj); } 100 | }, 101 | 102 | defer: function(fn) { setTimeout(fn, 0); }, 103 | 104 | debounce: function(func, wait, immediate) { 105 | var timeout, result; 106 | 107 | return function() { 108 | var context = this, args = arguments, later, callNow; 109 | 110 | later = function() { 111 | timeout = null; 112 | if (!immediate) { result = func.apply(context, args); } 113 | }; 114 | 115 | callNow = immediate && !timeout; 116 | 117 | clearTimeout(timeout); 118 | timeout = setTimeout(later, wait); 119 | 120 | if (callNow) { result = func.apply(context, args); } 121 | 122 | return result; 123 | }; 124 | }, 125 | 126 | throttle: function(func, wait) { 127 | var context, args, timeout, result, previous, later; 128 | 129 | previous = 0; 130 | later = function() { 131 | previous = new Date(); 132 | timeout = null; 133 | result = func.apply(context, args); 134 | }; 135 | 136 | return function() { 137 | var now = new Date(), 138 | remaining = wait - (now - previous); 139 | 140 | context = this; 141 | args = arguments; 142 | 143 | if (remaining <= 0) { 144 | clearTimeout(timeout); 145 | timeout = null; 146 | previous = now; 147 | result = func.apply(context, args); 148 | } 149 | 150 | else if (!timeout) { 151 | timeout = setTimeout(later, remaining); 152 | } 153 | 154 | return result; 155 | }; 156 | }, 157 | 158 | stringify: function(val) { 159 | return _.isString(val) ? val : JSON.stringify(val); 160 | }, 161 | 162 | guid: function() { 163 | function _p8(s) { 164 | var p = (Math.random().toString(16)+'000000000').substr(2,8); 165 | return s ? '-' + p.substr(0,4) + '-' + p.substr(4,4) : p ; 166 | } 167 | return 'tt-' + _p8() + _p8(true) + _p8(true) + _p8(); 168 | }, 169 | 170 | noop: function() {} 171 | }; 172 | })(); 173 | -------------------------------------------------------------------------------- /test/typeahead/highlight_spec.js: -------------------------------------------------------------------------------- 1 | describe('highlight', function() { 2 | it('should allow tagName to be specified', function() { 3 | var before = 'abcde', 4 | after = 'abcde', 5 | testNode = buildTestNode(before); 6 | 7 | highlight({ node: testNode, pattern: 'bcd', tagName: 'span' }); 8 | expect(testNode.innerHTML).toEqual(after); 9 | }); 10 | 11 | it('should allow className to be specified', function() { 12 | var before = 'abcde', 13 | after = 'abcde', 14 | testNode = buildTestNode(before); 15 | 16 | highlight({ node: testNode, pattern: 'bcd', className: 'one two' }); 17 | expect(testNode.innerHTML).toEqual(after); 18 | }); 19 | 20 | it('should be case insensitive by default', function() { 21 | var before = 'ABCDE', 22 | after = 'ABCDE', 23 | testNode = buildTestNode(before); 24 | 25 | highlight({ node: testNode, pattern: 'bcd' }); 26 | expect(testNode.innerHTML).toEqual(after); 27 | }); 28 | 29 | it('should support case sensitivity', function() { 30 | var before = 'ABCDE', 31 | after = 'ABCDE', 32 | testNode = buildTestNode(before); 33 | 34 | highlight({ node: testNode, pattern: 'bcd', caseSensitive: true }); 35 | expect(testNode.innerHTML).toEqual(after); 36 | }); 37 | 38 | it('should support diacritic insensitivity', function() { 39 | var before = 'ABƠDE', 40 | after = 'ABƠDE', 41 | testNode = buildTestNode(before); 42 | 43 | highlight({ node: testNode, pattern: 'bod', diacriticInsensitive: true }); 44 | expect(testNode.innerHTML).toEqual(after); 45 | }); 46 | 47 | it('should be diacritic sensitive by default', function() { 48 | var before = 'ABƠDE', 49 | after = 'ABƠDE', 50 | testNode = buildTestNode(before); 51 | 52 | highlight({ node: testNode, pattern: 'BOD'}); 53 | expect(testNode.innerHTML).toEqual(after); 54 | }); 55 | 56 | it('should support words only matching', function() { 57 | var before = 'tone one phone', 58 | after = 'tone one phone', 59 | testNode = buildTestNode(before); 60 | 61 | highlight({ node: testNode, pattern: 'one', wordsOnly: true }); 62 | expect(testNode.innerHTML).toEqual(after); 63 | }); 64 | 65 | it('should support matching multiple patterns', function() { 66 | var before = 'tone one phone', 67 | after = 'tone one phone', 68 | testNode = buildTestNode(before); 69 | 70 | highlight({ node: testNode, pattern: ['tone', 'phone'] }); 71 | expect(testNode.innerHTML).toEqual(after); 72 | }); 73 | 74 | it('should support regex chars in the pattern', function() { 75 | var before = '*.js when?', 76 | after = '*.js when?', 77 | testNode = buildTestNode(before); 78 | 79 | highlight({ node: testNode, pattern: ['*.', '?'] }); 80 | expect(testNode.innerHTML).toEqual(after); 81 | }); 82 | 83 | it('should work on complex html structures', function() { 84 | var before = [ 85 | '
abcde', 86 | 'abcde', 87 | '

abcde

', 88 | '
' 89 | ].join(''), 90 | after = [ 91 | '
abcde', 92 | 'abcde', 93 | '

abcde

', 94 | '
' 95 | ].join(''), 96 | testNode = buildTestNode(before); 97 | 98 | highlight({ node: testNode, pattern: 'abc' }); 99 | expect(testNode.innerHTML).toEqual(after); 100 | }); 101 | 102 | it('should ignore html tags and attributes', function() { 103 | var before = '', 104 | after = '', 105 | testNode = buildTestNode(before); 106 | 107 | highlight({ node: testNode, pattern: ['span', 'class'] }); 108 | expect(testNode.innerHTML).toEqual(after); 109 | }); 110 | 111 | it('should not match across tags', function() { 112 | var before = 'abc', 113 | after = 'abc', 114 | testNode = buildTestNode(before); 115 | 116 | highlight({ node: testNode, pattern: 'abc' }); 117 | expect(testNode.innerHTML).toEqual(after); 118 | }); 119 | 120 | it('should ignore html comments', function() { 121 | var before = '', 122 | after = '', 123 | testNode = buildTestNode(before); 124 | 125 | highlight({ node: testNode, pattern: 'abc' }); 126 | expect(testNode.innerHTML).toEqual(after); 127 | }); 128 | 129 | function buildTestNode(content) { 130 | var node = document.createElement('div'); 131 | node.innerHTML = content; 132 | 133 | return node; 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /src/bloodhound/search_index.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 SearchIndex = window.SearchIndex = (function() { 8 | 'use strict'; 9 | 10 | var CHILDREN = 'c', IDS = 'i'; 11 | 12 | // constructor 13 | // ----------- 14 | 15 | function SearchIndex(o) { 16 | o = o || {}; 17 | 18 | if (!o.datumTokenizer || !o.queryTokenizer) { 19 | $.error('datumTokenizer and queryTokenizer are both required'); 20 | } 21 | 22 | this.identify = o.identify || _.stringify; 23 | this.datumTokenizer = o.datumTokenizer; 24 | this.queryTokenizer = o.queryTokenizer; 25 | this.matchAnyQueryToken = o.matchAnyQueryToken; 26 | 27 | this.reset(); 28 | } 29 | 30 | // instance methods 31 | // ---------------- 32 | 33 | _.mixin(SearchIndex.prototype, { 34 | 35 | // ### public 36 | 37 | bootstrap: function bootstrap(o) { 38 | this.datums = o.datums; 39 | this.trie = o.trie; 40 | }, 41 | 42 | add: function(data) { 43 | var that = this; 44 | 45 | data = _.isArray(data) ? data : [data]; 46 | 47 | _.each(data, function(datum) { 48 | var id, tokens; 49 | 50 | that.datums[id = that.identify(datum)] = datum; 51 | tokens = normalizeTokens(that.datumTokenizer(datum)); 52 | 53 | _.each(tokens, function(token) { 54 | var node, chars, ch; 55 | 56 | node = that.trie; 57 | chars = token.split(''); 58 | 59 | while (ch = chars.shift()) { 60 | node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); 61 | node[IDS].push(id); 62 | } 63 | }); 64 | }); 65 | }, 66 | 67 | get: function get(ids) { 68 | var that = this; 69 | 70 | return _.map(ids, function(id) { return that.datums[id]; }); 71 | }, 72 | 73 | search: function search(query) { 74 | var that = this, tokens, matches; 75 | 76 | tokens = normalizeTokens(this.queryTokenizer(query)); 77 | 78 | _.each(tokens, function(token) { 79 | var node, chars, ch, ids; 80 | 81 | // previous tokens didn't share any matches 82 | if (matches && matches.length === 0 && !that.matchAnyQueryToken) { 83 | return false; 84 | } 85 | 86 | node = that.trie; 87 | chars = token.split(''); 88 | 89 | while (node && (ch = chars.shift())) { 90 | node = node[CHILDREN][ch]; 91 | } 92 | 93 | if (node && chars.length === 0) { 94 | ids = node[IDS].slice(0); 95 | matches = matches ? getIntersection(matches, ids) : ids; 96 | } 97 | 98 | // break early if we find out there are no possible matches 99 | else { 100 | if (!that.matchAnyQueryToken) { 101 | matches = []; 102 | return false; 103 | } 104 | } 105 | }); 106 | 107 | return matches ? 108 | _.map(unique(matches), function(id) { return that.datums[id]; }) : []; 109 | }, 110 | 111 | all: function all() { 112 | var values = []; 113 | 114 | for (var key in this.datums) { 115 | values.push(this.datums[key]); 116 | } 117 | 118 | return values; 119 | }, 120 | 121 | reset: function reset() { 122 | this.datums = {}; 123 | this.trie = newNode(); 124 | }, 125 | 126 | serialize: function serialize() { 127 | return { datums: this.datums, trie: this.trie }; 128 | } 129 | }); 130 | 131 | return SearchIndex; 132 | 133 | // helper functions 134 | // ---------------- 135 | 136 | function normalizeTokens(tokens) { 137 | // filter out falsy tokens 138 | tokens = _.filter(tokens, function(token) { return !!token; }); 139 | 140 | // normalize tokens 141 | tokens = _.map(tokens, function(token) { return token.toLowerCase(); }); 142 | 143 | return tokens; 144 | } 145 | 146 | function newNode() { 147 | var node = {}; 148 | 149 | node[IDS] = []; 150 | node[CHILDREN] = {}; 151 | 152 | return node; 153 | } 154 | 155 | function unique(array) { 156 | var seen = {}, uniques = []; 157 | 158 | for (var i = 0, len = array.length; i < len; i++) { 159 | if (!seen[array[i]]) { 160 | seen[array[i]] = true; 161 | uniques.push(array[i]); 162 | } 163 | } 164 | 165 | return uniques; 166 | } 167 | 168 | function getIntersection(arrayA, arrayB) { 169 | var ai = 0, bi = 0, intersection = []; 170 | 171 | arrayA = arrayA.sort(); 172 | arrayB = arrayB.sort(); 173 | 174 | var lenArrayA = arrayA.length, lenArrayB = arrayB.length; 175 | 176 | while (ai < lenArrayA && bi < lenArrayB) { 177 | if (arrayA[ai] < arrayB[bi]) { 178 | ai++; 179 | } 180 | 181 | else if (arrayA[ai] > arrayB[bi]) { 182 | bi++; 183 | } 184 | 185 | else { 186 | intersection.push(arrayA[ai]); 187 | ai++; 188 | bi++; 189 | } 190 | } 191 | 192 | return intersection; 193 | } 194 | })(); 195 | -------------------------------------------------------------------------------- /src/bloodhound/options_parser.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 oParser = (function() { 8 | 'use strict'; 9 | 10 | return function parse(o) { 11 | var defaults, sorter; 12 | 13 | defaults = { 14 | initialize: true, 15 | identify: _.stringify, 16 | datumTokenizer: null, 17 | queryTokenizer: null, 18 | matchAnyQueryToken: false, 19 | sufficient: 5, 20 | indexRemote: false, 21 | sorter: null, 22 | local: [], 23 | prefetch: null, 24 | remote: null 25 | }; 26 | 27 | o = _.mixin(defaults, o || {}); 28 | 29 | // throw error if required options are not set 30 | !o.datumTokenizer && $.error('datumTokenizer is required'); 31 | !o.queryTokenizer && $.error('queryTokenizer is required'); 32 | 33 | sorter = o.sorter; 34 | o.sorter = sorter ? function(x) { return x.sort(sorter); } : _.identity; 35 | 36 | o.local = _.isFunction(o.local) ? o.local() : o.local; 37 | o.prefetch = parsePrefetch(o.prefetch); 38 | o.remote = parseRemote(o.remote); 39 | 40 | return o; 41 | }; 42 | 43 | function parsePrefetch(o) { 44 | var defaults; 45 | 46 | if (!o) { return null; } 47 | 48 | defaults = { 49 | url: null, 50 | ttl: 24 * 60 * 60 * 1000, // 1 day 51 | cache: true, 52 | cacheKey: null, 53 | thumbprint: '', 54 | prepare: _.identity, 55 | transform: _.identity, 56 | transport: null 57 | }; 58 | 59 | // support basic (url) and advanced configuration 60 | o = _.isString(o) ? { url: o } : o; 61 | o = _.mixin(defaults, o); 62 | 63 | // throw error if required options are not set 64 | !o.url && $.error('prefetch requires url to be set'); 65 | 66 | // DEPRECATED: filter will be dropped in v1 67 | o.transform = o.filter || o.transform; 68 | 69 | o.cacheKey = o.cacheKey || o.url; 70 | o.thumbprint = VERSION + o.thumbprint; 71 | o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; 72 | 73 | return o; 74 | } 75 | 76 | function parseRemote(o) { 77 | var defaults; 78 | 79 | if (!o) { return; } 80 | 81 | defaults = { 82 | url: null, 83 | cache: true, // leave undocumented 84 | prepare: null, 85 | replace: null, 86 | wildcard: null, 87 | limiter: null, 88 | rateLimitBy: 'debounce', 89 | rateLimitWait: 300, 90 | transform: _.identity, 91 | transport: null 92 | }; 93 | 94 | // support basic (url) and advanced configuration 95 | o = _.isString(o) ? { url: o } : o; 96 | o = _.mixin(defaults, o); 97 | 98 | // throw error if required options are not set 99 | !o.url && $.error('remote requires url to be set'); 100 | 101 | // DEPRECATED: filter will be dropped in v1 102 | o.transform = o.filter || o.transform; 103 | 104 | o.prepare = toRemotePrepare(o); 105 | o.limiter = toLimiter(o); 106 | o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; 107 | 108 | delete o.replace; 109 | delete o.wildcard; 110 | delete o.rateLimitBy; 111 | delete o.rateLimitWait; 112 | 113 | return o; 114 | } 115 | 116 | function toRemotePrepare(o) { 117 | var prepare, replace, wildcard; 118 | 119 | prepare = o.prepare; 120 | replace = o.replace; 121 | wildcard = o.wildcard; 122 | 123 | if (prepare) { return prepare; } 124 | 125 | if (replace) { 126 | prepare = prepareByReplace; 127 | } 128 | 129 | else if (o.wildcard) { 130 | prepare = prepareByWildcard; 131 | } 132 | 133 | else { 134 | prepare = identityPrepare; 135 | } 136 | 137 | return prepare; 138 | 139 | function prepareByReplace(query, settings) { 140 | settings.url = replace(settings.url, query); 141 | return settings; 142 | } 143 | 144 | function prepareByWildcard(query, settings) { 145 | settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); 146 | return settings; 147 | } 148 | 149 | function identityPrepare(query, settings) { 150 | return settings; 151 | } 152 | } 153 | 154 | function toLimiter(o) { 155 | var limiter, method, wait; 156 | 157 | limiter = o.limiter; 158 | method = o.rateLimitBy; 159 | wait = o.rateLimitWait; 160 | 161 | if (!limiter) { 162 | limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); 163 | } 164 | 165 | return limiter; 166 | 167 | function debounce(wait) { 168 | return function debounce(fn) { return _.debounce(fn, wait); }; 169 | } 170 | 171 | function throttle(wait) { 172 | return function throttle(fn) { return _.throttle(fn, wait); }; 173 | } 174 | } 175 | 176 | function callbackToDeferred(fn) { 177 | return function wrapper(o) { 178 | var deferred = $.Deferred(); 179 | 180 | fn(o, onSuccess, onError); 181 | 182 | return deferred; 183 | 184 | function onSuccess(resp) { 185 | // defer in case fn is synchronous, otherwise done 186 | // and always handlers will be attached after the resolution 187 | _.defer(function() { deferred.resolve(resp); }); 188 | } 189 | 190 | function onError(err) { 191 | // defer in case fn is synchronous, otherwise done 192 | // and always handlers will be attached after the resolution 193 | _.defer(function() { deferred.reject(err); }); 194 | } 195 | }; 196 | } 197 | })(); 198 | -------------------------------------------------------------------------------- /src/bloodhound/bloodhound.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 Bloodhound = (function() { 8 | 'use strict'; 9 | 10 | var old; 11 | 12 | old = window && window.Bloodhound; 13 | 14 | // constructor 15 | // ----------- 16 | 17 | function Bloodhound(o) { 18 | o = oParser(o); 19 | 20 | this.sorter = o.sorter; 21 | this.identify = o.identify; 22 | this.sufficient = o.sufficient; 23 | this.indexRemote = o.indexRemote; 24 | 25 | this.local = o.local; 26 | this.remote = o.remote ? new Remote(o.remote) : null; 27 | this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; 28 | 29 | // the backing data structure used for fast pattern matching 30 | this.index = new SearchIndex({ 31 | identify: this.identify, 32 | datumTokenizer: o.datumTokenizer, 33 | queryTokenizer: o.queryTokenizer 34 | }); 35 | 36 | // hold off on initialization if the initialize option was explicitly false 37 | o.initialize !== false && this.initialize(); 38 | } 39 | 40 | // static methods 41 | // -------------- 42 | 43 | Bloodhound.noConflict = function noConflict() { 44 | window && (window.Bloodhound = old); 45 | return Bloodhound; 46 | }; 47 | 48 | Bloodhound.tokenizers = tokenizers; 49 | 50 | // instance methods 51 | // ---------------- 52 | 53 | _.mixin(Bloodhound.prototype, { 54 | 55 | // ### super secret stuff used for integration with jquery plugin 56 | 57 | __ttAdapter: function ttAdapter() { 58 | var that = this; 59 | 60 | return this.remote ? withAsync : withoutAsync; 61 | 62 | function withAsync(query, sync, async) { 63 | return that.search(query, sync, async); 64 | } 65 | 66 | function withoutAsync(query, sync) { 67 | return that.search(query, sync); 68 | } 69 | }, 70 | 71 | // ### private 72 | 73 | _loadPrefetch: function loadPrefetch() { 74 | var that = this, deferred, serialized; 75 | 76 | deferred = $.Deferred(); 77 | 78 | if (!this.prefetch) { 79 | deferred.resolve(); 80 | } 81 | 82 | else if (serialized = this.prefetch.fromCache()) { 83 | this.index.bootstrap(serialized); 84 | deferred.resolve(); 85 | } 86 | 87 | else { 88 | this.prefetch.fromNetwork(done); 89 | } 90 | 91 | return deferred.promise(); 92 | 93 | function done(err, data) { 94 | if (err) { return deferred.reject(); } 95 | 96 | that.add(data); 97 | that.prefetch.store(that.index.serialize()); 98 | deferred.resolve(); 99 | } 100 | }, 101 | 102 | _initialize: function initialize() { 103 | var that = this, deferred; 104 | 105 | // in case this is a reinitialization, clear previous data 106 | this.clear(); 107 | 108 | (this.initPromise = this._loadPrefetch()) 109 | .done(addLocalToIndex); // local must be added to index after prefetch 110 | 111 | return this.initPromise; 112 | 113 | function addLocalToIndex() { that.add(that.local); } 114 | }, 115 | 116 | // ### public 117 | 118 | initialize: function initialize(force) { 119 | return !this.initPromise || force ? this._initialize() : this.initPromise; 120 | }, 121 | 122 | // TODO: before initialize what happens? 123 | add: function add(data) { 124 | this.index.add(data); 125 | return this; 126 | }, 127 | 128 | get: function get(ids) { 129 | ids = _.isArray(ids) ? ids : [].slice.call(arguments); 130 | return this.index.get(ids); 131 | }, 132 | 133 | search: function search(query, sync, async) { 134 | var that = this, local; 135 | 136 | sync = sync || _.noop; 137 | async = async || _.noop; 138 | 139 | local = this.sorter(this.index.search(query)); 140 | 141 | // return a copy to guarantee no changes within this scope 142 | // as this array will get used when processing the remote results 143 | sync(this.remote ? local.slice() : local); 144 | 145 | if (this.remote && local.length < this.sufficient) { 146 | this.remote.get(query, processRemote); 147 | } 148 | 149 | else if (this.remote) { 150 | // #149: prevents outdated rate-limited requests from being sent 151 | this.remote.cancelLastRequest(); 152 | } 153 | 154 | return this; 155 | 156 | function processRemote(remote) { 157 | var nonDuplicates = []; 158 | 159 | // exclude duplicates 160 | _.each(remote, function(r) { 161 | !_.some(local, function(l) { 162 | return that.identify(r) === that.identify(l); 163 | }) && nonDuplicates.push(r); 164 | }); 165 | 166 | // #1148: Should Bloodhound index remote datums? 167 | that.indexRemote && that.add(nonDuplicates); 168 | 169 | async(nonDuplicates); 170 | } 171 | }, 172 | 173 | all: function all() { 174 | return this.index.all(); 175 | }, 176 | 177 | clear: function clear() { 178 | this.index.reset(); 179 | return this; 180 | }, 181 | 182 | clearPrefetchCache: function clearPrefetchCache() { 183 | this.prefetch && this.prefetch.clear(); 184 | return this; 185 | }, 186 | 187 | clearRemoteCache: function clearRemoteCache() { 188 | Transport.resetCache(); 189 | return this; 190 | }, 191 | 192 | // DEPRECATED: will be removed in v1 193 | ttAdapter: function ttAdapter() { 194 | return this.__ttAdapter(); 195 | } 196 | }); 197 | 198 | return Bloodhound; 199 | })(); 200 | -------------------------------------------------------------------------------- /test/bloodhound/prefetch_spec.js: -------------------------------------------------------------------------------- 1 | describe('Prefetch', function() { 2 | 3 | function build(o) { 4 | return new Prefetch(_.mixin({ 5 | url: '/prefetch', 6 | ttl: 3600, 7 | cache: true, 8 | thumbprint: '', 9 | cacheKey: 'cachekey', 10 | prepare: function(x) { return x; }, 11 | transform: function(x) { return x; }, 12 | transport: $.ajax 13 | }, o || {})); 14 | } 15 | 16 | beforeEach(function() { 17 | jasmine.PersistentStorage.useMock(); 18 | 19 | this.prefetch = build(); 20 | this.storage = this.prefetch.storage; 21 | this.thumbprint = this.prefetch.thumbprint; 22 | }); 23 | 24 | describe('#clear', function() { 25 | it('should clear cache storage', function() { 26 | this.prefetch.clear(); 27 | expect(this.storage.clear).toHaveBeenCalled(); 28 | }); 29 | }); 30 | 31 | describe('#store', function() { 32 | it('should store data in the storage cache', function() { 33 | this.prefetch.store({ foo: 'bar' }); 34 | 35 | expect(this.storage.set) 36 | .toHaveBeenCalledWith('data', { foo: 'bar' }, 3600); 37 | }); 38 | 39 | it('should store thumbprint in the storage cache', function() { 40 | this.prefetch.store({ foo: 'bar' }); 41 | 42 | expect(this.storage.set) 43 | .toHaveBeenCalledWith('thumbprint', jasmine.any(String), 3600); 44 | }); 45 | 46 | it('should store protocol in the storage cache', function() { 47 | this.prefetch.store({ foo: 'bar' }); 48 | 49 | expect(this.storage.set) 50 | .toHaveBeenCalledWith('protocol', location.protocol, 3600); 51 | }); 52 | 53 | it('should be noop if cache option is false', function() { 54 | this.prefetch = build({ cache: false }); 55 | 56 | this.prefetch.store({ foo: 'bar' }); 57 | 58 | expect(this.storage.set).not.toHaveBeenCalled(); 59 | }); 60 | }); 61 | 62 | describe('#fromCache', function() { 63 | it('should return data if available', function() { 64 | this.storage.get 65 | .andCallFake(fakeStorageGet({ foo: 'bar' }, this.thumbprint)); 66 | 67 | expect(this.prefetch.fromCache()).toEqual({ foo: 'bar' }); 68 | }); 69 | 70 | it('should return null if data is expired', function() { 71 | this.storage.get 72 | .andCallFake(fakeStorageGet({ foo: 'bar' }, 'foo')); 73 | 74 | expect(this.prefetch.fromCache()).toBeNull(); 75 | }); 76 | 77 | it('should return null if data does not exist', function() { 78 | this.storage.get 79 | .andCallFake(fakeStorageGet(null, this.thumbprint)); 80 | 81 | expect(this.prefetch.fromCache()).toBeNull(); 82 | }); 83 | 84 | it('should return null if cache option is false', function() { 85 | this.prefetch = build({ cache: false }); 86 | 87 | this.storage.get 88 | .andCallFake(fakeStorageGet({ foo: 'bar' }, this.thumbprint)); 89 | 90 | expect(this.prefetch.fromCache()).toBeNull(); 91 | expect(this.storage.get).not.toHaveBeenCalled(); 92 | }); 93 | }); 94 | 95 | describe('#fromNetwork', function() { 96 | it('should have sensible default request settings', function() { 97 | var spy; 98 | 99 | spy = jasmine.createSpy(); 100 | spyOn(this.prefetch, 'transport').andReturn($.Deferred()); 101 | 102 | this.prefetch.fromNetwork(spy); 103 | 104 | expect(this.prefetch.transport).toHaveBeenCalledWith({ 105 | url: '/prefetch', 106 | type: 'GET', 107 | dataType: 'json' 108 | }); 109 | }); 110 | 111 | it('should transform request settings with prepare', function() { 112 | var spy; 113 | 114 | spy = jasmine.createSpy(); 115 | spyOn(this.prefetch, 'prepare').andReturn({ foo: 'bar' }); 116 | spyOn(this.prefetch, 'transport').andReturn($.Deferred()); 117 | 118 | this.prefetch.fromNetwork(spy); 119 | 120 | expect(this.prefetch.transport).toHaveBeenCalledWith({ foo: 'bar' }); 121 | }); 122 | 123 | it('should transform the response using transform', function() { 124 | var spy; 125 | 126 | this.prefetch = build({ 127 | transform: function() { return { bar: 'foo' }; } 128 | }); 129 | 130 | spy = jasmine.createSpy(); 131 | spyOn(this.prefetch, 'transport') 132 | .andReturn($.Deferred().resolve({ foo: 'bar' })); 133 | 134 | this.prefetch.fromNetwork(spy); 135 | 136 | expect(spy).toHaveBeenCalledWith(null, { bar: 'foo' }); 137 | }); 138 | 139 | it('should invoke callback with data if success', function() { 140 | var spy; 141 | 142 | spy = jasmine.createSpy(); 143 | spyOn(this.prefetch, 'transport') 144 | .andReturn($.Deferred().resolve({ foo: 'bar' })); 145 | 146 | this.prefetch.fromNetwork(spy); 147 | 148 | expect(spy).toHaveBeenCalledWith(null, { foo: 'bar' }); 149 | }); 150 | 151 | it('should invoke callback with err argument true if failure', function() { 152 | var spy; 153 | 154 | spy = jasmine.createSpy(); 155 | spyOn(this.prefetch, 'transport').andReturn($.Deferred().reject()); 156 | 157 | this.prefetch.fromNetwork(spy); 158 | 159 | expect(spy).toHaveBeenCalledWith(true); 160 | }); 161 | }); 162 | 163 | function fakeStorageGet(data, thumbprint, protocol) { 164 | return function(key) { 165 | var val; 166 | 167 | switch (key) { 168 | case 'data': 169 | val = data; 170 | break; 171 | case 'protocol': 172 | val = protocol || location.protocol; 173 | break; 174 | case 'thumbprint': 175 | val = thumbprint; 176 | break; 177 | } 178 | 179 | return val; 180 | }; 181 | } 182 | }); 183 | -------------------------------------------------------------------------------- /test/fixtures/data.js: -------------------------------------------------------------------------------- 1 | var fixtures = fixtures || {}; 2 | 3 | fixtures.data = { 4 | simple: [ 5 | { value: 'big' }, 6 | { value: 'bigger' }, 7 | { value: 'biggest' }, 8 | { value: 'small' }, 9 | { value: 'smaller' }, 10 | { value: 'smallest' } 11 | ], 12 | animals: [ 13 | { value: 'dog' }, 14 | { value: 'cat' }, 15 | { value: 'moose' } 16 | ] 17 | }; 18 | 19 | fixtures.serialized = { 20 | simple: { 21 | "datums": { 22 | "{\"value\":\"big\"}": { 23 | "value": "big" 24 | }, 25 | "{\"value\":\"bigger\"}": { 26 | "value": "bigger" 27 | }, 28 | "{\"value\":\"biggest\"}": { 29 | "value": "biggest" 30 | }, 31 | "{\"value\":\"small\"}": { 32 | "value": "small" 33 | }, 34 | "{\"value\":\"smaller\"}": { 35 | "value": "smaller" 36 | }, 37 | "{\"value\":\"smallest\"}": { 38 | "value": "smallest" 39 | } 40 | }, 41 | "trie": { 42 | "i": [], 43 | "c": { 44 | "b": { 45 | "i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"], 46 | "c": { 47 | "i": { 48 | "i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"], 49 | "c": { 50 | "g": { 51 | "i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"], 52 | "c": { 53 | "g": { 54 | "i": ["{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"], 55 | "c": { 56 | "e": { 57 | "i": ["{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"], 58 | "c": { 59 | "r": { 60 | "i": ["{\"value\":\"bigger\"}"], 61 | "c": {} 62 | }, 63 | "s": { 64 | "i": ["{\"value\":\"biggest\"}"], 65 | "c": { 66 | "t": { 67 | "i": ["{\"value\":\"biggest\"}"], 68 | "c": {} 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | }, 82 | "s": { 83 | "i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"], 84 | "c": { 85 | "m": { 86 | "i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"], 87 | "c": { 88 | "a": { 89 | "i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"], 90 | "c": { 91 | "l": { 92 | "i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"], 93 | "c": { 94 | "l": { 95 | "i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"], 96 | "c": { 97 | "e": { 98 | "i": ["{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"], 99 | "c": { 100 | "r": { 101 | "i": ["{\"value\":\"smaller\"}"], 102 | "c": {} 103 | }, 104 | "s": { 105 | "i": ["{\"value\":\"smallest\"}"], 106 | "c": { 107 | "t": { 108 | "i": ["{\"value\":\"smallest\"}"], 109 | "c": {} 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/corejavascript/typeahead.js.svg?branch=master)](https://travis-ci.org/corejavascript/typeahead.js) 2 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/corejavascript/typeahead.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/iron/iron/master/LICENSE) 4 | 5 | # [corejs-typeahead](https://typeahead.js.org/) 6 | 7 | This is a maintained fork of [twitter.com](https://twitter.com)'s autocomplete search library, [typeahead.js](https://github.com/twitter/typeahead.js). 8 | 9 | The typeahead.js library consists of 2 components: the suggestion engine, 10 | [Bloodhound](https://github.com/corejavascript/typeahead.js/blob/master/doc/bloodhound.md), and the UI view, [Typeahead](https://github.com/corejavascript/typeahead.js/blob/master/doc/jquery_typeahead.md). 11 | The suggestion engine is responsible for computing suggestions for a given 12 | query. The UI view is responsible for rendering suggestions and handling DOM 13 | interactions. Both components can be used separately, but when used together, 14 | they can provide a rich typeahead experience. 15 | 16 | ## Getting Started 17 | 18 | How you acquire typeahead.js is up to you: 19 | 20 | * Install with [Bower](https://bower.io/): `$ bower install corejs-typeahead` 21 | 22 | * Install with [npm](https://www.npmjs.com): `$ npm install corejs-typeahead` 23 | 24 | * Install with [composer](https://getcomposer.org/): `$ composer require corejavascript/typeahead.js` 25 | 26 | * [Download zipball of latest release](https://github.com/corejavascript/typeahead.js/archive/master.zip) 27 | 28 | * Download the latest dist files individually: 29 | * [bloodhound.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/bloodhound.js) (standalone suggestion engine) 30 | * [typeahead.jquery.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/typeahead.jquery.js) (standalone UI view) 31 | * [typeahead.bundle.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/typeahead.bundle.js) (*bloodhound.js* + *typeahead.jquery.js*) 32 | * [typeahead.bundle.min.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/typeahead.bundle.min.js) 33 | 34 | **Note:** both *bloodhound.js* and *typeahead.jquery.js* have a dependency on 35 | [jQuery](http://jquery.com/) 1.9+. 36 | 37 | ## Documentation 38 | 39 | * [Typeahead Docs](https://github.com/corejavascript/typeahead.js/blob/master/doc/jquery_typeahead.md) 40 | * [Bloodhound Docs](https://github.com/corejavascript/typeahead.js/blob/master/doc/bloodhound.md) 41 | 42 | ## Examples 43 | 44 | For some working examples of typeahead.js, visit the [examples page](https://typeahead.js.org/examples). 45 | 46 | ## Browser Support 47 | 48 | * Chrome 49 | * Firefox 3.5+ 50 | * Safari 4+ 51 | * Internet Explorer 8+ 52 | * Opera 11+ 53 | 54 | **NOTE:** typeahead.js is not tested on mobile browsers. 55 | 56 | ## Customer Support 57 | 58 | For general questions about typeahead.js, tweet at [@typeahead](https://twitter.com/typeahead). 59 | 60 | For technical questions, you should post a question on [Stack Overflow](http://stackoverflow.com/) and tag 61 | it with [typeahead.js](http://stackoverflow.com/questions/tagged/typeahead.js). 62 | 63 | ## Issues 64 | 65 | Discovered a bug? Please create an issue here on GitHub! 66 | 67 | [github.com/corejavascript/typeahead.js/issues](https://github.com/corejavascript/typeahead.js/issues) 68 | 69 | ## Versioning 70 | 71 | For transparency and insight into our release cycle, releases will be numbered 72 | with the following format: 73 | 74 | `..` 75 | 76 | And constructed with the following guidelines: 77 | 78 | * Breaking backwards compatibility bumps the major 79 | * New additions without breaking backwards compatibility bumps the minor 80 | * Bug fixes and misc changes bump the patch 81 | 82 | For more information on semantic versioning, please visit [semver.org](http://semver.org/). 83 | 84 | ## Testing 85 | 86 | Tests are written using [Jasmine](http://jasmine.github.io/) and ran with [Karma](http://karma-runner.github.io/). To run 87 | the test suite with PhantomJS, run `$ npm test`. 88 | 89 | ## Developers 90 | 91 | If you plan on contributing to typeahead.js, be sure to read the 92 | [contributing guidelines](https://github.com/corejavascript/typeahead.js/blob/master/contributing.md). A good starting place for new contributors are issues 93 | labeled with [entry-level](https://github.com/corejavascript/typeahead.js/issues?&labels=entry-level&state=open). Entry-level issues tend to require minor changes 94 | and provide developers a chance to get more familiar with typeahead.js before 95 | taking on more challenging work. 96 | 97 | In order to build and test typeahead.js, you'll need to install its dev 98 | dependencies (`$ npm install`) and have [grunt-cli](https://github.com/gruntjs/grunt-cli) 99 | installed (`$ npm install -g grunt-cli`). Below is an overview of the available 100 | Grunt tasks that'll be useful in development. 101 | 102 | * `grunt build` – Builds *typeahead.js* from source. 103 | * `grunt lint` – Runs source and test files through JSHint. 104 | * `grunt watch` – Rebuilds *typeahead.js* whenever a source file is modified. 105 | * `grunt server` – Serves files from the root of typeahead.js on localhost:8888. 106 | Useful for using *test/playground.html* for debugging/testing. 107 | * `grunt dev` – Runs `grunt watch` and `grunt server` in parallel. 108 | 109 | ## Maintainers 110 | 111 | * [CoreJS Collaborators](https://github.com/orgs/corejavascript/teams/collaborators) 112 | 113 | * **You?** 114 | 115 | ## Authors 116 | 117 | * **Jake Harding** 118 | * [@JakeHarding](https://twitter.com/JakeHarding) 119 | * [GitHub](https://github.com/jharding) 120 | 121 | * **Veljko Skarich** 122 | * [@vskarich](https://twitter.com/vskarich) 123 | * [GitHub](https://github.com/vskarich) 124 | 125 | * **Tim Trueman** 126 | * [@timtrueman](https://twitter.com/timtrueman) 127 | * [GitHub](https://github.com/timtrueman) 128 | 129 | ## License 130 | 131 | Copyright 2013 Twitter, Inc. 132 | 133 | Licensed under the MIT License 134 | -------------------------------------------------------------------------------- /test/bloodhound/transport_spec.js: -------------------------------------------------------------------------------- 1 | describe('Transport', function() { 2 | 3 | beforeEach(function() { 4 | jasmine.Ajax.useMock(); 5 | jasmine.Clock.useMock(); 6 | 7 | this.transport = new Transport({ transport: $.ajax }); 8 | }); 9 | 10 | afterEach(function() { 11 | // run twice to flush out on-deck requests 12 | $.each(ajaxRequests, drop); 13 | $.each(ajaxRequests, drop); 14 | 15 | clearAjaxRequests(); 16 | Transport.resetCache(); 17 | 18 | function drop(i, req) { 19 | req.readyState !== 4 && req.response(fixtures.ajaxResps.ok); 20 | } 21 | }); 22 | 23 | it('should use jQuery.ajax as the default transport mechanism', function() { 24 | var req, resp = fixtures.ajaxResps.ok, spy = jasmine.createSpy(); 25 | 26 | this.transport.get('/test', spy); 27 | 28 | req = mostRecentAjaxRequest(); 29 | req.response(resp); 30 | 31 | expect(req.url).toBe('/test'); 32 | expect(spy).toHaveBeenCalledWith(null, resp.parsed); 33 | }); 34 | 35 | it('should use maxPendingRequests configuration option', function() { 36 | this.transport = new Transport({ transport: $.ajax, maxPendingRequests: 2 }); 37 | 38 | for (var i = 0; i < 5; i++) { 39 | this.transport.get('/test' + i, $.noop); 40 | } 41 | 42 | expect(ajaxRequests.length).toBe(2); 43 | }); 44 | 45 | it('should open up to 6 maxPendingRequests by default', function() { 46 | for (var i = 0; i < 10; i++) { 47 | this.transport.get('/test' + i, $.noop); 48 | } 49 | 50 | expect(ajaxRequests.length).toBe(6); 51 | }); 52 | 53 | it('should support rate limiting', function() { 54 | this.transport = new Transport({ transport: $.ajax, limiter: limiter }); 55 | 56 | for (var i = 0; i < 5; i++) { 57 | this.transport.get('/test' + i, $.noop); 58 | } 59 | 60 | jasmine.Clock.tick(100); 61 | expect(ajaxRequests.length).toBe(1); 62 | 63 | function limiter(fn) { return _.debounce(fn, 20); } 64 | }); 65 | 66 | it('should cache most recent requests', function() { 67 | var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy(); 68 | 69 | this.transport.get('/test1', $.noop); 70 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); 71 | 72 | this.transport.get('/test2', $.noop); 73 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok1); 74 | 75 | expect(ajaxRequests.length).toBe(2); 76 | 77 | this.transport.get('/test1', spy1); 78 | this.transport.get('/test2', spy2); 79 | 80 | jasmine.Clock.tick(0); 81 | 82 | // no ajax requests were made on subsequent requests 83 | expect(ajaxRequests.length).toBe(2); 84 | 85 | expect(spy1).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed); 86 | expect(spy2).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok1.parsed); 87 | }); 88 | 89 | it('should not cache requests if cache option is false', function() { 90 | this.transport = new Transport({ transport: $.ajax, cache: false }); 91 | 92 | this.transport.get('/test1', $.noop); 93 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); 94 | this.transport.get('/test1', $.noop); 95 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); 96 | 97 | expect(ajaxRequests.length).toBe(2); 98 | }); 99 | 100 | it('should prevent dog pile', function() { 101 | var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy(); 102 | 103 | this.transport.get('/test1', spy1); 104 | this.transport.get('/test1', spy2); 105 | 106 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); 107 | 108 | expect(ajaxRequests.length).toBe(1); 109 | 110 | waitsFor(function() { return spy1.callCount && spy2.callCount; }); 111 | 112 | runs(function() { 113 | expect(spy1).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed); 114 | expect(spy2).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed); 115 | }); 116 | }); 117 | 118 | it('should always make a request for the last call to #get', function() { 119 | var spy = jasmine.createSpy(); 120 | 121 | for (var i = 0; i < 6; i++) { 122 | this.transport.get('/test' + i, $.noop); 123 | } 124 | 125 | this.transport.get('/test' + i, spy); 126 | expect(ajaxRequests.length).toBe(6); 127 | 128 | _.each(ajaxRequests, function(req) { 129 | req.response(fixtures.ajaxResps.ok); 130 | }); 131 | 132 | expect(ajaxRequests.length).toBe(7); 133 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); 134 | 135 | expect(spy).toHaveBeenCalled(); 136 | }); 137 | 138 | it('should invoke the callback with err set to true on failure', function() { 139 | var req, resp = fixtures.ajaxResps.err, spy = jasmine.createSpy(); 140 | 141 | this.transport.get('/test', spy); 142 | 143 | req = mostRecentAjaxRequest(); 144 | req.response(resp); 145 | 146 | expect(req.url).toBe('/test'); 147 | expect(spy).toHaveBeenCalledWith(true); 148 | }); 149 | 150 | it('should not send cancelled requests', function() { 151 | this.transport = new Transport({ transport: $.ajax, limiter: limiter }); 152 | 153 | this.transport.get('/test', $.noop); 154 | this.transport.cancel(); 155 | 156 | jasmine.Clock.tick(100); 157 | expect(ajaxRequests.length).toBe(0); 158 | 159 | function limiter(fn) { return _.debounce(fn, 20); } 160 | }); 161 | 162 | it('should not send outdated requests', function() { 163 | this.transport = new Transport({ transport: $.ajax, limiter: limiter }); 164 | 165 | // warm cache 166 | this.transport.get('/test1', $.noop); 167 | jasmine.Clock.tick(100); 168 | mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); 169 | 170 | expect(mostRecentAjaxRequest().url).toBe('/test1'); 171 | expect(ajaxRequests.length).toBe(1); 172 | 173 | // within the same rate-limit cycle, request test2 and test1. test2 becomes 174 | // outdated after test1 is requested and no request is sent for test1 175 | // because it's a cache hit 176 | this.transport.get('/test2', $.noop); 177 | this.transport.get('/test1', $.noop); 178 | 179 | jasmine.Clock.tick(100); 180 | 181 | expect(ajaxRequests.length).toBe(1); 182 | 183 | function limiter(fn) { return _.debounce(fn, 20); } 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /test/bloodhound/persistent_storage_spec.js: -------------------------------------------------------------------------------- 1 | describe('PersistentStorage', function() { 2 | var engine, ls; 3 | 4 | // test suite is dependent on localStorage being available 5 | if (!window.localStorage) { 6 | console.warn('no localStorage support – skipping PersistentStorage suite'); 7 | return; 8 | } 9 | 10 | // for good measure! 11 | localStorage.clear(); 12 | 13 | beforeEach(function() { 14 | ls = { 15 | get length() { return localStorage.length; }, 16 | key: spyThrough('key'), 17 | clear: spyThrough('clear'), 18 | getItem: spyThrough('getItem'), 19 | setItem: spyThrough('setItem'), 20 | removeItem: spyThrough('removeItem') 21 | }; 22 | 23 | engine = new PersistentStorage('ns', ls); 24 | spyOn(Date.prototype, 'getTime').andReturn(0); 25 | }); 26 | 27 | afterEach(function() { 28 | localStorage.clear(); 29 | }); 30 | 31 | // public methods 32 | // -------------- 33 | 34 | describe('#get', function() { 35 | it('should access localStorage with prefixed key', function() { 36 | engine.get('key'); 37 | expect(ls.getItem).toHaveBeenCalledWith('__ns__key'); 38 | }); 39 | 40 | it('should return undefined when key does not exist', function() { 41 | expect(engine.get('does not exist')).toEqual(undefined); 42 | }); 43 | 44 | it('should return value as correct type', function() { 45 | engine.set('string', 'i am a string'); 46 | engine.set('number', 42); 47 | engine.set('boolean', true); 48 | engine.set('null', null); 49 | engine.set('object', { obj: true }); 50 | 51 | expect(engine.get('string')).toEqual('i am a string'); 52 | expect(engine.get('number')).toEqual(42); 53 | expect(engine.get('boolean')).toEqual(true); 54 | expect(engine.get('null')).toBeNull(); 55 | expect(engine.get('object')).toEqual({ obj: true }); 56 | }); 57 | 58 | it('should expire stale keys', function() { 59 | engine.set('key', 'value', -1); 60 | 61 | expect(engine.get('key')).toBeNull(); 62 | expect(ls.getItem('__ns__key__ttl')).toBeNull(); 63 | }); 64 | }); 65 | 66 | describe('#set', function() { 67 | it('should access localStorage with prefixed key', function() { 68 | engine.set('key', 'val'); 69 | expect(ls.setItem.mostRecentCall.args[0]).toEqual('__ns__key'); 70 | }); 71 | 72 | it('should JSON.stringify value before storing', function() { 73 | engine.set('key', 'val'); 74 | expect(ls.setItem.mostRecentCall.args[1]).toEqual(JSON.stringify('val')); 75 | }); 76 | 77 | it('should store ttl if provided', function() { 78 | var ttl = 1; 79 | engine.set('key', 'value', ttl); 80 | 81 | expect(ls.setItem.argsForCall[0]) 82 | .toEqual(['__ns__key__ttl__', ttl.toString()]); 83 | }); 84 | 85 | it('should call clear if the localStorage limit has been reached', function() { 86 | var spy; 87 | 88 | ls.setItem.andCallFake(function() { 89 | var err = new Error(); 90 | err.name = 'QuotaExceededError'; 91 | 92 | throw err; 93 | }); 94 | 95 | engine.clear = spy = jasmine.createSpy(); 96 | engine.set('key', 'value', 1); 97 | 98 | expect(spy).toHaveBeenCalled(); 99 | }); 100 | 101 | it('should noop if the localStorage limit has been reached', function() { 102 | var get, set, remove, clear, isExpired; 103 | 104 | ls.setItem.andCallFake(function() { 105 | var err = new Error(); 106 | err.name = 'QuotaExceededError'; 107 | 108 | throw err; 109 | }); 110 | 111 | get = engine.get; 112 | set = engine.set; 113 | remove = engine.remove; 114 | clear = engine.clear; 115 | isExpired = engine.isExpired; 116 | 117 | engine.set('key', 'value', 1); 118 | 119 | expect(engine.get).not.toBe(get); 120 | expect(engine.set).not.toBe(set); 121 | expect(engine.remove).not.toBe(remove); 122 | expect(engine.clear).not.toBe(clear); 123 | expect(engine.isExpired).not.toBe(isExpired); 124 | }); 125 | }); 126 | 127 | describe('#remove', function() { 128 | 129 | it('should remove key from storage', function() { 130 | engine.set('key', 'val'); 131 | engine.remove('key'); 132 | 133 | expect(engine.get('key')).toBeNull(); 134 | }); 135 | }); 136 | 137 | describe('#clear', function() { 138 | it('should work with namespaces that contain regex characters', function() { 139 | engine = new PersistentStorage('ns?()'); 140 | engine.set('key1', 'val1'); 141 | engine.set('key2', 'val2'); 142 | engine.clear(); 143 | 144 | expect(engine.get('key1')).toEqual(undefined); 145 | expect(engine.get('key2')).toEqual(undefined); 146 | }); 147 | 148 | it('should remove all keys that exist in namespace of engine', function() { 149 | engine.set('key1', 'val1'); 150 | engine.set('key2', 'val2'); 151 | engine.set('key3', 'val3'); 152 | engine.set('key4', 'val4', 0); 153 | engine.clear(); 154 | 155 | expect(engine.get('key1')).toEqual(undefined); 156 | expect(engine.get('key2')).toEqual(undefined); 157 | expect(engine.get('key3')).toEqual(undefined); 158 | expect(engine.get('key4')).toEqual(undefined); 159 | }); 160 | 161 | it('should not affect keys with different namespace', function() { 162 | ls.setItem('diff_namespace', 'val'); 163 | engine.clear(); 164 | 165 | expect(ls.getItem('diff_namespace')).toEqual('val'); 166 | }); 167 | }); 168 | 169 | describe('#isExpired', function() { 170 | it('should be false for keys without ttl', function() { 171 | engine.set('key', 'value'); 172 | expect(engine.isExpired('key')).toBe(false); 173 | }); 174 | 175 | it('should be false for fresh keys', function() { 176 | engine.set('key', 'value', 1); 177 | expect(engine.isExpired('key')).toBe(false); 178 | }); 179 | 180 | it('should be true for stale keys', function() { 181 | engine.set('key', 'value', -1); 182 | expect(engine.isExpired('key')).toBe(true); 183 | }); 184 | }); 185 | 186 | // compatible across browsers 187 | function spyThrough(method) { 188 | return jasmine.createSpy().andCallFake(fake); 189 | 190 | function fake() { 191 | return localStorage[method].apply(localStorage, arguments); 192 | } 193 | } 194 | }); 195 | -------------------------------------------------------------------------------- /test/bloodhound/options_parser_spec.js: -------------------------------------------------------------------------------- 1 | describe('options parser', function() { 2 | 3 | function build(o) { 4 | return oParser(_.mixin({ 5 | datumTokenizer: $.noop, 6 | queryTokenizer: $.noop 7 | }, o || {})); 8 | } 9 | 10 | function prefetch(o) { 11 | return oParser({ 12 | datumTokenizer: $.noop, 13 | queryTokenizer: $.noop, 14 | prefetch: _.mixin({ 15 | url: '/example' 16 | }, o || {}) 17 | }); 18 | } 19 | 20 | function remote(o) { 21 | return oParser({ 22 | datumTokenizer: $.noop, 23 | queryTokenizer: $.noop, 24 | remote: _.mixin({ 25 | url: '/example' 26 | }, o || {}) 27 | }); 28 | } 29 | 30 | it('should throw exception if datumTokenizer is not set', function() { 31 | expect(parse).toThrow(); 32 | function parse() { build({ datumTokenizer: null }); } 33 | }); 34 | 35 | it('should throw exception if queryTokenizer is not set', function() { 36 | expect(parse).toThrow(); 37 | function parse() { build({ queryTokenizer: null }); } 38 | }); 39 | 40 | it('should wrap sorter', function() { 41 | var o = build({ sorter: function(a, b) { return a -b; } }); 42 | expect(o.sorter([2, 1, 3])).toEqual([1, 2, 3]); 43 | }); 44 | 45 | it('should default sorter to identity function', function() { 46 | var o = build(); 47 | expect(o.sorter([2, 1, 3])).toEqual([2, 1, 3]); 48 | }); 49 | 50 | describe('local', function() { 51 | it('should default to empty array', function() { 52 | var o = build(); 53 | expect(o.local).toEqual([]); 54 | }); 55 | 56 | it('should support function', function() { 57 | var o = build({ local: function() { return [1]; } }); 58 | expect(o.local).toEqual([1]); 59 | }); 60 | 61 | it('should support arrays', function() { 62 | var o = build({ local: [1] }); 63 | expect(o.local).toEqual([1]); 64 | }); 65 | }); 66 | 67 | describe('prefetch', function() { 68 | it('should throw exception if url is not set', function() { 69 | expect(parse).toThrow(); 70 | function parse() { prefetch({ url: null }); } 71 | }); 72 | 73 | it('should support simple string format', function() { 74 | expect(build({ prefetch: '/prefetch' }).prefetch).toBeDefined(); 75 | }); 76 | 77 | it('should default ttl to 1 day', function() { 78 | var o = prefetch(); 79 | expect(o.prefetch.ttl).toBe(86400000); 80 | }); 81 | 82 | it('should default cache to true', function() { 83 | var o = prefetch(); 84 | expect(o.prefetch.cache).toBe(true); 85 | }); 86 | 87 | it('should default transform to identity function', function() { 88 | var o = prefetch(); 89 | expect(o.prefetch.transform('foo')).toBe('foo'); 90 | }); 91 | 92 | it('should default cacheKey to url', function() { 93 | var o = prefetch(); 94 | expect(o.prefetch.cacheKey).toBe(o.prefetch.url); 95 | }); 96 | 97 | it('should default transport to jQuery.ajax', function() { 98 | var o = prefetch(); 99 | expect(o.prefetch.transport).toBe($.ajax); 100 | }); 101 | 102 | it('should prepend version to thumbprint', function() { 103 | var o = prefetch(); 104 | expect(o.prefetch.thumbprint).toBe('%VERSION%'); 105 | 106 | o = prefetch({ thumbprint: 'foo' }); 107 | expect(o.prefetch.thumbprint).toBe('%VERSION%foo'); 108 | }); 109 | 110 | it('should wrap custom transport to be deferred compatible', function() { 111 | var o, errDeferred, successDeferred; 112 | 113 | o = prefetch({ transport: errTransport }); 114 | errDeferred = o.prefetch.transport('q'); 115 | 116 | o = prefetch({ transport: successTransport }); 117 | successDeferred = o.prefetch.transport('q'); 118 | 119 | waits(0); 120 | runs(function() { 121 | expect(errDeferred.state()).toBe('rejected'); 122 | expect(successDeferred.state()).toBe('resolved'); 123 | }); 124 | 125 | function errTransport(q, success, error) { error(); } 126 | function successTransport(q, success, error) { success(); } 127 | }); 128 | }); 129 | 130 | describe('remote', function() { 131 | it('should throw exception if url is not set', function() { 132 | expect(parse).toThrow(); 133 | function parse() { remote({ url: null }); } 134 | }); 135 | 136 | it('should support simple string format', function() { 137 | expect(build({ remote: '/remote' }).remote).toBeDefined(); 138 | }); 139 | 140 | it('should default transform to identity function', function() { 141 | var o = remote(); 142 | expect(o.remote.transform('foo')).toBe('foo'); 143 | }); 144 | 145 | it('should default transport to jQuery.ajax', function() { 146 | var o = remote(); 147 | expect(o.remote.transport).toBe($.ajax); 148 | }); 149 | 150 | it('should default limiter to debounce', function() { 151 | var o = remote(); 152 | expect(o.remote.limiter.name).toBe('debounce'); 153 | }); 154 | 155 | it('should default prepare to identity function', function() { 156 | var o = remote(); 157 | expect(o.remote.prepare('q', { url: '/foo' })).toEqual({ url: '/foo' }); 158 | }); 159 | 160 | it('should support wildcard for prepare', function() { 161 | var o = remote({ wildcard: '%FOO' }); 162 | expect(o.remote.prepare('=', { url: '/%FOO' })).toEqual({ url: '/%3D' }); 163 | }); 164 | 165 | it('should support replace for prepare', function() { 166 | var o = remote({ replace: function() { return '/bar'; } }); 167 | expect(o.remote.prepare('q', { url: '/foo' })).toEqual({ url: '/bar' }); 168 | }); 169 | 170 | it('should should rateLimitBy for limiter', function() { 171 | var o = remote({ rateLimitBy: 'throttle' }); 172 | expect(o.remote.limiter.name).toBe('throttle'); 173 | }); 174 | 175 | it('should wrap custom transport to be deferred compatible', function() { 176 | var o, errDeferred, successDeferred; 177 | 178 | o = remote({ transport: errTransport }); 179 | errDeferred = o.remote.transport('q'); 180 | 181 | o = remote({ transport: successTransport }); 182 | successDeferred = o.remote.transport('q'); 183 | 184 | waits(0); 185 | runs(function() { 186 | expect(errDeferred.state()).toBe('rejected'); 187 | expect(successDeferred.state()).toBe('resolved'); 188 | }); 189 | 190 | function errTransport(q, success, error) { error(); } 191 | function successTransport(q, success, error) { success(); } 192 | }); 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/typeahead/plugin_spec.js: -------------------------------------------------------------------------------- 1 | describe('$plugin', function() { 2 | 3 | beforeEach(function() { 4 | var $fixture; 5 | 6 | setFixtures(''); 7 | 8 | $fixture = $('#jasmine-fixtures'); 9 | this.$input = $fixture.find('.test-input'); 10 | 11 | this.$input.typeahead(null, { 12 | displayKey: 'v', 13 | source: function(q, sync) { 14 | sync([{ v: '1' }, { v: '2' }, { v: '3' }]); 15 | } 16 | }); 17 | }); 18 | 19 | it('#enable should enable the typeahead', function() { 20 | this.$input.typeahead('disable'); 21 | expect(this.$input.typeahead('isEnabled')).toBe(false); 22 | 23 | this.$input.typeahead('enable'); 24 | expect(this.$input.typeahead('isEnabled')).toBe(true); 25 | }); 26 | 27 | it('#disable should disable the typeahead', function() { 28 | this.$input.typeahead('enable'); 29 | expect(this.$input.typeahead('isEnabled')).toBe(true); 30 | 31 | this.$input.typeahead('disable'); 32 | expect(this.$input.typeahead('isEnabled')).toBe(false); 33 | }); 34 | 35 | it('#activate should activate the typeahead', function() { 36 | this.$input.typeahead('deactivate'); 37 | expect(this.$input.typeahead('isActive')).toBe(false); 38 | 39 | this.$input.typeahead('activate'); 40 | expect(this.$input.typeahead('isActive')).toBe(true); 41 | }); 42 | 43 | it('#activate should fail to activate the typeahead if disabled', function() { 44 | this.$input.typeahead('deactivate'); 45 | expect(this.$input.typeahead('isActive')).toBe(false); 46 | this.$input.typeahead('disable'); 47 | 48 | this.$input.typeahead('activate'); 49 | expect(this.$input.typeahead('isActive')).toBe(false); 50 | }); 51 | 52 | it('#deactivate should deactivate the typeahead', function() { 53 | this.$input.typeahead('activate'); 54 | expect(this.$input.typeahead('isActive')).toBe(true); 55 | 56 | this.$input.typeahead('deactivate'); 57 | expect(this.$input.typeahead('isActive')).toBe(false); 58 | }); 59 | 60 | it('#open should open the menu', function() { 61 | this.$input.typeahead('close'); 62 | expect(this.$input.typeahead('isOpen')).toBe(false); 63 | 64 | this.$input.typeahead('open'); 65 | expect(this.$input.typeahead('isOpen')).toBe(true); 66 | }); 67 | 68 | it('#close should close the menu', function() { 69 | this.$input.typeahead('open'); 70 | expect(this.$input.typeahead('isOpen')).toBe(true); 71 | 72 | this.$input.typeahead('close'); 73 | expect(this.$input.typeahead('isOpen')).toBe(false); 74 | }); 75 | 76 | it('#select should select selectable', function() { 77 | var $el; 78 | 79 | // activate and set val to render some selectables 80 | this.$input.typeahead('activate'); 81 | this.$input.typeahead('val', 'o'); 82 | $el = $('.tt-selectable').first(); 83 | 84 | expect(this.$input.typeahead('select', $el)).toBe(true); 85 | expect(this.$input.typeahead('val')).toBe('1'); 86 | }); 87 | 88 | it('#select should return false if not valid selectable', function() { 89 | var body; 90 | 91 | // activate and set val to render some selectables 92 | this.$input.typeahead('activate'); 93 | this.$input.typeahead('val', 'o'); 94 | body = document.body; 95 | 96 | expect(this.$input.typeahead('select', body)).toBe(false); 97 | }); 98 | 99 | it('#autocomplete should autocomplete to selectable', function() { 100 | var $el; 101 | 102 | // activate and set val to render some selectables 103 | this.$input.typeahead('activate'); 104 | this.$input.typeahead('val', 'o'); 105 | $el = $('.tt-selectable').first(); 106 | 107 | expect(this.$input.typeahead('autocomplete', $el)).toBe(true); 108 | expect(this.$input.typeahead('val')).toBe('1'); 109 | }); 110 | 111 | it('#autocomplete should return false if not valid selectable', function() { 112 | var body; 113 | 114 | // activate and set val to render some selectables 115 | this.$input.typeahead('activate'); 116 | this.$input.typeahead('val', 'o'); 117 | body = document.body; 118 | 119 | expect(this.$input.typeahead('autocomplete', body)).toBe(false); 120 | }); 121 | 122 | it('#moveCursor should move cursor', function() { 123 | var $el; 124 | 125 | // activate and set val to render some selectables 126 | this.$input.typeahead('activate'); 127 | this.$input.typeahead('val', 'o'); 128 | $el = $('.tt-selectable').first(); 129 | 130 | expect($el).not.toHaveClass('tt-cursor'); 131 | expect(this.$input.typeahead('moveCursor', 1)).toBe(true); 132 | expect($el).toHaveClass('tt-cursor'); 133 | }); 134 | 135 | it('#select should return false if not valid selectable', function() { 136 | var body; 137 | 138 | // activate and set val to render some selectables 139 | this.$input.typeahead('activate'); 140 | this.$input.typeahead('val', 'o'); 141 | body = document.body; 142 | 143 | expect(this.$input.typeahead('select', body)).toBe(false); 144 | }); 145 | 146 | it('#val() should typeahead value of element', function() { 147 | var $els; 148 | 149 | this.$input.typeahead('val', 'foo'); 150 | $els = this.$input.add('
'); 151 | 152 | expect($els.typeahead('val')).toBe('foo'); 153 | }); 154 | 155 | it('#val(q) should set query', function() { 156 | this.$input.typeahead('val', 'foo'); 157 | expect(this.$input.typeahead('val')).toBe('foo'); 158 | }); 159 | 160 | it('#val(q) should coerce null and undefined to empty string', function() { 161 | this.$input.typeahead('val', null); 162 | expect(this.$input.typeahead('val')).toBe(''); 163 | 164 | this.$input.typeahead('val', undefined); 165 | expect(this.$input.typeahead('val')).toBe(''); 166 | }); 167 | 168 | it('#destroy should revert modified attributes', function() { 169 | expect(this.$input).toHaveAttr('dir'); 170 | expect(this.$input).toHaveAttr('spellcheck'); 171 | expect(this.$input).toHaveAttr('style'); 172 | 173 | this.$input.typeahead('destroy'); 174 | 175 | expect(this.$input).not.toHaveAttr('dir'); 176 | expect(this.$input).not.toHaveAttr('spellcheck'); 177 | expect(this.$input).not.toHaveAttr('style'); 178 | }); 179 | 180 | it('#destroy should remove data', function() { 181 | expect(this.$input.data('tt-www')).toBeTruthy(); 182 | expect(this.$input.data('tt-attrs')).toBeTruthy(); 183 | expect(this.$input.data('tt-typeahead')).toBeTruthy(); 184 | 185 | this.$input.typeahead('destroy'); 186 | 187 | expect(this.$input.data('tt-www')).toBeFalsy(); 188 | expect(this.$input.data('tt-attrs')).toBeFalsy(); 189 | expect(this.$input.data('tt-typeahead')).toBeFalsy(); 190 | }); 191 | 192 | it('#destroy should remove add classes', function() { 193 | expect(this.$input).toHaveClass('tt-input'); 194 | this.$input.typeahead('destroy'); 195 | expect(this.$input).not.toHaveClass('tt-input'); 196 | }); 197 | 198 | it('#destroy should revert DOM changes', function() { 199 | expect($('.twitter-typeahead')).toExist(); 200 | this.$input.typeahead('destroy'); 201 | expect($('.twitter-typeahead')).not.toExist(); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /src/typeahead/menu.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 Menu = (function() { 8 | 'use strict'; 9 | 10 | // constructor 11 | // ----------- 12 | 13 | function Menu(o, www) { 14 | var that = this; 15 | 16 | o = o || {}; 17 | 18 | if (!o.node) { 19 | $.error('node is required'); 20 | } 21 | 22 | www.mixin(this); 23 | 24 | this.$node = $(o.node); 25 | 26 | // the latest query #update was called with 27 | this.query = null; 28 | this.datasets = _.map(o.datasets, initializeDataset); 29 | 30 | function initializeDataset(oDataset) { 31 | var node = that.$node.find(oDataset.node).first(); 32 | oDataset.node = node.length ? node : $('
').appendTo(that.$node); 33 | 34 | return new Dataset(oDataset, www); 35 | } 36 | } 37 | 38 | // instance methods 39 | // ---------------- 40 | 41 | _.mixin(Menu.prototype, EventEmitter, { 42 | 43 | // ### event handlers 44 | 45 | _onSelectableClick: function onSelectableClick($e) { 46 | this.trigger('selectableClicked', $($e.currentTarget)); 47 | }, 48 | 49 | _onRendered: function onRendered(type, dataset, suggestions, async) { 50 | this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); 51 | this.trigger('datasetRendered', dataset, suggestions, async); 52 | }, 53 | 54 | _onCleared: function onCleared() { 55 | this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); 56 | this.trigger('datasetCleared'); 57 | }, 58 | 59 | _propagate: function propagate() { 60 | this.trigger.apply(this, arguments); 61 | }, 62 | 63 | // ### private 64 | 65 | _allDatasetsEmpty: function allDatasetsEmpty() { 66 | return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) { 67 | var isEmpty = dataset.isEmpty(); 68 | this.$node.attr('aria-expanded', !isEmpty); 69 | return isEmpty; 70 | }, this)); 71 | }, 72 | 73 | _getSelectables: function getSelectables() { 74 | return this.$node.find(this.selectors.selectable); 75 | }, 76 | 77 | _removeCursor: function _removeCursor() { 78 | var $selectable = this.getActiveSelectable(); 79 | $selectable && $selectable.removeClass(this.classes.cursor); 80 | }, 81 | 82 | _ensureVisible: function ensureVisible($el) { 83 | var elTop, elBottom, nodeScrollTop, nodeHeight; 84 | 85 | elTop = $el.position().top; 86 | elBottom = elTop + $el.outerHeight(true); 87 | nodeScrollTop = this.$node.scrollTop(); 88 | nodeHeight = this.$node.height() + 89 | parseInt(this.$node.css('paddingTop'), 10) + 90 | parseInt(this.$node.css('paddingBottom'), 10); 91 | 92 | if (elTop < 0) { 93 | this.$node.scrollTop(nodeScrollTop + elTop); 94 | } 95 | 96 | else if (nodeHeight < elBottom) { 97 | this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); 98 | } 99 | }, 100 | 101 | // ### public 102 | 103 | bind: function() { 104 | var that = this, onSelectableClick; 105 | 106 | onSelectableClick = _.bind(this._onSelectableClick, this); 107 | this.$node.on('click.tt', this.selectors.selectable, onSelectableClick); 108 | this.$node.on('mouseover', this.selectors.selectable, function(){ that.setCursor($(this)); }); 109 | this.$node.on('mouseleave', function(){ that._removeCursor(); }); 110 | 111 | _.each(this.datasets, function(dataset) { 112 | dataset 113 | .onSync('asyncRequested', that._propagate, that) 114 | .onSync('asyncCanceled', that._propagate, that) 115 | .onSync('asyncReceived', that._propagate, that) 116 | .onSync('rendered', that._onRendered, that) 117 | .onSync('cleared', that._onCleared, that); 118 | }); 119 | 120 | return this; 121 | }, 122 | 123 | isOpen: function isOpen() { 124 | return this.$node.hasClass(this.classes.open); 125 | }, 126 | 127 | open: function open() { 128 | this.$node.scrollTop(0); 129 | this.$node.addClass(this.classes.open); 130 | }, 131 | 132 | close: function close() { 133 | this.$node.attr('aria-expanded', false); 134 | this.$node.removeClass(this.classes.open); 135 | this._removeCursor(); 136 | }, 137 | 138 | setLanguageDirection: function setLanguageDirection(dir) { 139 | this.$node.attr('dir', dir); 140 | }, 141 | 142 | selectableRelativeToCursor: function selectableRelativeToCursor(delta) { 143 | var $selectables, $oldCursor, oldIndex, newIndex; 144 | 145 | $oldCursor = this.getActiveSelectable(); 146 | $selectables = this._getSelectables(); 147 | 148 | // shifting before and after modulo to deal with -1 index 149 | oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; 150 | newIndex = oldIndex + delta; 151 | newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; 152 | 153 | // wrap new index if less than -1 154 | newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; 155 | 156 | return newIndex === -1 ? null : $selectables.eq(newIndex); 157 | }, 158 | 159 | setCursor: function setCursor($selectable) { 160 | this._removeCursor(); 161 | 162 | if ($selectable = $selectable && $selectable.first()) { 163 | $selectable.addClass(this.classes.cursor); 164 | 165 | // in the case of scrollable overflow 166 | // make sure the cursor is visible in the node 167 | this._ensureVisible($selectable); 168 | } 169 | }, 170 | 171 | getSelectableData: function getSelectableData($el) { 172 | return ($el && $el.length) ? Dataset.extractData($el) : null; 173 | }, 174 | 175 | getActiveSelectable: function getActiveSelectable() { 176 | var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); 177 | 178 | return $selectable.length ? $selectable : null; 179 | }, 180 | 181 | getTopSelectable: function getTopSelectable() { 182 | var $selectable = this._getSelectables().first(); 183 | 184 | return $selectable.length ? $selectable : null; 185 | }, 186 | 187 | update: function update(query) { 188 | var isValidUpdate = query !== this.query; 189 | 190 | // don't update if the query hasn't changed 191 | if (isValidUpdate) { 192 | this.query = query; 193 | _.each(this.datasets, updateDataset); 194 | } 195 | 196 | return isValidUpdate; 197 | 198 | function updateDataset(dataset) { dataset.update(query); } 199 | }, 200 | 201 | empty: function empty() { 202 | _.each(this.datasets, clearDataset); 203 | 204 | this.query = null; 205 | this.$node.addClass(this.classes.empty); 206 | 207 | function clearDataset(dataset) { dataset.clear(); } 208 | }, 209 | 210 | destroy: function destroy() { 211 | this.$node.off('.tt'); 212 | 213 | // #970 214 | this.$node = $('
'); 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('

{{value}}

'); 221 | ``` 222 | 223 | [Handlebars]: http://handlebarsjs.com/ 224 | 225 | ### CSS Class Changes 226 | 227 | `tt-is-under-cursor` is now `tt-cursor` - Applied to a hovered-on suggestion (either via cursor or arrow key). 228 | 229 | `tt-query` is now `tt-input` - Applied to the typeahead input field. 230 | 231 | Something Missing? 232 | ------------------ 233 | 234 | If something is missing from this migration guide, pull requests are accepted :) 235 | -------------------------------------------------------------------------------- /src/typeahead/plugin.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 | (function() { 8 | 'use strict'; 9 | 10 | var old, keys, methods; 11 | 12 | old = $.fn.typeahead; 13 | 14 | keys = { 15 | www: 'tt-www', 16 | attrs: 'tt-attrs', 17 | typeahead: 'tt-typeahead' 18 | }; 19 | 20 | methods = { 21 | // supported signatures: 22 | // function(o, dataset, dataset, ...) 23 | // function(o, [dataset, dataset, ...]) 24 | initialize: function initialize(o, datasets) { 25 | var www; 26 | 27 | datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); 28 | 29 | o = o || {}; 30 | www = WWW(o.classNames); 31 | 32 | return this.each(attach); 33 | 34 | function attach() { 35 | var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, 36 | eventBus, input, menu, status, typeahead, MenuConstructor; 37 | 38 | // highlight is a top-level config that needs to get inherited 39 | // from all of the datasets 40 | _.each(datasets, function(d) { d.highlight = !!o.highlight; }); 41 | 42 | $input = $(this); 43 | $wrapper = $(www.html.wrapper); 44 | $hint = $elOrNull(o.hint); 45 | $menu = $elOrNull(o.menu); 46 | 47 | defaultHint = o.hint !== false && !$hint; 48 | defaultMenu = o.menu !== false && !$menu; 49 | 50 | defaultHint && ($hint = buildHintFromInput($input, www)); 51 | defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); 52 | 53 | // hint should be empty on init 54 | $hint && $hint.val(''); 55 | $input = prepInput($input, www); 56 | 57 | // only apply inline styles and make dom changes if necessary 58 | if (defaultHint || defaultMenu) { 59 | $wrapper.css(www.css.wrapper); 60 | $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); 61 | 62 | $input 63 | .wrap($wrapper) 64 | .parent() 65 | .prepend(defaultHint ? $hint : null) 66 | .append(defaultMenu ? $menu : null); 67 | } 68 | 69 | MenuConstructor = defaultMenu ? DefaultMenu : Menu; 70 | 71 | eventBus = new EventBus({ el: $input }); 72 | input = new Input({ hint: $hint, input: $input, menu: $menu }, www); 73 | menu = new MenuConstructor({ 74 | node: $menu, 75 | datasets: datasets 76 | }, www); 77 | 78 | status = new Status({ 79 | $input: $input, 80 | menu: menu 81 | }); 82 | 83 | typeahead = new Typeahead({ 84 | input: input, 85 | menu: menu, 86 | eventBus: eventBus, 87 | minLength: o.minLength, 88 | autoselect: o.autoselect 89 | }, www); 90 | 91 | $input.data(keys.www, www); 92 | $input.data(keys.typeahead, typeahead); 93 | } 94 | }, 95 | 96 | isEnabled: function isEnabled() { 97 | var enabled; 98 | 99 | ttEach(this.first(), function(t) { enabled = t.isEnabled(); }); 100 | return enabled; 101 | }, 102 | 103 | enable: function enable() { 104 | ttEach(this, function(t) { t.enable(); }); 105 | return this; 106 | }, 107 | 108 | disable: function disable() { 109 | ttEach(this, function(t) { t.disable(); }); 110 | return this; 111 | }, 112 | 113 | isActive: function isActive() { 114 | var active; 115 | 116 | ttEach(this.first(), function(t) { active = t.isActive(); }); 117 | return active; 118 | }, 119 | 120 | activate: function activate() { 121 | ttEach(this, function(t) { t.activate(); }); 122 | return this; 123 | }, 124 | 125 | deactivate: function deactivate() { 126 | ttEach(this, function(t) { t.deactivate(); }); 127 | return this; 128 | }, 129 | 130 | isOpen: function isOpen() { 131 | var open; 132 | 133 | ttEach(this.first(), function(t) { open = t.isOpen(); }); 134 | return open; 135 | }, 136 | 137 | open: function open() { 138 | ttEach(this, function(t) { t.open(); }); 139 | return this; 140 | }, 141 | 142 | close: function close() { 143 | ttEach(this, function(t) { t.close(); }); 144 | return this; 145 | }, 146 | 147 | select: function select(el) { 148 | var success = false, $el = $(el); 149 | 150 | ttEach(this.first(), function(t) { success = t.select($el); }); 151 | return success; 152 | }, 153 | 154 | autocomplete: function autocomplete(el) { 155 | var success = false, $el = $(el); 156 | 157 | ttEach(this.first(), function(t) { success = t.autocomplete($el); }); 158 | return success; 159 | }, 160 | 161 | moveCursor: function moveCursoe(delta) { 162 | var success = false; 163 | 164 | ttEach(this.first(), function(t) { success = t.moveCursor(delta); }); 165 | return success; 166 | }, 167 | 168 | // mirror jQuery#val functionality: reads operate on first match, 169 | // write operates on all matches 170 | val: function val(newVal) { 171 | var query; 172 | 173 | if (!arguments.length) { 174 | ttEach(this.first(), function(t) { query = t.getVal(); }); 175 | return query; 176 | } 177 | 178 | else { 179 | ttEach(this, function(t) { t.setVal(_.toStr(newVal)); }); 180 | return this; 181 | } 182 | }, 183 | 184 | destroy: function destroy() { 185 | ttEach(this, function(typeahead, $input) { 186 | revert($input); 187 | typeahead.destroy(); 188 | }); 189 | 190 | return this; 191 | } 192 | }; 193 | 194 | $.fn.typeahead = function(method) { 195 | // methods that should only act on initialized typeaheads 196 | if (methods[method]) { 197 | return methods[method].apply(this, [].slice.call(arguments, 1)); 198 | } 199 | 200 | else { 201 | return methods.initialize.apply(this, arguments); 202 | } 203 | }; 204 | 205 | $.fn.typeahead.noConflict = function noConflict() { 206 | $.fn.typeahead = old; 207 | return this; 208 | }; 209 | 210 | // helper methods 211 | // -------------- 212 | 213 | function ttEach($els, fn) { 214 | $els.each(function() { 215 | var $input = $(this), typeahead; 216 | 217 | (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); 218 | }); 219 | } 220 | 221 | function buildHintFromInput($input, www) { 222 | return $input.clone() 223 | .addClass(www.classes.hint) 224 | .removeData() 225 | .css(www.css.hint) 226 | .css(getBackgroundStyles($input)) 227 | .prop({ readonly: true, required: false }) 228 | .removeAttr('id name placeholder') 229 | .removeClass('required') 230 | .attr({ spellcheck: 'false', tabindex: -1 }); 231 | } 232 | 233 | function prepInput($input, www) { 234 | // store the original values of the attrs that get modified 235 | // so modifications can be reverted on destroy 236 | $input.data(keys.attrs, { 237 | dir: $input.attr('dir'), 238 | autocomplete: $input.attr('autocomplete'), 239 | spellcheck: $input.attr('spellcheck'), 240 | style: $input.attr('style') 241 | }); 242 | 243 | $input 244 | .addClass(www.classes.input) 245 | .attr({ spellcheck: false }); 246 | 247 | // ie7 does not like it when dir is set to auto 248 | try { !$input.attr('dir') && $input.attr('dir', 'auto'); } catch (e) {} 249 | 250 | return $input; 251 | } 252 | 253 | function getBackgroundStyles($el) { 254 | return { 255 | backgroundAttachment: $el.css('background-attachment'), 256 | backgroundClip: $el.css('background-clip'), 257 | backgroundColor: $el.css('background-color'), 258 | backgroundImage: $el.css('background-image'), 259 | backgroundOrigin: $el.css('background-origin'), 260 | backgroundPosition: $el.css('background-position'), 261 | backgroundRepeat: $el.css('background-repeat'), 262 | backgroundSize: $el.css('background-size') 263 | }; 264 | } 265 | 266 | function revert($input) { 267 | var www, $wrapper; 268 | 269 | www = $input.data(keys.www); 270 | $wrapper = $input.parent().filter(www.selectors.wrapper); 271 | 272 | // need to remove attrs that weren't previously defined and 273 | // revert attrs that originally had a value 274 | _.each($input.data(keys.attrs), function(val, key) { 275 | _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); 276 | }); 277 | 278 | $input 279 | .removeData(keys.typeahead) 280 | .removeData(keys.www) 281 | .removeData(keys.attr) 282 | .removeClass(www.classes.input); 283 | 284 | if ($wrapper.length) { 285 | $input.detach().insertAfter($wrapper); 286 | $wrapper.remove(); 287 | } 288 | } 289 | 290 | function $elOrNull(obj) { 291 | var isValid, $el; 292 | 293 | isValid = _.isJQuery(obj) || _.isElement(obj); 294 | $el = isValid ? $(obj).first() : []; 295 | 296 | return $el.length ? $el : null; 297 | } 298 | })(); 299 | -------------------------------------------------------------------------------- /test/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 | 34 | 35 | 36 |
37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 | 65 | 345 | 346 | 347 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver'), 2 | f = require('util').format, 3 | files = { 4 | common: [ 5 | 'src/common/utils.js' 6 | ], 7 | bloodhound: [ 8 | 'src/bloodhound/version.js', 9 | 'src/bloodhound/tokenizers.js', 10 | 'src/bloodhound/lru_cache.js', 11 | 'src/bloodhound/persistent_storage.js', 12 | 'src/bloodhound/transport.js', 13 | 'src/bloodhound/search_index.js', 14 | 'src/bloodhound/prefetch.js', 15 | 'src/bloodhound/remote.js', 16 | 'src/bloodhound/options_parser.js', 17 | 'src/bloodhound/bloodhound.js' 18 | ], 19 | typeahead: [ 20 | 'src/typeahead/www.js', 21 | 'src/typeahead/event_bus.js', 22 | 'src/typeahead/event_emitter.js', 23 | 'src/typeahead/highlight.js', 24 | 'src/typeahead/input.js', 25 | 'src/typeahead/dataset.js', 26 | 'src/typeahead/menu.js', 27 | 'src/typeahead/status.js', 28 | 'src/typeahead/default_menu.js', 29 | 'src/typeahead/typeahead.js', 30 | 'src/typeahead/plugin.js' 31 | ] 32 | }; 33 | 34 | module.exports = function(grunt) { 35 | grunt.initConfig({ 36 | version: grunt.file.readJSON('package.json').version, 37 | 38 | tempDir: 'dist_temp', 39 | buildDir: 'dist', 40 | 41 | banner: [ 42 | '/*!', 43 | ' * typeahead.js <%= version %>', 44 | ' * https://github.com/corejavascript/typeahead.js', 45 | ' * Copyright 2013-<%= grunt.template.today("yyyy") %> Twitter, Inc. and other contributors; Licensed MIT', 46 | ' */\n\n' 47 | ].join('\n'), 48 | 49 | uglify: { 50 | options: { 51 | banner: '<%= banner %>' 52 | }, 53 | 54 | concatBloodhound: { 55 | options: { 56 | mangle: false, 57 | beautify: true, 58 | compress: false, 59 | banner: '' 60 | }, 61 | src: files.common.concat(files.bloodhound), 62 | dest: '<%= tempDir %>/bloodhound.js' 63 | }, 64 | concatTypeahead: { 65 | options: { 66 | mangle: false, 67 | beautify: true, 68 | compress: false, 69 | banner: '' 70 | }, 71 | src: files.common.concat(files.typeahead), 72 | dest: '<%= tempDir %>/typeahead.jquery.js' 73 | }, 74 | 75 | bloodhound: { 76 | options: { 77 | mangle: false, 78 | beautify: true, 79 | compress: false 80 | }, 81 | src: '<%= tempDir %>/bloodhound.js', 82 | dest: '<%= buildDir %>/bloodhound.js' 83 | }, 84 | bloodhoundMin: { 85 | options: { 86 | mangle: true, 87 | compress: {} 88 | }, 89 | src: '<%= tempDir %>/bloodhound.js', 90 | dest: '<%= buildDir %>/bloodhound.min.js' 91 | }, 92 | typeahead: { 93 | options: { 94 | mangle: false, 95 | beautify: true, 96 | compress: false 97 | }, 98 | src: '<%= tempDir %>/typeahead.jquery.js', 99 | dest: '<%= buildDir %>/typeahead.jquery.js' 100 | }, 101 | typeaheadMin: { 102 | options: { 103 | mangle: true, 104 | compress: {} 105 | }, 106 | src: '<%= tempDir %>/typeahead.jquery.js', 107 | dest: '<%= buildDir %>/typeahead.jquery.min.js' 108 | }, 109 | bundle: { 110 | options: { 111 | mangle: false, 112 | beautify: true, 113 | compress: false 114 | }, 115 | src: [ 116 | '<%= tempDir %>/bloodhound.js', 117 | '<%= tempDir %>/typeahead.jquery.js' 118 | ], 119 | dest: '<%= buildDir %>/typeahead.bundle.js' 120 | 121 | }, 122 | bundleMin: { 123 | options: { 124 | mangle: true, 125 | compress: {} 126 | }, 127 | src: [ 128 | '<%= tempDir %>/bloodhound.js', 129 | '<%= tempDir %>/typeahead.jquery.js' 130 | ], 131 | dest: '<%= buildDir %>/typeahead.bundle.min.js' 132 | } 133 | }, 134 | 135 | umd: { 136 | bloodhound: { 137 | src: '<%= tempDir %>/bloodhound.js', 138 | objectToExport: 'Bloodhound', 139 | deps: { 140 | default: ['$'], 141 | amd: ['jquery'], 142 | cjs: ['jquery'], 143 | global: ['jQuery'] 144 | } 145 | }, 146 | typeahead: { 147 | src: '<%= tempDir %>/typeahead.jquery.js', 148 | deps: { 149 | default: ['$'], 150 | amd: ['jquery'], 151 | cjs: ['jquery'], 152 | global: ['jQuery'] 153 | } 154 | } 155 | }, 156 | 157 | replace: { 158 | version: { 159 | options: { 160 | patterns: [ 161 | { 162 | match: '%VERSION%', 163 | replacement: '<%= version %>', 164 | } 165 | ], 166 | usePrefix: false 167 | }, 168 | files: [ 169 | { 170 | expand: true, 171 | flatten: true, 172 | src: '<%= buildDir %>/**/*.js', 173 | dest: '<%= buildDir %>' 174 | } 175 | ] 176 | } 177 | }, 178 | 179 | jshint: { 180 | options: { 181 | jshintrc: '.jshintrc' 182 | }, 183 | src: 'src/**/*.js', 184 | test: ['test/**/*_spec.js', 'test/integration/test.js'], 185 | gruntfile: ['Gruntfile.js'] 186 | }, 187 | 188 | watch: { 189 | js: { 190 | files: 'src/**/*', 191 | tasks: 'build' 192 | } 193 | }, 194 | 195 | exec: { 196 | npm_publish: 'npm publish', 197 | git_is_clean: 'test -z "$(git status --porcelain)"', 198 | git_on_master: 'test $(git symbolic-ref --short -q HEAD) = master', 199 | git_add: 'git add .', 200 | git_push: 'git push && git push --tags', 201 | git_commit: { 202 | cmd: function(m) { return f('git commit -m "%s"', m); } 203 | }, 204 | git_tag: { 205 | cmd: function(v) { return f('git tag v%s -am "%s"', v, v); } 206 | }, 207 | publish_assets: [ 208 | 'cp -r <%= buildDir %> typeahead.js', 209 | 'zip -r typeahead.js/typeahead.js.zip typeahead.js', 210 | 'git checkout gh-pages', 211 | 'rm -rf releases/latest', 212 | 'cp -r typeahead.js releases/<%= version %>', 213 | 'cp -r typeahead.js releases/latest', 214 | 'git add releases/<%= version %> releases/latest', 215 | 'sed -E -i "" \'s/v[0-9]+\\.[0-9]+\\.[0-9]+/v<%= version %>/\' index.html', 216 | 'git add index.html', 217 | 'git commit -m "Add assets for <%= version %>."', 218 | 'git push', 219 | 'git checkout -', 220 | 'rm -rf typeahead.js' 221 | ].join(' && ') 222 | }, 223 | 224 | clean: { 225 | dist: 'dist' 226 | }, 227 | 228 | connect: { 229 | server: { 230 | options: { port: 8888, keepalive: true } 231 | } 232 | }, 233 | 234 | concurrent: { 235 | options: { logConcurrentOutput: true }, 236 | dev: ['server', 'watch'] 237 | }, 238 | 239 | step: { 240 | options: { 241 | option: false 242 | } 243 | } 244 | }); 245 | 246 | grunt.registerTask('release', '#shipit', function(version) { 247 | var curVersion = grunt.config.get('version'); 248 | 249 | version = semver.inc(curVersion, version) || version; 250 | 251 | if (!semver.valid(version) || semver.lte(version, curVersion)) { 252 | grunt.fatal('hey dummy, that version is no good!'); 253 | } 254 | 255 | grunt.config.set('version', version); 256 | 257 | grunt.task.run([ 258 | 'exec:git_on_master', 259 | 'exec:git_is_clean', 260 | f('step:Update to version %s?', version), 261 | f('manifests:%s', version), 262 | 'build', 263 | 'exec:git_add', 264 | f('exec:git_commit:%s', version), 265 | f('exec:git_tag:%s', version), 266 | 'step:Push changes?', 267 | 'exec:git_push', 268 | 'step:Publish to npm?', 269 | 'exec:npm_publish', 270 | 'step:Publish assets?', 271 | 'exec:publish_assets' 272 | ]); 273 | }); 274 | 275 | grunt.registerTask('manifests', 'Update manifests.', function(version) { 276 | var _ = grunt.util._, 277 | pkg = grunt.file.readJSON('package.json'), 278 | bower = grunt.file.readJSON('bower.json'), 279 | jqueryPlugin = grunt.file.readJSON('typeahead.js.jquery.json'); 280 | 281 | bower = JSON.stringify(_.extend(bower, { 282 | name: pkg.name, 283 | version: version 284 | }), null, 2); 285 | 286 | jqueryPlugin = JSON.stringify(_.extend(jqueryPlugin, { 287 | name: pkg.name, 288 | title: pkg.name, 289 | version: version, 290 | author: pkg.author, 291 | description: pkg.description, 292 | keywords: pkg.keywords, 293 | homepage: pkg.homepage, 294 | bugs: pkg.bugs, 295 | maintainers: pkg.contributors 296 | }), null, 2); 297 | 298 | pkg = JSON.stringify(_.extend(pkg, { 299 | version: version 300 | }), null, 2); 301 | 302 | grunt.file.write('package.json', pkg); 303 | grunt.file.write('bower.json', bower); 304 | grunt.file.write('typeahead.js.jquery.json', jqueryPlugin); 305 | }); 306 | 307 | // aliases 308 | // ------- 309 | 310 | grunt.registerTask('default', 'build'); 311 | grunt.registerTask('server', 'connect:server'); 312 | grunt.registerTask('lint', 'jshint'); 313 | grunt.registerTask('dev', ['build', 'concurrent:dev']); 314 | grunt.registerTask('build', [ 315 | 'uglify:concatBloodhound', 316 | 'uglify:concatTypeahead', 317 | 'umd:bloodhound', 318 | 'umd:typeahead', 319 | 'uglify:bloodhound', 320 | 'uglify:bloodhoundMin', 321 | 'uglify:typeahead', 322 | 'uglify:typeaheadMin', 323 | 'uglify:bundle', 324 | 'uglify:bundleMin', 325 | 'replace:version' 326 | ]); 327 | 328 | // load tasks 329 | // ---------- 330 | 331 | grunt.loadNpmTasks('grunt-umd'); 332 | grunt.loadNpmTasks('grunt-replace'); 333 | grunt.loadNpmTasks('grunt-exec'); 334 | grunt.loadNpmTasks('grunt-step'); 335 | grunt.loadNpmTasks('grunt-concurrent'); 336 | grunt.loadNpmTasks('grunt-contrib-watch'); 337 | grunt.loadNpmTasks('grunt-contrib-clean'); 338 | grunt.loadNpmTasks('grunt-contrib-uglify'); 339 | grunt.loadNpmTasks('grunt-contrib-jshint'); 340 | grunt.loadNpmTasks('grunt-contrib-concat'); 341 | grunt.loadNpmTasks('grunt-contrib-connect'); 342 | }; 343 | -------------------------------------------------------------------------------- /src/typeahead/dataset.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 Dataset = (function() { 8 | 'use strict'; 9 | 10 | var keys, nameGenerator; 11 | 12 | keys = { 13 | dataset: 'tt-selectable-dataset', 14 | val: 'tt-selectable-display', 15 | obj: 'tt-selectable-object' 16 | }; 17 | 18 | nameGenerator = _.getIdGenerator(); 19 | 20 | // constructor 21 | // ----------- 22 | 23 | function Dataset(o, www) { 24 | o = o || {}; 25 | o.templates = o.templates || {}; 26 | 27 | // DEPRECATED: empty will be dropped in v1 28 | o.templates.notFound = o.templates.notFound || o.templates.empty; 29 | 30 | if (!o.source) { 31 | $.error('missing source'); 32 | } 33 | 34 | if (!o.node) { 35 | $.error('missing node'); 36 | } 37 | 38 | if (o.name && !isValidName(o.name)) { 39 | $.error('invalid dataset name: ' + o.name); 40 | } 41 | 42 | www.mixin(this); 43 | 44 | this.highlight = !!o.highlight; 45 | this.name = _.toStr(o.name || nameGenerator()); 46 | 47 | this.limit = o.limit || 5; 48 | this.displayFn = getDisplayFn(o.display || o.displayKey); 49 | this.templates = getTemplates(o.templates, this.displayFn); 50 | 51 | // use duck typing to see if source is a bloodhound instance by checking 52 | // for the __ttAdapter property; otherwise assume it is a function 53 | this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; 54 | 55 | // if the async option is undefined, inspect the source signature as 56 | // a hint to figuring out of the source will return async suggestions 57 | this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; 58 | 59 | this._resetLastSuggestion(); 60 | 61 | this.$el = $(o.node) 62 | .attr('role', 'presentation') 63 | .addClass(this.classes.dataset) 64 | .addClass(this.classes.dataset + '-' + this.name); 65 | } 66 | 67 | // static methods 68 | // -------------- 69 | 70 | Dataset.extractData = function extractData(el) { 71 | var $el = $(el); 72 | 73 | if ($el.data(keys.obj)) { 74 | return { 75 | dataset: $el.data(keys.dataset) || '', 76 | val: $el.data(keys.val) || '', 77 | obj: $el.data(keys.obj) || null 78 | }; 79 | } 80 | 81 | return null; 82 | }; 83 | 84 | // instance methods 85 | // ---------------- 86 | 87 | _.mixin(Dataset.prototype, EventEmitter, { 88 | 89 | // ### private 90 | 91 | _overwrite: function overwrite(query, suggestions) { 92 | suggestions = suggestions || []; 93 | 94 | // got suggestions: overwrite dom with suggestions 95 | if (suggestions.length) { 96 | this._renderSuggestions(query, suggestions); 97 | } 98 | 99 | // no suggestions, expecting async: overwrite dom with pending 100 | else if (this.async && this.templates.pending) { 101 | this._renderPending(query); 102 | } 103 | 104 | // no suggestions, not expecting async: overwrite dom with not found 105 | else if (!this.async && this.templates.notFound) { 106 | this._renderNotFound(query); 107 | } 108 | 109 | // nothing to render: empty dom 110 | else { 111 | this._empty(); 112 | } 113 | 114 | this.trigger('rendered', suggestions, false, this.name); 115 | }, 116 | 117 | _append: function append(query, suggestions) { 118 | suggestions = suggestions || []; 119 | 120 | // got suggestions, sync suggestions exist: append suggestions to dom 121 | if (suggestions.length && this.$lastSuggestion.length) { 122 | this._appendSuggestions(query, suggestions); 123 | } 124 | 125 | // got suggestions, no sync suggestions: overwrite dom with suggestions 126 | else if (suggestions.length) { 127 | this._renderSuggestions(query, suggestions); 128 | } 129 | 130 | // no async/sync suggestions: overwrite dom with not found 131 | else if (!this.$lastSuggestion.length && this.templates.notFound) { 132 | this._renderNotFound(query); 133 | } 134 | 135 | this.trigger('rendered', suggestions, true, this.name); 136 | }, 137 | 138 | _renderSuggestions: function renderSuggestions(query, suggestions) { 139 | var $fragment; 140 | 141 | $fragment = this._getSuggestionsFragment(query, suggestions); 142 | this.$lastSuggestion = $fragment.children().last(); 143 | 144 | this.$el.html($fragment) 145 | .prepend(this._getHeader(query, suggestions)) 146 | .append(this._getFooter(query, suggestions)); 147 | }, 148 | 149 | _appendSuggestions: function appendSuggestions(query, suggestions) { 150 | var $fragment, $lastSuggestion; 151 | 152 | $fragment = this._getSuggestionsFragment(query, suggestions); 153 | $lastSuggestion = $fragment.children().last(); 154 | 155 | this.$lastSuggestion.after($fragment); 156 | 157 | this.$lastSuggestion = $lastSuggestion; 158 | }, 159 | 160 | _renderPending: function renderPending(query) { 161 | var template = this.templates.pending; 162 | 163 | this._resetLastSuggestion(); 164 | template && this.$el.html(template({ 165 | query: query, 166 | dataset: this.name, 167 | })); 168 | }, 169 | 170 | _renderNotFound: function renderNotFound(query) { 171 | var template = this.templates.notFound; 172 | 173 | this._resetLastSuggestion(); 174 | template && this.$el.html(template({ 175 | query: query, 176 | dataset: this.name, 177 | })); 178 | }, 179 | 180 | _empty: function empty() { 181 | this.$el.empty(); 182 | this._resetLastSuggestion(); 183 | }, 184 | 185 | _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { 186 | var that = this, fragment; 187 | 188 | fragment = document.createDocumentFragment(); 189 | _.each(suggestions, function getSuggestionNode(suggestion) { 190 | var $el, context; 191 | 192 | context = that._injectQuery(query, suggestion); 193 | 194 | $el = $(that.templates.suggestion(context)) 195 | .data(keys.dataset, that.name) 196 | .data(keys.obj, suggestion) 197 | .data(keys.val, that.displayFn(suggestion)) 198 | .addClass(that.classes.suggestion + ' ' + that.classes.selectable); 199 | 200 | fragment.appendChild($el[0]); 201 | }); 202 | 203 | this.highlight && highlight({ 204 | className: this.classes.highlight, 205 | node: fragment, 206 | pattern: query 207 | }); 208 | 209 | return $(fragment); 210 | }, 211 | 212 | _getFooter: function getFooter(query, suggestions) { 213 | return this.templates.footer ? 214 | this.templates.footer({ 215 | query: query, 216 | suggestions: suggestions, 217 | dataset: this.name 218 | }) : null; 219 | }, 220 | 221 | _getHeader: function getHeader(query, suggestions) { 222 | return this.templates.header ? 223 | this.templates.header({ 224 | query: query, 225 | suggestions: suggestions, 226 | dataset: this.name 227 | }) : null; 228 | }, 229 | 230 | _resetLastSuggestion: function resetLastSuggestion() { 231 | this.$lastSuggestion = $(); 232 | }, 233 | 234 | _injectQuery: function injectQuery(query, obj) { 235 | return _.isObject(obj) ? _.mixin({ _query: query }, obj) : obj; 236 | }, 237 | 238 | // ### public 239 | 240 | update: function update(query) { 241 | var that = this, canceled = false, syncCalled = false, rendered = 0; 242 | 243 | // cancel possible pending update 244 | this.cancel(); 245 | 246 | this.cancel = function cancel() { 247 | canceled = true; 248 | that.cancel = $.noop; 249 | that.async && that.trigger('asyncCanceled', query, that.name); 250 | }; 251 | 252 | this.source(query, sync, async); 253 | !syncCalled && sync([]); 254 | 255 | function sync(suggestions) { 256 | if (syncCalled) { return; } 257 | 258 | syncCalled = true; 259 | suggestions = (suggestions || []).slice(0, that.limit); 260 | rendered = suggestions.length; 261 | 262 | that._overwrite(query, suggestions); 263 | 264 | if (rendered < that.limit && that.async) { 265 | that.trigger('asyncRequested', query, that.name); 266 | } 267 | } 268 | 269 | function async(suggestions) { 270 | suggestions = suggestions || []; 271 | 272 | // if the update has been canceled or if the query has changed 273 | // do not render the suggestions as they've become outdated 274 | if (!canceled && rendered < that.limit) { 275 | that.cancel = $.noop; 276 | var idx = Math.abs(rendered - that.limit); 277 | rendered += idx; 278 | that._append(query, suggestions.slice(0, idx)); 279 | that.async && that.trigger('asyncReceived', query, that.name); 280 | } 281 | } 282 | }, 283 | 284 | // cancel function gets set in #update 285 | cancel: $.noop, 286 | 287 | clear: function clear() { 288 | this._empty(); 289 | this.cancel(); 290 | this.trigger('cleared'); 291 | }, 292 | 293 | isEmpty: function isEmpty() { 294 | return this.$el.is(':empty'); 295 | }, 296 | 297 | destroy: function destroy() { 298 | // #970 299 | this.$el = $('
'); 300 | } 301 | }); 302 | 303 | return Dataset; 304 | 305 | // helper functions 306 | // ---------------- 307 | 308 | function getDisplayFn(display) { 309 | display = display || _.stringify; 310 | 311 | return _.isFunction(display) ? display : displayFn; 312 | 313 | function displayFn(obj) { return obj[display]; } 314 | } 315 | 316 | function getTemplates(templates, displayFn) { 317 | return { 318 | notFound: templates.notFound && _.templatify(templates.notFound), 319 | pending: templates.pending && _.templatify(templates.pending), 320 | header: templates.header && _.templatify(templates.header), 321 | footer: templates.footer && _.templatify(templates.footer), 322 | suggestion: templates.suggestion ? userSuggestionTemplate : suggestionTemplate 323 | }; 324 | 325 | function userSuggestionTemplate(context) { 326 | var template = templates.suggestion; 327 | return $(template(context)).attr("id", _.guid()); 328 | } 329 | 330 | function suggestionTemplate(context) { 331 | return $('
').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 | --------------------------------------------------------------------------------