From 4ddfe2ef3b79ae14d48b4e0e15a0dda8cf906832 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Fri, 27 Dec 2013 23:24:23 -0800 Subject: [PATCH 01/17] Remove pre-existing cursor on hover. Fixes #580. --- src/typeahead/dropdown.js | 7 ++++--- test/dropdown_view_spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/typeahead/dropdown.js b/src/typeahead/dropdown.js index 52a58c59..3c7c1bee 100644 --- a/src/typeahead/dropdown.js +++ b/src/typeahead/dropdown.js @@ -65,7 +65,8 @@ var Dropdown = (function() { }, _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { - this._setCursor($($e.currentTarget)); + this._removeCursor(); + this._setCursor($($e.currentTarget), true); }, _onSuggestionMouseLeave: function onSuggestionMouseLeave($e) { @@ -100,10 +101,10 @@ var Dropdown = (function() { return this.$menu.find('.tt-cursor').first(); }, - _setCursor: function setCursor($el) { + _setCursor: function setCursor($el, silent) { $el.first().addClass('tt-cursor'); - this.trigger('cursorMoved'); + !silent && this.trigger('cursorMoved'); }, _removeCursor: function removeCursor() { diff --git a/test/dropdown_view_spec.js b/test/dropdown_view_spec.js index 184ccf02..2b0cb07b 100644 --- a/test/dropdown_view_spec.js +++ b/test/dropdown_view_spec.js @@ -50,6 +50,19 @@ describe('Dropdown', function() { }); describe('when mouseenter is triggered on a suggestion', function() { + it('should remove pre-existing cursor', function() { + var $first, $last; + + $first = this.$menu.find('.tt-suggestion').first(); + $last = this.$menu.find('.tt-suggestion').last(); + + $first.addClass('tt-cursor'); + $last.mouseenter(); + + expect($first).not.toHaveClass('tt-cursor'); + expect($last).toHaveClass('tt-cursor'); + }); + it('should set the cursor', function() { var $suggestion; @@ -58,6 +71,17 @@ describe('Dropdown', function() { expect($suggestion).toHaveClass('tt-cursor'); }); + + it('should not trigger cursorMoved', function() { + var spy, $suggestion; + + this.view.onSync('cursorMoved', spy = jasmine.createSpy()); + + $suggestion = this.$menu.find('.tt-suggestion').first(); + $suggestion.mouseenter(); + + expect(spy).not.toHaveBeenCalled(); + }); }); describe('when mouseleave is triggered on a suggestion', function() { From 2c68b01b3b5f185945e487491495bbae0d611c17 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 30 Dec 2013 03:50:21 -0800 Subject: [PATCH 02/17] Fix return values of adapted dataset. --- src/dataset/dataset.js | 4 +--- src/typeahead/plugin.js | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/dataset/dataset.js b/src/dataset/dataset.js index fbfe89c8..f2286eaf 100644 --- a/src/dataset/dataset.js +++ b/src/dataset/dataset.js @@ -150,7 +150,7 @@ var Dataset = (function() { this.transport = this.remote ? new Transport(this.remote) : null; this.initialize = function initialize() { return that; }; - return this; + return deferred.promise(); function addLocalToIndex() { that.add(that.local); } }, @@ -162,8 +162,6 @@ var Dataset = (function() { normalized = this._normalize(data); this.index.add(normalized); - - return this; }, get: function get(query, cb) { diff --git a/src/typeahead/plugin.js b/src/typeahead/plugin.js index 0991f3a6..ee7a164e 100644 --- a/src/typeahead/plugin.js +++ b/src/typeahead/plugin.js @@ -121,13 +121,13 @@ source = _.bind(dataset.get, dataset); source.initialize = function() { - dataset.initialize(); - return source; + // returns a promise that is resolved after prefetch data + // is loaded and processed + return dataset.initialize(); }; source.add = function(data) { dataset.add(); - return source; }; return source; From 2c6d99a81f0a5c04d55ac484f09f5d995cffd9bc Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 30 Dec 2013 06:48:07 -0800 Subject: [PATCH 03/17] Rename dataset to bloodhound. --- Gruntfile.js | 24 ++--- karma.conf.js | 12 +-- .../dataset.js => bloodhound/bloodhound.js} | 10 +- src/{dataset => bloodhound}/lru_cache.js | 0 .../persistent_storage.js | 0 src/{dataset => bloodhound}/search_index.js | 0 src/{dataset => bloodhound}/transport.js | 0 src/{dataset => bloodhound}/version.js | 0 src/typeahead/plugin.js | 16 ++-- test/{dataset_spec.js => bloodhound_spec.js} | 94 +++++++++---------- test/helpers/typeahead_mocks.js | 2 +- 11 files changed, 79 insertions(+), 79 deletions(-) rename src/{dataset/dataset.js => bloodhound/bloodhound.js} (98%) rename src/{dataset => bloodhound}/lru_cache.js (100%) rename src/{dataset => bloodhound}/persistent_storage.js (100%) rename src/{dataset => bloodhound}/search_index.js (100%) rename src/{dataset => bloodhound}/transport.js (100%) rename src/{dataset => bloodhound}/version.js (100%) rename test/{dataset_spec.js => bloodhound_spec.js} (72%) diff --git a/Gruntfile.js b/Gruntfile.js index 12cc7dee..36ec2055 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,13 +4,13 @@ var semver = require('semver'), common: [ 'src/common/utils.js' ], - dataset: [ - 'src/dataset/version.js', - 'src/dataset/lru_cache.js', - 'src/dataset/persistent_storage.js', - 'src/dataset/transport.js', - 'src/dataset/search_index.js', - 'src/dataset/dataset.js' + bloodhound: [ + 'src/bloodhound/version.js', + 'src/bloodhound/lru_cache.js', + 'src/bloodhound/persistent_storage.js', + 'src/bloodhound/transport.js', + 'src/bloodhound/search_index.js', + 'src/bloodhound/bloodhound.js' ], typeahead: [ 'src/typeahead/html.js', @@ -45,14 +45,14 @@ module.exports = function(grunt) { banner: '<%= banner %>', enclose: { 'window.jQuery': '$' } }, - dataset: { + bloodhound: { options: { mangle: false, beautify: true, compress: false }, - src: files.common.concat(files.dataset), - dest: '<%= buildDir %>/dataset.js' + src: files.common.concat(files.bloodhound), + dest: '<%= buildDir %>/bloodhound.js' }, typeahead: { options: { @@ -70,7 +70,7 @@ module.exports = function(grunt) { beautify: true, compress: false }, - src: files.common.concat(files.dataset, files.typeahead), + src: files.common.concat(files.bloodhound, files.typeahead), dest: '<%= buildDir %>/typeahead.bundle.js' }, @@ -79,7 +79,7 @@ module.exports = function(grunt) { mangle: true, compress: true }, - src: files.common.concat(files.dataset, files.typeahead), + src: files.common.concat(files.bloodhound, files.typeahead), dest: '<%= buildDir %>/typeahead.bundle.min.js' } }, diff --git a/karma.conf.js b/karma.conf.js index 5b65b8b3..82fa3873 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -10,12 +10,12 @@ basePath = ''; files = [ 'test/vendor/**/*', 'src/common/utils.js', - 'src/dataset/version.js', - 'src/dataset/lru_cache.js', - 'src/dataset/persistent_storage.js', - 'src/dataset/transport.js', - 'src/dataset/search_index.js', - 'src/dataset/dataset.js', + 'src/bloodhound/version.js', + 'src/bloodhound/lru_cache.js', + 'src/bloodhound/persistent_storage.js', + 'src/bloodhound/transport.js', + 'src/bloodhound/search_index.js', + 'src/bloodhound/bloodhound.js', 'src/typeahead/html.js', 'src/typeahead/css.js', 'src/typeahead/event_bus.js', diff --git a/src/dataset/dataset.js b/src/bloodhound/bloodhound.js similarity index 98% rename from src/dataset/dataset.js rename to src/bloodhound/bloodhound.js index f2286eaf..b0096818 100644 --- a/src/dataset/dataset.js +++ b/src/bloodhound/bloodhound.js @@ -4,7 +4,7 @@ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ -var Dataset = (function() { +var Bloodhound = (function() { var keys; keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' }; @@ -12,7 +12,7 @@ var Dataset = (function() { // constructor // ----------- - function Dataset(o) { + function Bloodhound(o) { if (!o || (!o.local && !o.prefetch && !o.remote)) { $.error('one of local, prefetch, or remote is required'); } @@ -40,7 +40,7 @@ var Dataset = (function() { // instance methods // ---------------- - _.mixin(Dataset.prototype, { + _.mixin(Bloodhound.prototype, { // ### private @@ -137,7 +137,7 @@ var Dataset = (function() { // ### public // the contents of this function are broken out of the constructor - // to help improve the testability of datasets + // to help improve the testability of bloodhounds initialize: function initialize() { var that = this, deferred; @@ -207,7 +207,7 @@ var Dataset = (function() { } }); - return Dataset; + return Bloodhound; // helper functions // ---------------- diff --git a/src/dataset/lru_cache.js b/src/bloodhound/lru_cache.js similarity index 100% rename from src/dataset/lru_cache.js rename to src/bloodhound/lru_cache.js diff --git a/src/dataset/persistent_storage.js b/src/bloodhound/persistent_storage.js similarity index 100% rename from src/dataset/persistent_storage.js rename to src/bloodhound/persistent_storage.js diff --git a/src/dataset/search_index.js b/src/bloodhound/search_index.js similarity index 100% rename from src/dataset/search_index.js rename to src/bloodhound/search_index.js diff --git a/src/dataset/transport.js b/src/bloodhound/transport.js similarity index 100% rename from src/dataset/transport.js rename to src/bloodhound/transport.js diff --git a/src/dataset/version.js b/src/bloodhound/version.js similarity index 100% rename from src/dataset/version.js rename to src/bloodhound/version.js diff --git a/src/typeahead/plugin.js b/src/typeahead/plugin.js index ee7a164e..abb56d27 100644 --- a/src/typeahead/plugin.js +++ b/src/typeahead/plugin.js @@ -24,9 +24,9 @@ // HACK: force highlight as a top-level config section.highlight = !!o.highlight; - // if source is an object, convert it to a dataset + // if source is an object, convert it to a bloodhound section.source = _.isObject(section.source) ? - datasetAdapter(section.source).initialize() : section.source; + bloodhoundAdapter(section.source).initialize() : section.source; }); typeahead = new Typeahead({ @@ -112,22 +112,22 @@ } }; - jQuery.fn.typeahead.datasetAdapter = datasetAdapter; + jQuery.fn.typeahead.bloodhoundAdapter = bloodhoundAdapter; - function datasetAdapter(dataset) { + function bloodhoundAdapter() { var source; - dataset = _.isObject(dataset) ? new Dataset(dataset) : dataset; - source = _.bind(dataset.get, dataset); + bloodhound = _.isObject(bloodhound) ? new Bloodhound(bloodhound) : bloodhound; + source = _.bind(bloodhound.get, bloodhound); source.initialize = function() { // returns a promise that is resolved after prefetch data // is loaded and processed - return dataset.initialize(); + return bloodhound.initialize(); }; source.add = function(data) { - dataset.add(); + bloodhound.add(); }; return source; diff --git a/test/dataset_spec.js b/test/bloodhound_spec.js similarity index 72% rename from test/dataset_spec.js rename to test/bloodhound_spec.js index 3ab9547a..96c00c65 100644 --- a/test/dataset_spec.js +++ b/test/bloodhound_spec.js @@ -1,4 +1,4 @@ -describe('Dataset', function() { +describe('Bloodhound', function() { beforeEach(function() { jasmine.Ajax.useMock(); @@ -12,14 +12,14 @@ describe('Dataset', function() { describe('local', function() { beforeEach(function() { - this.dataset = new Dataset({ local: fixtures.data.simple }); - this.dataset.initialize(); + this.bloodhound = new Bloodhound({ local: fixtures.data.simple }); + this.bloodhound.initialize(); }); - it('should hydrate the dataset', function() { + it('should hydrate the bloodhound', function() { var spy = jasmine.createSpy(); - this.dataset.get('big', spy); + this.bloodhound.get('big', spy); expect(spy).toHaveBeenCalledWith([ { value: 'big' }, @@ -33,30 +33,30 @@ describe('Dataset', function() { it('should throw error if url is not set', function() { expect(test).toThrow(); - function test() { var d = new Dataset({ prefetch: {} }); } + function test() { var d = new Bloodhound({ prefetch: {} }); } }); it('should use url or cacheKey to store data locally', function() { var ttl = 100; - this.dataset1 = new Dataset({ + this.bloodhound1 = new Bloodhound({ prefetch: { url: '/test1', cacheKey: 'woah' } }); expect(PersistentStorage).toHaveBeenCalledWith('woah'); - this.dataset2 = new Dataset({ + this.bloodhound2 = new Bloodhound({ prefetch: { url: '/test2', ttl: ttl, thumbprint: '!' } }); expect(PersistentStorage).toHaveBeenCalledWith('/test2'); - this.dataset2.initialize(); + this.bloodhound2.initialize(); ajaxRequests[0].response(fixtures.ajaxResps.ok); - expect(this.dataset2.storage.set) + expect(this.bloodhound2.storage.set) .toHaveBeenCalledWith('data', fixtures.serialized.simple, ttl); - expect(this.dataset2.storage.set) + expect(this.bloodhound2.storage.set) .toHaveBeenCalledWith('protocol', 'http:', ttl); - expect(this.dataset2.storage.set) + expect(this.bloodhound2.storage.set) .toHaveBeenCalledWith('thumbprint', '%VERSION%!', ttl); }); @@ -66,10 +66,10 @@ describe('Dataset', function() { spy1 = jasmine.createSpy(); spy2 = jasmine.createSpy(); - this.dataset1 = new Dataset({ prefetch: '/test1' }); - this.dataset2 = new Dataset({ prefetch: { url: '/test2' } }); - this.dataset1.initialize(); - this.dataset2.initialize(); + this.bloodhound1 = new Bloodhound({ prefetch: '/test1' }); + this.bloodhound2 = new Bloodhound({ prefetch: { url: '/test2' } }); + this.bloodhound1.initialize(); + this.bloodhound2.initialize(); ajaxRequests[0].response(fixtures.ajaxResps.ok); ajaxRequests[1].response(fixtures.ajaxResps.ok); @@ -77,8 +77,8 @@ describe('Dataset', function() { expect(ajaxRequests[0].url).toBe('/test1'); expect(ajaxRequests[1].url).toBe('/test2'); - this.dataset1.get('big', spy1); - this.dataset2.get('big', spy2); + this.bloodhound1.get('big', spy1); + this.bloodhound2.get('big', spy2); expect(spy1).toHaveBeenCalledWith([ { value: 'big' }, @@ -99,16 +99,16 @@ describe('Dataset', function() { filterSpy = jasmine.createSpy().andCallFake(fakeFilter); spy = jasmine.createSpy(); - this.dataset = new Dataset({ + this.bloodhound = new Bloodhound({ prefetch: { url: '/test', filter: filterSpy } }); - this.dataset.initialize(); + this.bloodhound.initialize(); mostRecentAjaxRequest().response(fixtures.ajaxResps.ok); expect(filterSpy).toHaveBeenCalled(); - this.dataset.get('big', spy); + this.bloodhound.get('big', spy); expect(spy).toHaveBeenCalledWith([ { value: 'BIG' }, @@ -124,13 +124,13 @@ describe('Dataset', function() { it('should not make a request if data is available in storage', function() { var that = this, spy = jasmine.createSpy(); - this.dataset = new Dataset({ name: 'name', prefetch: '/test' }); - this.dataset.storage.get.andCallFake(fakeGet); - this.dataset.initialize(); + this.bloodhound = new Bloodhound({ name: 'name', prefetch: '/test' }); + this.bloodhound.storage.get.andCallFake(fakeGet); + this.bloodhound.initialize(); expect(mostRecentAjaxRequest()).toBeNull(); - this.dataset.get('big', spy); + this.bloodhound.get('big', spy); expect(spy).toHaveBeenCalledWith([ { value: 'big' }, @@ -149,7 +149,7 @@ describe('Dataset', function() { val = 'http:'; break; case 'thumbprint': - val = that.dataset.prefetch.thumbprint; + val = that.bloodhound.prefetch.thumbprint; break; } @@ -160,29 +160,29 @@ describe('Dataset', function() { describe('remote', function() { it('should perform query substitution on the provided url', function() { - this.dataset1 = new Dataset({ + this.bloodhound1 = new Bloodhound({ remote: { url: '/test?q=$$', wildcard: '$$' } }); - this.dataset2 = new Dataset({ + this.bloodhound2 = new Bloodhound({ remote: { url: '/test?q=%QUERY', replace: function(str, query) {return str.replace('%QUERY', query); } } }); - this.dataset1.initialize(); - this.dataset2.initialize(); + this.bloodhound1.initialize(); + this.bloodhound2.initialize(); - this.dataset1.get('one two', $.noop); - this.dataset2.get('one two', $.noop); + this.bloodhound1.get('one two', $.noop); + this.bloodhound2.get('one two', $.noop); - expect(this.dataset1.transport.get).toHaveBeenCalledWith( + expect(this.bloodhound1.transport.get).toHaveBeenCalledWith( '/test?q=one%20two', { method: 'get', dataType: 'json' }, jasmine.any(Function) ); - expect(this.dataset2.transport.get).toHaveBeenCalledWith( + expect(this.bloodhound2.transport.get).toHaveBeenCalledWith( '/test?q=one two', { method: 'get', dataType: 'json' }, jasmine.any(Function) @@ -195,13 +195,13 @@ describe('Dataset', function() { spy = jasmine.createSpy(); filterSpy = jasmine.createSpy().andCallFake(fakeFilter); - this.dataset = new Dataset({ + this.bloodhound = new Bloodhound({ remote: { url: '/test', filter: filterSpy } }); - this.dataset.initialize(); + this.bloodhound.initialize(); - this.dataset.transport.get.andCallFake(fakeGet); - this.dataset.get('big', spy); + this.bloodhound.transport.get.andCallFake(fakeGet); + this.bloodhound.get('big', spy); waitsFor(function() { return spy.callCount; }); @@ -227,11 +227,11 @@ describe('Dataset', function() { it('should call #get callback once if cache hit', function() { var spy = jasmine.createSpy(); - this.dataset = new Dataset({ remote: '/test?q=%QUERY' }); - this.dataset.initialize(); - this.dataset.transport.get.andCallFake(fakeGet); + this.bloodhound = new Bloodhound({ remote: '/test?q=%QUERY' }); + this.bloodhound.initialize(); + this.bloodhound.transport.get.andCallFake(fakeGet); - this.dataset.get('dog', spy); + this.bloodhound.get('dog', spy); expect(spy.callCount).toBe(1); @@ -249,17 +249,17 @@ describe('Dataset', function() { spy1 = jasmine.createSpy(); spy2 = jasmine.createSpy(); - this.dataset = new Dataset({ + this.bloodhound = new Bloodhound({ limit: 3, local: fixtures.data.simple, remote: { url: '/test?q=%QUERY' } }); - this.dataset.initialize(); + this.bloodhound.initialize(); - this.dataset.transport.get.andCallFake(fakeGet); + this.bloodhound.transport.get.andCallFake(fakeGet); - this.dataset.get('big', spy1); - this.dataset.get('bigg', spy2); + this.bloodhound.get('big', spy1); + this.bloodhound.get('bigg', spy2); expect(spy1.callCount).toBe(1); expect(spy2.callCount).toBe(1); diff --git a/test/helpers/typeahead_mocks.js b/test/helpers/typeahead_mocks.js index 2306d526..1c8ea64a 100644 --- a/test/helpers/typeahead_mocks.js +++ b/test/helpers/typeahead_mocks.js @@ -2,7 +2,7 @@ var components; components = [ - 'Dataset', + 'Bloodhound', 'PersistentStorage', 'Transport', 'SearchIndex', From 9e5ddf4a74ccdd9d7a1b75af73e2d8898ceba2bc Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 30 Dec 2013 04:42:08 -0800 Subject: [PATCH 04/17] Rename section to dataset. Conflicts: src/typeahead/plugin.js --- Gruntfile.js | 2 +- karma.conf.js | 2 +- src/bloodhound/bloodhound.js | 2 +- src/typeahead/{section.js => dataset.js} | 24 +++--- src/typeahead/dropdown.js | 38 ++++----- src/typeahead/html.js | 2 +- src/typeahead/plugin.js | 44 ++++++++--- src/typeahead/typeahead.js | 18 ++--- ...tion_view_spec.js => dataset_view_spec.js} | 78 +++++++++---------- test/dropdown_view_spec.js | 42 +++++----- test/fixtures/html.js | 4 +- test/helpers/typeahead_mocks.js | 2 +- test/playground.html | 1 + test/typeahead_view_spec.js | 8 +- 14 files changed, 145 insertions(+), 122 deletions(-) rename src/typeahead/{section.js => dataset.js} (87%) rename test/{section_view_spec.js => dataset_view_spec.js} (61%) diff --git a/Gruntfile.js b/Gruntfile.js index 36ec2055..328dc9fc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -19,7 +19,7 @@ var semver = require('semver'), 'src/typeahead/event_emitter.js', 'src/typeahead/highlight.js', 'src/typeahead/input.js', - 'src/typeahead/section.js', + 'src/typeahead/dataset.js', 'src/typeahead/dropdown.js', 'src/typeahead/typeahead.js', 'src/typeahead/plugin.js' diff --git a/karma.conf.js b/karma.conf.js index 82fa3873..8a7719fe 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -22,7 +22,7 @@ files = [ 'src/typeahead/event_emitter.js', 'src/typeahead/highlight.js', 'src/typeahead/input.js', - 'src/typeahead/section.js', + 'src/typeahead/dataset.js', 'src/typeahead/dropdown.js', 'src/typeahead/typeahead.js', 'src/typeahead/plugin.js', diff --git a/src/bloodhound/bloodhound.js b/src/bloodhound/bloodhound.js index b0096818..3c5dcbd0 100644 --- a/src/bloodhound/bloodhound.js +++ b/src/bloodhound/bloodhound.js @@ -148,7 +148,7 @@ var Bloodhound = (function() { this.local && deferred.done(addLocalToIndex); this.transport = this.remote ? new Transport(this.remote) : null; - this.initialize = function initialize() { return that; }; + this.initialize = function initialize() { return deferred.promise(); }; return deferred.promise(); diff --git a/src/typeahead/section.js b/src/typeahead/dataset.js similarity index 87% rename from src/typeahead/section.js rename to src/typeahead/dataset.js index 191ab9c3..46082e35 100644 --- a/src/typeahead/section.js +++ b/src/typeahead/dataset.js @@ -4,13 +4,13 @@ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ -var Section = (function() { - var sectionKey = 'ttSection', valueKey = 'ttValue', datumKey = 'ttDatum'; +var Dataset = (function() { + var datasetKey = 'ttDataset', valueKey = 'ttValue', datumKey = 'ttDatum'; // constructor // ----------- - function Section(o) { + function Dataset(o) { o = o || {}; o.templates = o.templates || {}; @@ -18,7 +18,7 @@ var Section = (function() { $.error('missing source'); } - // tracks the last query the section was updated for + // tracks the last query the dataset was updated for this.query = null; this.highlight = !!o.highlight; @@ -29,28 +29,28 @@ var Section = (function() { this.templates = getTemplates(o.templates, this.valueKey); - this.$el = $(html.section.replace('%CLASS%', this.name)); + this.$el = $(html.dataset.replace('%CLASS%', this.name)); } // static methods // -------------- - Section.extractSectionName = function extractSectionName(el) { - return $(el).data(sectionKey); + Dataset.extractDatasetName = function extractDatasetName(el) { + return $(el).data(datasetKey); }; - Section.extractValue = function extractDatum(el) { + Dataset.extractValue = function extractDatum(el) { return $(el).data(valueKey); }; - Section.extractDatum = function extractDatum(el) { + Dataset.extractDatum = function extractDatum(el) { return $(el).data(datumKey); }; // instance methods // ---------------- - _.mixin(Section.prototype, EventEmitter, { + _.mixin(Dataset.prototype, EventEmitter, { // ### private @@ -99,7 +99,7 @@ var Section = (function() { innerHtml = that.templates.suggestion(suggestion); outerHtml = html.suggestion.replace('%BODY%', innerHtml); $el = $(outerHtml) - .data(sectionKey, that.name) + .data(datasetKey, that.name) .data(valueKey, suggestion[that.valueKey]) .data(datumKey, suggestion); @@ -154,7 +154,7 @@ var Section = (function() { } }); - return Section; + return Dataset; // helper functions // ---------------- diff --git a/src/typeahead/dropdown.js b/src/typeahead/dropdown.js index 3c7c1bee..5ae8c4f2 100644 --- a/src/typeahead/dropdown.js +++ b/src/typeahead/dropdown.js @@ -16,14 +16,14 @@ var Dropdown = (function() { o = o || {}; if (!o.menu) { - $.error('menu and/or sections are required'); + $.error('menu is required'); } this.isOpen = false; this.isEmpty = true; this.isMouseOverDropdown = false; - this.sections = _.map(o.sections, initializeSection); + this.datasets = _.map(o.datasets, initializeDataset); // bound functions onMouseEnter = _.bind(this._onMouseEnter, this); @@ -39,9 +39,9 @@ var Dropdown = (function() { .on('mouseenter.tt', '.tt-suggestion', onSuggestionMouseEnter) .on('mouseleave.tt', '.tt-suggestion', onSuggestionMouseLeave); - _.each(this.sections, function(section) { - that.$menu.append(section.getRoot()); - section.onSync('rendered', that._onRendered, that); + _.each(this.datasets, function(dataset) { + that.$menu.append(dataset.getRoot()); + dataset.onSync('rendered', that._onRendered, that); }); } @@ -74,13 +74,13 @@ var Dropdown = (function() { }, _onRendered: function onRendered() { - this.isEmpty = _.every(this.sections, isSectionEmpty); + this.isEmpty = _.every(this.datasets, isDatasetEmpty); this.isEmpty ? this._hide() : (this.isOpen && this._show()); - this.trigger('sectionRendered'); + this.trigger('datasetRendered'); - function isSectionEmpty(section) { return section.isEmpty(); } + function isDatasetEmpty(dataset) { return dataset.isEmpty(); } }, _hide: function() { @@ -201,9 +201,9 @@ var Dropdown = (function() { if ($el.length) { datum = { - raw: Section.extractDatum($el), - value: Section.extractValue($el), - sectionName: Section.extractSectionName($el) + raw: Dataset.extractDatum($el), + value: Dataset.extractValue($el), + datasetName: Dataset.extractDatasetName($el) }; } @@ -219,15 +219,15 @@ var Dropdown = (function() { }, update: function update(query) { - _.each(this.sections, updateSection); + _.each(this.datasets, updateDataset); - function updateSection(section) { section.update(query); } + function updateDataset(dataset) { dataset.update(query); } }, empty: function empty() { - _.each(this.sections, clearSection); + _.each(this.datasets, clearDataset); - function clearSection(section) { section.clear(); } + function clearDataset(dataset) { dataset.clear(); } }, isVisible: function isVisible() { @@ -239,9 +239,9 @@ var Dropdown = (function() { this.$menu = null; - _.each(this.sections, destroySection); + _.each(this.datasets, destroyDataset); - function destroySection(section) { section.destroy(); } + function destroyDataset(dataset) { dataset.destroy(); } } }); @@ -250,7 +250,7 @@ var Dropdown = (function() { // helper functions // ---------------- - function initializeSection(oSection) { - return new Section(oSection); + function initializeDataset(oDataset) { + return new Dataset(oDataset); } })(); diff --git a/src/typeahead/html.js b/src/typeahead/html.js index 798315bb..6061cf36 100644 --- a/src/typeahead/html.js +++ b/src/typeahead/html.js @@ -7,7 +7,7 @@ var html = { wrapper: '', dropdown: '', - section: '
', + dataset: '
', suggestions: '', suggestion: '
%BODY%
', }; diff --git a/src/typeahead/plugin.js b/src/typeahead/plugin.js index abb56d27..f5c6b104 100644 --- a/src/typeahead/plugin.js +++ b/src/typeahead/plugin.js @@ -10,34 +10,55 @@ typeaheadKey = 'ttTypeahead'; methods = { - initialize: function initialize(o /*, splot of sections */) { - var sections = [].slice.call(arguments, 1); + initialize: function initialize(o /*, splot of datasets */) { + var datasets = [].slice.call(arguments, 1); o = o || {}; return this.each(attach); function attach() { - var $input = $(this), typeahead; + var $input = $(this), promises, eventBus, typeahead; + + promises = _.map(datasets, function(dataset) { + var promise = $.Deferred().resolve().promise(); - _.each(sections, function(section) { // HACK: force highlight as a top-level config - section.highlight = !!o.highlight; + dataset.highlight = !!o.highlight; // if source is an object, convert it to a bloodhound - section.source = _.isObject(section.source) ? - bloodhoundAdapter(section.source).initialize() : section.source; + if (_.isObject(dataset.source)) { + dataset.source = bloodhoundAdapter(dataset.source); + promise = dataset.source.initialize(); + } + + return promise; }); + eventBus = new EventBus({ el: $input }); + typeahead = new Typeahead({ input: $input, + eventBus: eventBus, withHint: _.isUndefined(o.hint) ? true : !!o.hint, minLength: o.minLength, autoselect: o.autoselect, - sections: sections + datasets: datasets }); $input.data(typeaheadKey, typeahead); + + $.when.apply($, promises) + .done(trigger('initialized')) + .fail(trigger('initialized:err')); + + // defer trigging of events to make it possible to attach + // a listener immediately after jQuery#typeahead is invoked + function trigger(eventName) { + return function() { + _.defer(function() { eventBus.trigger(eventName); }); + }; + } } }, @@ -114,10 +135,10 @@ jQuery.fn.typeahead.bloodhoundAdapter = bloodhoundAdapter; - function bloodhoundAdapter() { - var source; + function bloodhoundAdapter(o) { + var bloodhound, source; - bloodhound = _.isObject(bloodhound) ? new Bloodhound(bloodhound) : bloodhound; + bloodhound = (o instanceof Bloodhound) ? o : new Bloodhound(o); source = _.bind(bloodhound.get, bloodhound); source.initialize = function() { @@ -128,6 +149,7 @@ source.add = function(data) { bloodhound.add(); + return dataSource.initialize(); }; return source; diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 94db7ad4..346a0a05 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -10,9 +10,9 @@ var Typeahead = (function() { // constructor // ----------- - // THOUGHT: what if sections could dynamically be added/removed? + // THOUGHT: what if datasets could dynamically be added/removed? function Typeahead(o) { - var $menu, $input, $hint, sections; + var $menu, $input, $hint, datasets; o = o || {}; @@ -28,15 +28,15 @@ var Typeahead = (function() { $input = this.$node.find('.tt-input'); $hint = this.$node.find('.tt-hint'); - this.eventBus = new EventBus({ el: $input }); + this.eventBus = o.eventBus || new EventBus({ el: $input }); - this.dropdown = new Dropdown({ menu: $menu, sections: o.sections }) + this.dropdown = new Dropdown({ menu: $menu, datasets: o.datasets }) .onSync('suggestionClicked', this._onSuggestionClicked, this) .onSync('cursorMoved', this._onCursorMoved, this) .onSync('cursorRemoved', this._onCursorRemoved, this) .onSync('opened', this._onOpened, this) .onSync('closed', this._onClosed, this) - .onAsync('sectionRendered', this._onSectionRendered, this); + .onAsync('datasetRendered', this._onDatasetRendered, this); this.input = new Input({ input: $input, hint: $hint }) .onSync('focused', this._onFocused, this) @@ -86,7 +86,7 @@ var Typeahead = (function() { this.input.clearHint(); this.input.setInputValue(datum.value, true); - this.eventBus.trigger('cursorchanged', datum.raw, datum.sectionName); + this.eventBus.trigger('cursorchanged', datum.raw, datum.datasetName); }, _onCursorRemoved: function onCursorRemoved() { @@ -94,7 +94,7 @@ var Typeahead = (function() { this._updateHint(); }, - _onSectionRendered: function onSectionRendered() { + _onDatasetRendered: function onDatasetRendered() { this._updateHint(); }, @@ -236,7 +236,7 @@ var Typeahead = (function() { datum = this.dropdown.getDatumForTopSuggestion(); datum && this.input.setInputValue(datum.value); - this.eventBus.trigger('autocompleted', datum.raw, datum.sectionName); + this.eventBus.trigger('autocompleted', datum.raw, datum.datasetName); } }, @@ -253,7 +253,7 @@ var Typeahead = (function() { // defer the closing of the dropdown otherwise it'll stay open _.defer(_.bind(this.dropdown.close, this.dropdown)); - this.eventBus.trigger('selected', datum.raw, datum.sectionName); + this.eventBus.trigger('selected', datum.raw, datum.datasetName); }, // ### public diff --git a/test/section_view_spec.js b/test/dataset_view_spec.js similarity index 61% rename from test/section_view_spec.js rename to test/dataset_view_spec.js index 80c2ec08..cfb5c24f 100644 --- a/test/section_view_spec.js +++ b/test/dataset_view_spec.js @@ -1,7 +1,7 @@ -describe('Section', function() { +describe('Dataset', function() { beforeEach(function() { - this.section = new Section({ + this.dataset = new Dataset({ name: 'test', source: this.source = jasmine.createSpy('source') }); @@ -10,27 +10,27 @@ describe('Section', function() { it('should throw an error if source is missing', function() { expect(noSource).toThrow(); - function noSource() { new Section(); } + function noSource() { new Dataset(); } }); describe('#getRoot', function() { it('should return the root element', function() { - expect(this.section.getRoot()).toBe('div.tt-section-test'); + expect(this.dataset.getRoot()).toBe('div.tt-dataset-test'); }); }); describe('#update', function() { it('should render suggestions', function() { this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('woah'); + this.dataset.update('woah'); - expect(this.section.getRoot()).toContainText('one'); - expect(this.section.getRoot()).toContainText('two'); - expect(this.section.getRoot()).toContainText('three'); + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); }); it('should render empty when no suggestions are available', function() { - this.section = new Section({ + this.dataset = new Dataset({ source: this.source, templates: { empty: '

empty

' @@ -38,13 +38,13 @@ describe('Section', function() { }); this.source.andCallFake(fakeGetWithSyncEmptyResults); - this.section.update('woah'); + this.dataset.update('woah'); - expect(this.section.getRoot()).toContainText('empty'); + expect(this.dataset.getRoot()).toContainText('empty'); }); it('should render header', function() { - this.section = new Section({ + this.dataset = new Dataset({ source: this.source, templates: { header: '

header

' @@ -52,13 +52,13 @@ describe('Section', function() { }); this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('woah'); + this.dataset.update('woah'); - expect(this.section.getRoot()).toContainText('header'); + expect(this.dataset.getRoot()).toContainText('header'); }); it('should render footer', function() { - this.section = new Section({ + this.dataset = new Dataset({ source: this.source, templates: { footer: function(c) { return '

' + c.query + '

'; } @@ -66,13 +66,13 @@ describe('Section', function() { }); this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('woah'); + this.dataset.update('woah'); - expect(this.section.getRoot()).toContainText('woah'); + expect(this.dataset.getRoot()).toContainText('woah'); }); it('should not render header/footer if there is no content', function() { - this.section = new Section({ + this.dataset = new Dataset({ source: this.source, templates: { header: '

header

', @@ -81,37 +81,37 @@ describe('Section', function() { }); this.source.andCallFake(fakeGetWithSyncEmptyResults); - this.section.update('woah'); + this.dataset.update('woah'); - expect(this.section.getRoot()).not.toContainText('header'); - expect(this.section.getRoot()).not.toContainText('footer'); + expect(this.dataset.getRoot()).not.toContainText('header'); + expect(this.dataset.getRoot()).not.toContainText('footer'); }); it('should not render stale suggestions', function() { this.source.andCallFake(fakeGetWithAsyncResults); - this.section.update('woah'); + this.dataset.update('woah'); this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('nelly'); + this.dataset.update('nelly'); waits(100); runs(function() { - expect(this.section.getRoot()).toContainText('one'); - expect(this.section.getRoot()).toContainText('two'); - expect(this.section.getRoot()).toContainText('three'); - expect(this.section.getRoot()).not.toContainText('four'); - expect(this.section.getRoot()).not.toContainText('five'); + expect(this.dataset.getRoot()).toContainText('one'); + expect(this.dataset.getRoot()).toContainText('two'); + expect(this.dataset.getRoot()).toContainText('three'); + expect(this.dataset.getRoot()).not.toContainText('four'); + expect(this.dataset.getRoot()).not.toContainText('five'); }); }); it('should trigger rendered after suggestions are rendered', function() { var spy; - this.section.onSync('rendered', spy = jasmine.createSpy()); + this.dataset.onSync('rendered', spy = jasmine.createSpy()); this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('woah'); + this.dataset.update('woah'); waitsFor(function() { return spy.callCount; }); }); @@ -120,31 +120,31 @@ describe('Section', function() { describe('#clear', function() { it('should clear suggestions', function() { this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('woah'); + this.dataset.update('woah'); - this.section.clear(); - expect(this.section.getRoot()).toBeEmpty(); + this.dataset.clear(); + expect(this.dataset.getRoot()).toBeEmpty(); }); }); describe('#isEmpty', function() { it('should return true when empty', function() { - expect(this.section.isEmpty()).toBe(true); + expect(this.dataset.isEmpty()).toBe(true); }); it('should return false when not empty', function() { this.source.andCallFake(fakeGetWithSyncResults); - this.section.update('woah'); + this.dataset.update('woah'); - expect(this.section.isEmpty()).toBe(false); + expect(this.dataset.isEmpty()).toBe(false); }); }); describe('#destroy', function() { - it('should null out the reference to the section element', function() { - this.section.destroy(); + it('should null out the reference to the dataset element', function() { + this.dataset.destroy(); - expect(this.section.$el).toBeNull(); + expect(this.dataset.$el).toBeNull(); }); }); diff --git a/test/dropdown_view_spec.js b/test/dropdown_view_spec.js index 2b0cb07b..71f8b3ef 100644 --- a/test/dropdown_view_spec.js +++ b/test/dropdown_view_spec.js @@ -3,24 +3,24 @@ describe('Dropdown', function() { beforeEach(function() { var $fixture; - jasmine.Section.useMock(); + jasmine.Dataset.useMock(); setFixtures(fixtures.html.menu); $fixture = $('#jasmine-fixtures'); this.$menu = $fixture.find('.tt-dropdown-menu'); - this.$menu.html(fixtures.html.section); + this.$menu.html(fixtures.html.dataset); - this.view = new Dropdown({ menu: this.$menu, sections: [{}] }); - this.section = this.view.sections[0]; + this.view = new Dropdown({ menu: this.$menu, datasets: [{}] }); + this.dataset = this.view.datasets[0]; }); - it('should throw an error if menu and/or sections is missing', function() { + it('should throw an error if menu and/or datasets is missing', function() { expect(noMenu).toThrow(); - expect(noSections).toThrow(); + expect(noDatasets).toThrow(); function noMenu() { new Dropdown({ menu: '.menu' }); } - function noSections() { new Dropdown({ sections: true }); } + function noDatasets() { new Dropdown({ datasets: true }); } }); describe('when mouseenter is triggered', function() { @@ -95,32 +95,32 @@ describe('Dropdown', function() { }); }); - describe('when rendered is triggered on a section', function() { + describe('when rendered is triggered on a dataset', function() { it('should hide the dropdown if empty', function() { - this.section.isEmpty.andReturn(true); + this.dataset.isEmpty.andReturn(true); this.view.open(); this.view._show(); - this.section.trigger('rendered'); + this.dataset.trigger('rendered'); expect(this.$menu).not.toBeVisible(); }); it('should show the dropdown if not empty', function() { - this.section.isEmpty.andReturn(false); + this.dataset.isEmpty.andReturn(false); this.view.open(); this.view._hide(); - this.section.trigger('rendered'); + this.dataset.trigger('rendered'); expect(this.$menu).toBeVisible(); }); - it('should trigger sectionRendered', function() { + it('should trigger datasetRendered', function() { var spy; - this.view.onSync('sectionRendered', spy = jasmine.createSpy()); - this.section.trigger('rendered'); + this.view.onSync('datasetRendered', spy = jasmine.createSpy()); + this.dataset.trigger('rendered'); expect(spy).toHaveBeenCalled(); }); @@ -304,16 +304,16 @@ describe('Dropdown', function() { }); describe('#update', function() { - it('should invoke update on each section', function() { + it('should invoke update on each dataset', function() { this.view.update(); - expect(this.section.update).toHaveBeenCalled(); + expect(this.dataset.update).toHaveBeenCalled(); }); }); describe('#empty', function() { - it('should invoke clear on each section', function() { + it('should invoke clear on each dataset', function() { this.view.empty(); - expect(this.section.clear).toHaveBeenCalled(); + expect(this.dataset.clear).toHaveBeenCalled(); }); }); @@ -352,10 +352,10 @@ describe('Dropdown', function() { expect($menu.off).toHaveBeenCalledWith('.tt'); }); - it('should destroy its sections', function() { + it('should destroy its datasets', function() { this.view.destroy(); - expect(this.section.destroy).toHaveBeenCalled(); + expect(this.dataset.destroy).toHaveBeenCalled(); }); it('should null out its reference to the menu element', function() { diff --git a/test/fixtures/html.js b/test/fixtures/html.js index f1f482d4..3b4a284d 100644 --- a/test/fixtures/html.js +++ b/test/fixtures/html.js @@ -5,8 +5,8 @@ fixtures.html = { input: '', hint: '', menu: '', - section: [ - '
', + dataset: [ + '
', '', '

one

', '

two

', diff --git a/test/helpers/typeahead_mocks.js b/test/helpers/typeahead_mocks.js index 1c8ea64a..4df2529d 100644 --- a/test/helpers/typeahead_mocks.js +++ b/test/helpers/typeahead_mocks.js @@ -7,7 +7,7 @@ 'Transport', 'SearchIndex', 'Input', - 'Section', + 'Dataset', 'Dropdown' ]; diff --git a/test/playground.html b/test/playground.html index d45047ea..98589891 100644 --- a/test/playground.html +++ b/test/playground.html @@ -175,6 +175,7 @@ $('input').on([ 'typeahead:initialized', + 'typeahead:initialized:err', 'typeahead:selected', 'typeahead:autocompleted', 'typeahead:cursorchanged', diff --git a/test/typeahead_view_spec.js b/test/typeahead_view_spec.js index 1ac22a26..8a1da234 100644 --- a/test/typeahead_view_spec.js +++ b/test/typeahead_view_spec.js @@ -5,7 +5,7 @@ describe('Typeahead', function() { var $fixture, $input; jasmine.Input.useMock(); - jasmine.Section.useMock(); + jasmine.Dataset.useMock(); jasmine.Dropdown.useMock(); setFixtures(fixtures.html.textInput); @@ -18,7 +18,7 @@ describe('Typeahead', function() { this.view = new Typeahead({ input: this.$input, withHint: true, - sections: {} + datasets: {} }); this.input = this.view.input; @@ -94,14 +94,14 @@ describe('Typeahead', function() { }); }); - describe('when dropdown triggers sectionRendered', function() { + describe('when dropdown triggers datasetRendered', function() { it('should update the hint asynchronously', function() { this.dropdown.getDatumForTopSuggestion.andReturn(testDatum); this.dropdown.isVisible.andReturn(true); this.input.hasOverflow.andReturn(false); this.input.getInputValue.andReturn(testDatum.value.slice(0, 2)); - this.dropdown.trigger('sectionRendered'); + this.dropdown.trigger('datasetRendered'); waitsFor(function() { return !!this.input.setHintValue.callCount; From 561ef555888035eb596092a9686b403dcbc44a5d Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 30 Dec 2013 09:26:44 -0800 Subject: [PATCH 05/17] Support bloodhound instances and update README. --- README.md | 206 +++++++++++++++++++---------------- src/bloodhound/bloodhound.js | 2 +- src/typeahead/plugin.js | 46 ++++---- 3 files changed, 141 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index ffa7c89f..4afce6c7 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ Inspired by [twitter.com]'s autocomplete search functionality, typeahead.js is a flexible JavaScript library that provides a strong foundation for building robust typeaheads. -The typeahead.js library is built on top of 2 components: the data component, -[dataset](#dataset), and the UI component, [typeahead](#typeahead). Datasets are -responsible for providing suggestions for a given query. Typeaheads are -responsible for rendering suggestions and handling DOM interactions. Both -components can be used separately, but when used together, they provided a rich -typeahead experience. +The typeahead.js library consists of 2 components: the suggestion engine, +[Bloodhound](#bloodhound), and the UI view, [Typeahead](#typeahead). +The suggestion engine is responsible for computing suggestions for a given +query. The UI view is responsible for rendering suggestions and handling DOM +interactions. Both components can be used separately, but when used together, +they can provided a rich typeahead experience. @@ -30,18 +30,19 @@ Preferred method: Other methods: * [Download zipball of latest release][zipball]. * Download the latest dist files individually: - * *[dataset.js]* - * *[typeahead.js]* - * *[typeahead.bundle.js]* (dataset + typeahead) + * *[bloodhound.js]* (standalone suggestion engine) + * *[typeahead.js]* (standalone UI view) + * *[typeahead.bundle.js]* (*bloodhound.js* + *typeahead.js*) * *[typeahead.bundle.min.js]* -**Note:** both dataset.js and typeahead.js have a dependency on [jQuery] 1.9+. +**Note:** both *bloodhound.js* and *typeahead.js* have a dependency on +[jQuery] 1.9+. [Bower]: http://bower.io/ [zipball]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.js.zip -[dataset.js]: http://twitter.github.com/typeahead.js/releases/latest/dataset.js +[bloodhound.js]: http://twitter.github.com/typeahead.js/releases/latest/bloodhound.js [typeahead.js]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.js [typeahead.bundle.js]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.bundle.js [typeahead.bundle.min.js]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.bundle.min.js @@ -55,12 +56,12 @@ Table of Contents * [Typeahead](#typeahead) * [API](#typeahead-api) * [Options](#typeahead-options) - * [Sections](#sections) + * [Datasets](#datasets) * [Custom Events](#custom-events) * [Look and Feel](#look-and-feel) -* [Dataset](#dataset) - * [API](#dataset-api) - * [Options](#dataset-options) +* [Bloodhound](#bloodhound) + * [API](#bloodhound-api) + * [Options](#bloodhound-options) * [Prefetch](#prefetch) * [Remote](#remote) * [Tokens](#tokens) @@ -86,7 +87,7 @@ Features * Highlights query matches within the suggestion * Triggers custom events -**Dataset** +**Bloodhound** * Works with hardcoded data * Prefetches data on initialization to reduce suggestion latency @@ -97,7 +98,7 @@ Features Examples -------- -For some working examples of typeahead.js, visit our [examples page]. +For some working examples of typeahead.js, visit the [examples page]. @@ -112,14 +113,13 @@ interactions. ### Typeahead API -#### jQuery#typeahead(options, [\*sections]) +#### jQuery#typeahead(options, [\*datasets]) Turns any `input[type="text"]` element into a typeahead. `options` is an -options hash that's used to configure the typeahead to your liking. For more -info about what options are available, check out the -[Options](#typeahead-options) section. Subsequent arguments (`\*sections`), are -individual option hashes for sections. Refer to [Sections](#sections) for more -info. +options hash that's used to configure the typeahead to your liking. +[Typeahead Options](#typeahead-options) goes over the available configs. +Subsequent arguments (`\*datasets`), are individual option hashes for datasets. +For more details regarding datasets, refer to [Datasets](#datasets). ```javascript $('.typeahead').typeahead({ @@ -136,8 +136,8 @@ $('.typeahead').typeahead({ #### jQuery#typeahead('destroy') -Removes typeahead.js functionality and reverts the `input` element back to how -it was before it was turned into a typeahead. +Removes typeahead functionality and reverts the `input` element back to its +original state. ```javascript $('.typeahead').typeahead('destroy'); @@ -146,7 +146,7 @@ $('.typeahead').typeahead('destroy'); #### jQuery#typeahead('open') Opens the dropdown menu of typeahead. Note that being open does not mean that -the menu is visible. The menu is only visible when it is open and not empty. +the menu is visible. The menu is only visible when it is open and has content. ```javascript $('.typeahead').typeahead('open'); @@ -181,8 +181,9 @@ $('.typeahead').typeahead('val', myVal); When initializing a typeahead, there are a number of options you can configure. -* `autoselect` – If `true`, when the dropdown menu is open and the user hits - enter, the top suggestion will be selected. Defaults to `false`. +* `autoselect` – If `true`, defaults the suggestion selection to the top + suggestion when the user keys enter while the dropdown menu is open. Defaults + to `false`. * `highlight` – If `true`, when suggestions are rendered, pattern matches for the current query in text nodes will be wrapped in a `strong` element. @@ -191,49 +192,58 @@ When initializing a typeahead, there are a number of options you can configure. * `hint` – If `false`, the typeahead will not show a hint. Defaults to `true`. * `minLength` – The minimum character length needed before suggestions start - getting renderd. Defaults to `1`. + getting rendered. Defaults to `1`. -### Sections +### Dataset -A typeahead is composed of one or more sections. For simple use cases, one -section will usually suffice. If however you wanted to build something like -the search typeahead on twitter.com, you'd need multiple sections. +A typeahead is composed of one or more datasets. For simple use cases, one +dataset will usually suffice. If however you wanted to build something like +the search typeahead on twitter.com, you'd need multiple datasets. -Sections can be configured using the following options. +Datasets can be configured using the following options. -* `name` – The name of the section. This will be prepened to `tt-section-` to - form the class name of the containging DOM element. Defaults to a random +* `name` – The name of the dataset. This will be prepended to `tt-dataset-` to + form the class name of the containing DOM element. Defaults to a random number. -* `source` – The backing data source for the section. Can be either a function - with the signature `(query, cb)` or a - [dataset options hash](#dataset-options). If the former, it is expected `cb` - will be invoked with an array of [datums](#datums) that are a match for - `query`. If the latter, a dataset will be constructed and initialized and - that will be used as the backing data source. **Required**. +* `source` – The backing data source that provides suggestions. Expected to be + either an [options hash for a bloodhound](#bloodhound-options), an instance + of a Bloodhoud, or a function with the signature `(query, cb)`. If the last + option is used, it is expected that the function will compute the suggestion + set for `query`, convert the suggestions to an array of [datums](#datums), + and then invoke `cb` with that array as the first and only argument. + + that `cb` will be invoked with an array of + [datums](#datums) that are a match for `query`. **Required**. -* `templates` – A hash of templates to be used when rendering the section. +* `templates` – A hash of templates to be used when rendering the dataset. * `empty` – Rendered when `0` suggestions are available for the given query. Can be either a HTML string or a precompiled template. If it's a precompiled template, the passed in context will contain `query`. - * `footer`– Rendered at the bottom of the section. Can be either a HTML + * `footer`– Rendered at the bottom of the dataset. Can be either a HTML string or a precompiled template. If it's a precompiled template, the passed in context will contain `query` and `isEmpty`. - * `header` – Rendered at the top of the section. Can be either a HTML string + * `header` – Rendered at the top of the dataset. Can be either a HTML string or a precompiled template. If it's a precompiled template, the passed in context will contain `query` and `isEmpty`. * `suggestion` – Used to render a single suggestion. If set, this has to be a - precompiled tempate. The associated datum object will serves as the context. - Defaults to the value of the datum wrapped in a `p` tag. + precompiled template. The associated datum object will serves as the context. + Defaults to the value of the datum wrapped in a `p` tag i.e. + `

{{query}}

`. ### Custom Events The typeahead component triggers the following custom events. +* `typeahead:initialized` – Triggered after the source of each dataset has + been initialized. Note, for non-bloodhound sources, initialization cannot + be detected so those sources are ignored i.e. if you use all non-bloodhound + sources, this event will be triggered right away. + * `typeahead:opened` – Triggered when the dropdown menu of a typeahead is opened. @@ -243,16 +253,16 @@ The typeahead component triggers the following custom events. * `typeahead:cursorchanged` – Triggered when the dropdown menu cursor is moved to a different suggestion. The datum for the suggestion that the cursor was moved to is passed to the event handler as an argument in addition to the name - of the section it belongs to. + of the dataset it belongs to. * `typeahead:selected` – Triggered when a suggestion from the dropdown menu is selected. The datum for the selected suggestion is passed to the event handler - as an argument in addition to the name of the section it belongs to. + as an argument in addition to the name of the dataset it belongs to. * `typeahead:autocompleted` – Triggered when the query is autocompleted. Autocompleted means the query was changed to the hint. The datum used for autocompletion is passed to the event handler as an argument in addition to - the name of the section it belongs to. + the name of the dataset it belongs to. All custom events are triggered on the element initialized as a typeahead. @@ -260,12 +270,12 @@ All custom events are triggered on the element initialized as a typeahead. Below is a faux mustache template describing the DOM structure of a typeahead dropdown menu. Keep in mind that `header`, `footer`, `suggestion`, and `empty` -come from the templates mentioned [here](#sections). +come from the provided templates detailed [here](#datasets). ```html - {{#sections}} -
+ {{#datasets}} +
{{{header}}} {{#suggestions}} @@ -277,7 +287,7 @@ come from the templates mentioned [here](#sections). {{{footer}}}
- {{/sections}} + {{/datasets}} ``` @@ -285,71 +295,80 @@ When an end-user mouses or keys over a `.tt-suggestion`, the class `tt-cursor` will be added to it. You can use this class as a hook for styling the "under cursor" state of suggestions. -Dataset -------- +Bloodhound +---------- -Datasets are the data component that is responsible for computing a suggestion -set for a given query. They're robust, flexible, and offer advanced +Bloodhounds are the data components responsible for computing a set of +suggestions for a given query. They're robust, flexible, and offer advanced functionality such as prefetching, intelligent caching, fast lookups, and backfilling with remote data. -### Dataset API +### Bloodhound API #### Constructor -The constructor function. It takes an [options hash](#dataset-options). +The constructor function. It takes an [options hash](#bloodhound-options). ```javascript -var dataset = new Dataset({ - name: myDatasetName, +var bloodhound = new Bloodhound({ + name: 'animals', local: ['dog', 'pig', 'moose'], remote: 'http://example.com/animals?q=%QUERY' }); ``` -#### Dataset#initialize() +#### Bloodhound#initialize() -Kicks off the initialization of the dataset. This includes processing the data -provided through `local` and fetching and processing the data provided by -`prefetch`. `Dataset#get` and `Dataset#add` will be useless until this method -is called. +Kicks off the initialization of the bloodhound. This includes processing the +data provided through `local` and fetching and processing the data provided by +`prefetch`. `Bloodhound#get` will be useless until the bloodhound has been +intialized. Returns a [jQuery promise] which is resolved when the fetching and +processing has finished. ```javascript -dataset.initialize(); +bloodhound.initialize(); ``` -#### Dataset#get(query, cb) + + +[jQuery promise]: http://api.jquery.com/Types/#Promise + +#### Bloodhound#get(query, cb) -Retrieves datums from the dataset matching `query` and invokes `cb` with +Retrieves datums from the data source matching `query` and invokes `cb` with them. `cb` will always be called at least once with the mixed results from `local` and `prefetch`. If those results are insufficent, `cb` will be called again later with the mixed results from `local`, `prefetch`, **and** `remote`. ```javascript -dataset.get(myQuery, function(suggestions) { +bloodhound.get(myQuery, function(suggestions) { suggestions.each(function(suggestion) { console.log(suggestion.value); }); }); ``` -### Dataset Options +### Bloodhound Options -When initializing a dataset, there are a number of options you can configure. +When initializing a Bloodhound, there are a number of options you can +configure. * `valueKey` – The key used to access the value of the datum in the datum object. Defaults to `value`. -* `limit` – The max number of suggestions to return from `Dataset#get`. If not - reached, the dataset will attempt to backfill the suggestions from `remote`. +* `limit` – The max number of suggestions to return from `Bloodhound#get`. If + not reached, the data source will attempt to backfill the suggestions from + `remote`. * `tokenizer` – A function with the signature `(str)` that returns an array of tokens. The default implementation of `tokenizer` splits `str` on whitespace. -* `dupChecker` – A function with the signature `(datum1, datum2)` that returns - `true` if the datums are duplicates or `false` otherwise. If `dupChecker` is - `true`, a function that compares value properties will be used. This is used - for making sure no duplicate suggestions are introduced from `remote`. +* `dupDetector` – This is used for making sure no duplicate suggestions are + introduced from `remote`. Expected to be either `true`, `false`, or a + function with the signature `(datum1, datum2)` that returns `true` if the + datums are duplicates or `false` otherwise. If set to `true`, a function that + compares value properties will be used. If set to `false`, duplicate + detection will not be performed. * `sorter` – A [compare function] used to sort matched datums for a given query. @@ -376,8 +395,8 @@ When configuring `prefetch`, the following options are available. * `url` – A URL to a JSON file containing an array of datums. **Required.** -* `cacheKey` – The key data will be stored in local storage under. Defaults to - `url`. +* `cacheKey` – The key that data will be stored in local storage under. + Defaults to `url`. * `ttl` – The time (in milliseconds) the prefetched data should be cached in local storage. Defaults to `86400000` (1 day). @@ -411,7 +430,7 @@ When configuring `remote`, the following options are available. * `replace` – A function with the signature `replace(url, query)` that can be used to override the request URL. Expected to return a valid URL. If set, no - wildcard substitution will be performed on `url`. + `wildcard` substitution will be performed on `url`. * `rateLimitBy` – The method used to rate-limit network requests. Can be either `debounce` or `throttle`. Defaults to `debounce`. @@ -427,18 +446,18 @@ When configuring `remote`, the following options are available. -[ajax settings object]:http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings +[ajax settings object]: http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings ### Tokens -The algorithm used by datasets for providing suggestions for a given query is -token-based. When `Dataset#get` is called, it tokenizes `query` using +The algorithm used by bloodhounds for providing suggestions for a given query +is token-based. When `Bloodhound#get` is called, it tokenizes `query` using `tokenizer` and then invokes `cb` with all of the datums that contain those tokens. -By default, a dataset will generate tokens for a datum by tokenizing its value. -However, it is possible to explicitly set the tokens for a datum by including a -`tokens` property. +By default, a bloodhound will generate tokens for a datum by tokenizing its +value. However, it is possible to explicitly set the tokens for a datum by +including a `tokens` property. ```javascript { @@ -452,19 +471,20 @@ The above datum would be a valid suggestion for queries such as: * `typehead` * `typehead.js` * `autoco` -* `javascript type` +* `java type` Datum ----- The data representation of a suggestion is referred to as a datum. A datum is an object that can contain arbitrary properties. When a suggestion is -rendered, its datum will be the context passed the suggestion template. +rendered, its datum will be the context passed the precompiled suggestion +template. Datums are expected to contain a value property – when a suggestion is selected, this will be what the value of the `input` is set to. By default, it's expected the name of this property will be `value`, but it's configurable. -See [Dataset](#dataset) for more details. +See [Bloodhound Options](#bloodhound-options) for more details. For ease of use, datums can also be represented as a string. Strings found in place of datum objects are implicitly converted to an object with its value @@ -483,7 +503,6 @@ typeahead. The value property here is `handle` and the datum contains additional properties to make it possible to render richer suggestions. This datum also explicitly sets its [tokens](#tokens). - ```javascript { name: 'Jake Harding', @@ -526,7 +545,8 @@ https://github.com/twitter/typeahead.js/issues Versioning ---------- -For transparency and insight into our release cycle, releases will be numbered with the follow format: +For transparency and insight into our release cycle, releases will be numbered +with the follow format: `..` diff --git a/src/bloodhound/bloodhound.js b/src/bloodhound/bloodhound.js index 3c5dcbd0..bb25ab2d 100644 --- a/src/bloodhound/bloodhound.js +++ b/src/bloodhound/bloodhound.js @@ -4,7 +4,7 @@ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ -var Bloodhound = (function() { +var Bloodhound = window.Bloodhound = (function() { var keys; keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' }; diff --git a/src/typeahead/plugin.js b/src/typeahead/plugin.js index f5c6b104..10f73d52 100644 --- a/src/typeahead/plugin.js +++ b/src/typeahead/plugin.js @@ -4,7 +4,7 @@ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ -(function() { +(function(Bloodhound) { var typeaheadKey, methods; typeaheadKey = 'ttTypeahead'; @@ -20,26 +20,24 @@ function attach() { var $input = $(this), promises, eventBus, typeahead; - promises = _.map(datasets, function(dataset) { - var promise = $.Deferred().resolve().promise(); + promises = _.map(datasets, function(d) { + var promise = $.Deferred().resolve(); // HACK: force highlight as a top-level config - dataset.highlight = !!o.highlight; + d.highlight = !!o.highlight; // if source is an object, convert it to a bloodhound - if (_.isObject(dataset.source)) { - dataset.source = bloodhoundAdapter(dataset.source); - promise = dataset.source.initialize(); + if (_.isObject(d.source) || isBloodhound(d.source)) { + d.source = bloodhoundAdapter(d.source); + promise = d.source.initialize(); } return promise; }); - eventBus = new EventBus({ el: $input }); - typeahead = new Typeahead({ input: $input, - eventBus: eventBus, + eventBus: eventBus = new EventBus({ el: $input }), withHint: _.isUndefined(o.hint) ? true : !!o.hint, minLength: o.minLength, autoselect: o.autoselect, @@ -48,9 +46,13 @@ $input.data(typeaheadKey, typeahead); - $.when.apply($, promises) - .done(trigger('initialized')) - .fail(trigger('initialized:err')); + // only trigger these events if at least one dataset is + // using a bloodhound as a source + if (promises.length) { + $.when.apply($, promises) + .done(trigger('initialized')) + .fail(trigger('initialized:err')); + } // defer trigging of events to make it possible to attach // a listener immediately after jQuery#typeahead is invoked @@ -133,25 +135,31 @@ } }; - jQuery.fn.typeahead.bloodhoundAdapter = bloodhoundAdapter; + // helper functions + // ---------------- function bloodhoundAdapter(o) { var bloodhound, source; - bloodhound = (o instanceof Bloodhound) ? o : new Bloodhound(o); + if (!Bloodhound) { + $.error('Bloodhound constructor has not been loaded'); + } + + bloodhound = isBloodhound(o) ? o : new Bloodhound(o); source = _.bind(bloodhound.get, bloodhound); source.initialize = function() { - // returns a promise that is resolved after prefetch data - // is loaded and processed return bloodhound.initialize(); }; source.add = function(data) { bloodhound.add(); - return dataSource.initialize(); }; return source; }; -})(); + + function isBloodhound(obj) { + return Bloodhound && obj instanceof Bloodhound; + } +})(window.Bloodhound); From efc6c1bc18c915cb303874f45f68acb979eaf1ca Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Mon, 30 Dec 2013 09:32:31 -0800 Subject: [PATCH 06/17] Add grunt badge to README. --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4afce6c7..8e407916 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ [![build status](https://secure.travis-ci.org/twitter/typeahead.js.png?branch=master)](http://travis-ci.org/twitter/typeahead.js) +[![Built with Grunt](https://cdn.gruntjs.com/builtwith.png)](http://gruntjs.com/) + [typeahead.js][gh-page] ======================= @@ -118,7 +120,7 @@ interactions. Turns any `input[type="text"]` element into a typeahead. `options` is an options hash that's used to configure the typeahead to your liking. [Typeahead Options](#typeahead-options) goes over the available configs. -Subsequent arguments (`\*datasets`), are individual option hashes for datasets. +Subsequent arguments (`*datasets`), are individual option hashes for datasets. For more details regarding datasets, refer to [Datasets](#datasets). ```javascript @@ -194,7 +196,7 @@ When initializing a typeahead, there are a number of options you can configure. * `minLength` – The minimum character length needed before suggestions start getting rendered. Defaults to `1`. -### Dataset +### Datasets A typeahead is composed of one or more datasets. For simple use cases, one dataset will usually suffice. If however you wanted to build something like @@ -207,14 +209,11 @@ Datasets can be configured using the following options. number. * `source` – The backing data source that provides suggestions. Expected to be - either an [options hash for a bloodhound](#bloodhound-options), an instance - of a Bloodhoud, or a function with the signature `(query, cb)`. If the last - option is used, it is expected that the function will compute the suggestion - set for `query`, convert the suggestions to an array of [datums](#datums), - and then invoke `cb` with that array as the first and only argument. - - that `cb` will be invoked with an array of - [datums](#datums) that are a match for `query`. **Required**. + either an [options hash for a bloodhound](#bloodhound-options), a Bloodhound + instance, or a function with the signature `(query, cb)`. If the last option + is used, it is expected that the function will compute the suggestion set for + `query`, convert the suggestions to an array of [datums](#datums), and then + invoke `cb` with that array as the first and only argument. **Required**. * `templates` – A hash of templates to be used when rendering the dataset. @@ -335,10 +334,10 @@ bloodhound.initialize(); #### Bloodhound#get(query, cb) -Retrieves datums from the data source matching `query` and invokes `cb` with -them. `cb` will always be called at least once with the mixed results from -`local` and `prefetch`. If those results are insufficent, `cb` will be called -again later with the mixed results from `local`, `prefetch`, **and** `remote`. +Retrieves suggestions for `query` and invokes `cb` with them. `cb` will always +be called at least once with the mixed results from `local` and `prefetch`. If +those results are insufficent, `cb` will be called again later with the mixed +results from `local`, `prefetch`, **and** `remote`. ```javascript bloodhound.get(myQuery, function(suggestions) { @@ -456,8 +455,8 @@ is token-based. When `Bloodhound#get` is called, it tokenizes `query` using tokens. By default, a bloodhound will generate tokens for a datum by tokenizing its -value. However, it is possible to explicitly set the tokens for a datum by -including a `tokens` property. +value with `tokenizer`. However, it is possible to explicitly set the tokens +for a datum by including a `tokens` property. ```javascript { From 8909c6115c88aa6fed4580a9b39f0488effa4ce9 Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Sat, 1 Feb 2014 21:44:46 -0800 Subject: [PATCH 07/17] Make final API changes for v0.10. --- src/bloodhound/bloodhound.js | 147 ++++--------------------------- src/bloodhound/options_parser.js | 87 ++++++++++++++++++ src/bloodhound/search_index.js | 22 ++--- src/typeahead/dataset.js | 4 +- src/typeahead/plugin.js | 54 +----------- 5 files changed, 117 insertions(+), 197 deletions(-) create mode 100644 src/bloodhound/options_parser.js diff --git a/src/bloodhound/bloodhound.js b/src/bloodhound/bloodhound.js index bb25ab2d..2ce1f4df 100644 --- a/src/bloodhound/bloodhound.js +++ b/src/bloodhound/bloodhound.js @@ -18,19 +18,21 @@ var Bloodhound = window.Bloodhound = (function() { } this.limit = o.limit || 5; - this.valueKey = o.valueKey || 'value'; - this.sorter = getSorter(o.sorter); - this.dupDetector = getDupDetector(o.dupDetector, this.valueKey); + this.sorter = o.sorter || noSort; + this.dupDetector = o.dupDetector || ignoreDuplicates; - this.local = getLocal(o); - this.prefetch = getPrefetch(o); - this.remote = getRemote(o); + this.local = oParser.local(o); + this.prefetch = oParser.prefetch(o); + this.remote = oParser.remote(o); this.cacheKey = this.prefetch ? (this.prefetch.cacheKey || this.prefetch.url) : null; // the backing data structure used for fast pattern matching - this.index = new SearchIndex({ tokenizer: o.tokenizer }); + this.index = new SearchIndex({ + datumTokenizer: o.datumTokenizer, + queryTokenizer: o.queryTokenizer + }); // only initialize storage if there's a cacheKey otherwise // loading from storage on subsequent page loads is impossible @@ -59,7 +61,7 @@ var Bloodhound = window.Bloodhound = (function() { return deferred; function handlePrefetchResponse(resp) { - var filtered, normalized; + var filtered; filtered = o.filter ? o.filter(resp) : resp; that.add(filtered); @@ -72,7 +74,6 @@ var Bloodhound = window.Bloodhound = (function() { var that = this, url, uriEncodedQuery; query = query || ''; - uriEncodedQuery = encodeURIComponent(query); url = this.remote.replace ? @@ -84,28 +85,7 @@ var Bloodhound = window.Bloodhound = (function() { function handleRemoteResponse(resp) { var filtered = that.remote.filter ? that.remote.filter(resp) : resp; - cb(that._normalize(filtered)); - } - }, - - _normalize: function normalize(data) { - var that = this; - - return _.map(data, normalizeRawDatum); - - function normalizeRawDatum(raw) { - var value, datum; - - value = _.isString(raw) ? raw : raw[that.valueKey]; - datum = { value: value, tokens: raw.tokens }; - - // if the raw datum is a string, transform it into an - // object as that's what gets passed to the precompiled templates - _.isString(raw) ? - (datum.raw = {})[that.valueKey] = raw : - datum.raw = raw; - - return datum; + cb(filtered); } }, @@ -156,18 +136,13 @@ var Bloodhound = window.Bloodhound = (function() { }, add: function add(data) { - var normalized; - - data = _.isArray(data) ? data : [data]; - normalized = this._normalize(data); - - this.index.add(normalized); + this.index.add(data); }, get: function get(query, cb) { var that = this, matches, cacheHit = false; - matches = _.map(this.index.get(query), pickRaw) + matches = this.index.get(query) .sort(this.sorter) .slice(0, this.limit); @@ -183,8 +158,6 @@ var Bloodhound = window.Bloodhound = (function() { function returnRemoteMatches(remoteMatches) { var matchesWithBackfill = matches.slice(0); - remoteMatches = _.map(remoteMatches, pickRaw); - _.each(remoteMatches, function(remoteMatch) { var isDuplicate; @@ -202,9 +175,9 @@ var Bloodhound = window.Bloodhound = (function() { cb && cb(matchesWithBackfill.sort(that.sorter)); } + }, - function pickRaw(obj) { return obj.raw; } - } + ttAdapter: function ttAdapter() { return _.bind(this.get, this); } }); return Bloodhound; @@ -212,93 +185,7 @@ var Bloodhound = window.Bloodhound = (function() { // helper functions // ---------------- - function getSorter(sorter) { - return sorter || defaultSorter; - - function defaultSorter() { return 0; } - } - - function getDupDetector(dupDetector, valueKey) { - if (!_.isFunction(dupDetector)) { - dupDetector = dupDetector === false ? ignoreDups : defaultDupDetector; - } - - return dupDetector; - - function ignoreDups() { return false; } - function defaultDupDetector(a, b) { return a[valueKey] === b[valueKey]; } - } - - function getLocal(o) { - return o.local || null; - } + function noSort() { return 0; } - function getPrefetch(o) { - var prefetch, defaults; - - defaults = { - url: null, - thumbprint: '', - ttl: 24 * 60 * 60 * 1000, // 1 day - filter: null, - ajax: {} - }; - - if (prefetch = o.prefetch || null) { - // support basic (url) and advanced configuration - prefetch = _.isString(prefetch) ? { url: prefetch } : prefetch; - - prefetch = _.mixin(defaults, prefetch); - prefetch.thumbprint = VERSION + prefetch.thumbprint; - - prefetch.ajax.method = prefetch.ajax.method || 'get'; - prefetch.ajax.dataType = prefetch.ajax.dataType || 'json'; - - !prefetch.url && $.error('prefetch requires url to be set'); - } - - return prefetch; - } - - function getRemote(o) { - var remote, defaults; - - defaults = { - url: null, - wildcard: '%QUERY', - replace: null, - rateLimitBy: 'debounce', - rateLimitWait: 300, - send: null, - filter: null, - ajax: {} - }; - - if (remote = o.remote || null) { - // support basic (url) and advanced configuration - remote = _.isString(remote) ? { url: remote } : remote; - - remote = _.mixin(defaults, remote); - remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? - byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); - - remote.ajax.method = remote.ajax.method || 'get'; - remote.ajax.dataType = remote.ajax.dataType || 'json'; - - delete remote.rateLimitBy; - delete remote.rateLimitWait; - - !remote.url && $.error('remote requires url to be set'); - } - - return remote; - - function byDebounce(wait) { - return function(fn) { return _.debounce(fn, wait); }; - } - - function byThrottle(wait) { - return function(fn) { return _.throttle(fn, wait); }; - } - } + function ignoreDuplicates() { return false; } })(); diff --git a/src/bloodhound/options_parser.js b/src/bloodhound/options_parser.js new file mode 100644 index 00000000..42c5d813 --- /dev/null +++ b/src/bloodhound/options_parser.js @@ -0,0 +1,87 @@ +/* + * typeahead.js + * https://github.com/twitter/typeahead.js + * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT + */ + +var oParser = (function() { + + return { + local: getLocal, + prefetch: getPrefetch, + remote: getRemote + }; + + function getLocal(o) { + return o.local || null; + } + + function getPrefetch(o) { + var prefetch, defaults; + + defaults = { + url: null, + thumbprint: '', + ttl: 24 * 60 * 60 * 1000, // 1 day + filter: null, + ajax: {} + }; + + if (prefetch = o.prefetch || null) { + // support basic (url) and advanced configuration + prefetch = _.isString(prefetch) ? { url: prefetch } : prefetch; + + prefetch = _.mixin(defaults, prefetch); + prefetch.thumbprint = VERSION + prefetch.thumbprint; + + prefetch.ajax.method = prefetch.ajax.method || 'get'; + prefetch.ajax.dataType = prefetch.ajax.dataType || 'json'; + + !prefetch.url && $.error('prefetch requires url to be set'); + } + + return prefetch; + } + + function getRemote(o) { + var remote, defaults; + + defaults = { + url: null, + wildcard: '%QUERY', + replace: null, + rateLimitBy: 'debounce', + rateLimitWait: 300, + send: null, + filter: null, + ajax: {} + }; + + if (remote = o.remote || null) { + // support basic (url) and advanced configuration + remote = _.isString(remote) ? { url: remote } : remote; + + remote = _.mixin(defaults, remote); + remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? + byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); + + remote.ajax.method = remote.ajax.method || 'get'; + remote.ajax.dataType = remote.ajax.dataType || 'json'; + + delete remote.rateLimitBy; + delete remote.rateLimitWait; + + !remote.url && $.error('remote requires url to be set'); + } + + return remote; + + function byDebounce(wait) { + return function(fn) { return _.debounce(fn, wait); }; + } + + function byThrottle(wait) { + return function(fn) { return _.throttle(fn, wait); }; + } + } +})(); diff --git a/src/bloodhound/search_index.js b/src/bloodhound/search_index.js index c62619be..85f27522 100644 --- a/src/bloodhound/search_index.js +++ b/src/bloodhound/search_index.js @@ -12,7 +12,12 @@ var SearchIndex = (function() { function SearchIndex(o) { o = o || {}; - this.tokenize = o.tokenizer || tokenize; + if (!o.datumTokenizer || !o.queryTokenizer) { + $.error('datumTokenizer and queryTokenizer are both required'); + } + + this.datumTokenizer = o.datumTokenizer; + this.queryTokenizer = o.queryTokenizer; this.datums = []; this.trie = newNode(); @@ -39,10 +44,7 @@ var SearchIndex = (function() { var id, tokens; id = that.datums.push(datum) - 1; - tokens = normalizeTokens(datum.tokens || that.tokenize(datum.value)); - - // delete the tokens from the datum object to save storage space - delete datum.tokens; + tokens = normalizeTokens(that.datumTokenizer(datum)); _.each(tokens, function(token) { var node, chars, ch, ids; @@ -58,14 +60,10 @@ var SearchIndex = (function() { }); }, - remove: function remove() { - $.error('not implemented'); - }, - get: function get(query) { var that = this, tokens, matches; - tokens = this.tokenize(query); + tokens = normalizeTokens(this.queryTokenizer(query)); _.each(tokens, function(token) { var node, chars, ch, ids; @@ -107,10 +105,6 @@ var SearchIndex = (function() { // helper functions // ---------------- - function tokenize(str) { - return $.trim(str).toLowerCase().split(/\s+/); - } - function normalizeTokens(tokens) { // filter out falsy tokens tokens = _.filter(tokens, function(token) { return !!token; }); diff --git a/src/typeahead/dataset.js b/src/typeahead/dataset.js index 46082e35..3a034e91 100644 --- a/src/typeahead/dataset.js +++ b/src/typeahead/dataset.js @@ -25,7 +25,7 @@ var Dataset = (function() { this.name = o.name || _.getUniqueId(); this.source = o.source; - this.valueKey = o.valueKey || 'value'; + this.valueKey = o.displayKey || 'value'; this.templates = getTemplates(o.templates, this.valueKey); @@ -160,8 +160,6 @@ var Dataset = (function() { // ---------------- function getTemplates(templates, valueKey) { - valueKey = valueKey || 'value'; - return { empty: templates.empty && _.templatify(templates.empty), header: templates.header && _.templatify(templates.header), diff --git a/src/typeahead/plugin.js b/src/typeahead/plugin.js index 10f73d52..6e34da78 100644 --- a/src/typeahead/plugin.js +++ b/src/typeahead/plugin.js @@ -4,7 +4,7 @@ * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ -(function(Bloodhound) { +(function() { var typeaheadKey, methods; typeaheadKey = 'ttTypeahead'; @@ -18,21 +18,11 @@ return this.each(attach); function attach() { - var $input = $(this), promises, eventBus, typeahead; - - promises = _.map(datasets, function(d) { - var promise = $.Deferred().resolve(); + var $input = $(this), eventBus, typeahead; + _.each(datasets, function(d) { // HACK: force highlight as a top-level config d.highlight = !!o.highlight; - - // if source is an object, convert it to a bloodhound - if (_.isObject(d.source) || isBloodhound(d.source)) { - d.source = bloodhoundAdapter(d.source); - promise = d.source.initialize(); - } - - return promise; }); typeahead = new Typeahead({ @@ -46,14 +36,6 @@ $input.data(typeaheadKey, typeahead); - // only trigger these events if at least one dataset is - // using a bloodhound as a source - if (promises.length) { - $.when.apply($, promises) - .done(trigger('initialized')) - .fail(trigger('initialized:err')); - } - // defer trigging of events to make it possible to attach // a listener immediately after jQuery#typeahead is invoked function trigger(eventName) { @@ -134,32 +116,4 @@ return methods.initialize.apply(this, arguments); } }; - - // helper functions - // ---------------- - - function bloodhoundAdapter(o) { - var bloodhound, source; - - if (!Bloodhound) { - $.error('Bloodhound constructor has not been loaded'); - } - - bloodhound = isBloodhound(o) ? o : new Bloodhound(o); - source = _.bind(bloodhound.get, bloodhound); - - source.initialize = function() { - return bloodhound.initialize(); - }; - - source.add = function(data) { - bloodhound.add(); - }; - - return source; - }; - - function isBloodhound(obj) { - return Bloodhound && obj instanceof Bloodhound; - } -})(window.Bloodhound); +})(); From 2e7d917fe2068316d607b297cc7454a74ddc67eb Mon Sep 17 00:00:00 2001 From: Jake Harding Date: Sat, 1 Feb 2014 21:45:20 -0800 Subject: [PATCH 08/17] Update tests for latest API. --- Gruntfile.js | 1 + karma.conf.js | 1 + test/bloodhound_spec.js | 55 +++++++- test/fixtures/ajax_responses.js | 2 +- test/fixtures/data.js | 57 ++------ test/playground.html | 228 ++++++++++++++++++-------------- test/search_index_spec.js | 27 ++-- test/typeahead_view_spec.js | 2 +- 8 files changed, 210 insertions(+), 163 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 328dc9fc..b84e3f78 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -10,6 +10,7 @@ var semver = require('semver'), 'src/bloodhound/persistent_storage.js', 'src/bloodhound/transport.js', 'src/bloodhound/search_index.js', + 'src/bloodhound/options_parser.js', 'src/bloodhound/bloodhound.js' ], typeahead: [ diff --git a/karma.conf.js b/karma.conf.js index 8a7719fe..1571ceba 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,6 +15,7 @@ files = [ 'src/bloodhound/persistent_storage.js', 'src/bloodhound/transport.js', 'src/bloodhound/search_index.js', + 'src/bloodhound/options_parser.js', 'src/bloodhound/bloodhound.js', 'src/typeahead/html.js', 'src/typeahead/css.js', diff --git a/test/bloodhound_spec.js b/test/bloodhound_spec.js index 96c00c65..1edbe595 100644 --- a/test/bloodhound_spec.js +++ b/test/bloodhound_spec.js @@ -12,7 +12,12 @@ describe('Bloodhound', function() { describe('local', function() { beforeEach(function() { - this.bloodhound = new Bloodhound({ local: fixtures.data.simple }); + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + local: fixtures.data.simple + }); + this.bloodhound.initialize(); }); @@ -40,11 +45,15 @@ describe('Bloodhound', function() { var ttl = 100; this.bloodhound1 = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, prefetch: { url: '/test1', cacheKey: 'woah' } }); expect(PersistentStorage).toHaveBeenCalledWith('woah'); this.bloodhound2 = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, prefetch: { url: '/test2', ttl: ttl, thumbprint: '!' } }); expect(PersistentStorage).toHaveBeenCalledWith('/test2'); @@ -66,8 +75,16 @@ describe('Bloodhound', function() { spy1 = jasmine.createSpy(); spy2 = jasmine.createSpy(); - this.bloodhound1 = new Bloodhound({ prefetch: '/test1' }); - this.bloodhound2 = new Bloodhound({ prefetch: { url: '/test2' } }); + this.bloodhound1 = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + prefetch: '/test1' + }); + this.bloodhound2 = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + prefetch: { url: '/test2' } + }); this.bloodhound1.initialize(); this.bloodhound2.initialize(); @@ -100,6 +117,8 @@ describe('Bloodhound', function() { spy = jasmine.createSpy(); this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, prefetch: { url: '/test', filter: filterSpy } }); this.bloodhound.initialize(); @@ -117,14 +136,18 @@ describe('Bloodhound', function() { ]); function fakeFilter(resp) { - return ['BIG', 'BIGGER', 'BIGGEST']; + return [{ value: 'BIG' }, { value: 'BIGGER' }, { value: 'BIGGEST' }]; } }); it('should not make a request if data is available in storage', function() { var that = this, spy = jasmine.createSpy(); - this.bloodhound = new Bloodhound({ name: 'name', prefetch: '/test' }); + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + prefetch: '/test' + }); this.bloodhound.storage.get.andCallFake(fakeGet); this.bloodhound.initialize(); @@ -161,9 +184,13 @@ describe('Bloodhound', function() { describe('remote', function() { it('should perform query substitution on the provided url', function() { this.bloodhound1 = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, remote: { url: '/test?q=$$', wildcard: '$$' } }); this.bloodhound2 = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, remote: { url: '/test?q=%QUERY', replace: function(str, query) {return str.replace('%QUERY', query); } @@ -196,6 +223,8 @@ describe('Bloodhound', function() { filterSpy = jasmine.createSpy().andCallFake(fakeFilter); this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, remote: { url: '/test', filter: filterSpy } }); this.bloodhound.initialize(); @@ -216,7 +245,7 @@ describe('Bloodhound', function() { }); function fakeFilter(resp) { - return ['BIG', 'BIGGER', 'BIGGEST']; + return [{ value: 'BIG' }, { value: 'BIGGER' }, { value: 'BIGGEST' }]; } function fakeGet(url, o, cb) { @@ -227,7 +256,11 @@ describe('Bloodhound', function() { it('should call #get callback once if cache hit', function() { var spy = jasmine.createSpy(); - this.bloodhound = new Bloodhound({ remote: '/test?q=%QUERY' }); + this.bloodhound = new Bloodhound({ + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, + remote: '/test?q=%QUERY' + }); this.bloodhound.initialize(); this.bloodhound.transport.get.andCallFake(fakeGet); @@ -251,6 +284,8 @@ describe('Bloodhound', function() { this.bloodhound = new Bloodhound({ limit: 3, + datumTokenizer: datumTokenizer, + queryTokenizer: queryTokenizer, local: fixtures.data.simple, remote: { url: '/test?q=%QUERY' } }); @@ -291,4 +326,10 @@ describe('Bloodhound', function() { } }); }); + + // helper functions + // ---------------- + + function datumTokenizer(d) { return $.trim(d.value).split(/\s+/); } + function queryTokenizer(s) { return $.trim(s).split(/\s+/); } }); diff --git a/test/fixtures/ajax_responses.js b/test/fixtures/ajax_responses.js index c6e4f342..c24a6982 100644 --- a/test/fixtures/ajax_responses.js +++ b/test/fixtures/ajax_responses.js @@ -3,7 +3,7 @@ var fixtures = fixtures || {}; fixtures.ajaxResps = { ok: { status: 200, - responseText: '["big", "bigger", "biggest", "small", "smaller", "smallest"]' + responseText: '[{ "value": "big" }, { "value": "bigger" }, { "value": "biggest" }, { "value": "small" }, { "value": "smaller" }, { "value": "smallest" }]' }, ok1: { status: 200, diff --git a/test/fixtures/data.js b/test/fixtures/data.js index ea310c3e..2f7fb9ad 100644 --- a/test/fixtures/data.js +++ b/test/fixtures/data.js @@ -9,58 +9,23 @@ fixtures.data = { { value: 'smaller' }, { value: 'smallest' } ], - animals: ['dog', 'cat', 'moose'] -}; - -fixtures.normalized = { - simple: [ - { value: 'big', raw: { value: 'big' } }, - { value: 'bigger', raw: { value: 'bigger' } }, - { value: 'biggest', raw: { value: 'biggest' } }, - { value: 'small', raw: { value: 'small' } }, - { value: 'smaller', raw: { value: 'smaller' } }, - { value: 'smallest', raw: { value: 'smallest' } } - ], animals: [ - { value: 'dog', raw: { value: 'dog' } }, - { value: 'cat', raw: { value: 'cat' } }, - { value: 'moose', raw: { value: 'moose' } } + { value: 'dog' }, + { value: 'cat' }, + { value: 'moose' } ] }; fixtures.serialized = { simple: { - "datums": [{ - "value": "big", - "raw": { - "value": "big" - } - }, { - "value": "bigger", - "raw": { - "value": "bigger" - } - }, { - "value": "biggest", - "raw": { - "value": "biggest" - } - }, { - "value": "small", - "raw": { - "value": "small" - } - }, { - "value": "smaller", - "raw": { - "value": "smaller" - } - }, { - "value": "smallest", - "raw": { - "value": "smallest" - } - }], + "datums": [ + { "value": "big" }, + { "value": "bigger" }, + { "value": "biggest" }, + { "value": "small" }, + { "value": "smaller" }, + { "value": "smallest" } + ], "trie": { "ids": [], "children": { diff --git a/test/playground.html b/test/playground.html index 98589891..682a6824 100644 --- a/test/playground.html +++ b/test/playground.html @@ -50,122 +50,158 @@