webui: Removed unused JS causing issues with typeahead (#8307)

* webui: Removed unused JS causing issues with typeahead

* updated reference url for typeahead
This commit is contained in:
Neil Lathwood 2018-03-09 18:11:24 +01:00 committed by GitHub
parent 0e09e09f04
commit 9897b5fbd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 9 additions and 16018 deletions

View File

@ -38,9 +38,6 @@ datetime-subtree:
font-awesome:
$(GIT_SUBTREE) --prefix=lib/Font-Awesome https://github.com/FortAwesome/Font-Awesome.git master
typeahead:
$(GIT_SUBTREE) --prefix=lib/typeahead https://github.com/corejavascript/typeahead.js.git master
gridster:
$(GIT_SUBTREE) --prefix=lib/gridster https://github.com/dsmorse/gridster.js.git master

View File

@ -9,7 +9,7 @@ We list below what we make use of including the license compliance.
- [Font Awesome](http://fontawesome.io/icons/): MIT License
- [Jquery Bootgrid](http://www.jquery-bootgrid.com/): MIT License
- [Pace](https://github.com/HubSpot/pace): Open License
- [Twitter typeahead](http://twitter.github.io/typeahead.js/): Open License
- [Twitter typeahead](https://github.com/corejavascript/typeahead.js): Open License
- [Vis](http://visjs.org/): MIT / Apache 2.0
- [TCPDF](http://www.tcpdf.org): LGPLv3
- [Bootstrap 3 Datepicker](http://eonasdan.github.io/bootstrap-datetimepicker/): MIT

View File

@ -269,15 +269,3 @@ $(document).ready(function() {
}
});
});
$(document).ajaxComplete(function(){
if($('.alert-status').length !== 0) {
$('.alert-status').each(function() {
if ($(this).parent().height() < 27) {
$(this).height('27px');
} else {
$(this).height($(this).parent().height());
}
})
}
});

View File

@ -1 +0,0 @@
../../lib/typeahead/dist/typeahead.bundle.min.js

8
html/js/typeahead.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
*.swp
.DS_Store
.grunt
_SpecRunner.html
test/coverage
dist_temp
node_modules
npm-debug.log
bower_components
*.iml
.idea

View File

@ -1,16 +0,0 @@
{
"curly": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"trailing": true,
"boss": true,
"eqnull": true,
"expr": true,
"validthis": true,
"browser": true,
"jquery": true
}

View File

@ -1,34 +0,0 @@
language: node_js
env:
matrix:
- TEST_SUITE=unit
- TEST_SUITE=integration BROWSER='firefox'
- TEST_SUITE=integration BROWSER='firefox:3.5'
- TEST_SUITE=integration BROWSER='firefox:3.6'
- TEST_SUITE=integration BROWSER='safari:5'
- TEST_SUITE=integration BROWSER='safari:6'
- TEST_SUITE=integration BROWSER='safari:7'
- TEST_SUITE=integration BROWSER='internet explorer:8'
- TEST_SUITE=integration BROWSER='internet explorer:9'
- TEST_SUITE=integration BROWSER='internet explorer:10'
- TEST_SUITE=integration BROWSER='internet explorer:11'
- TEST_SUITE=integration BROWSER='chrome'
global:
- secure: VY4J2ERfrMEin++f4+UDDtTMWLuE3jaYAVchRxfO2c6PQUYgR+SW4SMekz855U/BuptMtiVMR2UUoNGMgOSKIFkIXpPfHhx47G5a541v0WNjXfQ2qzivXAWaXNK3l3C58z4dKxgPWsFY9JtMVCddJd2vQieAILto8D8G09p7bpo=
- secure: kehbNCoYUG2gLnhmCH/oKhlJG6LoxgcOPMCtY7KOI4ropG8qlypb+O2b/19+BWeO3aIuMB0JajNh3p2NL0UKgLmUK7EYBA9fQz+vesFReRk0V/KqMTSxHJuseM4aLOWA2Wr9US843VGltfODVvDN5sNrfY7RcoRx2cTK/k1CXa8=
node_js:
- "4.1"
cache:
directories:
- node_modules
- bower_components
before_install:
- npm install -g grunt-cli@0.1.13
- npm install -g bower@1.3.8
install:
- npm install
before_script:
- grunt build
script: test/ci
addons:
sauce_connect: true

View File

@ -1,328 +0,0 @@
var semver = require('semver'),
f = require('util').format,
files = {
common: [
'src/common/utils.js'
],
bloodhound: [
'src/bloodhound/version.js',
'src/bloodhound/tokenizers.js',
'src/bloodhound/lru_cache.js',
'src/bloodhound/persistent_storage.js',
'src/bloodhound/transport.js',
'src/bloodhound/search_index.js',
'src/bloodhound/prefetch.js',
'src/bloodhound/remote.js',
'src/bloodhound/options_parser.js',
'src/bloodhound/bloodhound.js'
],
typeahead: [
'src/typeahead/www.js',
'src/typeahead/event_bus.js',
'src/typeahead/event_emitter.js',
'src/typeahead/highlight.js',
'src/typeahead/input.js',
'src/typeahead/dataset.js',
'src/typeahead/menu.js',
'src/typeahead/default_menu.js',
'src/typeahead/typeahead.js',
'src/typeahead/plugin.js'
]
};
module.exports = function(grunt) {
grunt.initConfig({
version: grunt.file.readJSON('package.json').version,
tempDir: 'dist_temp',
buildDir: 'dist',
banner: [
'/*!',
' * typeahead.js <%= version %>',
' * https://github.com/twitter/typeahead.js',
' * Copyright 2013-<%= grunt.template.today("yyyy") %> Twitter, Inc. and other contributors; Licensed MIT',
' */\n\n'
].join('\n'),
uglify: {
options: {
banner: '<%= banner %>'
},
concatBloodhound: {
options: {
mangle: false,
beautify: true,
compress: false,
banner: ''
},
src: files.common.concat(files.bloodhound),
dest: '<%= tempDir %>/bloodhound.js'
},
concatTypeahead: {
options: {
mangle: false,
beautify: true,
compress: false,
banner: ''
},
src: files.common.concat(files.typeahead),
dest: '<%= tempDir %>/typeahead.jquery.js'
},
bloodhound: {
options: {
mangle: false,
beautify: true,
compress: false
},
src: '<%= tempDir %>/bloodhound.js',
dest: '<%= buildDir %>/bloodhound.js'
},
bloodhoundMin: {
options: {
mangle: true,
compress: {}
},
src: '<%= tempDir %>/bloodhound.js',
dest: '<%= buildDir %>/bloodhound.min.js'
},
typeahead: {
options: {
mangle: false,
beautify: true,
compress: false
},
src: '<%= tempDir %>/typeahead.jquery.js',
dest: '<%= buildDir %>/typeahead.jquery.js'
},
typeaheadMin: {
options: {
mangle: true,
compress: {}
},
src: '<%= tempDir %>/typeahead.jquery.js',
dest: '<%= buildDir %>/typeahead.jquery.min.js'
},
bundle: {
options: {
mangle: false,
beautify: true,
compress: false
},
src: [
'<%= tempDir %>/bloodhound.js',
'<%= tempDir %>/typeahead.jquery.js'
],
dest: '<%= buildDir %>/typeahead.bundle.js'
},
bundleMin: {
options: {
mangle: true,
compress: {}
},
src: [
'<%= tempDir %>/bloodhound.js',
'<%= tempDir %>/typeahead.jquery.js'
],
dest: '<%= buildDir %>/typeahead.bundle.min.js'
}
},
umd: {
bloodhound: {
src: '<%= tempDir %>/bloodhound.js',
objectToExport: 'Bloodhound',
deps: {
default: ['$'],
amd: ['jquery'],
cjs: ['jquery'],
global: ['jQuery']
}
},
typeahead: {
src: '<%= tempDir %>/typeahead.jquery.js',
deps: {
default: ['$'],
amd: ['jquery'],
cjs: ['jquery'],
global: ['jQuery']
}
}
},
sed: {
version: {
pattern: '%VERSION%',
replacement: '<%= version %>',
recursive: true,
path: '<%= buildDir %>'
}
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
src: 'src/**/*.js',
test: ['test/**/*_spec.js', 'test/integration/test.js'],
gruntfile: ['Gruntfile.js']
},
watch: {
js: {
files: 'src/**/*',
tasks: 'build'
}
},
exec: {
npm_publish: 'npm publish',
git_is_clean: 'test -z "$(git status --porcelain)"',
git_on_master: 'test $(git symbolic-ref --short -q HEAD) = master',
git_add: 'git add .',
git_push: 'git push && git push --tags',
git_commit: {
cmd: function(m) { return f('git commit -m "%s"', m); }
},
git_tag: {
cmd: function(v) { return f('git tag v%s -am "%s"', v, v); }
},
publish_assets: [
'cp -r <%= buildDir %> typeahead.js',
'zip -r typeahead.js/typeahead.js.zip typeahead.js',
'git checkout gh-pages',
'rm -rf releases/latest',
'cp -r typeahead.js releases/<%= version %>',
'cp -r typeahead.js releases/latest',
'git add releases/<%= version %> releases/latest',
'sed -E -i "" \'s/v[0-9]+\\.[0-9]+\\.[0-9]+/v<%= version %>/\' index.html',
'git add index.html',
'git commit -m "Add assets for <%= version %>."',
'git push',
'git checkout -',
'rm -rf typeahead.js'
].join(' && ')
},
clean: {
dist: 'dist'
},
connect: {
server: {
options: { port: 8888, keepalive: true }
}
},
concurrent: {
options: { logConcurrentOutput: true },
dev: ['server', 'watch']
},
step: {
options: {
option: false
}
}
});
grunt.registerTask('release', '#shipit', function(version) {
var curVersion = grunt.config.get('version');
version = semver.inc(curVersion, version) || version;
if (!semver.valid(version) || semver.lte(version, curVersion)) {
grunt.fatal('hey dummy, that version is no good!');
}
grunt.config.set('version', version);
grunt.task.run([
'exec:git_on_master',
'exec:git_is_clean',
f('step:Update to version %s?', version),
f('manifests:%s', version),
'build',
'exec:git_add',
f('exec:git_commit:%s', version),
f('exec:git_tag:%s', version),
'step:Push changes?',
'exec:git_push',
'step:Publish to npm?',
'exec:npm_publish',
'step:Publish assets?',
'exec:publish_assets'
]);
});
grunt.registerTask('manifests', 'Update manifests.', function(version) {
var _ = grunt.util._,
pkg = grunt.file.readJSON('package.json'),
bower = grunt.file.readJSON('bower.json'),
jqueryPlugin = grunt.file.readJSON('typeahead.js.jquery.json');
bower = JSON.stringify(_.extend(bower, {
name: pkg.name,
version: version
}), null, 2);
jqueryPlugin = JSON.stringify(_.extend(jqueryPlugin, {
name: pkg.name,
title: pkg.name,
version: version,
author: pkg.author,
description: pkg.description,
keywords: pkg.keywords,
homepage: pkg.homepage,
bugs: pkg.bugs,
maintainers: pkg.contributors
}), null, 2);
pkg = JSON.stringify(_.extend(pkg, {
version: version
}), null, 2);
grunt.file.write('package.json', pkg);
grunt.file.write('bower.json', bower);
grunt.file.write('typeahead.js.jquery.json', jqueryPlugin);
});
// aliases
// -------
grunt.registerTask('default', 'build');
grunt.registerTask('server', 'connect:server');
grunt.registerTask('lint', 'jshint');
grunt.registerTask('dev', ['build', 'concurrent:dev']);
grunt.registerTask('build', [
'uglify:concatBloodhound',
'uglify:concatTypeahead',
'umd:bloodhound',
'umd:typeahead',
'uglify:bloodhound',
'uglify:bloodhoundMin',
'uglify:typeahead',
'uglify:typeaheadMin',
'uglify:bundle',
'uglify:bundleMin',
'sed:version'
]);
// load tasks
// ----------
grunt.loadNpmTasks('grunt-umd');
grunt.loadNpmTasks('grunt-sed');
grunt.loadNpmTasks('grunt-exec');
grunt.loadNpmTasks('grunt-step');
grunt.loadNpmTasks('grunt-concurrent');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-connect');
};

View File

@ -1,16 +0,0 @@
{
"name": "corejs-typeahead",
"version": "0.11.1",
"main": "dist/typeahead.bundle.js",
"dependencies": {
"jquery": ">=1.7"
},
"devDependencies": {
"jquery": "~1.7",
"jasmine-ajax": "~1.3.1",
"jasmine-jquery": "~1.7.0"
},
"resolutions": {
"jquery": "1.7.2"
}
}

View File

@ -1,171 +0,0 @@
# Changelog
### 0.11.2 TBD, 2015
* Add matchAnyQueryToken option. [#2](https://github.com/corejavascript/typeahead.js/pull/2)
* Update rendered-count after async results have displayed. [#8](https://github.com/corejavascript/typeahead.js/pull/8)
* Add default on option to stop propagation when selecting an entry. [#13](https://github.com/corejavascript/typeahead.js/pull/13)
### 0.11.1 April 26, 2015
* Add prepare option to prefetch. [#1181](https://github.com/twitter/typeahead.js/pull/1181)
* Handle QuotaExceededError. [#1110](https://github.com/twitter/typeahead.js/pull/1110)
* Escape HTML entities from suggestion display value when rendering with default
template. [#964](https://github.com/twitter/typeahead.js/pull/964)
* List jquery as a dependency in package.json. [#1143](https://github.com/twitter/typeahead.js/pull/1143)
### 0.11.0 April 25, 2015
An overhaul of typeahead.js  consider this a release candidate for v1. There
are bunch of API changes with this release so don't expect backwards
compatibility with previous versions. There are also many new undocumented
features that have been introduced. Documentation for those features will be
added before v1 ships.
Beware that since this release is pretty much a rewrite, there are bound to be
some bugs. To be safe, you should consider this release beta software and
throughly test your integration of it before using it in production
environments. This caveat only applies to this release as subsequent releases
will address any issues that come up.
### 0.10.5 August 7, 2014
* Increase supported version range for jQuery dependency. [#917](https://github.com/twitter/typeahead.js/pull/917)
### 0.10.4 July 13, 2014
**Hotfix**
* Fix regression that breaks Bloodhound instances when more than 1 instance is
relying on remote data. [#899](https://github.com/twitter/typeahead.js/pull/899)
### 0.10.3 July 10, 2014
**Bug fixes**
* `Bloodhound#clearPrefetchCache` now works with cache keys that contain regex
characters. [#771](https://github.com/twitter/typeahead.js/pull/771)
* Prevent outdated network requests from being sent. [#809](https://github.com/twitter/typeahead.js/pull/809)
* Add support to object tokenizers for multiple property tokenization. [#811](https://github.com/twitter/typeahead.js/pull/811)
* Fix broken `jQuery#typeahead('val')` method. [#815](https://github.com/twitter/typeahead.js/pull/815)
* Remove `disabled` attribute from the hint input control. [#839](https://github.com/twitter/typeahead.js/pull/839)
* Add `tt-highlight` class to highlighted text. [#833](https://github.com/twitter/typeahead.js/pull/833)
* Handle non-string types that are passed to `jQuery#typeahead('val', val)`. [#881](https://github.com/twitter/typeahead.js/pull/881)
### 0.10.2 March 10, 2014
* Prevent flickering of dropdown menu when requesting remote suggestions. [#718](https://github.com/twitter/typeahead.js/pull/718)
* Reduce hint flickering. [#754](https://github.com/twitter/typeahead.js/pull/754)
* Added `Bloodhound#{clear, clearPrefetchCache, clearRemoteCache}` and made it
possible to reinitialize Bloodhound instances. [#703](https://github.com/twitter/typeahead.js/pull/703)
* Invoke `local` function during initialization. [#687](https://github.com/twitter/typeahead.js/pull/687)
* In addition to HTML strings, templates can now return DOM nodes. [#742](https://github.com/twitter/typeahead.js/pull/742)
* Prevent `jQuery#typeahead('val', val)` from opening dropdown menus of
non-active typeaheads. [#646](https://github.com/twitter/typeahead.js/pull/646)
* Fix bug in IE that resulted in dropdown menus with overflow being closed
when clicking on the scrollbar. [#705](https://github.com/twitter/typeahead.js/pull/705)
* Only show dropdown menu if `minLength` is satisfied. [#710](https://github.com/twitter/typeahead.js/pull/710)
### 0.10.1 February 9, 2014
**Hotfix**
* Fixed bug that prevented some ajax configs from being respected. [#630](https://github.com/twitter/typeahead.js/pull/630)
* Event delegation on suggestion clicks is no longer broken. [#118](https://github.com/twitter/typeahead.js/pull/118)
* Ensure dataset names are valid class name suffixes. [#610](https://github.com/twitter/typeahead.js/pull/610)
* Added support for `displayKey` to be a function. [#633](https://github.com/twitter/typeahead.js/pull/633)
* `jQuery#typeahead('val')` now mirrors `jQuery#val()`. [#659](https://github.com/twitter/typeahead.js/pull/659)
* Datasets can now be passed to jQuery plugin as an array. [#664](https://github.com/twitter/typeahead.js/pull/664)
* Added a `noConflict` method to the jQuery plugin. [#612](https://github.com/twitter/typeahead.js/pull/612)
* Bloodhound's `local` property can now be a function. [#485](https://github.com/twitter/typeahead.js/pull/485)
### 0.10.0 February 2, 2014
**Introducting Bloodhound**
This release was almost a complete rewrite of typeahead.js and will hopefully
lay the foundation for the 1.0.0 release. It's impossible to enumerate all of
the issues that were fixed. If you want to get an idea of what issues 0.10.0
resolved, take a look at the closed issues in the [0.10.0 milestone](https://github.com/twitter/typeahead.js/issues?milestone=8&page=1&state=closed).
The most important change in 0.10.0 is that typeahead.js was broken up into 2
individual components: Bloodhound and jQuery#typeahead. Bloodhound is an
feature-rich suggestion engine. jQuery#typeahead is a jQuery plugin that turns
input controls into typeaheads.
It's impossible to write a typeahead library that supports every use-case out
of the box  that was the main motivation behind decomposing typeahead.js.
Previously, some prospective typeahead.js users were unable to use the library
because either the suggestion engine or the typeahead UI did not meet their
requirements. In those cases, they were either forced to fork typeahead.js and
make the necessary modifications or they had to give up on using typeahead.js
entirely. Now they have the option of swapping out the component that doesn't
work for them with a custom implementation.
### 0.9.3 June 24, 2013
* Ensure cursor visibility in menus with overflow. [#209](https://github.com/twitter/typeahead.js/pull/209)
* Fixed bug that led to the menu staying open when it should have been closed. [#260](https://github.com/twitter/typeahead.js/pull/260)
* Private browsing in Safari no longer breaks prefetch. [#270](https://github.com/twitter/typeahead.js/pull/270)
* Pressing tab while a suggestion is highlighted now results in a selection. [#266](https://github.com/twitter/typeahead.js/pull/266)
* Dataset name is now passed as an argument for typeahead:selected event. [#207](https://github.com/twitter/typeahead.js/pull/207)
### 0.9.2 April 14, 2013
* Prefetch usage no longer breaks when cookies are disabled. [#190](https://github.com/twitter/typeahead.js/pull/190)
* Precompiled templates are now wrapped in the appropriate DOM element. [#172](https://github.com/twitter/typeahead.js/pull/172)
### 0.9.1 April 1, 2013
* Multiple requests no longer get sent for a query when datasets share a remote source. [#152](https://github.com/twitter/typeahead.js/pull/152)
* Datasets now support precompiled templates. [#137](https://github.com/twitter/typeahead.js/pull/137)
* Cached remote suggestions now get rendered immediately. [#156](https://github.com/twitter/typeahead.js/pull/156)
* Added typeahead:autocompleted event. [#132](https://github.com/twitter/typeahead.js/pull/132)
* Added a plugin method for programmatically setting the query. Experimental. [#159](https://github.com/twitter/typeahead.js/pull/159)
* Added minLength option for datasets. Experimental. [#131](https://github.com/twitter/typeahead.js/pull/131)
* Prefetch objects now support thumbprint option. Experimental. [#157](https://github.com/twitter/typeahead.js/pull/157)
### 0.9.0 March 24, 2013
**Custom events, no more typeahead.css, and an improved API**
* Implemented the triggering of custom events. [#106](https://github.com/twitter/typeahead.js/pull/106)
* Got rid of typeahead.css and now apply styling through JavaScript. [#15](https://github.com/twitter/typeahead.js/pull/15)
* Made the API more flexible and addressed a handful of remote issues by rewriting the transport component. [#25](https://github.com/twitter/typeahead.js/pull/81)
* Added support for dataset headers and footers. [#81](https://github.com/twitter/typeahead.js/pull/81)
* No longer cache unnamed datasets. [#116](https://github.com/twitter/typeahead.js/pull/116)
* Made the key name of the value property configurable. [#115](https://github.com/twitter/typeahead.js/pull/115)
* Input values set before initialization of typeaheads are now respected. [#109](https://github.com/twitter/typeahead.js/pull/109)
* Fixed an input value/hint casing bug. [#108](https://github.com/twitter/typeahead.js/pull/108)
### 0.8.2 March 04, 2013
* Fixed bug causing error to be thrown when initializing a typeahead on multiple elements. [#51](https://github.com/twitter/typeahead.js/pull/51)
* Tokens with falsy values are now filtered out was causing wonky behavior. [#75](https://github.com/twitter/typeahead.js/pull/75)
* No longer making remote requests for blank queries. [#74](https://github.com/twitter/typeahead.js/pull/74)
* Datums with regex characters in their value no longer cause errors. [#77](https://github.com/twitter/typeahead.js/pull/77)
* Now compatible with the Closure Compiler. [#48](https://github.com/twitter/typeahead.js/pull/48)
* Reference to jQuery is now obtained through window.jQuery, not window.$. [#47](https://github.com/twitter/typeahead.js/pull/47)
* Added a plugin method for destroying typeaheads. Won't be documented until v0.9 and might change before then. [#59](https://github.com/twitter/typeahead.js/pull/59)
### 0.8.1 February 25, 2013
* Fixed bug preventing local and prefetch from being used together. [#39](https://github.com/twitter/typeahead.js/pull/39)
* No longer prevent default browser behavior when up or down arrow is pressed with a modifier. [#6](https://github.com/twitter/typeahead.js/pull/6)
* Hint is hidden when user entered query is wider than the input. [#26](https://github.com/twitter/typeahead.js/pull/26)
* Data stored in localStorage now expires properly. [#34](https://github.com/twitter/typeahead.js/pull/34)
* Normalized search tokens and fixed query tokenization. [#38](https://github.com/twitter/typeahead.js/pull/38)
* Remote suggestions now are appended, not prepended to suggestions list. [#40](https://github.com/twitter/typeahead.js/pull/40)
* Fixed some typos through the codebase. [#3](https://github.com/twitter/typeahead.js/pull/3)
### 0.8.0 February 19, 2013
**Initial public release**
* Prefetch and search data locally insanely fast.
* Search hard-coded, prefetched, and/or remote data.
* Hinting.
* RTL/IME/international support.
* Search multiple datasets.
* Share datasets (and caching) between multiple inputs.
* And much, much more...

View File

@ -1,17 +0,0 @@
{
"name": "twitter/typeahead.js",
"description": "fast and fully-featured autocomplete library",
"keywords": ["typeahead", "autocomplete"],
"homepage": "http://twitter.github.com/typeahead.js",
"authors": [
{
"name": "Twitter Inc.",
"homepage": "https://twitter.com/twitteross"
}
],
"support": {
"issues": "https://github.com/twitter/typeahead.js/issues"
},
"author": "Twitter Inc.",
"license": "MIT"
}

View File

@ -1,120 +0,0 @@
Contributing to typeahead.js
============================
*These contributing guidelines were proudly stolen from the
[Flight](https://github.com/flightjs/flight) project*
Looking to contribute something to typeahead.js? Here's how you can help.
Bugs Reports
------------
A bug is a _demonstrable problem_ that is caused by the code in the
repository. Good bug reports are extremely helpful thank you!
Guidelines for bug reports:
1. **Use the GitHub issue search** &mdash; check if the issue has already been
reported.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest `master` or integration branch in the repository.
3. **Isolate the problem** &mdash; ideally create a reduced test
case and a live example.
4. Please try to be as detailed as possible in your report. Include specific
information about the environment operating system and version, browser
and version, version of typeahead.js and steps required to reproduce the
issue.
Feature Requests & Contribution Enquiries
-----------------------------------------
Feature requests are welcome. But take a moment to find out whether your idea
fits with the scope and aims of the project. It's up to *you* to make a strong
case for the inclusion of your feature. Please provide as much detail and
context as possible.
Contribution enquiries should take place before any significant pull request,
otherwise you risk spending a lot of time working on something that we might
have good reasons for rejecting.
Pull Requests
-------------
Good pull requests patches, improvements, new features are a fantastic
help. They should remain focused in scope and avoid containing unrelated
commits.
Make sure to adhere to the coding conventions used throughout the codebase
(indentation, accurate comments, etc.) and any other requirements (such as test
coverage).
Please follow this process; it's the best way to get your work included in the
project:
1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork,
and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com/<your-username>/typeahead.js
# Navigate to the newly cloned directory
cd <repo-name>
# Assign the original repo to a remote called "upstream"
git remote add upstream git://github.com/twitter/typeahead.js
```
2. If you cloned a while ago, get the latest changes from upstream:
```bash
git checkout master
git pull upstream master
```
3. Install the dependencies (you must have Node.js and [Bower](http://bower.io)
installed), and create a new topic branch (off the main project development
branch) to contain your feature, change, or fix:
```bash
npm install
bower install
git checkout -b <topic-branch-name>
```
4. Make sure to update, or add to the tests when appropriate. Patches and
features will not be accepted without tests. Run `npm test` to check that
all tests pass after you've made changes.
5. Commit your changes in logical chunks. Provide clear and explanatory commit
messages. Use Git's [interactive rebase](https://help.github.com/articles/interactive-rebase) feature to tidy up
your commits before making them public.
6. Locally merge (or rebase) the upstream development branch into your topic branch:
```bash
git pull [--rebase] upstream master
```
7. Push your topic branch up to your fork:
```bash
git push origin <topic-branch-name>
```
8. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
with a clear title and description.
9. If you are asked to amend your changes before they can be merged in, please
use `git commit --amend` (or rebasing for multi-commit Pull Requests) and
force push to your remote feature branch. You may also be asked to squash
commits.
License
-------
By contributing your code,
You agree to license your contribution under the terms of the MIT License
https://github.com/twitter/typeahead.js/blob/master/LICENSE

View File

@ -1,928 +0,0 @@
/*!
* typeahead.js 0.11.1
* https://github.com/twitter/typeahead.js
* Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT
*/
(function(root, factory) {
if (typeof define === "function" && define.amd) {
define([ "jquery" ], function(a0) {
return root["Bloodhound"] = factory(a0);
});
} else if (typeof exports === "object") {
module.exports = factory(require("jquery"));
} else {
root["Bloodhound"] = factory(jQuery);
}
})(this, function($) {
var _ = function() {
"use strict";
return {
isMsie: function() {
return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
},
isBlankString: function(str) {
return !str || /^\s*$/.test(str);
},
escapeRegExChars: function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
isString: function(obj) {
return typeof obj === "string";
},
isNumber: function(obj) {
return typeof obj === "number";
},
isArray: $.isArray,
isFunction: $.isFunction,
isObject: $.isPlainObject,
isUndefined: function(obj) {
return typeof obj === "undefined";
},
isElement: function(obj) {
return !!(obj && obj.nodeType === 1);
},
isJQuery: function(obj) {
return obj instanceof $;
},
toStr: function toStr(s) {
return _.isUndefined(s) || s === null ? "" : s + "";
},
bind: $.proxy,
each: function(collection, cb) {
$.each(collection, reverseArgs);
function reverseArgs(index, value) {
return cb(value, index);
}
},
map: $.map,
filter: $.grep,
every: function(obj, test) {
var result = true;
if (!obj) {
return result;
}
$.each(obj, function(key, val) {
if (!(result = test.call(null, val, key, obj))) {
return false;
}
});
return !!result;
},
some: function(obj, test) {
var result = false;
if (!obj) {
return result;
}
$.each(obj, function(key, val) {
if (result = test.call(null, val, key, obj)) {
return false;
}
});
return !!result;
},
mixin: $.extend,
identity: function(x) {
return x;
},
clone: function(obj) {
return $.extend(true, {}, obj);
},
getIdGenerator: function() {
var counter = 0;
return function() {
return counter++;
};
},
templatify: function templatify(obj) {
return $.isFunction(obj) ? obj : template;
function template() {
return String(obj);
}
},
defer: function(fn) {
setTimeout(fn, 0);
},
debounce: function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments, later, callNow;
later = function() {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
}
};
callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
}
return result;
};
},
throttle: function(func, wait) {
var context, args, timeout, result, previous, later;
previous = 0;
later = function() {
previous = new Date();
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date(), remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val);
},
noop: function() {}
};
}();
var VERSION = "0.11.1";
var tokenizers = function() {
"use strict";
return {
nonword: nonword,
whitespace: whitespace,
obj: {
nonword: getObjTokenizer(nonword),
whitespace: getObjTokenizer(whitespace)
}
};
function whitespace(str) {
str = _.toStr(str);
return str ? str.split(/\s+/) : [];
}
function nonword(str) {
str = _.toStr(str);
return str ? str.split(/\W+/) : [];
}
function getObjTokenizer(tokenizer) {
return function setKey(keys) {
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
return function tokenize(o) {
var tokens = [];
_.each(keys, function(k) {
tokens = tokens.concat(tokenizer(_.toStr(o[k])));
});
return tokens;
};
};
}
}();
var LruCache = function() {
"use strict";
function LruCache(maxSize) {
this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
this.reset();
if (this.maxSize <= 0) {
this.set = this.get = $.noop;
}
}
_.mixin(LruCache.prototype, {
set: function set(key, val) {
var tailItem = this.list.tail, node;
if (this.size >= this.maxSize) {
this.list.remove(tailItem);
delete this.hash[tailItem.key];
this.size--;
}
if (node = this.hash[key]) {
node.val = val;
this.list.moveToFront(node);
} else {
node = new Node(key, val);
this.list.add(node);
this.hash[key] = node;
this.size++;
}
},
get: function get(key) {
var node = this.hash[key];
if (node) {
this.list.moveToFront(node);
return node.val;
}
},
reset: function reset() {
this.size = 0;
this.hash = {};
this.list = new List();
}
});
function List() {
this.head = this.tail = null;
}
_.mixin(List.prototype, {
add: function add(node) {
if (this.head) {
node.next = this.head;
this.head.prev = node;
}
this.head = node;
this.tail = this.tail || node;
},
remove: function remove(node) {
node.prev ? node.prev.next = node.next : this.head = node.next;
node.next ? node.next.prev = node.prev : this.tail = node.prev;
},
moveToFront: function(node) {
this.remove(node);
this.add(node);
}
});
function Node(key, val) {
this.key = key;
this.val = val;
this.prev = this.next = null;
}
return LruCache;
}();
var PersistentStorage = function() {
"use strict";
var LOCAL_STORAGE;
try {
LOCAL_STORAGE = window.localStorage;
LOCAL_STORAGE.setItem("~~~", "!");
LOCAL_STORAGE.removeItem("~~~");
} catch (err) {
LOCAL_STORAGE = null;
}
function PersistentStorage(namespace, override) {
this.prefix = [ "__", namespace, "__" ].join("");
this.ttlKey = "__ttl__";
this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
this.ls = override || LOCAL_STORAGE;
!this.ls && this._noop();
}
_.mixin(PersistentStorage.prototype, {
_prefix: function(key) {
return this.prefix + key;
},
_ttlKey: function(key) {
return this._prefix(key) + this.ttlKey;
},
_noop: function() {
this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
},
_safeSet: function(key, val) {
try {
this.ls.setItem(key, val);
} catch (err) {
if (err.name === "QuotaExceededError") {
this.clear();
this._noop();
}
}
},
get: function(key) {
if (this.isExpired(key)) {
this.remove(key);
}
return decode(this.ls.getItem(this._prefix(key)));
},
set: function(key, val, ttl) {
if (_.isNumber(ttl)) {
this._safeSet(this._ttlKey(key), encode(now() + ttl));
} else {
this.ls.removeItem(this._ttlKey(key));
}
return this._safeSet(this._prefix(key), encode(val));
},
remove: function(key) {
this.ls.removeItem(this._ttlKey(key));
this.ls.removeItem(this._prefix(key));
return this;
},
clear: function() {
var i, keys = gatherMatchingKeys(this.keyMatcher);
for (i = keys.length; i--; ) {
this.remove(keys[i]);
}
return this;
},
isExpired: function(key) {
var ttl = decode(this.ls.getItem(this._ttlKey(key)));
return _.isNumber(ttl) && now() > ttl ? true : false;
}
});
return PersistentStorage;
function now() {
return new Date().getTime();
}
function encode(val) {
return JSON.stringify(_.isUndefined(val) ? null : val);
}
function decode(val) {
return $.parseJSON(val);
}
function gatherMatchingKeys(keyMatcher) {
var i, key, keys = [], len = LOCAL_STORAGE.length;
for (i = 0; i < len; i++) {
if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
keys.push(key.replace(keyMatcher, ""));
}
}
return keys;
}
}();
var Transport = function() {
"use strict";
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10);
function Transport(o) {
o = o || {};
this.cancelled = false;
this.lastReq = null;
this._send = o.transport;
this._get = o.limiter ? o.limiter(this._get) : this._get;
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
}
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
maxPendingRequests = num;
};
Transport.resetCache = function resetCache() {
sharedCache.reset();
};
_.mixin(Transport.prototype, {
_fingerprint: function fingerprint(o) {
o = o || {};
return o.url + o.type + $.param(o.data || {});
},
_get: function(o, cb) {
var that = this, fingerprint, jqXhr;
fingerprint = this._fingerprint(o);
if (this.cancelled || fingerprint !== this.lastReq) {
return;
}
if (jqXhr = pendingRequests[fingerprint]) {
jqXhr.done(done).fail(fail);
} else if (pendingRequestsCount < maxPendingRequests) {
pendingRequestsCount++;
pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
} else {
this.onDeckRequestArgs = [].slice.call(arguments, 0);
}
function done(resp) {
cb(null, resp);
that._cache.set(fingerprint, resp);
}
function fail() {
cb(true);
}
function always() {
pendingRequestsCount--;
delete pendingRequests[fingerprint];
if (that.onDeckRequestArgs) {
that._get.apply(that, that.onDeckRequestArgs);
that.onDeckRequestArgs = null;
}
}
},
get: function(o, cb) {
var resp, fingerprint;
cb = cb || $.noop;
o = _.isString(o) ? {
url: o
} : o || {};
fingerprint = this._fingerprint(o);
this.cancelled = false;
this.lastReq = fingerprint;
if (resp = this._cache.get(fingerprint)) {
cb(null, resp);
} else {
this._get(o, cb);
}
},
cancel: function() {
this.cancelled = true;
}
});
return Transport;
}();
var SearchIndex = window.SearchIndex = function() {
"use strict";
var CHILDREN = "c", IDS = "i";
function SearchIndex(o) {
o = o || {};
if (!o.datumTokenizer || !o.queryTokenizer) {
$.error("datumTokenizer and queryTokenizer are both required");
}
this.identify = o.identify || _.stringify;
this.datumTokenizer = o.datumTokenizer;
this.queryTokenizer = o.queryTokenizer;
this.matchAnyQueryToken = o.matchAnyQueryToken;
this.reset();
}
_.mixin(SearchIndex.prototype, {
bootstrap: function bootstrap(o) {
this.datums = o.datums;
this.trie = o.trie;
},
add: function(data) {
var that = this;
data = _.isArray(data) ? data : [ data ];
_.each(data, function(datum) {
var id, tokens;
that.datums[id = that.identify(datum)] = datum;
tokens = normalizeTokens(that.datumTokenizer(datum));
_.each(tokens, function(token) {
var node, chars, ch;
node = that.trie;
chars = token.split("");
while (ch = chars.shift()) {
node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
node[IDS].push(id);
}
});
});
},
get: function get(ids) {
var that = this;
return _.map(ids, function(id) {
return that.datums[id];
});
},
search: function search(query) {
var that = this, tokens, matches;
tokens = normalizeTokens(this.queryTokenizer(query));
_.each(tokens, function(token) {
var node, chars, ch, ids;
if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
return false;
}
node = that.trie;
chars = token.split("");
while (node && (ch = chars.shift())) {
node = node[CHILDREN][ch];
}
if (node && chars.length === 0) {
ids = node[IDS].slice(0);
matches = matches ? getIntersection(matches, ids) : ids;
} else {
if (!that.matchAnyQueryToken) {
matches = [];
return false;
}
}
});
return matches ? _.map(unique(matches), function(id) {
return that.datums[id];
}) : [];
},
all: function all() {
var values = [];
for (var key in this.datums) {
values.push(this.datums[key]);
}
return values;
},
reset: function reset() {
this.datums = {};
this.trie = newNode();
},
serialize: function serialize() {
return {
datums: this.datums,
trie: this.trie
};
}
});
return SearchIndex;
function normalizeTokens(tokens) {
tokens = _.filter(tokens, function(token) {
return !!token;
});
tokens = _.map(tokens, function(token) {
return token.toLowerCase();
});
return tokens;
}
function newNode() {
var node = {};
node[IDS] = [];
node[CHILDREN] = {};
return node;
}
function unique(array) {
var seen = {}, uniques = [];
for (var i = 0, len = array.length; i < len; i++) {
if (!seen[array[i]]) {
seen[array[i]] = true;
uniques.push(array[i]);
}
}
return uniques;
}
function getIntersection(arrayA, arrayB) {
var ai = 0, bi = 0, intersection = [];
arrayA = arrayA.sort();
arrayB = arrayB.sort();
var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
while (ai < lenArrayA && bi < lenArrayB) {
if (arrayA[ai] < arrayB[bi]) {
ai++;
} else if (arrayA[ai] > arrayB[bi]) {
bi++;
} else {
intersection.push(arrayA[ai]);
ai++;
bi++;
}
}
return intersection;
}
}();
var Prefetch = function() {
"use strict";
var keys;
keys = {
data: "data",
protocol: "protocol",
thumbprint: "thumbprint"
};
function Prefetch(o) {
this.url = o.url;
this.ttl = o.ttl;
this.cache = o.cache;
this.prepare = o.prepare;
this.transform = o.transform;
this.transport = o.transport;
this.thumbprint = o.thumbprint;
this.storage = new PersistentStorage(o.cacheKey);
}
_.mixin(Prefetch.prototype, {
_settings: function settings() {
return {
url: this.url,
type: "GET",
dataType: "json"
};
},
store: function store(data) {
if (!this.cache) {
return;
}
this.storage.set(keys.data, data, this.ttl);
this.storage.set(keys.protocol, location.protocol, this.ttl);
this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
},
fromCache: function fromCache() {
var stored = {}, isExpired;
if (!this.cache) {
return null;
}
stored.data = this.storage.get(keys.data);
stored.protocol = this.storage.get(keys.protocol);
stored.thumbprint = this.storage.get(keys.thumbprint);
isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
return stored.data && !isExpired ? stored.data : null;
},
fromNetwork: function(cb) {
var that = this, settings;
if (!cb) {
return;
}
settings = this.prepare(this._settings());
this.transport(settings).fail(onError).done(onResponse);
function onError() {
cb(true);
}
function onResponse(resp) {
cb(null, that.transform(resp));
}
},
clear: function clear() {
this.storage.clear();
return this;
}
});
return Prefetch;
}();
var Remote = function() {
"use strict";
function Remote(o) {
this.url = o.url;
this.prepare = o.prepare;
this.transform = o.transform;
this.indexResponse = o.indexResponse;
this.transport = new Transport({
cache: o.cache,
limiter: o.limiter,
transport: o.transport
});
}
_.mixin(Remote.prototype, {
_settings: function settings() {
return {
url: this.url,
type: "GET",
dataType: "json"
};
},
get: function get(query, cb) {
var that = this, settings;
if (!cb) {
return;
}
query = query || "";
settings = this.prepare(query, this._settings());
return this.transport.get(settings, onResponse);
function onResponse(err, resp) {
err ? cb([]) : cb(that.transform(resp));
}
},
cancelLastRequest: function cancelLastRequest() {
this.transport.cancel();
}
});
return Remote;
}();
var oParser = function() {
"use strict";
return function parse(o) {
var defaults, sorter;
defaults = {
initialize: true,
identify: _.stringify,
datumTokenizer: null,
queryTokenizer: null,
matchAnyQueryToken: false,
sufficient: 5,
indexRemote: false,
sorter: null,
local: [],
prefetch: null,
remote: null
};
o = _.mixin(defaults, o || {});
!o.datumTokenizer && $.error("datumTokenizer is required");
!o.queryTokenizer && $.error("queryTokenizer is required");
sorter = o.sorter;
o.sorter = sorter ? function(x) {
return x.sort(sorter);
} : _.identity;
o.local = _.isFunction(o.local) ? o.local() : o.local;
o.prefetch = parsePrefetch(o.prefetch);
o.remote = parseRemote(o.remote);
return o;
};
function parsePrefetch(o) {
var defaults;
if (!o) {
return null;
}
defaults = {
url: null,
ttl: 24 * 60 * 60 * 1e3,
cache: true,
cacheKey: null,
thumbprint: "",
prepare: _.identity,
transform: _.identity,
transport: null
};
o = _.isString(o) ? {
url: o
} : o;
o = _.mixin(defaults, o);
!o.url && $.error("prefetch requires url to be set");
o.transform = o.filter || o.transform;
o.cacheKey = o.cacheKey || o.url;
o.thumbprint = VERSION + o.thumbprint;
o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
return o;
}
function parseRemote(o) {
var defaults;
if (!o) {
return;
}
defaults = {
url: null,
cache: true,
prepare: null,
replace: null,
wildcard: null,
limiter: null,
rateLimitBy: "debounce",
rateLimitWait: 300,
transform: _.identity,
transport: null
};
o = _.isString(o) ? {
url: o
} : o;
o = _.mixin(defaults, o);
!o.url && $.error("remote requires url to be set");
o.transform = o.filter || o.transform;
o.prepare = toRemotePrepare(o);
o.limiter = toLimiter(o);
o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
delete o.replace;
delete o.wildcard;
delete o.rateLimitBy;
delete o.rateLimitWait;
return o;
}
function toRemotePrepare(o) {
var prepare, replace, wildcard;
prepare = o.prepare;
replace = o.replace;
wildcard = o.wildcard;
if (prepare) {
return prepare;
}
if (replace) {
prepare = prepareByReplace;
} else if (o.wildcard) {
prepare = prepareByWildcard;
} else {
prepare = idenityPrepare;
}
return prepare;
function prepareByReplace(query, settings) {
settings.url = replace(settings.url, query);
return settings;
}
function prepareByWildcard(query, settings) {
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
return settings;
}
function idenityPrepare(query, settings) {
return settings;
}
}
function toLimiter(o) {
var limiter, method, wait;
limiter = o.limiter;
method = o.rateLimitBy;
wait = o.rateLimitWait;
if (!limiter) {
limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
}
return limiter;
function debounce(wait) {
return function debounce(fn) {
return _.debounce(fn, wait);
};
}
function throttle(wait) {
return function throttle(fn) {
return _.throttle(fn, wait);
};
}
}
function callbackToDeferred(fn) {
return function wrapper(o) {
var deferred = $.Deferred();
fn(o, onSuccess, onError);
return deferred;
function onSuccess(resp) {
_.defer(function() {
deferred.resolve(resp);
});
}
function onError(err) {
_.defer(function() {
deferred.reject(err);
});
}
};
}
}();
var Bloodhound = function() {
"use strict";
var old;
old = window && window.Bloodhound;
function Bloodhound(o) {
o = oParser(o);
this.sorter = o.sorter;
this.identify = o.identify;
this.sufficient = o.sufficient;
this.indexRemote = o.indexRemote;
this.local = o.local;
this.remote = o.remote ? new Remote(o.remote) : null;
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
this.index = new SearchIndex({
identify: this.identify,
datumTokenizer: o.datumTokenizer,
queryTokenizer: o.queryTokenizer
});
o.initialize !== false && this.initialize();
}
Bloodhound.noConflict = function noConflict() {
window && (window.Bloodhound = old);
return Bloodhound;
};
Bloodhound.tokenizers = tokenizers;
_.mixin(Bloodhound.prototype, {
__ttAdapter: function ttAdapter() {
var that = this;
return this.remote ? withAsync : withoutAsync;
function withAsync(query, sync, async) {
return that.search(query, sync, async);
}
function withoutAsync(query, sync) {
return that.search(query, sync);
}
},
_loadPrefetch: function loadPrefetch() {
var that = this, deferred, serialized;
deferred = $.Deferred();
if (!this.prefetch) {
deferred.resolve();
} else if (serialized = this.prefetch.fromCache()) {
this.index.bootstrap(serialized);
deferred.resolve();
} else {
this.prefetch.fromNetwork(done);
}
return deferred.promise();
function done(err, data) {
if (err) {
return deferred.reject();
}
that.add(data);
that.prefetch.store(that.index.serialize());
deferred.resolve();
}
},
_initialize: function initialize() {
var that = this, deferred;
this.clear();
(this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
return this.initPromise;
function addLocalToIndex() {
that.add(that.local);
}
},
initialize: function initialize(force) {
return !this.initPromise || force ? this._initialize() : this.initPromise;
},
add: function add(data) {
this.index.add(data);
return this;
},
get: function get(ids) {
ids = _.isArray(ids) ? ids : [].slice.call(arguments);
return this.index.get(ids);
},
search: function search(query, sync, async) {
var that = this, local;
sync = sync || _.noop;
async = async || _.noop;
local = this.sorter(this.index.search(query));
sync(this.remote ? local.slice() : local);
if (this.remote && local.length < this.sufficient) {
this.remote.get(query, processRemote);
} else if (this.remote) {
this.remote.cancelLastRequest();
}
return this;
function processRemote(remote) {
var nonDuplicates = [];
_.each(remote, function(r) {
!_.some(local, function(l) {
return that.identify(r) === that.identify(l);
}) && nonDuplicates.push(r);
});
that.indexRemote && that.add(nonDuplicates);
async(nonDuplicates);
}
},
all: function all() {
return this.index.all();
},
clear: function clear() {
this.index.reset();
return this;
},
clearPrefetchCache: function clearPrefetchCache() {
this.prefetch && this.prefetch.clear();
return this;
},
clearRemoteCache: function clearRemoteCache() {
Transport.resetCache();
return this;
},
ttAdapter: function ttAdapter() {
return this.__ttAdapter();
}
});
return Bloodhound;
}();
return Bloodhound;
});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,284 +0,0 @@
Bloodhound
==========
Bloodhound is the typeahead.js suggestion engine. Bloodhound is robust,
flexible, and offers advanced functionalities such as prefetching, intelligent
caching, fast lookups, and backfilling with remote data.
Table of Contents
-----------------
* [Features](#features)
* [Usage](#usage)
* [API](#api)
* [Options](#options)
* [Prefetch](#prefetch)
* [Remote](#remote)
Features
--------
* Works with hardcoded data
* Prefetches data on initialization to reduce suggestion latency
* Uses local storage intelligently to cut down on network requests
* Backfills suggestions from a remote source
* Rate-limits and caches network requests to remote sources to lighten the load
Usage
-----
### API
* [`new Bloodhound(options)`](#new-bloodhoundoptions)
* [`Bloodhound.noConflict()`](#bloodhoundnoconflict)
* [`Bloodhound#initialize(reinitialize)`](#bloodhoundinitializereinitialize)
* [`Bloodhound#add(data)`](#bloodhoundadddata)
* [`Bloodhound#get(ids)`](#bloodhoundgetids)
* [`Bloodhound#search(query, sync, async)`](#bloodhoundsearchquery-sync-async)
* [`Bloodhound#clear()`](#bloodhoundclear)
#### new Bloodhound(options)
The constructor function. It takes an [options hash](#options) as its only
argument.
```javascript
var engine = new Bloodhound({
local: ['dog', 'pig', 'moose'],
queryTokenizer: Bloodhound.tokenizers.whitespace,
datumTokenizer: Bloodhound.tokenizers.whitespace
});
```
#### Bloodhound.noConflict()
Returns a reference to `Bloodhound` and reverts `window.Bloodhound` to its
previous value. Can be used to avoid naming collisions.
```javascript
var Dachshund = Bloodhound.noConflict();
```
#### Bloodhound#initialize(reinitialize)
Kicks off the initialization of the suggestion engine. Initialization entails
adding the data provided by `local` and `prefetch` to the internal search
index as well as setting up transport mechanism used by `remote`. Before
`#initialize` is called, the `#get` and `#search` methods will effectively be
no-ops.
Note, unless the `initialize` option is `false`, this method is implicitly
called by the constructor.
```javascript
var engine = new Bloodhound({
initialize: false,
local: ['dog', 'pig', 'moose'],
queryTokenizer: Bloodhound.tokenizers.whitespace,
datumTokenizer: Bloodhound.tokenizers.whitespace
});
var promise = engine.initialize();
promise
.done(function() { console.log('ready to go!'); })
.fail(function() { console.log('err, something went wrong :('); });
```
After initialization, how subsequent invocations of `#initialize` behave
depends on the `reinitialize` argument. If `reinitialize` is falsy, the
method will not execute the initialization logic and will just return the same
jQuery promise returned by the initial invocation. If `reinitialize` is truthy,
the method will behave as if it were being called for the first time.
```javascript
var promise1 = engine.initialize();
var promise2 = engine.initialize();
var promise3 = engine.initialize(true);
assert(promise1 === promise2);
assert(promise3 !== promise1 && promise3 !== promise2);
```
<!-- section links -->
[jQuery promise]: http://api.jquery.com/Types/#Promise
#### Bloodhound#add(data)
Takes one argument, `data`, which is expected to be an array. The data passed
in will get added to the internal search index.
```javascript
engine.add([{ val: 'one' }, { val: 'two' }]);
```
#### Bloodhound#get(ids)
Returns the data in the local search index corresponding to `ids`.
```javascript
var engine = new Bloodhound({
local: [{ id: 1, name: 'dog' }, { id: 2, name: 'pig' }],
identify: function(obj) { return obj.id; },
queryTokenizer: Bloodhound.tokenizers.whitespace,
datumTokenizer: Bloodhound.tokenizers.whitespace
});
engine.get([1, 3]); // [{ id: 1, name: 'dog' }, null]
```
#### Bloodhound#search(query, sync, async)
Returns the data that matches `query`. Matches found in the local search index
will be passed to the `sync` callback. If the data passed to `sync` doesn't
contain at least `sufficient` number of datums, `remote` data will be requested
and then passed to the `async` callback.
```javascript
bloodhound.search(myQuery, sync, async);
function sync(datums) {
console.log('datums from `local`, `prefetch`, and `#add`');
console.log(datums);
}
function async(datums) {
console.log('datums from `remote`');
console.log(datums);
}
```
#### Bloodhound#clear()
Clears the internal search index that's powered by `local`, `prefetch`, and
`#add`.
```javascript
engine.clear();
```
### Options
When instantiating a Bloodhound suggestion engine, there are a number of
options you can configure.
* `datumTokenizer`  A function with the signature `(datum)` that transforms a
datum into an array of string tokens. **Required**.
* `queryTokenizer`  A function with the signature `(query)` that transforms a
query into an array of string tokens. **Required**.
* `matchAnyQueryToken` - By default a search result must match ALL query-tokens.
Instead, this option returns results that match ANY query-tokens. Defaults to
`false`.
* `initialize`  If set to `false`, the Bloodhound instance will not be
implicitly initialized by the constructor function. Defaults to `true`.
* `identify`  Given a datum, this function is expected to return a unique id
for it. Defaults to `JSON.stringify`. Note that it is **highly recommended**
to override this option.
* `sufficient` If the number of datums provided from the internal search
index is less than `sufficient`, `remote` will be used to backfill search
requests triggered by calling `#search`. Defaults to `5`.
* `sorter`  A [compare function] used to sort data returned from the internal
search index.
* `local` An array of data or a function that returns an array of data. The
data will be added to the internal search index when `#initialize` is called.
* `prefetch` Can be a URL to a JSON file containing an array of data or, if
more configurability is needed, a [prefetch options hash](#prefetch).
* `remote` Can be a URL to fetch data from when the data provided by
the internal search index is insufficient or, if more configurability is
needed, a [remote options hash](#remote).
* `indexRemote` Adds the data loaded from `remote` to the search index (where
`local` and `prefetch` are stored for retrieval). Defaults to `false`.
<!-- section links -->
[compare function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
### Prefetch
Prefetched data is fetched and processed on initialization. If the browser
supports local storage, the processed data will be cached there to
prevent additional network requests on subsequent page loads.
**WARNING:** While it's possible to get away with it for smaller data sets,
prefetched data isn't meant to contain entire sets of data. Rather, it should
act as a first-level cache. Ignoring this warning means you'll run the risk of
hitting [local storage limits].
When configuring `prefetch`, the following options are available.
* `url` The URL prefetch data should be loaded from. **Required.**
* `cache`  If `false`, will not attempt to read or write to local storage and
will always load prefetch data from `url` on initialization. Defaults to
`true`.
* `ttl` The time (in milliseconds) the prefetched data should be cached in
local storage. Defaults to `86400000` (1 day).
* `cacheKey` The key that data will be stored in local storage under.
Defaults to value of `url`.
* `thumbprint` A string used for thumbprinting prefetched data. If this
doesn't match what's stored in local storage, the data will be refetched.
* `prepare`  A function that provides a hook to allow you to prepare the
settings object passed to `transport` when a request is about to be made.
The function signature should be `prepare(settings)` where `settings` is the
default settings object created internally by the Bloodhound instance. The
`prepare` function should return a settings object. Defaults to the
[identity function].
* `transform`  A function with the signature `transform(response)` that allows
you to transform the prefetch response before the Bloodhound instance operates
on it. Defaults to the [identity function].
<!-- section links -->
[local storage limits]: http://stackoverflow.com/a/2989317
[identity function]: http://en.wikipedia.org/wiki/Identity_function
### Remote
Bloodhound only goes to the network when the internal search engine cannot
provide a sufficient number of results. In order to prevent an obscene number
of requests being made to the remote endpoint, requests are rate-limited.
When configuring `remote`, the following options are available.
* `url` The URL remote data should be loaded from. **Required.**
* `prepare`  A function that provides a hook to allow you to prepare the
settings object passed to `transport` when a request is about to be made.
The function signature should be `prepare(query, settings)`, where `query` is
the query `#search` was called with and `settings` is the default settings
object created internally by the Bloodhound instance. The `prepare` function
should return a settings object. Defaults to the [identity function].
* `wildcard` A convenience option for `prepare`. If set, `prepare` will be a
function that replaces the value of this option in `url` with the URI encoded
query.
* `rateLimitBy` The method used to rate-limit network requests. Can be either
`debounce` or `throttle`. Defaults to `debounce`.
* `rateLimitWait` The time interval in milliseconds that will be used by
`rateLimitBy`. Defaults to `300`.
* `transform`  A function with the signature `transform(response)` that allows
you to transform the remote response before the Bloodhound instance operates
on it. Defaults to the [identity function].
<!-- section links -->
[identity function]: http://en.wikipedia.org/wiki/Identity_function

View File

@ -1,288 +0,0 @@
jQuery#typeahead
----------------
The UI component of typeahead.js is available as a jQuery plugin. It's
responsible for rendering suggestions and handling DOM interactions.
Table of Contents
-----------------
* [Features](#features)
* [Usage](#usage)
* [API](#api)
* [Options](#options)
* [Datasets](#datasets)
* [Custom Events](#custom-events)
* [Class Names](#class-names)
Features
--------
* Displays suggestions to end-users as they type
* Shows top suggestion as a hint (i.e. background text)
* Supports custom templates to allow for UI flexibility
* Works well with RTL languages and input method editors
* Highlights query matches within the suggestion
* Triggers custom events to encourage extensibility
Usage
-----
### API
* [`jQuery#typeahead(options, [*datasets])`](#jquerytypeaheadoptions-datasets)
* [`jQuery#typeahead('val')`](#jquerytypeaheadval)
* [`jQuery#typeahead('val', val)`](#jquerytypeaheadval-val)
* [`jQuery#typeahead('destroy')`](#jquerytypeaheaddestroy)
* [`jQuery.fn.typeahead.noConflict()`](#jqueryfntypeaheadnoconflict)
#### jQuery#typeahead(options, [\*datasets])
For a given `input[type="text"]`, enables typeahead functionality. `options`
is an options hash that's used for configuration. Refer to [Options](#options)
for more info regarding the available configs. Subsequent arguments
(`*datasets`), are individual option hashes for datasets. For more details
regarding datasets, refer to [Datasets](#datasets).
```javascript
$('.typeahead').typeahead({
minLength: 3,
highlight: true
},
{
name: 'my-dataset',
source: mySource
});
```
#### jQuery#typeahead('val')
Returns the current value of the typeahead. The value is the text the user has
entered into the `input` element.
```javascript
var myVal = $('.typeahead').typeahead('val');
```
#### jQuery#typeahead('val', val)
Sets the value of the typeahead. This should be used in place of `jQuery#val`.
```javascript
$('.typeahead').typeahead('val', myVal);
```
#### jQuery#typeahead('open')
Opens the suggestion menu.
```javascript
$('.typeahead').typeahead('open');
```
#### jQuery#typeahead('close')
Closes the suggestion menu.
```javascript
$('.typeahead').typeahead('close');
```
#### jQuery#typeahead('destroy')
Removes typeahead functionality and reverts the `input` element back to its
original state.
```javascript
$('.typeahead').typeahead('destroy');
```
#### jQuery.fn.typeahead.noConflict()
Returns a reference to the typeahead plugin and reverts `jQuery.fn.typeahead`
to its previous value. Can be used to avoid naming collisions.
```javascript
var typeahead = jQuery.fn.typeahead.noConflict();
jQuery.fn._typeahead = typeahead;
```
### Options
When initializing a typeahead, there are a number of options you can configure.
* `highlight` If `true`, when suggestions are rendered, pattern matches
for the current query in text nodes will be wrapped in a `strong` element with
its class set to `{{classNames.highlight}}`. Defaults to `false`.
* `hint` If `false`, the typeahead will not show a hint. Defaults to `true`.
* `minLength`  The minimum character length needed before suggestions start
getting rendered. Defaults to `1`.
* `classNames`  For overriding the default class names used. See
[Class Names](#class-names) for more details.
### Datasets
A typeahead is composed of one or more datasets. When an end-user modifies the
value of a typeahead, each dataset will attempt to render suggestions for the
new value.
For most use cases, one dataset should suffice. It's only in the scenario where
you want rendered suggestions to be grouped based on some sort of categorical
relationship that you'd need to use multiple datasets. For example, on
twitter.com, the search typeahead groups results into recent searches, trends,
and accounts  that would be a great use case for using multiple datasets.
Datasets can be configured using the following options.
* `source` The backing data source for suggestions. Expected to be a function
with the signature `(query, syncResults, asyncResults)`. `syncResults` should
be called with suggestions computed synchronously and `asyncResults` should be
called with suggestions computed asynchronously (e.g. suggestions that come
for an AJAX request). `source` can also be a Bloodhound instance.
**Required**.
* `async`  Lets the dataset know if async suggestions should be expected. If
not set, this information is inferred from the signature of `source` i.e.
if the `source` function expects 3 arguments, `async` will be set to `true`.
* `name`  The name of the dataset. This will be appended to
`{{classNames.dataset}}-` to form the class name of the containing DOM
element. Must only consist of underscores, dashes, letters (`a-z`), and
numbers. Defaults to a random number.
* `limit`  The max number of suggestions to be displayed. Defaults to `5`.
* `display` For a given suggestion, determines the string representation
of it. This will be used when setting the value of the input control after a
suggestion is selected. Can be either a key string or a function that
transforms a suggestion object into a string. Defaults to stringifying the
suggestion.
* `templates`  A hash of templates to be used when rendering the dataset. Note
a precompiled template is a function that takes a JavaScript object as its
first argument and returns a HTML string.
* `notFound` 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`.
* `pending` - Rendered when `0` synchronous suggestions are available but
asynchronous suggestions are expected. Can be either a HTML string or a
precompiled template. If it's a precompiled template, the passed in context
will contain `query`.
* `header` Rendered at the top of the dataset when suggestions are present.
Can be either a HTML string or a precompiled template. If it's a precompiled
template, the passed in context will contain `query` and `suggestions`.
* `footer` Rendered at the bottom of the dataset when suggestions are
present. Can be either a HTML string or a precompiled template. If it's a
precompiled template, the passed in context will contain `query` and
`suggestions`.
* `suggestion` Used to render a single suggestion. If set, this has to be a
precompiled template. The associated suggestion object will serve as the
context. Defaults to the value of `display` wrapped in a `div` tag i.e.
`<div>{{value}}</div>`.
### Custom Events
The following events get triggered on the input element during the life-cycle of
a typeahead.
* `typeahead:active` Fired when the typeahead moves to active state.
* `typeahead:idle` Fired when the typeahead moves to idle state.
* `typeahead:open` Fired when the results container is opened.
* `typeahead:close` Fired when the results container is closed.
* `typeahead:change` Normalized version of the native [`change` event].
Fired when input loses focus and the value has changed since it originally
received focus.
* `typeahead:render` Fired when suggestions are rendered for a dataset. The
event handler will be invoked with 4 arguments: the jQuery event object, the
suggestions that were rendered, a flag indicating whether the suggestions
were fetched asynchronously, and the name of the dataset the rendering
occurred in.
* `typeahead:select` Fired when a suggestion is selected. The event handler
will be invoked with 2 arguments: the jQuery event object and the suggestion
object that was selected.
* `typeahead:autocomplete` Fired when a autocompletion occurs. The
event handler will be invoked with 2 arguments: the jQuery event object and
the suggestion object that was used for autocompletion.
* `typeahead:cursorchange` Fired when the results container cursor moves. The
event handler will be invoked with 2 arguments: the jQuery event object and
the suggestion object that was moved to.
* `typeahead:asyncrequest` Fired when an async request for suggestions is
sent. The event handler will be invoked with 3 arguments: the jQuery event
object, the current query, and the name of the dataset the async request
belongs to.
* `typeahead:asynccancel` Fired when an async request is cancelled. The event
handler will be invoked with 3 arguments: the jQuery event object, the current
query, and the name of the dataset the async request belonged to.
* `typeahead:asyncreceive` Fired when an async request completes. The event
handler will be invoked with 3 arguments: the jQuery event object, the current
query, and the name of the dataset the async request belongs to.
Example usage:
```
$('.typeahead').bind('typeahead:select', function(ev, suggestion) {
console.log('Selection: ' + suggestion);
});
```
**NOTE**: Every event does not supply the same arguments. See the event
descriptions above for details on each event's argument list.
<!-- section links -->
[`change` event]: https://developer.mozilla.org/en-US/docs/Web/Events/change
### Class Names
* `input` - Added to input that's initialized into a typeahead. Defaults to
`tt-input`.
* `hint` - Added to hint input. Defaults to `tt-hint`.
* `menu` - Added to menu element. Defaults to `tt-menu`.
* `dataset` - Added to dataset elements. to Defaults to `tt-dataset`.
* `suggestion` - Added to suggestion elements. Defaults to `tt-suggestion`.
* `empty` - Added to menu element when it contains no content. Defaults to
`tt-empty`.
* `open` - Added to menu element when it is opened. Defaults to `tt-open`.
* `cursor` - Added to suggestion element when menu cursor moves to said
suggestion. Defaults to `tt-cursor`.
* `highlight` - Added to the element that wraps highlighted text. Defaults to
`tt-highlight`.
To override any of these defaults, you can use the `classNames` option:
```javascript
$('.typeahead').typeahead({
classNames: {
input: 'Typeahead-input',
hint: 'Typeahead-hint',
selectable: 'Typeahead-selectable'
}
});
```

View File

@ -1,234 +0,0 @@
Migrating to typeahead.js v0.10.0
=================================
Preamble
--------
v0.10.0 of typeahead.js ended up being almost a complete rewrite. Many things
stayed the same, but there were a handful of changes you need to be aware of
if you plan on upgrading from an older version. This document aims to call out
those changes and explain what you need to do in order to have an painless
upgrade.
Notable Changes
----------------
### First Argument to the jQuery Plugin
In v0.10.0, the first argument to `jQuery#typeahead` is an options hash that
can be used to configure the behavior of the typeahead. This is in contrast
to previous versions where `jQuery#typeahead` expected just a series of datasets
to be passed to it:
```javascript
// pre-v0.10.0
$('.typeahead').typeahead(myDataset);
// v0.10.0
$('.typeahead').typeahead({
highlight: true,
hint: false
}, myDataset);
```
If you're fine with the default configuration, you can just pass `null` as the
first argument:
```javascript
$('.typeahead').typeahead(null, myDataset);
```
### Bloodhound Suggestion Engine
The most notable change in v0.10.0 is that typeahead.js has been decomposed into
a suggestion engine and a UI view. As part of this change, the way you configure
datasets has changed. Previously, a dataset config would have looked like:
```javascript
{
valueKey: 'num',
local: [{ num: 'one' }, { num: 'two' }, { num: 'three' }],
prefetch: '/prefetch',
remote: '/remote?q=%QUERY'
}
```
In v0.10.0, an equivalent dataset config would look like:
```javascript
{
displayKey: 'num',
source: mySource
}
```
As you can see, `local`, `prefetch`, and `remote` are no longer defined at the
dataset level. Instead, all you set in a dataset config is `source`. `source` is
expected to be a function with the signature `function(query, callback)`. When a
typeahead's query changes, suggestions will be requested from `source`. It's
expected `source` will compute the suggestion set and invoke `callback` with an array
of suggestion objects. The typeahead will then go on to render those suggestions.
If you're wondering if you can still configure `local`, `prefetch`, and
`remote`, don't worry, that's where the Bloodhound suggestion engine comes in.
Here's how you would define `mySource` which was referenced in the previous
code snippet:
```
var mySource = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.num);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [{ num: 'one' }, { num: 'two' }, { num: 'three' }],
prefetch: '/prefetch',
remote: '/remote?q=%QUERY'
});
// this kicks off the loading and processing of local and prefetch data
// the suggestion engine will be useless until it is initialized
mySource.initialize();
```
In the above snippet, a Bloodhound suggestion engine is initialized and that's
what will be used as the source of your dataset. There's still one last thing
that needs to be done before you can use a Bloodhound suggestion engine as the
source of a dataset. Because datasets expect `source` to be function, the
Bloodhound instance needs to be wrapped in an adapter so it can meet that
expectation.
```
mySource = mySource.ttAdapter();
```
Put it all together:
```javascript
var mySource = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.num);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [{ num: 'one' }, { num: 'two' }, { num: 'three' }],
prefetch: '/prefetch',
remote: '/remote?q=%QUERY'
});
mySource.initialize();
$('.typeahead').typeahead(null, {
displayKey: 'num',
source: mySource.ttAdapter()
});
```
### Tokenization Methods Must Be Provided
The Bloodhound suggestion engine is token-based, so how datums and queries are
tokenized plays a vital role in the quality of search results. Pre-v0.10.0,
it was not possible to configure the tokenization method. Starting in v0.10.0,
you **must** specify how you want datums and queries tokenized.
The most common tokenization methods split a given string on whitespace or
non-word characters. Bloodhound provides implementations for those methods
out of the box:
```javascript
// returns ['one', 'two', 'twenty-five']
Bloodhound.tokenizers.whitespace(' one two twenty-five');
// returns ['one', 'two', 'twenty', 'five']
Bloodhound.tokenizers.nonword(' one two twenty-five');
```
For query tokenization, you'll probably want to use one of the above methods.
For datum tokenization, this is where you may want to do something a tad bit
more advanced.
For datums, sometimes you want tokens to be dervied from more than one property.
For example, if you were building a search engine for GitHub repositories, it'd
probably be wise to have tokens derived from the repo's name, owner, and
primary language:
```javascript
var repos = [
{ name: 'example', owner: 'John Doe', language: 'JavaScript' },
{ name: 'another example', owner: 'Joe Doe', language: 'Scala' }
];
function customTokenizer(datum) {
var nameTokens = Bloodhound.tokenizers.whitespace(datum.name);
var ownerTokens = Bloodhound.tokenizers.whitespace(datum.owner);
var languageTokens = Bloodhound.tokenizers.whitespace(datum.language);
return nameTokens.concat(ownerTokens).concat(languageTokens);
}
```
There may also be the scenario where you want datum tokenization to be performed
on the backend. The best way to do that is to just add a property to your datums
that contains those tokens. You can then provide a tokenizer that just returns
the already existing tokens:
```javascript
var sports = [
{ value: 'football', tokens: ['football', 'pigskin'] },
{ value: 'basketball', tokens: ['basketball', 'bball'] }
];
function customTokenizer(datum) { return datum.tokens; }
```
There are plenty of other ways you could go about tokenizing datums, it really
just depends on what you are trying to accomplish.
### String Datums Are No Longer Supported
Dropping support for string datums was a difficult choice, but in the end it
made sense for a number of reasons. If you still want to hydrate the suggestion
engine with string datums, you'll need to use the `filter` function:
```javascript
var engine = new Bloodhound({
prefetch: {
url: '/data',
filter: function(data) {
// assume data is an array of strings e.g. ['one', 'two', 'three']
return $.map(data, function(str) { return { value: str }; });
},
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.value);
},
queryTokenizer: Bloodhound.tokenizers.whitespace
}
});
```
### Precompiled Templates Are Now Required
In previous versions of typeahead.js, you could specify a string template along
with the templating engine that should be used to compile/render it. In
v0.10.0, you can no longer specify templating engines; instead you must provide
precompiled templates. Precompiled templates are functions that take one
argument: the context the template should be rendered with.
Most of the popular templating engines allow for the creation of precompiled
templates. For example, you can generate one using Handlebars by doing the
following:
```javascript
var precompiledTemplate = Handlebars.compile('<p>{{value}}</p>');
```
[Handlebars]: http://handlebarsjs.com/
### CSS Class Changes
`tt-is-under-cursor` is now `tt-cursor` - Applied to a hovered-on suggestion (either via cursor or arrow key).
`tt-query` is now `tt-input` - Applied to the typeahead input field.
Something Missing?
------------------
If something is missing from this migration guide, pull requests are accepted :)

View File

@ -1,50 +0,0 @@
module.exports = function(config) {
config.set({
basePath: '',
preprocessors: {
'src/**/*.js': 'coverage'
},
reporters: ['progress', 'coverage'],
browsers: ['Chrome'],
frameworks: ['jasmine'],
coverageReporter: {
type: 'html',
dir: 'test/coverage/'
},
files: [
'bower_components/jquery/jquery.js',
'src/common/utils.js',
'src/bloodhound/version.js',
'src/bloodhound/tokenizers.js',
'src/bloodhound/lru_cache.js',
'src/bloodhound/persistent_storage.js',
'src/bloodhound/transport.js',
'src/bloodhound/remote.js',
'src/bloodhound/prefetch.js',
'src/bloodhound/search_index.js',
'src/bloodhound/options_parser.js',
'src/bloodhound/bloodhound.js',
'src/typeahead/www.js',
'src/typeahead/event_bus.js',
'src/typeahead/event_emitter.js',
'src/typeahead/highlight.js',
'src/typeahead/input.js',
'src/typeahead/dataset.js',
'src/typeahead/menu.js',
'src/typeahead/default_menu.js',
'src/typeahead/typeahead.js',
'src/typeahead/plugin.js',
'test/fixtures/**/*',
'bower_components/jasmine-jquery/lib/jasmine-jquery.js',
'bower_components/jasmine-ajax/lib/mock-ajax.js',
'test/helpers/**/*',
'test/**/*_spec.js'
]
});
};

View File

@ -1,19 +0,0 @@
Copyright (c) 2013-2014 Twitter, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,71 +0,0 @@
{
"name": "corejs-typeahead",
"description": "fast and fully-featured autocomplete library",
"keywords": [
"typeahead",
"autocomplete"
],
"homepage": "http://corejavascript.github.io/typeahead.js/",
"bugs": "https://github.com/corejavascript/typeahead.js/issues",
"repository": {
"type": "git",
"url": "https://github.com/corejavascript/typeahead.js.git"
},
"author": {
"name": "Twitter, Inc.",
"url": "https://twitter.com/twitteross"
},
"contributors": [
{
"name": "Jake Harding",
"url": "https://twitter.com/JakeHarding"
},
{
"name": "Tim Trueman",
"url": "https://twitter.com/timtrueman"
},
{
"name": "Veljko Skarich",
"url": "https://twitter.com/vskarich"
}
],
"license": "See license in LICENSE",
"dependencies": {
"jquery": ">=1.7"
},
"devDependencies": {
"chai": "^3.3.0",
"colors": "^1.1.2",
"grunt": "~0.4",
"grunt-concurrent": "^2.0.3",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-connect": "^0.11.2",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-uglify": "^0.9.2",
"grunt-contrib-watch": "^0.6.1",
"grunt-exec": "~0.4.5",
"grunt-sed": "~0.1",
"grunt-step": "~0.2.0",
"grunt-umd": "^2.3.3",
"karma": "^0.13.14",
"karma-chrome-launcher": "^0.2.1",
"karma-coverage": "^0.5.2",
"karma-firefox-launcher": "^0.1.3",
"karma-jasmine": "^0.1.6",
"karma-opera-launcher": "^0.3.0",
"karma-phantomjs-launcher": "^0.2.1",
"karma-safari-launcher": "^0.1.1",
"mocha": "^2.3.3",
"node-static": "^0.7.7",
"semver": "^5.0.3",
"underscore": "^1.6.0",
"yiewd": "^0.6.0"
},
"scripts": {
"postinstall": "bower install",
"test": "./node_modules/karma/bin/karma start --single-run --browsers PhantomJS"
},
"version": "0.11.1",
"main": "dist/typeahead.bundle.js"
}

View File

@ -1,135 +0,0 @@
[![Build Status](https://travis-ci.org/corejavascript/typeahead.js.svg?branch=master)](https://travis-ci.org/corejavascript/typeahead.js)
[![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)
[![bitHound Score](https://www.bithound.io/github/corejavascript/typeahead.js/badges/score.svg)](https://www.bithound.io/github/corejavascript/typeahead.js)
[![bitHound Dependencies](https://www.bithound.io/github/corejavascript/typeahead.js/badges/dependencies.svg)](https://www.bithound.io/github/corejavascript/typeahead.js/master/dependencies/npm)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/iron/iron/master/LICENSE)
# [corejs-typeahead](https://typeahead.js.org/)
This is a maintained fork of [twitter.com](https://twitter.com)'s autocomplete search library, [typeahead.js](https://github.com/twitter/typeahead.js).
The typeahead.js library consists of 2 components: the suggestion engine,
[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).
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 provide a rich typeahead experience.
## Getting Started
How you acquire typeahead.js is up to you:
* Install with [Bower](https://bower.io/): `$ bower install corejs-typeahead`
* Install with [npm](https://www.npmjs.com): `$ npm install corejs-typeahead`
* [Download zipball of latest release](https://github.com/corejavascript/typeahead.js/archive/master.zip)
* Download the latest dist files individually:
* [bloodhound.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/bloodhound.js) (standalone suggestion engine)
* [typeahead.jquery.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/typeahead.jquery.js) (standalone UI view)
* [typeahead.bundle.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/typeahead.bundle.js) (*bloodhound.js* + *typeahead.jquery.js*)
* [typeahead.bundle.min.js](https://github.com/corejavascript/typeahead.js/raw/master/dist/typeahead.bundle.min.js)
**Note:** both *bloodhound.js* and *typeahead.jquery.js* have a dependency on
[jQuery](http://jquery.com/) 1.9+.
## Documentation
* [Typeahead Docs](https://github.com/corejavascript/typeahead.js/blob/master/doc/jquery_typeahead.md)
* [Bloodhound Docs](https://github.com/corejavascript/typeahead.js/blob/master/doc/bloodhound.md)
## Examples
For some working examples of typeahead.js, visit the [examples page](https://typeahead.js.org/examples).
## Browser Support
* Chrome
* Firefox 3.5+
* Safari 4+
* Internet Explorer 8+
* Opera 11+
**NOTE:** typeahead.js is not tested on mobile browsers.
## Customer Support
For general questions about typeahead.js, tweet at [@typeahead](https://twitter.com/typeahead).
For technical questions, you should post a question on [Stack Overflow](http://stackoverflow.com/) and tag
it with [typeahead.js](http://stackoverflow.com/questions/tagged/typeahead.js).
## Issues
Discovered a bug? Please create an issue here on GitHub!
[github.com/corejavascript/typeahead.js/issues](https://github.com/corejavascript/typeahead.js/issues)
## Versioning
For transparency and insight into our release cycle, releases will be numbered
with the following format:
`<major>.<minor>.<patch>`
And constructed with the following guidelines:
* Breaking backwards compatibility bumps the major
* New additions without breaking backwards compatibility bumps the minor
* Bug fixes and misc changes bump the patch
For more information on semantic versioning, please visit [semver.org](http://semver.org/).
## Testing
Tests are written using [Jasmine](http://jasmine.github.io/) and ran with [Karma](http://karma-runner.github.io/). To run
the test suite with PhantomJS, run `$ npm test`.
## Developers
If you plan on contributing to typeahead.js, be sure to read the
[contributing guidelines](https://github.com/corejavascript/typeahead.js/blob/master/CONTRIBUTING.md). A good starting place for new contributors are issues
labeled with [entry-level](https://github.com/corejavascript/typeahead.js/issues?&labels=entry-level&state=open). Entry-level issues tend to require minor changes
and provide developers a chance to get more familiar with typeahead.js before
taking on more challenging work.
In order to build and test typeahead.js, you'll need to install its dev
dependencies (`$ npm install`) and have [grunt-cli](https://github.com/gruntjs/grunt-cli)
installed (`$ npm install -g grunt-cli`). Below is an overview of the available
Grunt tasks that'll be useful in development.
* `grunt build` Builds *typeahead.js* from source.
* `grunt lint` Runs source and test files through JSHint.
* `grunt watch` Rebuilds *typeahead.js* whenever a source file is modified.
* `grunt server` Serves files from the root of typeahead.js on localhost:8888.
Useful for using *test/playground.html* for debugging/testing.
* `grunt dev` Runs `grunt watch` and `grunt server` in parallel.
## Maintainers
* **Jake Harding**
* [@JakeHarding](https://twitter.com/JakeHarding)
* [GitHub](https://github.com/jharding)
* **You?**
## Authors
* **Jake Harding**
* [@JakeHarding](https://twitter.com/JakeHarding)
* [GitHub](https://github.com/jharding)
* **Veljko Skarich**
* [@vskarich](https://twitter.com/vskarich)
* [GitHub](https://github.com/vskarich)
* **Tim Trueman**
* [@timtrueman](https://twitter.com/timtrueman)
* [GitHub](https://github.com/timtrueman)
## License
Copyright 2013 Twitter, Inc.
Licensed under the MIT License

View File

@ -1,199 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Bloodhound = (function() {
'use strict';
var old;
old = window && window.Bloodhound;
// constructor
// -----------
function Bloodhound(o) {
o = oParser(o);
this.sorter = o.sorter;
this.identify = o.identify;
this.sufficient = o.sufficient;
this.indexRemote = o.indexRemote;
this.local = o.local;
this.remote = o.remote ? new Remote(o.remote) : null;
this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
// the backing data structure used for fast pattern matching
this.index = new SearchIndex({
identify: this.identify,
datumTokenizer: o.datumTokenizer,
queryTokenizer: o.queryTokenizer
});
// hold off on intialization if the intialize option was explicitly false
o.initialize !== false && this.initialize();
}
// static methods
// --------------
Bloodhound.noConflict = function noConflict() {
window && (window.Bloodhound = old);
return Bloodhound;
};
Bloodhound.tokenizers = tokenizers;
// instance methods
// ----------------
_.mixin(Bloodhound.prototype, {
// ### super secret stuff used for integration with jquery plugin
__ttAdapter: function ttAdapter() {
var that = this;
return this.remote ? withAsync : withoutAsync;
function withAsync(query, sync, async) {
return that.search(query, sync, async);
}
function withoutAsync(query, sync) {
return that.search(query, sync);
}
},
// ### private
_loadPrefetch: function loadPrefetch() {
var that = this, deferred, serialized;
deferred = $.Deferred();
if (!this.prefetch) {
deferred.resolve();
}
else if (serialized = this.prefetch.fromCache()) {
this.index.bootstrap(serialized);
deferred.resolve();
}
else {
this.prefetch.fromNetwork(done);
}
return deferred.promise();
function done(err, data) {
if (err) { return deferred.reject(); }
that.add(data);
that.prefetch.store(that.index.serialize());
deferred.resolve();
}
},
_initialize: function initialize() {
var that = this, deferred;
// in case this is a reinitialization, clear previous data
this.clear();
(this.initPromise = this._loadPrefetch())
.done(addLocalToIndex); // local must be added to index after prefetch
return this.initPromise;
function addLocalToIndex() { that.add(that.local); }
},
// ### public
initialize: function initialize(force) {
return !this.initPromise || force ? this._initialize() : this.initPromise;
},
// TODO: before initialize what happens?
add: function add(data) {
this.index.add(data);
return this;
},
get: function get(ids) {
ids = _.isArray(ids) ? ids : [].slice.call(arguments);
return this.index.get(ids);
},
search: function search(query, sync, async) {
var that = this, local;
sync = sync || _.noop;
async = async || _.noop;
local = this.sorter(this.index.search(query));
// return a copy to guarantee no changes within this scope
// as this array will get used when processing the remote results
sync(this.remote ? local.slice() : local);
if (this.remote && local.length < this.sufficient) {
this.remote.get(query, processRemote);
}
else if (this.remote) {
// #149: prevents outdated rate-limited requests from being sent
this.remote.cancelLastRequest();
}
return this;
function processRemote(remote) {
var nonDuplicates = [];
// exclude duplicates
_.each(remote, function(r) {
!_.some(local, function(l) {
return that.identify(r) === that.identify(l);
}) && nonDuplicates.push(r);
});
// #1148: Should Bloodhound index remote datums?
that.indexRemote && that.add(nonDuplicates);
async(nonDuplicates);
}
},
all: function all() {
return this.index.all();
},
clear: function clear() {
this.index.reset();
return this;
},
clearPrefetchCache: function clearPrefetchCache() {
this.prefetch && this.prefetch.clear();
return this;
},
clearRemoteCache: function clearRemoteCache() {
Transport.resetCache();
return this;
},
// DEPRECATED: will be removed in v1
ttAdapter: function ttAdapter() {
return this.__ttAdapter();
}
});
return Bloodhound;
})();

View File

@ -1,101 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
// inspired by https://github.com/jharding/lru-cache
var LruCache = (function() {
'use strict';
function LruCache(maxSize) {
this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
this.reset();
// if max size is less than 0, provide a noop cache
if (this.maxSize <= 0) {
this.set = this.get = $.noop;
}
}
_.mixin(LruCache.prototype, {
set: function set(key, val) {
var tailItem = this.list.tail, node;
// at capacity
if (this.size >= this.maxSize) {
this.list.remove(tailItem);
delete this.hash[tailItem.key];
this.size--;
}
// writing over existing key
if (node = this.hash[key]) {
node.val = val;
this.list.moveToFront(node);
}
// new key
else {
node = new Node(key, val);
this.list.add(node);
this.hash[key] = node;
this.size++;
}
},
get: function get(key) {
var node = this.hash[key];
if (node) {
this.list.moveToFront(node);
return node.val;
}
},
reset: function reset() {
this.size = 0;
this.hash = {};
this.list = new List();
}
});
function List() {
this.head = this.tail = null;
}
_.mixin(List.prototype, {
add: function add(node) {
if (this.head) {
node.next = this.head;
this.head.prev = node;
}
this.head = node;
this.tail = this.tail || node;
},
remove: function remove(node) {
node.prev ? node.prev.next = node.next : this.head = node.next;
node.next ? node.next.prev = node.prev : this.tail = node.prev;
},
moveToFront: function(node) {
this.remove(node);
this.add(node);
}
});
function Node(key, val) {
this.key = key;
this.val = val;
this.prev = this.next = null;
}
return LruCache;
})();

View File

@ -1,197 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var oParser = (function() {
'use strict';
return function parse(o) {
var defaults, sorter;
defaults = {
initialize: true,
identify: _.stringify,
datumTokenizer: null,
queryTokenizer: null,
matchAnyQueryToken: false,
sufficient: 5,
indexRemote: false,
sorter: null,
local: [],
prefetch: null,
remote: null
};
o = _.mixin(defaults, o || {});
// throw error if required options are not set
!o.datumTokenizer && $.error('datumTokenizer is required');
!o.queryTokenizer && $.error('queryTokenizer is required');
sorter = o.sorter;
o.sorter = sorter ? function(x) { return x.sort(sorter); } : _.identity;
o.local = _.isFunction(o.local) ? o.local() : o.local;
o.prefetch = parsePrefetch(o.prefetch);
o.remote = parseRemote(o.remote);
return o;
};
function parsePrefetch(o) {
var defaults;
if (!o) { return null; }
defaults = {
url: null,
ttl: 24 * 60 * 60 * 1000, // 1 day
cache: true,
cacheKey: null,
thumbprint: '',
prepare: _.identity,
transform: _.identity,
transport: null
};
// support basic (url) and advanced configuration
o = _.isString(o) ? { url: o } : o;
o = _.mixin(defaults, o);
// throw error if required options are not set
!o.url && $.error('prefetch requires url to be set');
// DEPRECATED: filter will be dropped in v1
o.transform = o.filter || o.transform;
o.cacheKey = o.cacheKey || o.url;
o.thumbprint = VERSION + o.thumbprint;
o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
return o;
}
function parseRemote(o) {
var defaults;
if (!o) { return; }
defaults = {
url: null,
cache: true, // leave undocumented
prepare: null,
replace: null,
wildcard: null,
limiter: null,
rateLimitBy: 'debounce',
rateLimitWait: 300,
transform: _.identity,
transport: null
};
// support basic (url) and advanced configuration
o = _.isString(o) ? { url: o } : o;
o = _.mixin(defaults, o);
// throw error if required options are not set
!o.url && $.error('remote requires url to be set');
// DEPRECATED: filter will be dropped in v1
o.transform = o.filter || o.transform;
o.prepare = toRemotePrepare(o);
o.limiter = toLimiter(o);
o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
delete o.replace;
delete o.wildcard;
delete o.rateLimitBy;
delete o.rateLimitWait;
return o;
}
function toRemotePrepare(o) {
var prepare, replace, wildcard;
prepare = o.prepare;
replace = o.replace;
wildcard = o.wildcard;
if (prepare) { return prepare; }
if (replace) {
prepare = prepareByReplace;
}
else if (o.wildcard) {
prepare = prepareByWildcard;
}
else {
prepare = idenityPrepare;
}
return prepare;
function prepareByReplace(query, settings) {
settings.url = replace(settings.url, query);
return settings;
}
function prepareByWildcard(query, settings) {
settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
return settings;
}
function idenityPrepare(query, settings) {
return settings;
}
}
function toLimiter(o) {
var limiter, method, wait;
limiter = o.limiter;
method = o.rateLimitBy;
wait = o.rateLimitWait;
if (!limiter) {
limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
}
return limiter;
function debounce(wait) {
return function debounce(fn) { return _.debounce(fn, wait); };
}
function throttle(wait) {
return function throttle(fn) { return _.throttle(fn, wait); };
}
}
function callbackToDeferred(fn) {
return function wrapper(o) {
var deferred = $.Deferred();
fn(o, onSuccess, onError);
return deferred;
function onSuccess(resp) {
// defer in case fn is synchronous, otherwise done
// and always handlers will be attached after the resolution
_.defer(function() { deferred.resolve(resp); });
}
function onError(err) {
// defer in case fn is synchronous, otherwise done
// and always handlers will be attached after the resolution
_.defer(function() { deferred.reject(err); });
}
};
}
})();

View File

@ -1,147 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var PersistentStorage = (function() {
'use strict';
var LOCAL_STORAGE;
try {
LOCAL_STORAGE = window.localStorage;
// while in private browsing mode, some browsers make
// localStorage available, but throw an error when used
LOCAL_STORAGE.setItem('~~~', '!');
LOCAL_STORAGE.removeItem('~~~');
} catch (err) {
LOCAL_STORAGE = null;
}
// constructor
// -----------
function PersistentStorage(namespace, override) {
this.prefix = ['__', namespace, '__'].join('');
this.ttlKey = '__ttl__';
this.keyMatcher = new RegExp('^' + _.escapeRegExChars(this.prefix));
// for testing purpose
this.ls = override || LOCAL_STORAGE;
// if local storage isn't available, everything becomes a noop
!this.ls && this._noop();
}
// instance methods
// ----------------
_.mixin(PersistentStorage.prototype, {
// ### private
_prefix: function(key) {
return this.prefix + key;
},
_ttlKey: function(key) {
return this._prefix(key) + this.ttlKey;
},
_noop: function() {
this.get =
this.set =
this.remove =
this.clear =
this.isExpired = _.noop;
},
_safeSet: function(key, val) {
try {
this.ls.setItem(key, val);
} catch (err) {
// hit the localstorage limit so clean up and better luck next time
if (err.name === 'QuotaExceededError') {
this.clear();
this._noop();
}
}
},
// ### public
get: function(key) {
if (this.isExpired(key)) {
this.remove(key);
}
return decode(this.ls.getItem(this._prefix(key)));
},
set: function(key, val, ttl) {
if (_.isNumber(ttl)) {
this._safeSet(this._ttlKey(key), encode(now() + ttl));
}
else {
this.ls.removeItem(this._ttlKey(key));
}
return this._safeSet(this._prefix(key), encode(val));
},
remove: function(key) {
this.ls.removeItem(this._ttlKey(key));
this.ls.removeItem(this._prefix(key));
return this;
},
clear: function() {
var i, keys = gatherMatchingKeys(this.keyMatcher);
for (i = keys.length; i--;) {
this.remove(keys[i]);
}
return this;
},
isExpired: function(key) {
var ttl = decode(this.ls.getItem(this._ttlKey(key)));
return _.isNumber(ttl) && now() > ttl ? true : false;
}
});
return PersistentStorage;
// helper functions
// ----------------
function now() {
return new Date().getTime();
}
function encode(val) {
// convert undefined to null to avoid issues with JSON.parse
return JSON.stringify(_.isUndefined(val) ? null : val);
}
function decode(val) {
return $.parseJSON(val);
}
function gatherMatchingKeys(keyMatcher) {
var i, key, keys = [], len = LOCAL_STORAGE.length;
for (i = 0; i < len; i++) {
if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
keys.push(key.replace(keyMatcher, ''));
}
}
return keys;
}
})();

View File

@ -1,91 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Prefetch = (function() {
'use strict';
var keys;
keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' };
// constructor
// -----------
// defaults for options are handled in options_parser
function Prefetch(o) {
this.url = o.url;
this.ttl = o.ttl;
this.cache = o.cache;
this.prepare = o.prepare;
this.transform = o.transform;
this.transport = o.transport;
this.thumbprint = o.thumbprint;
this.storage = new PersistentStorage(o.cacheKey);
}
// instance methods
// ----------------
_.mixin(Prefetch.prototype, {
// ### private
_settings: function settings() {
return { url: this.url, type: 'GET', dataType: 'json' };
},
// ### public
store: function store(data) {
if (!this.cache) { return; }
this.storage.set(keys.data, data, this.ttl);
this.storage.set(keys.protocol, location.protocol, this.ttl);
this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
},
fromCache: function fromCache() {
var stored = {}, isExpired;
if (!this.cache) { return null; }
stored.data = this.storage.get(keys.data);
stored.protocol = this.storage.get(keys.protocol);
stored.thumbprint = this.storage.get(keys.thumbprint);
// the stored data is considered expired if the thumbprints
// don't match or if the protocol it was originally stored under
// has changed
isExpired =
stored.thumbprint !== this.thumbprint ||
stored.protocol !== location.protocol;
// TODO: if expired, remove from local storage
return stored.data && !isExpired ? stored.data : null;
},
fromNetwork: function(cb) {
var that = this, settings;
if (!cb) { return; }
settings = this.prepare(this._settings());
this.transport(settings).fail(onError).done(onResponse);
function onError() { cb(true); }
function onResponse(resp) { cb(null, that.transform(resp)); }
},
clear: function clear() {
this.storage.clear();
return this;
}
});
return Prefetch;
})();

View File

@ -1,59 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Remote = (function() {
'use strict';
// constructor
// -----------
function Remote(o) {
this.url = o.url;
this.prepare = o.prepare;
this.transform = o.transform;
this.indexResponse = o.indexResponse;
this.transport = new Transport({
cache: o.cache,
limiter: o.limiter,
transport: o.transport
});
}
// instance methods
// ----------------
_.mixin(Remote.prototype, {
// ### private
_settings: function settings() {
return { url: this.url, type: 'GET', dataType: 'json' };
},
// ### public
get: function get(query, cb) {
var that = this, settings;
if (!cb) { return; }
query = query || '';
settings = this.prepare(query, this._settings());
return this.transport.get(settings, onResponse);
function onResponse(err, resp) {
err ? cb([]) : cb(that.transform(resp));
}
},
cancelLastRequest: function cancelLastRequest() {
this.transport.cancel();
}
});
return Remote;
})();

View File

@ -1,194 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var SearchIndex = window.SearchIndex = (function() {
'use strict';
var CHILDREN = 'c', IDS = 'i';
// constructor
// -----------
function SearchIndex(o) {
o = o || {};
if (!o.datumTokenizer || !o.queryTokenizer) {
$.error('datumTokenizer and queryTokenizer are both required');
}
this.identify = o.identify || _.stringify;
this.datumTokenizer = o.datumTokenizer;
this.queryTokenizer = o.queryTokenizer;
this.matchAnyQueryToken = o.matchAnyQueryToken;
this.reset();
}
// instance methods
// ----------------
_.mixin(SearchIndex.prototype, {
// ### public
bootstrap: function bootstrap(o) {
this.datums = o.datums;
this.trie = o.trie;
},
add: function(data) {
var that = this;
data = _.isArray(data) ? data : [data];
_.each(data, function(datum) {
var id, tokens;
that.datums[id = that.identify(datum)] = datum;
tokens = normalizeTokens(that.datumTokenizer(datum));
_.each(tokens, function(token) {
var node, chars, ch;
node = that.trie;
chars = token.split('');
while (ch = chars.shift()) {
node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
node[IDS].push(id);
}
});
});
},
get: function get(ids) {
var that = this;
return _.map(ids, function(id) { return that.datums[id]; });
},
search: function search(query) {
var that = this, tokens, matches;
tokens = normalizeTokens(this.queryTokenizer(query));
_.each(tokens, function(token) {
var node, chars, ch, ids;
// previous tokens didn't share any matches
if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
return false;
}
node = that.trie;
chars = token.split('');
while (node && (ch = chars.shift())) {
node = node[CHILDREN][ch];
}
if (node && chars.length === 0) {
ids = node[IDS].slice(0);
matches = matches ? getIntersection(matches, ids) : ids;
}
// break early if we find out there are no possible matches
else {
if (!that.matchAnyQueryToken) {
matches = [];
return false;
}
}
});
return matches ?
_.map(unique(matches), function(id) { return that.datums[id]; }) : [];
},
all: function all() {
var values = [];
for (var key in this.datums) {
values.push(this.datums[key]);
}
return values;
},
reset: function reset() {
this.datums = {};
this.trie = newNode();
},
serialize: function serialize() {
return { datums: this.datums, trie: this.trie };
}
});
return SearchIndex;
// helper functions
// ----------------
function normalizeTokens(tokens) {
// filter out falsy tokens
tokens = _.filter(tokens, function(token) { return !!token; });
// normalize tokens
tokens = _.map(tokens, function(token) { return token.toLowerCase(); });
return tokens;
}
function newNode() {
var node = {};
node[IDS] = [];
node[CHILDREN] = {};
return node;
}
function unique(array) {
var seen = {}, uniques = [];
for (var i = 0, len = array.length; i < len; i++) {
if (!seen[array[i]]) {
seen[array[i]] = true;
uniques.push(array[i]);
}
}
return uniques;
}
function getIntersection(arrayA, arrayB) {
var ai = 0, bi = 0, intersection = [];
arrayA = arrayA.sort();
arrayB = arrayB.sort();
var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
while (ai < lenArrayA && bi < lenArrayB) {
if (arrayA[ai] < arrayB[bi]) {
ai++;
}
else if (arrayA[ai] > arrayB[bi]) {
bi++;
}
else {
intersection.push(arrayA[ai]);
ai++;
bi++;
}
}
return intersection;
}
})();

View File

@ -1,44 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var tokenizers = (function() {
'use strict';
return {
nonword: nonword,
whitespace: whitespace,
obj: {
nonword: getObjTokenizer(nonword),
whitespace: getObjTokenizer(whitespace)
}
};
function whitespace(str) {
str = _.toStr(str);
return str ? str.split(/\s+/) : [];
}
function nonword(str) {
str = _.toStr(str);
return str ? str.split(/\W+/) : [];
}
function getObjTokenizer(tokenizer) {
return function setKey(keys) {
keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
return function tokenize(o) {
var tokens = [];
_.each(keys, function(k) {
tokens = tokens.concat(tokenizer(_.toStr(o[k])));
});
return tokens;
};
};
}
})();

View File

@ -1,130 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Transport = (function() {
'use strict';
var pendingRequestsCount = 0,
pendingRequests = {},
maxPendingRequests = 6,
sharedCache = new LruCache(10);
// constructor
// -----------
function Transport(o) {
o = o || {};
this.cancelled = false;
this.lastReq = null;
this._send = o.transport;
this._get = o.limiter ? o.limiter(this._get) : this._get;
this._cache = o.cache === false ? new LruCache(0) : sharedCache;
}
// static methods
// --------------
Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
maxPendingRequests = num;
};
Transport.resetCache = function resetCache() {
sharedCache.reset();
};
// instance methods
// ----------------
_.mixin(Transport.prototype, {
// ### private
_fingerprint: function fingerprint(o) {
o = o || {};
return o.url + o.type + $.param(o.data || {});
},
_get: function(o, cb) {
var that = this, fingerprint, jqXhr;
fingerprint = this._fingerprint(o);
// #149: don't make a network request if there has been a cancellation
// or if the url doesn't match the last url Transport#get was invoked with
if (this.cancelled || fingerprint !== this.lastReq) { return; }
// a request is already in progress, piggyback off of it
if (jqXhr = pendingRequests[fingerprint]) {
jqXhr.done(done).fail(fail);
}
// under the pending request threshold, so fire off a request
else if (pendingRequestsCount < maxPendingRequests) {
pendingRequestsCount++;
pendingRequests[fingerprint] =
this._send(o).done(done).fail(fail).always(always);
}
// at the pending request threshold, so hang out in the on deck circle
else {
this.onDeckRequestArgs = [].slice.call(arguments, 0);
}
function done(resp) {
cb(null, resp);
that._cache.set(fingerprint, resp);
}
function fail() {
cb(true);
}
function always() {
pendingRequestsCount--;
delete pendingRequests[fingerprint];
// ensures request is always made for the last query
if (that.onDeckRequestArgs) {
that._get.apply(that, that.onDeckRequestArgs);
that.onDeckRequestArgs = null;
}
}
},
// ### public
get: function(o, cb) {
var resp, fingerprint;
cb = cb || $.noop;
o = _.isString(o) ? { url: o } : (o || {});
fingerprint = this._fingerprint(o);
this.cancelled = false;
this.lastReq = fingerprint;
// in-memory cache hit
if (resp = this._cache.get(fingerprint)) {
cb(null, resp);
}
// go to network
else {
this._get(o, cb);
}
},
cancel: function() {
this.cancelled = true;
}
});
return Transport;
})();

View File

@ -1,7 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var VERSION = '%VERSION%';

View File

@ -1,164 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var _ = (function() {
'use strict';
return {
isMsie: function() {
// from https://github.com/ded/bowser/blob/master/bowser.js
return (/(msie|trident)/i).test(navigator.userAgent) ?
navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
},
isBlankString: function(str) { return !str || /^\s*$/.test(str); },
// http://stackoverflow.com/a/6969486
escapeRegExChars: function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
},
isString: function(obj) { return typeof obj === 'string'; },
isNumber: function(obj) { return typeof obj === 'number'; },
isArray: $.isArray,
isFunction: $.isFunction,
isObject: $.isPlainObject,
isUndefined: function(obj) { return typeof obj === 'undefined'; },
isElement: function(obj) { return !!(obj && obj.nodeType === 1); },
isJQuery: function(obj) { return obj instanceof $; },
toStr: function toStr(s) {
return (_.isUndefined(s) || s === null) ? '' : s + '';
},
bind: $.proxy,
each: function(collection, cb) {
// stupid argument order for jQuery.each
$.each(collection, reverseArgs);
function reverseArgs(index, value) { return cb(value, index); }
},
map: $.map,
filter: $.grep,
every: function(obj, test) {
var result = true;
if (!obj) { return result; }
$.each(obj, function(key, val) {
if (!(result = test.call(null, val, key, obj))) {
return false;
}
});
return !!result;
},
some: function(obj, test) {
var result = false;
if (!obj) { return result; }
$.each(obj, function(key, val) {
if (result = test.call(null, val, key, obj)) {
return false;
}
});
return !!result;
},
mixin: $.extend,
identity: function(x) { return x; },
clone: function(obj) { return $.extend(true, {}, obj); },
getIdGenerator: function() {
var counter = 0;
return function() { return counter++; };
},
templatify: function templatify(obj) {
return $.isFunction(obj) ? obj : template;
function template() { return String(obj); }
},
defer: function(fn) { setTimeout(fn, 0); },
debounce: function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments, later, callNow;
later = function() {
timeout = null;
if (!immediate) { result = func.apply(context, args); }
};
callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) { result = func.apply(context, args); }
return result;
};
},
throttle: function(func, wait) {
var context, args, timeout, result, previous, later;
previous = 0;
later = function() {
previous = new Date();
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date(),
remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
}
else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
stringify: function(val) {
return _.isString(val) ? val : JSON.stringify(val);
},
noop: function() {}
};
})();

View File

@ -1,330 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Dataset = (function() {
'use strict';
var keys, nameGenerator;
keys = {
val: 'tt-selectable-display',
obj: 'tt-selectable-object'
};
nameGenerator = _.getIdGenerator();
// constructor
// -----------
function Dataset(o, www) {
o = o || {};
o.templates = o.templates || {};
// DEPRECATED: empty will be dropped in v1
o.templates.notFound = o.templates.notFound || o.templates.empty;
if (!o.source) {
$.error('missing source');
}
if (!o.node) {
$.error('missing node');
}
if (o.name && !isValidName(o.name)) {
$.error('invalid dataset name: ' + o.name);
}
www.mixin(this);
this.highlight = !!o.highlight;
this.name = o.name || nameGenerator();
this.limit = o.limit || 5;
this.displayFn = getDisplayFn(o.display || o.displayKey);
this.templates = getTemplates(o.templates, this.displayFn);
// use duck typing to see if source is a bloodhound instance by checking
// for the __ttAdapter property; otherwise assume it is a function
this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
// if the async option is undefined, inspect the source signature as
// a hint to figuring out of the source will return async suggestions
this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
this._resetLastSuggestion();
this.$el = $(o.node)
.addClass(this.classes.dataset)
.addClass(this.classes.dataset + '-' + this.name);
}
// static methods
// --------------
Dataset.extractData = function extractData(el) {
var $el = $(el);
if ($el.data(keys.obj)) {
return {
val: $el.data(keys.val) || '',
obj: $el.data(keys.obj) || null
};
}
return null;
};
// instance methods
// ----------------
_.mixin(Dataset.prototype, EventEmitter, {
// ### private
_overwrite: function overwrite(query, suggestions) {
suggestions = suggestions || [];
// got suggestions: overwrite dom with suggestions
if (suggestions.length) {
this._renderSuggestions(query, suggestions);
}
// no suggestions, expecting async: overwrite dom with pending
else if (this.async && this.templates.pending) {
this._renderPending(query);
}
// no suggestions, not expecting async: overwrite dom with not found
else if (!this.async && this.templates.notFound) {
this._renderNotFound(query);
}
// nothing to render: empty dom
else {
this._empty();
}
this.trigger('rendered', this.name, suggestions, false);
},
_append: function append(query, suggestions) {
suggestions = suggestions || [];
// got suggestions, sync suggestions exist: append suggestions to dom
if (suggestions.length && this.$lastSuggestion.length) {
this._appendSuggestions(query, suggestions);
}
// got suggestions, no sync suggestions: overwrite dom with suggestions
else if (suggestions.length) {
this._renderSuggestions(query, suggestions);
}
// no async/sync suggestions: overwrite dom with not found
else if (!this.$lastSuggestion.length && this.templates.notFound) {
this._renderNotFound(query);
}
this.trigger('rendered', this.name, suggestions, true);
},
_renderSuggestions: function renderSuggestions(query, suggestions) {
var $fragment;
$fragment = this._getSuggestionsFragment(query, suggestions);
this.$lastSuggestion = $fragment.children().last();
this.$el.html($fragment)
.prepend(this._getHeader(query, suggestions))
.append(this._getFooter(query, suggestions));
},
_appendSuggestions: function appendSuggestions(query, suggestions) {
var $fragment, $lastSuggestion;
$fragment = this._getSuggestionsFragment(query, suggestions);
$lastSuggestion = $fragment.children().last();
this.$lastSuggestion.after($fragment);
this.$lastSuggestion = $lastSuggestion;
},
_renderPending: function renderPending(query) {
var template = this.templates.pending;
this._resetLastSuggestion();
template && this.$el.html(template({
query: query,
dataset: this.name,
}));
},
_renderNotFound: function renderNotFound(query) {
var template = this.templates.notFound;
this._resetLastSuggestion();
template && this.$el.html(template({
query: query,
dataset: this.name,
}));
},
_empty: function empty() {
this.$el.empty();
this._resetLastSuggestion();
},
_getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
var that = this, fragment;
fragment = document.createDocumentFragment();
_.each(suggestions, function getSuggestionNode(suggestion) {
var $el, context;
context = that._injectQuery(query, suggestion);
$el = $(that.templates.suggestion(context))
.data(keys.obj, suggestion)
.data(keys.val, that.displayFn(suggestion))
.addClass(that.classes.suggestion + ' ' + that.classes.selectable);
fragment.appendChild($el[0]);
});
this.highlight && highlight({
className: this.classes.highlight,
node: fragment,
pattern: query
});
return $(fragment);
},
_getFooter: function getFooter(query, suggestions) {
return this.templates.footer ?
this.templates.footer({
query: query,
suggestions: suggestions,
dataset: this.name
}) : null;
},
_getHeader: function getHeader(query, suggestions) {
return this.templates.header ?
this.templates.header({
query: query,
suggestions: suggestions,
dataset: this.name
}) : null;
},
_resetLastSuggestion: function resetLastSuggestion() {
this.$lastSuggestion = $();
},
_injectQuery: function injectQuery(query, obj) {
return _.isObject(obj) ? _.mixin({ _query: query }, obj) : obj;
},
// ### public
update: function update(query) {
var that = this, canceled = false, syncCalled = false, rendered = 0;
// cancel possible pending update
this.cancel();
this.cancel = function cancel() {
canceled = true;
that.cancel = $.noop;
that.async && that.trigger('asyncCanceled', query);
};
this.source(query, sync, async);
!syncCalled && sync([]);
function sync(suggestions) {
if (syncCalled) { return; }
syncCalled = true;
suggestions = (suggestions || []).slice(0, that.limit);
rendered = suggestions.length;
that._overwrite(query, suggestions);
if (rendered < that.limit && that.async) {
that.trigger('asyncRequested', query);
}
}
function async(suggestions) {
suggestions = suggestions || [];
// if the update has been canceled or if the query has changed
// do not render the suggestions as they've become outdated
if (!canceled && rendered < that.limit) {
that.cancel = $.noop;
that._append(query, suggestions.slice(0, that.limit - rendered));
rendered += suggestions.length;
that.async && that.trigger('asyncReceived', query);
}
}
},
// cancel function gets set in #update
cancel: $.noop,
clear: function clear() {
this._empty();
this.cancel();
this.trigger('cleared');
},
isEmpty: function isEmpty() {
return this.$el.is(':empty');
},
destroy: function destroy() {
// #970
this.$el = $('<div>');
}
});
return Dataset;
// helper functions
// ----------------
function getDisplayFn(display) {
display = display || _.stringify;
return _.isFunction(display) ? display : displayFn;
function displayFn(obj) { return obj[display]; }
}
function getTemplates(templates, displayFn) {
return {
notFound: templates.notFound && _.templatify(templates.notFound),
pending: templates.pending && _.templatify(templates.pending),
header: templates.header && _.templatify(templates.header),
footer: templates.footer && _.templatify(templates.footer),
suggestion: templates.suggestion || suggestionTemplate
};
function suggestionTemplate(context) {
return $('<div>').text(displayFn(context));
}
}
function isValidName(str) {
// dashes, underscores, letters, and numbers
return (/^[_a-zA-Z0-9-]+$/).test(str);
}
})();

View File

@ -1,75 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var DefaultMenu = (function() {
'use strict';
var s = Menu.prototype;
function DefaultMenu() {
Menu.apply(this, [].slice.call(arguments, 0));
}
_.mixin(DefaultMenu.prototype, Menu.prototype, {
// overrides
// ---------
open: function open() {
// only display the menu when there's something to be shown
!this._allDatasetsEmpty() && this._show();
return s.open.apply(this, [].slice.call(arguments, 0));
},
close: function close() {
this._hide();
return s.close.apply(this, [].slice.call(arguments, 0));
},
_onRendered: function onRendered() {
if (this._allDatasetsEmpty()) {
this._hide();
}
else {
this.isOpen() && this._show();
}
return s._onRendered.apply(this, [].slice.call(arguments, 0));
},
_onCleared: function onCleared() {
if (this._allDatasetsEmpty()) {
this._hide();
}
else {
this.isOpen() && this._show();
}
return s._onCleared.apply(this, [].slice.call(arguments, 0));
},
setLanguageDirection: function setLanguageDirection(dir) {
this.$node.css(dir === 'ltr' ? this.css.ltr : this.css.rtl);
return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0));
},
// private
// ---------
_hide: function hide() {
this.$node.hide();
},
_show: function show() {
// can't use jQuery#show because $node is a span element we want
// display: block; not dislay: inline;
this.$node.css('display', 'block');
}
});
return DefaultMenu;
})();

View File

@ -1,78 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var EventBus = (function() {
'use strict';
var namespace, deprecationMap;
namespace = 'typeahead:';
// DEPRECATED: will be remove in v1
//
// NOTE: there is no deprecation plan for the opened and closed event
// as their behavior has changed enough that it wouldn't make sense
deprecationMap = {
render: 'rendered',
cursorchange: 'cursorchanged',
select: 'selected',
autocomplete: 'autocompleted'
};
// constructor
// -----------
function EventBus(o) {
if (!o || !o.el) {
$.error('EventBus initialized without el');
}
this.$el = $(o.el);
}
// instance methods
// ----------------
_.mixin(EventBus.prototype, {
// ### private
_trigger: function(type, args) {
var $e;
$e = $.Event(namespace + type);
(args = args || []).unshift($e);
this.$el.trigger.apply(this.$el, args);
return $e;
},
// ### public
before: function(type) {
var args, $e;
args = [].slice.call(arguments, 1);
$e = this._trigger('before' + type, args);
return $e.isDefaultPrevented();
},
trigger: function(type) {
var deprecatedType;
this._trigger(type, [].slice.call(arguments, 1));
// TODO: remove in v1
if (deprecatedType = deprecationMap[type]) {
this._trigger(deprecatedType, [].slice.call(arguments, 1));
}
}
});
return EventBus;
})();

View File

@ -1,119 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
// inspired by https://github.com/jharding/boomerang
var EventEmitter = (function() {
'use strict';
var splitter = /\s+/, nextTick = getNextTick();
return {
onSync: onSync,
onAsync: onAsync,
off: off,
trigger: trigger
};
function on(method, types, cb, context) {
var type;
if (!cb) { return this; }
types = types.split(splitter);
cb = context ? bindContext(cb, context) : cb;
this._callbacks = this._callbacks || {};
while (type = types.shift()) {
this._callbacks[type] = this._callbacks[type] || { sync: [], async: [] };
this._callbacks[type][method].push(cb);
}
return this;
}
function onAsync(types, cb, context) {
return on.call(this, 'async', types, cb, context);
}
function onSync(types, cb, context) {
return on.call(this, 'sync', types, cb, context);
}
function off(types) {
var type;
if (!this._callbacks) { return this; }
types = types.split(splitter);
while (type = types.shift()) {
delete this._callbacks[type];
}
return this;
}
function trigger(types) {
var type, callbacks, args, syncFlush, asyncFlush;
if (!this._callbacks) { return this; }
types = types.split(splitter);
args = [].slice.call(arguments, 1);
while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
syncFlush = getFlush(callbacks.sync, this, [type].concat(args));
asyncFlush = getFlush(callbacks.async, this, [type].concat(args));
syncFlush() && nextTick(asyncFlush);
}
return this;
}
function getFlush(callbacks, context, args) {
return flush;
function flush() {
var cancelled;
for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
// only cancel if the callback explicitly returns false
cancelled = callbacks[i].apply(context, args) === false;
}
return !cancelled;
}
}
function getNextTick() {
var nextTickFn;
// IE10+
if (window.setImmediate) {
nextTickFn = function nextTickSetImmediate(fn) {
setImmediate(function() { fn(); });
};
}
// old browsers
else {
nextTickFn = function nextTickSetTimeout(fn) {
setTimeout(function() { fn(); }, 0);
};
}
return nextTickFn;
}
function bindContext(fn, context) {
return fn.bind ?
fn.bind(context) :
function() { fn.apply(context, [].slice.call(arguments, 0)); };
}
})();

View File

@ -1,84 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
// inspired by https://github.com/jharding/bearhug
var highlight = (function(doc) {
'use strict';
var defaults = {
node: null,
pattern: null,
tagName: 'strong',
className: null,
wordsOnly: false,
caseSensitive: false
};
return function hightlight(o) {
var regex;
o = _.mixin({}, defaults, o);
if (!o.node || !o.pattern) {
// fail silently
return;
}
// support wrapping multiple patterns
o.pattern = _.isArray(o.pattern) ? o.pattern : [o.pattern];
regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly);
traverse(o.node, hightlightTextNode);
function hightlightTextNode(textNode) {
var match, patternNode, wrapperNode;
if (match = regex.exec(textNode.data)) {
wrapperNode = doc.createElement(o.tagName);
o.className && (wrapperNode.className = o.className);
patternNode = textNode.splitText(match.index);
patternNode.splitText(match[0].length);
wrapperNode.appendChild(patternNode.cloneNode(true));
textNode.parentNode.replaceChild(wrapperNode, patternNode);
}
return !!match;
}
function traverse(el, hightlightTextNode) {
var childNode, TEXT_NODE_TYPE = 3;
for (var i = 0; i < el.childNodes.length; i++) {
childNode = el.childNodes[i];
if (childNode.nodeType === TEXT_NODE_TYPE) {
i += hightlightTextNode(childNode) ? 1 : 0;
}
else {
traverse(childNode, hightlightTextNode);
}
}
}
};
function getRegex(patterns, caseSensitive, wordsOnly) {
var escapedPatterns = [], regexStr;
for (var i = 0, len = patterns.length; i < len; i++) {
escapedPatterns.push(_.escapeRegExChars(patterns[i]));
}
regexStr = wordsOnly ?
'\\b(' + escapedPatterns.join('|') + ')\\b' :
'(' + escapedPatterns.join('|') + ')';
return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, 'i');
}
})(window.document);

View File

@ -1,339 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Input = (function() {
'use strict';
var specialKeyCodeMap;
specialKeyCodeMap = {
9: 'tab',
27: 'esc',
37: 'left',
39: 'right',
13: 'enter',
38: 'up',
40: 'down'
};
// constructor
// -----------
function Input(o, www) {
o = o || {};
if (!o.input) {
$.error('input is missing');
}
www.mixin(this);
this.$hint = $(o.hint);
this.$input = $(o.input);
// the query defaults to whatever the value of the input is
// on initialization, it'll most likely be an empty string
this.query = this.$input.val();
// for tracking when a change event should be triggered
this.queryWhenFocused = this.hasFocus() ? this.query : null;
// helps with calculating the width of the input's value
this.$overflowHelper = buildOverflowHelper(this.$input);
// detect the initial lang direction
this._checkLanguageDirection();
// if no hint, noop all the hint related functions
if (this.$hint.length === 0) {
this.setHint =
this.getHint =
this.clearHint =
this.clearHintIfInvalid = _.noop;
}
}
// static methods
// --------------
Input.normalizeQuery = function(str) {
// strips leading whitespace and condenses all whitespace
return (_.toStr(str)).replace(/^\s*/g, '').replace(/\s{2,}/g, ' ');
};
// instance methods
// ----------------
_.mixin(Input.prototype, EventEmitter, {
// ### event handlers
_onBlur: function onBlur() {
this.resetInputValue();
this.trigger('blurred');
},
_onFocus: function onFocus() {
this.queryWhenFocused = this.query;
this.trigger('focused');
},
_onKeydown: function onKeydown($e) {
// which is normalized and consistent (but not for ie)
var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
this._managePreventDefault(keyName, $e);
if (keyName && this._shouldTrigger(keyName, $e)) {
this.trigger(keyName + 'Keyed', $e);
}
},
_onInput: function onInput() {
this._setQuery(this.getInputValue());
this.clearHintIfInvalid();
this._checkLanguageDirection();
},
// ### private
_managePreventDefault: function managePreventDefault(keyName, $e) {
var preventDefault;
switch (keyName) {
case 'up':
case 'down':
preventDefault = !withModifier($e);
break;
default:
preventDefault = false;
}
preventDefault && $e.preventDefault();
},
_shouldTrigger: function shouldTrigger(keyName, $e) {
var trigger;
switch (keyName) {
case 'tab':
trigger = !withModifier($e);
break;
default:
trigger = true;
}
return trigger;
},
_checkLanguageDirection: function checkLanguageDirection() {
var dir = (this.$input.css('direction') || 'ltr').toLowerCase();
if (this.dir !== dir) {
this.dir = dir;
this.$hint.attr('dir', dir);
this.trigger('langDirChanged', dir);
}
},
_setQuery: function setQuery(val, silent) {
var areEquivalent, hasDifferentWhitespace;
areEquivalent = areQueriesEquivalent(val, this.query);
hasDifferentWhitespace = areEquivalent ?
this.query.length !== val.length : false;
this.query = val;
if (!silent && !areEquivalent) {
this.trigger('queryChanged', this.query);
}
else if (!silent && hasDifferentWhitespace) {
this.trigger('whitespaceChanged', this.query);
}
},
// ### public
bind: function() {
var that = this, onBlur, onFocus, onKeydown, onInput;
// bound functions
onBlur = _.bind(this._onBlur, this);
onFocus = _.bind(this._onFocus, this);
onKeydown = _.bind(this._onKeydown, this);
onInput = _.bind(this._onInput, this);
this.$input
.on('blur.tt', onBlur)
.on('focus.tt', onFocus)
.on('keydown.tt', onKeydown);
// ie8 don't support the input event
// ie9 doesn't fire the input event when characters are removed
if (!_.isMsie() || _.isMsie() > 9) {
this.$input.on('input.tt', onInput);
}
else {
this.$input.on('keydown.tt keypress.tt cut.tt paste.tt', function($e) {
// if a special key triggered this, ignore it
if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; }
// give the browser a chance to update the value of the input
// before checking to see if the query changed
_.defer(_.bind(that._onInput, that, $e));
});
}
return this;
},
focus: function focus() {
this.$input.focus();
},
blur: function blur() {
this.$input.blur();
},
getLangDir: function getLangDir() {
return this.dir;
},
getQuery: function getQuery() {
return this.query || '';
},
setQuery: function setQuery(val, silent) {
this.setInputValue(val);
this._setQuery(val, silent);
},
hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
return this.query !== this.queryWhenFocused;
},
getInputValue: function getInputValue() {
return this.$input.val();
},
setInputValue: function setInputValue(value) {
this.$input.val(value);
this.clearHintIfInvalid();
this._checkLanguageDirection();
},
resetInputValue: function resetInputValue() {
this.setInputValue(this.query);
},
getHint: function getHint() {
return this.$hint.val();
},
setHint: function setHint(value) {
this.$hint.val(value);
},
clearHint: function clearHint() {
this.setHint('');
},
clearHintIfInvalid: function clearHintIfInvalid() {
var val, hint, valIsPrefixOfHint, isValid;
val = this.getInputValue();
hint = this.getHint();
valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
isValid = val !== '' && valIsPrefixOfHint && !this.hasOverflow();
!isValid && this.clearHint();
},
hasFocus: function hasFocus() {
return this.$input.is(':focus');
},
hasOverflow: function hasOverflow() {
// 2 is arbitrary, just picking a small number to handle edge cases
var constraint = this.$input.width() - 2;
this.$overflowHelper.text(this.getInputValue());
return this.$overflowHelper.width() >= constraint;
},
isCursorAtEnd: function() {
var valueLength, selectionStart, range;
valueLength = this.$input.val().length;
selectionStart = this.$input[0].selectionStart;
if (_.isNumber(selectionStart)) {
return selectionStart === valueLength;
}
else if (document.selection) {
// NOTE: this won't work unless the input has focus, the good news
// is this code should only get called when the input has focus
range = document.selection.createRange();
range.moveStart('character', -valueLength);
return valueLength === range.text.length;
}
return true;
},
destroy: function destroy() {
this.$hint.off('.tt');
this.$input.off('.tt');
this.$overflowHelper.remove();
// #970
this.$hint = this.$input = this.$overflowHelper = $('<div>');
}
});
return Input;
// helper functions
// ----------------
function buildOverflowHelper($input) {
return $('<pre aria-hidden="true"></pre>')
.css({
// position helper off-screen
position: 'absolute',
visibility: 'hidden',
// avoid line breaks and whitespace collapsing
whiteSpace: 'pre',
// use same font css as input to calculate accurate width
fontFamily: $input.css('font-family'),
fontSize: $input.css('font-size'),
fontStyle: $input.css('font-style'),
fontVariant: $input.css('font-variant'),
fontWeight: $input.css('font-weight'),
wordSpacing: $input.css('word-spacing'),
letterSpacing: $input.css('letter-spacing'),
textIndent: $input.css('text-indent'),
textRendering: $input.css('text-rendering'),
textTransform: $input.css('text-transform')
})
.insertAfter($input);
}
function areQueriesEquivalent(a, b) {
return Input.normalizeQuery(a) === Input.normalizeQuery(b);
}
function withModifier($e) {
return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
}
})();

View File

@ -1,219 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Menu = (function() {
'use strict';
// constructor
// -----------
function Menu(o, www) {
var that = this;
o = o || {};
if (!o.node) {
$.error('node is required');
}
www.mixin(this);
this.$node = $(o.node);
// the latest query #update was called with
this.query = null;
this.datasets = _.map(o.datasets, initializeDataset);
function initializeDataset(oDataset) {
var node = that.$node.find(oDataset.node).first();
oDataset.node = node.length ? node : $('<div>').appendTo(that.$node);
return new Dataset(oDataset, www);
}
}
// instance methods
// ----------------
_.mixin(Menu.prototype, EventEmitter, {
// ### event handlers
_onSelectableClick: function onSelectableClick($e) {
this.trigger('selectableClicked', $($e.currentTarget));
},
_onRendered: function onRendered(type, dataset, suggestions, async) {
this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
this.trigger('datasetRendered', dataset, suggestions, async);
},
_onCleared: function onCleared() {
this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
this.trigger('datasetCleared');
},
_propagate: function propagate() {
this.trigger.apply(this, arguments);
},
// ### private
_allDatasetsEmpty: function allDatasetsEmpty() {
return _.every(this.datasets, isDatasetEmpty);
function isDatasetEmpty(dataset) { return dataset.isEmpty(); }
},
_getSelectables: function getSelectables() {
return this.$node.find(this.selectors.selectable);
},
_removeCursor: function _removeCursor() {
var $selectable = this.getActiveSelectable();
$selectable && $selectable.removeClass(this.classes.cursor);
},
_ensureVisible: function ensureVisible($el) {
var elTop, elBottom, nodeScrollTop, nodeHeight;
elTop = $el.position().top;
elBottom = elTop + $el.outerHeight(true);
nodeScrollTop = this.$node.scrollTop();
nodeHeight = this.$node.height() +
parseInt(this.$node.css('paddingTop'), 10) +
parseInt(this.$node.css('paddingBottom'), 10);
if (elTop < 0) {
this.$node.scrollTop(nodeScrollTop + elTop);
}
else if (nodeHeight < elBottom) {
this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
}
},
// ### public
bind: function() {
var that = this, onSelectableClick;
onSelectableClick = _.bind(this._onSelectableClick, this);
this.$node.on('click.tt', this.selectors.selectable, onSelectableClick);
this.$node.on('mouseover', this.selectors.selectable, function(){ that.setCursor($(this)) });
_.each(this.datasets, function(dataset) {
dataset
.onSync('asyncRequested', that._propagate, that)
.onSync('asyncCanceled', that._propagate, that)
.onSync('asyncReceived', that._propagate, that)
.onSync('rendered', that._onRendered, that)
.onSync('cleared', that._onCleared, that);
});
return this;
},
isOpen: function isOpen() {
return this.$node.hasClass(this.classes.open);
},
open: function open() {
this.$node.scrollTop(0);
this.$node.addClass(this.classes.open);
},
close: function close() {
this.$node.removeClass(this.classes.open);
this._removeCursor();
},
setLanguageDirection: function setLanguageDirection(dir) {
this.$node.attr('dir', dir);
},
selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
var $selectables, $oldCursor, oldIndex, newIndex;
$oldCursor = this.getActiveSelectable();
$selectables = this._getSelectables();
// shifting before and after modulo to deal with -1 index
oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
newIndex = oldIndex + delta;
newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
// wrap new index if less than -1
newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
return newIndex === -1 ? null : $selectables.eq(newIndex);
},
setCursor: function setCursor($selectable) {
this._removeCursor();
if ($selectable = $selectable && $selectable.first()) {
$selectable.addClass(this.classes.cursor);
// in the case of scrollable overflow
// make sure the cursor is visible in the node
this._ensureVisible($selectable);
}
},
getSelectableData: function getSelectableData($el) {
return ($el && $el.length) ? Dataset.extractData($el) : null;
},
getActiveSelectable: function getActiveSelectable() {
var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
return $selectable.length ? $selectable : null;
},
getTopSelectable: function getTopSelectable() {
var $selectable = this._getSelectables().first();
return $selectable.length ? $selectable : null;
},
update: function update(query) {
var isValidUpdate = query !== this.query;
// don't update if the query hasn't changed
if (isValidUpdate) {
this.query = query;
_.each(this.datasets, updateDataset);
}
return isValidUpdate;
function updateDataset(dataset) { dataset.update(query); }
},
empty: function empty() {
_.each(this.datasets, clearDataset);
this.query = null;
this.$node.addClass(this.classes.empty);
function clearDataset(dataset) { dataset.clear(); }
},
destroy: function destroy() {
this.$node.off('.tt');
// #970
this.$node = $('<div>');
_.each(this.datasets, destroyDataset);
function destroyDataset(dataset) { dataset.destroy(); }
}
});
return Menu;
})();

View File

@ -1,291 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
(function() {
'use strict';
var old, keys, methods;
old = $.fn.typeahead;
keys = {
www: 'tt-www',
attrs: 'tt-attrs',
typeahead: 'tt-typeahead'
};
methods = {
// supported signatures:
// function(o, dataset, dataset, ...)
// function(o, [dataset, dataset, ...])
initialize: function initialize(o, datasets) {
var www;
datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
o = o || {};
www = WWW(o.classNames);
return this.each(attach);
function attach() {
var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu,
eventBus, input, menu, typeahead, MenuConstructor;
// highlight is a top-level config that needs to get inherited
// from all of the datasets
_.each(datasets, function(d) { d.highlight = !!o.highlight; });
$input = $(this);
$wrapper = $(www.html.wrapper);
$hint = $elOrNull(o.hint);
$menu = $elOrNull(o.menu);
defaultHint = o.hint !== false && !$hint;
defaultMenu = o.menu !== false && !$menu;
defaultHint && ($hint = buildHintFromInput($input, www));
defaultMenu && ($menu = $(www.html.menu).css(www.css.menu));
// hint should be empty on init
$hint && $hint.val('');
$input = prepInput($input, www);
// only apply inline styles and make dom changes if necessary
if (defaultHint || defaultMenu) {
$wrapper.css(www.css.wrapper);
$input.css(defaultHint ? www.css.input : www.css.inputWithNoHint);
$input
.wrap($wrapper)
.parent()
.prepend(defaultHint ? $hint : null)
.append(defaultMenu ? $menu : null);
}
MenuConstructor = defaultMenu ? DefaultMenu : Menu;
eventBus = new EventBus({ el: $input });
input = new Input({ hint: $hint, input: $input, }, www);
menu = new MenuConstructor({
node: $menu,
datasets: datasets
}, www);
typeahead = new Typeahead({
input: input,
menu: menu,
eventBus: eventBus,
minLength: o.minLength
}, www);
$input.data(keys.www, www);
$input.data(keys.typeahead, typeahead);
}
},
isEnabled: function isEnabled() {
var enabled;
ttEach(this.first(), function(t) { enabled = t.isEnabled(); });
return enabled;
},
enable: function enable() {
ttEach(this, function(t) { t.enable(); });
return this;
},
disable: function disable() {
ttEach(this, function(t) { t.disable(); });
return this;
},
isActive: function isActive() {
var active;
ttEach(this.first(), function(t) { active = t.isActive(); });
return active;
},
activate: function activate() {
ttEach(this, function(t) { t.activate(); });
return this;
},
deactivate: function deactivate() {
ttEach(this, function(t) { t.deactivate(); });
return this;
},
isOpen: function isOpen() {
var open;
ttEach(this.first(), function(t) { open = t.isOpen(); });
return open;
},
open: function open() {
ttEach(this, function(t) { t.open(); });
return this;
},
close: function close() {
ttEach(this, function(t) { t.close(); });
return this;
},
select: function select(el) {
var success = false, $el = $(el);
ttEach(this.first(), function(t) { success = t.select($el); });
return success;
},
autocomplete: function autocomplete(el) {
var success = false, $el = $(el);
ttEach(this.first(), function(t) { success = t.autocomplete($el); });
return success;
},
moveCursor: function moveCursoe(delta) {
var success = false;
ttEach(this.first(), function(t) { success = t.moveCursor(delta); });
return success;
},
// mirror jQuery#val functionality: reads operate on first match,
// write operates on all matches
val: function val(newVal) {
var query;
if (!arguments.length) {
ttEach(this.first(), function(t) { query = t.getVal(); });
return query;
}
else {
ttEach(this, function(t) { t.setVal(_.toStr(newVal)); });
return this;
}
},
destroy: function destroy() {
ttEach(this, function(typeahead, $input) {
revert($input);
typeahead.destroy();
});
return this;
}
};
$.fn.typeahead = function(method) {
// methods that should only act on initialized typeaheads
if (methods[method]) {
return methods[method].apply(this, [].slice.call(arguments, 1));
}
else {
return methods.initialize.apply(this, arguments);
}
};
$.fn.typeahead.noConflict = function noConflict() {
$.fn.typeahead = old;
return this;
};
// helper methods
// --------------
function ttEach($els, fn) {
$els.each(function() {
var $input = $(this), typeahead;
(typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input);
});
}
function buildHintFromInput($input, www) {
return $input.clone()
.addClass(www.classes.hint)
.removeData()
.css(www.css.hint)
.css(getBackgroundStyles($input))
.prop('readonly', true)
.removeAttr('id name placeholder required')
.attr({ autocomplete: 'off', spellcheck: 'false', tabindex: -1 });
}
function prepInput($input, www) {
// store the original values of the attrs that get modified
// so modifications can be reverted on destroy
$input.data(keys.attrs, {
dir: $input.attr('dir'),
autocomplete: $input.attr('autocomplete'),
spellcheck: $input.attr('spellcheck'),
style: $input.attr('style')
});
$input
.addClass(www.classes.input)
.attr({ autocomplete: 'off', spellcheck: false });
// ie7 does not like it when dir is set to auto
try { !$input.attr('dir') && $input.attr('dir', 'auto'); } catch (e) {}
return $input;
}
function getBackgroundStyles($el) {
return {
backgroundAttachment: $el.css('background-attachment'),
backgroundClip: $el.css('background-clip'),
backgroundColor: $el.css('background-color'),
backgroundImage: $el.css('background-image'),
backgroundOrigin: $el.css('background-origin'),
backgroundPosition: $el.css('background-position'),
backgroundRepeat: $el.css('background-repeat'),
backgroundSize: $el.css('background-size')
};
}
function revert($input) {
var www, $wrapper;
www = $input.data(keys.www);
$wrapper = $input.parent().filter(www.selectors.wrapper);
// need to remove attrs that weren't previously defined and
// revert attrs that originally had a value
_.each($input.data(keys.attrs), function(val, key) {
_.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
});
$input
.removeData(keys.typeahead)
.removeData(keys.www)
.removeData(keys.attr)
.removeClass(www.classes.input);
if ($wrapper.length) {
$input.detach().insertAfter($wrapper);
$wrapper.remove();
}
}
function $elOrNull(obj) {
var isValid, $el;
isValid = _.isJQuery(obj) || _.isElement(obj);
$el = isValid ? $(obj).first() : [];
return $el.length ? $el : null;
}
})();

View File

@ -1,438 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var Typeahead = (function() {
'use strict';
// constructor
// -----------
function Typeahead(o, www) {
var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed,
onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged,
onWhitespaceChanged;
o = o || {};
if (!o.input) {
$.error('missing input');
}
if (!o.menu) {
$.error('missing menu');
}
if (!o.eventBus) {
$.error('missing event bus');
}
www.mixin(this);
this.eventBus = o.eventBus;
this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
this.input = o.input;
this.menu = o.menu;
this.enabled = true;
// activate the typeahead on init if the input has focus
this.active = false;
this.input.hasFocus() && this.activate();
// detect the initial lang direction
this.dir = this.input.getLangDir();
this._hacks();
this.menu.bind()
.onSync('selectableClicked', this._onSelectableClicked, this)
.onSync('asyncRequested', this._onAsyncRequested, this)
.onSync('asyncCanceled', this._onAsyncCanceled, this)
.onSync('asyncReceived', this._onAsyncReceived, this)
.onSync('datasetRendered', this._onDatasetRendered, this)
.onSync('datasetCleared', this._onDatasetCleared, this);
// composed event handlers for input
onFocused = c(this, 'activate', 'open', '_onFocused');
onBlurred = c(this, 'deactivate', '_onBlurred');
onEnterKeyed = c(this, 'isActive', 'isOpen', '_onEnterKeyed');
onTabKeyed = c(this, 'isActive', 'isOpen', '_onTabKeyed');
onEscKeyed = c(this, 'isActive', '_onEscKeyed');
onUpKeyed = c(this, 'isActive', 'open', '_onUpKeyed');
onDownKeyed = c(this, 'isActive', 'open', '_onDownKeyed');
onLeftKeyed = c(this, 'isActive', 'isOpen', '_onLeftKeyed');
onRightKeyed = c(this, 'isActive', 'isOpen', '_onRightKeyed');
onQueryChanged = c(this, '_openIfActive', '_onQueryChanged');
onWhitespaceChanged = c(this, '_openIfActive', '_onWhitespaceChanged');
this.input.bind()
.onSync('focused', onFocused, this)
.onSync('blurred', onBlurred, this)
.onSync('enterKeyed', onEnterKeyed, this)
.onSync('tabKeyed', onTabKeyed, this)
.onSync('escKeyed', onEscKeyed, this)
.onSync('upKeyed', onUpKeyed, this)
.onSync('downKeyed', onDownKeyed, this)
.onSync('leftKeyed', onLeftKeyed, this)
.onSync('rightKeyed', onRightKeyed, this)
.onSync('queryChanged', onQueryChanged, this)
.onSync('whitespaceChanged', onWhitespaceChanged, this)
.onSync('langDirChanged', this._onLangDirChanged, this);
}
// instance methods
// ----------------
_.mixin(Typeahead.prototype, {
// here's where hacks get applied and we don't feel bad about it
_hacks: function hacks() {
var $input, $menu;
// these default values are to make testing easier
$input = this.input.$input || $('<div>');
$menu = this.menu.$node || $('<div>');
// #705: if there's scrollable overflow, ie doesn't support
// blur cancellations when the scrollbar is clicked
//
// #351: preventDefault won't cancel blurs in ie <= 8
$input.on('blur.tt', function($e) {
var active, isActive, hasActive;
active = document.activeElement;
isActive = $menu.is(active);
hasActive = $menu.has(active).length > 0;
if (_.isMsie() && (isActive || hasActive)) {
$e.preventDefault();
// stop immediate in order to prevent Input#_onBlur from
// getting exectued
$e.stopImmediatePropagation();
_.defer(function() { $input.focus(); });
}
});
// #351: prevents input blur due to clicks within menu
$menu.on('mousedown.tt', function($e) { $e.preventDefault(); });
},
// ### event handlers
_onSelectableClicked: function onSelectableClicked(type, $el) {
this.select($el);
},
_onDatasetCleared: function onDatasetCleared() {
this._updateHint();
},
_onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) {
this._updateHint();
this.eventBus.trigger('render', suggestions, async, dataset);
},
_onAsyncRequested: function onAsyncRequested(type, dataset, query) {
this.eventBus.trigger('asyncrequest', query, dataset);
},
_onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
this.eventBus.trigger('asynccancel', query, dataset);
},
_onAsyncReceived: function onAsyncReceived(type, dataset, query) {
this.eventBus.trigger('asyncreceive', query, dataset);
},
_onFocused: function onFocused() {
this._minLengthMet() && this.menu.update(this.input.getQuery());
},
_onBlurred: function onBlurred() {
if (this.input.hasQueryChangedSinceLastFocus()) {
this.eventBus.trigger('change', this.input.getQuery());
}
},
_onEnterKeyed: function onEnterKeyed(type, $e) {
var $selectable;
if ($selectable = this.menu.getActiveSelectable()) {
this.select($selectable) && $e.preventDefault();
}
},
_onTabKeyed: function onTabKeyed(type, $e) {
var $selectable;
if ($selectable = this.menu.getActiveSelectable()) {
this.select($selectable) && $e.preventDefault();
}
else if ($selectable = this.menu.getTopSelectable()) {
this.autocomplete($selectable) && $e.preventDefault();
}
},
_onEscKeyed: function onEscKeyed() {
this.close();
},
_onUpKeyed: function onUpKeyed() {
this.moveCursor(-1);
},
_onDownKeyed: function onDownKeyed() {
this.moveCursor(+1);
},
_onLeftKeyed: function onLeftKeyed() {
if (this.dir === 'rtl' && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
}
},
_onRightKeyed: function onRightKeyed() {
if (this.dir === 'ltr' && this.input.isCursorAtEnd()) {
this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
}
},
_onQueryChanged: function onQueryChanged(e, query) {
this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
},
_onWhitespaceChanged: function onWhitespaceChanged() {
this._updateHint();
},
_onLangDirChanged: function onLangDirChanged(e, dir) {
if (this.dir !== dir) {
this.dir = dir;
this.menu.setLanguageDirection(dir);
}
},
// ### private
_openIfActive: function openIfActive() {
this.isActive() && this.open();
},
_minLengthMet: function minLengthMet(query) {
query = _.isString(query) ? query : (this.input.getQuery() || '');
return query.length >= this.minLength;
},
_updateHint: function updateHint() {
var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
$selectable = this.menu.getTopSelectable();
data = this.menu.getSelectableData($selectable);
val = this.input.getInputValue();
if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
query = Input.normalizeQuery(val);
escapedQuery = _.escapeRegExChars(query);
// match input value, then capture trailing text
frontMatchRegEx = new RegExp('^(?:' + escapedQuery + ')(.+$)', 'i');
match = frontMatchRegEx.exec(data.val);
// clear hint if there's no trailing text
match && this.input.setHint(val + match[1]);
}
else {
this.input.clearHint();
}
},
// ### public
isEnabled: function isEnabled() {
return this.enabled;
},
enable: function enable() {
this.enabled = true;
},
disable: function disable() {
this.enabled = false;
},
isActive: function isActive() {
return this.active;
},
activate: function activate() {
// already active
if (this.isActive()) {
return true;
}
// unable to activate either due to the typeahead being disabled
// or due to the active event being prevented
else if (!this.isEnabled() || this.eventBus.before('active')) {
return false;
}
// activate
else {
this.active = true;
this.eventBus.trigger('active');
return true;
}
},
deactivate: function deactivate() {
// already idle
if (!this.isActive()) {
return true;
}
// unable to deactivate due to the idle event being prevented
else if (this.eventBus.before('idle')) {
return false;
}
// deactivate
else {
this.active = false;
this.close();
this.eventBus.trigger('idle');
return true;
}
},
isOpen: function isOpen() {
return this.menu.isOpen();
},
open: function open() {
if (!this.isOpen() && !this.eventBus.before('open')) {
this.menu.open();
this._updateHint();
this.eventBus.trigger('open');
}
return this.isOpen();
},
close: function close() {
if (this.isOpen() && !this.eventBus.before('close')) {
this.menu.close();
this.input.clearHint();
this.input.resetInputValue();
this.eventBus.trigger('close');
}
return !this.isOpen();
},
setVal: function setVal(val) {
// expect val to be a string, so be safe, and coerce
this.input.setQuery(_.toStr(val));
},
getVal: function getVal() {
return this.input.getQuery();
},
select: function select($selectable) {
var data = this.menu.getSelectableData($selectable);
if (data && !this.eventBus.before('select', data.obj)) {
this.input.setQuery(data.val, true);
this.eventBus.trigger('select', data.obj);
this.close();
// return true if selection succeeded
return true;
}
return false;
},
autocomplete: function autocomplete($selectable) {
var query, data, isValid;
query = this.input.getQuery();
data = this.menu.getSelectableData($selectable);
isValid = data && query !== data.val;
if (isValid && !this.eventBus.before('autocomplete', data.obj)) {
this.input.setQuery(data.val);
this.eventBus.trigger('autocomplete', data.obj);
// return true if autocompletion succeeded
return true;
}
return false;
},
moveCursor: function moveCursor(delta) {
var query, $candidate, data, payload, cancelMove;
query = this.input.getQuery();
$candidate = this.menu.selectableRelativeToCursor(delta);
data = this.menu.getSelectableData($candidate);
payload = data ? data.obj : null;
// update will return true when it's a new query and new suggestions
// need to be fetched in this case we don't want to move the cursor
cancelMove = this._minLengthMet() && this.menu.update(query);
if (!cancelMove && !this.eventBus.before('cursorchange', payload)) {
this.menu.setCursor($candidate);
// cursor moved to different selectable
if (data) {
this.input.setInputValue(data.val);
}
// cursor moved off of selectables, back to input
else {
this.input.resetInputValue();
this._updateHint();
}
this.eventBus.trigger('cursorchange', payload);
// return true if move succeeded
return true;
}
return false;
},
destroy: function destroy() {
this.input.destroy();
this.menu.destroy();
}
});
return Typeahead;
// helper functions
// ----------------
function c(ctx) {
var methods = [].slice.call(arguments, 1);
return function() {
var args = [].slice.call(arguments);
_.each(methods, function(method) {
return ctx[method].apply(ctx, args);
});
};
}
})();

View File

@ -1,113 +0,0 @@
/*
* typeahead.js
* https://github.com/twitter/typeahead.js
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
*/
var WWW = (function() {
'use strict';
var defaultClassNames = {
wrapper: 'twitter-typeahead',
input: 'tt-input',
hint: 'tt-hint',
menu: 'tt-menu',
dataset: 'tt-dataset',
suggestion: 'tt-suggestion',
selectable: 'tt-selectable',
empty: 'tt-empty',
open: 'tt-open',
cursor: 'tt-cursor',
highlight: 'tt-highlight'
};
return build;
function build(o) {
var www, classes;
classes = _.mixin({}, defaultClassNames, o);
www = {
css: buildCss(),
classes: classes,
html: buildHtml(classes),
selectors: buildSelectors(classes)
};
return {
css: www.css,
html: www.html,
classes: www.classes,
selectors: www.selectors,
mixin: function(o) { _.mixin(o, www); }
};
}
function buildHtml(c) {
return {
wrapper: '<span class="' + c.wrapper + '"></span>',
menu: '<div class="' + c.menu + '"></div>'
};
}
function buildSelectors(classes) {
var selectors = {};
_.each(classes, function(v, k) { selectors[k] = '.' + v; });
return selectors;
}
function buildCss() {
var css = {
wrapper: {
position: 'relative',
display: 'inline-block'
},
hint: {
position: 'absolute',
top: '0',
left: '0',
borderColor: 'transparent',
boxShadow: 'none',
// #741: fix hint opacity issue on iOS
opacity: '1'
},
input: {
position: 'relative',
verticalAlign: 'top',
backgroundColor: 'transparent'
},
inputWithNoHint: {
position: 'relative',
verticalAlign: 'top'
},
menu: {
position: 'absolute',
top: '100%',
left: '0',
zIndex: '100',
display: 'none'
},
ltr: {
left: '0',
right: 'auto'
},
rtl: {
left: 'auto',
right:' 0'
}
};
// ie specific styling
if (_.isMsie()) {
// ie6-8 (and 9?) doesn't fire hover and click events for elements with
// transparent backgrounds, for a workaround, use 1x1 transparent gif
_.mixin(css.input, {
backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)'
});
}
return css;
}
})();

View File

@ -1,350 +0,0 @@
describe('Bloodhound', function() {
function build(o) {
return new Bloodhound(_.mixin({
datumTokenizer: datumTokenizer,
queryTokenizer: queryTokenizer
}, o || {}));
}
beforeEach(function() {
jasmine.Remote.useMock();
jasmine.Prefetch.useMock();
jasmine.Transport.useMock();
jasmine.PersistentStorage.useMock();
});
afterEach(function() {
clearAjaxRequests();
});
describe('#initialize', function() {
beforeEach(function() {
this.bloodhound = build({ initialize: false });
spyOn(this.bloodhound, '_initialize').andCallThrough();
});
it('should not initialize if intialize option is false', function() {
expect(this.bloodhound._initialize).not.toHaveBeenCalled();
});
it('should not support reinitialization by default', function() {
var p1, p2;
p1 = this.bloodhound.initialize();
p2 = this.bloodhound.initialize();
expect(p1).toBe(p2);
expect(this.bloodhound._initialize.callCount).toBe(1);
});
it('should reinitialize if reintialize flag is true', function() {
var p1, p2;
p1 = this.bloodhound.initialize();
p2 = this.bloodhound.initialize(true);
expect(p1).not.toBe(p2);
expect(this.bloodhound._initialize.callCount).toBe(2);
});
it('should clear the index', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
spyOn(this.bloodhound, 'clear');
this.bloodhound.initialize();
expect(this.bloodhound.clear).toHaveBeenCalled();
});
it('should load data from prefetch cache if available', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(fixtures.serialized.simple);
this.bloodhound.initialize();
expect(this.bloodhound.all()).toEqual(fixtures.data.simple);
expect(this.bloodhound.prefetch.fromNetwork).not.toHaveBeenCalled();
});
it('should load data from prefetch network as fallback', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(null);
this.bloodhound.prefetch.fromNetwork.andCallFake(fakeFromNetwork);
this.bloodhound.initialize();
expect(this.bloodhound.all()).toEqual(fixtures.data.simple);
function fakeFromNetwork(cb) { cb(null, fixtures.data.simple); }
});
it('should store prefetch network data in the prefetch cache', function() {
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(null);
this.bloodhound.prefetch.fromNetwork.andCallFake(fakeFromNetwork);
this.bloodhound.initialize();
expect(this.bloodhound.prefetch.store)
.toHaveBeenCalledWith(fixtures.serialized.simple);
function fakeFromNetwork(cb) { cb(null, fixtures.data.simple); }
});
it('should add local after prefetch is loaded', function() {
this.bloodhound = build({
initialize: false,
local: [{ foo: 'bar' }],
prefetch: '/prefetch'
});
this.bloodhound.prefetch.fromNetwork.andCallFake(fakeFromNetwork);
expect(this.bloodhound.all()).toEqual([]);
this.bloodhound.initialize();
expect(this.bloodhound.all()).toEqual([{ foo: 'bar' }]);
function fakeFromNetwork(cb) { cb(null, []); }
});
});
describe('#add', function() {
it('should add datums to search index', function() {
var spy = jasmine.createSpy();
this.bloodhound = build().add(fixtures.data.simple);
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
});
});
describe('#get', function() {
beforeEach(function() {
this.bloodhound = build({
identify: function(d) { return d.value; },
local: fixtures.data.simple
});
});
it('should support array signature', function() {
expect(this.bloodhound.get(['big', 'bigger'])).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
it('should support splat signature', function() {
expect(this.bloodhound.get('big', 'bigger')).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
it('should return nothing for unknown ids', function() {
expect(this.bloodhound.get('big', 'foo', 'bigger')).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
});
describe('#clear', function() {
it('should remove all datums to search index', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ local: fixtures.data.simple }).clear();
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([]);
});
});
describe('#clearPrefetchCache', function() {
it('should clear persistent storage', function() {
this.bloodhound = build({ prefetch: '/prefetch' }).clearPrefetchCache();
expect(this.bloodhound.prefetch.clear).toHaveBeenCalled();
});
});
describe('#clearRemoteCache', function() {
it('should clear remote request cache', function() {
spyOn(Transport, 'resetCache');
this.bloodhound = build({ remote: '/remote' }).clearRemoteCache();
expect(Transport.resetCache).toHaveBeenCalled();
});
});
describe('#all', function() {
it('should return all local results', function() {
this.bloodhound = build({ local: fixtures.data.simple });
expect(this.bloodhound.all()).toEqual(fixtures.data.simple);
});
});
describe('#search  local', function() {
it('should return sync matches', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ local: fixtures.data.simple });
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
});
});
describe('#search  prefetch', function() {
it('should return sync matches', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ initialize: false, prefetch: '/prefetch' });
this.bloodhound.prefetch.fromCache.andReturn(fixtures.serialized.simple);
this.bloodhound.initialize();
this.bloodhound.search('big', spy);
expect(spy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
});
});
describe('#search  remote', function() {
it('should return async matches', function() {
var spy = jasmine.createSpy();
this.bloodhound = build({ remote: '/remote' });
this.bloodhound.remote.get.andCallFake(fakeGet);
this.bloodhound.search('dog', $.noop, spy);
expect(spy.callCount).toBe(1);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
});
describe('#search integration', function() {
it('should backfill when local/prefetch is not sufficient', function() {
var syncSpy, asyncSpy;
syncSpy = jasmine.createSpy();
asyncSpy = jasmine.createSpy();
this.bloodhound = build({
sufficient: 3,
local: fixtures.data.simple,
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
this.bloodhound.search('big', syncSpy, asyncSpy);
expect(syncSpy).toHaveBeenCalledWith([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
expect(asyncSpy).not.toHaveBeenCalled();
this.bloodhound.search('bigg', syncSpy, asyncSpy);
expect(syncSpy).toHaveBeenCalledWith([
{ value: 'bigger' },
{ value: 'biggest' }
]);
expect(asyncSpy).toHaveBeenCalledWith(fixtures.data.animals);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
it('should remove duplicates from backfill', function() {
var syncSpy, asyncSpy;
syncSpy = jasmine.createSpy();
asyncSpy = jasmine.createSpy();
this.bloodhound = build({
identify: function(d) { return d.value; },
local: fixtures.data.animals,
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
this.bloodhound.search('dog', syncSpy, asyncSpy);
expect(syncSpy).toHaveBeenCalledWith([{ value: 'dog' }]);
expect(asyncSpy).toHaveBeenCalledWith([
{ value: 'cat' },
{ value: 'moose' }
]);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
it('should not add remote data to index if indexRemote is false', function() {
this.bloodhound = build({
identify: function(d) { return d.value; },
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
spyOn(this.bloodhound, 'add');
this.bloodhound.search('dog');
expect(this.bloodhound.add).not.toHaveBeenCalled();
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
it('should add remote data to index if indexRemote is true', function() {
this.bloodhound = build({
identify: function(d) { return d.value; },
indexRemote: true,
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
spyOn(this.bloodhound, 'add');
this.bloodhound.search('dog');
expect(this.bloodhound.add).toHaveBeenCalledWith(fixtures.data.animals);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
it('should not add duplicates from remote to index', function() {
this.bloodhound = build({
identify: function(d) { return d.value; },
indexRemote: true,
local: fixtures.data.animals,
remote: '/remote'
});
this.bloodhound.remote.get.andCallFake(fakeGet);
spyOn(this.bloodhound, 'add');
this.bloodhound.search('dog');
expect(this.bloodhound.add).toHaveBeenCalledWith([
{ value: 'cat' },
{ value: 'moose' }
]);
function fakeGet(o, cb) { cb(fixtures.data.animals); }
});
});
// helper functions
// ----------------
function datumTokenizer(d) { return $.trim(d.value).split(/\s+/); }
function queryTokenizer(s) { return $.trim(s).split(/\s+/); }
});

View File

@ -1,43 +0,0 @@
describe('LruCache', function() {
beforeEach(function() {
this.cache = new LruCache(3);
});
it('should make entries retrievable by their keys', function() {
var key = 'key', val = 42;
this.cache.set(key, val);
expect(this.cache.get(key)).toBe(val);
});
it('should return undefined if key has not been set', function() {
expect(this.cache.get('wat?')).toBeUndefined();
});
it('should hold up to maxSize entries', function() {
this.cache.set('one', 1);
this.cache.set('two', 2);
this.cache.set('three', 3);
this.cache.set('four', 4);
expect(this.cache.get('one')).toBeUndefined();
expect(this.cache.get('two')).toBe(2);
expect(this.cache.get('three')).toBe(3);
expect(this.cache.get('four')).toBe(4);
});
it('should evict lru entry if cache is full', function() {
this.cache.set('one', 1);
this.cache.set('two', 2);
this.cache.set('three', 3);
this.cache.get('one');
this.cache.set('four', 4);
expect(this.cache.get('one')).toBe(1);
expect(this.cache.get('two')).toBeUndefined();
expect(this.cache.get('three')).toBe(3);
expect(this.cache.get('four')).toBe(4);
expect(this.cache.size).toBe(3);
});
});

View File

@ -1,194 +0,0 @@
describe('options parser', function() {
function build(o) {
return oParser(_.mixin({
datumTokenizer: $.noop,
queryTokenizer: $.noop
}, o || {}));
}
function prefetch(o) {
return oParser({
datumTokenizer: $.noop,
queryTokenizer: $.noop,
prefetch: _.mixin({
url: '/example'
}, o || {})
});
}
function remote(o) {
return oParser({
datumTokenizer: $.noop,
queryTokenizer: $.noop,
remote: _.mixin({
url: '/example'
}, o || {})
});
}
it('should throw exception if datumTokenizer is not set', function() {
expect(parse).toThrow();
function parse() { build({ datumTokenizer: null }); }
});
it('should throw exception if queryTokenizer is not set', function() {
expect(parse).toThrow();
function parse() { build({ queryTokenizer: null }); }
});
it('should wrap sorter', function() {
var o = build({ sorter: function(a, b) { return a -b; } });
expect(o.sorter([2, 1, 3])).toEqual([1, 2, 3]);
});
it('should default sorter to identity function', function() {
var o = build();
expect(o.sorter([2, 1, 3])).toEqual([2, 1, 3]);
});
describe('local', function() {
it('should default to empty array', function() {
var o = build();
expect(o.local).toEqual([]);
});
it('should support function', function() {
var o = build({ local: function() { return [1]; } });
expect(o.local).toEqual([1]);
});
it('should support arrays', function() {
var o = build({ local: [1] });
expect(o.local).toEqual([1]);
});
});
describe('prefetch', function() {
it('should throw exception if url is not set', function() {
expect(parse).toThrow();
function parse() { prefetch({ url: null }); }
});
it('should support simple string format', function() {
expect(build({ prefetch: '/prefetch' }).prefetch).toBeDefined();
});
it('should default ttl to 1 day', function() {
var o = prefetch();
expect(o.prefetch.ttl).toBe(86400000);
});
it('should default cache to true', function() {
var o = prefetch();
expect(o.prefetch.cache).toBe(true);
});
it('should default transform to identiy function', function() {
var o = prefetch();
expect(o.prefetch.transform('foo')).toBe('foo');
});
it('should default cacheKey to url', function() {
var o = prefetch();
expect(o.prefetch.cacheKey).toBe(o.prefetch.url);
});
it('should default transport to jQuery.ajax', function() {
var o = prefetch();
expect(o.prefetch.transport).toBe($.ajax);
});
it('should prepend verison to thumbprint', function() {
var o = prefetch();
expect(o.prefetch.thumbprint).toBe('%VERSION%');
o = prefetch({ thumbprint: 'foo' });
expect(o.prefetch.thumbprint).toBe('%VERSION%foo');
});
it('should wrap custom transport to be deferred compatible', function() {
var o, errDeferred, successDeferred;
o = prefetch({ transport: errTransport });
errDeferred = o.prefetch.transport('q');
o = prefetch({ transport: successTransport });
successDeferred = o.prefetch.transport('q');
waits(0);
runs(function() {
expect(errDeferred.isRejected()).toBe(true);
expect(successDeferred.isResolved()).toBe(true);
});
function errTransport(q, success, error) { error(); }
function successTransport(q, success, error) { success(); }
});
});
describe('remote', function() {
it('should throw exception if url is not set', function() {
expect(parse).toThrow();
function parse() { remote({ url: null }); }
});
it('should support simple string format', function() {
expect(build({ remote: '/remote' }).remote).toBeDefined();
});
it('should default transform to identiy function', function() {
var o = remote();
expect(o.remote.transform('foo')).toBe('foo');
});
it('should default transport to jQuery.ajax', function() {
var o = remote();
expect(o.remote.transport).toBe($.ajax);
});
it('should default limiter to debouce', function() {
var o = remote();
expect(o.remote.limiter.name).toBe('debounce');
});
it('should default prepare to identity function', function() {
var o = remote();
expect(o.remote.prepare('q', { url: '/foo' })).toEqual({ url: '/foo' });
});
it('should support wildcard for prepare', function() {
var o = remote({ wildcard: '%FOO' });
expect(o.remote.prepare('=', { url: '/%FOO' })).toEqual({ url: '/%3D' });
});
it('should support replace for prepare', function() {
var o = remote({ replace: function() { return '/bar'; } });
expect(o.remote.prepare('q', { url: '/foo' })).toEqual({ url: '/bar' });
});
it('should should rateLimitBy for limiter', function() {
var o = remote({ rateLimitBy: 'throttle' });
expect(o.remote.limiter.name).toBe('throttle');
});
it('should wrap custom transport to be deferred compatible', function() {
var o, errDeferred, successDeferred;
o = remote({ transport: errTransport });
errDeferred = o.remote.transport('q');
o = remote({ transport: successTransport });
successDeferred = o.remote.transport('q');
waits(0);
runs(function() {
expect(errDeferred.isRejected()).toBe(true);
expect(successDeferred.isResolved()).toBe(true);
});
function errTransport(q, success, error) { error(); }
function successTransport(q, success, error) { success(); }
});
});
});

View File

@ -1,194 +0,0 @@
describe('PersistentStorage', function() {
var engine, ls;
// test suite is dependent on localStorage being available
if (!window.localStorage) {
console.warn('no localStorage support  skipping PersistentStorage suite');
return;
}
// for good measure!
localStorage.clear();
beforeEach(function() {
ls = {
get length() { return localStorage.length; },
key: spyThrough('key'),
clear: spyThrough('clear'),
getItem: spyThrough('getItem'),
setItem: spyThrough('setItem'),
removeItem: spyThrough('removeItem')
};
engine = new PersistentStorage('ns', ls);
spyOn(Date.prototype, 'getTime').andReturn(0);
});
afterEach(function() {
localStorage.clear();
});
// public methods
// --------------
describe('#get', function() {
it('should access localStorage with prefixed key', function() {
engine.get('key');
expect(ls.getItem).toHaveBeenCalledWith('__ns__key');
});
it('should return undefined when key does not exist', function() {
expect(engine.get('does not exist')).toEqual(undefined);
});
it('should return value as correct type', function() {
engine.set('string', 'i am a string');
engine.set('number', 42);
engine.set('boolean', true);
engine.set('null', null);
engine.set('object', { obj: true });
expect(engine.get('string')).toEqual('i am a string');
expect(engine.get('number')).toEqual(42);
expect(engine.get('boolean')).toEqual(true);
expect(engine.get('null')).toBeNull();
expect(engine.get('object')).toEqual({ obj: true });
});
it('should expire stale keys', function() {
engine.set('key', 'value', -1);
expect(engine.get('key')).toBeNull();
expect(ls.getItem('__ns__key__ttl')).toBeNull();
});
});
describe('#set', function() {
it('should access localStorage with prefixed key', function() {
engine.set('key', 'val');
expect(ls.setItem.mostRecentCall.args[0]).toEqual('__ns__key');
});
it('should JSON.stringify value before storing', function() {
engine.set('key', 'val');
expect(ls.setItem.mostRecentCall.args[1]).toEqual(JSON.stringify('val'));
});
it('should store ttl if provided', function() {
var ttl = 1;
engine.set('key', 'value', ttl);
expect(ls.setItem.argsForCall[0])
.toEqual(['__ns__key__ttl__', ttl.toString()]);
});
it('should call clear if the localStorage limit has been reached', function() {
var spy;
ls.setItem.andCallFake(function() {
var err = new Error();
err.name = 'QuotaExceededError';
throw err;
});
engine.clear = spy = jasmine.createSpy();
engine.set('key', 'value', 1);
expect(spy).toHaveBeenCalled();
});
it('should noop if the localStorage limit has been reached', function() {
var get, set, remove, clear, isExpired;
ls.setItem.andCallFake(function() {
var err = new Error();
err.name = 'QuotaExceededError';
throw err;
});
get = engine.get;
set = engine.set;
remove = engine.remove;
clear = engine.clear;
isExpired = engine.isExpired;
engine.set('key', 'value', 1);
expect(engine.get).not.toBe(get);
expect(engine.set).not.toBe(set);
expect(engine.remove).not.toBe(remove);
expect(engine.clear).not.toBe(clear);
expect(engine.isExpired).not.toBe(isExpired);
});
});
describe('#remove', function() {
it('should remove key from storage', function() {
engine.set('key', 'val');
engine.remove('key');
expect(engine.get('key')).toBeNull();
});
});
describe('#clear', function() {
it('should work with namespaces that contain regex characters', function() {
engine = new PersistentStorage('ns?()');
engine.set('key1', 'val1');
engine.set('key2', 'val2');
engine.clear();
expect(engine.get('key1')).toEqual(undefined);
expect(engine.get('key2')).toEqual(undefined);
});
it('should remove all keys that exist in namespace of engine', function() {
engine.set('key1', 'val1');
engine.set('key2', 'val2');
engine.set('key3', 'val3');
engine.set('key4', 'val4', 0);
engine.clear();
expect(engine.get('key1')).toEqual(undefined);
expect(engine.get('key2')).toEqual(undefined);
expect(engine.get('key3')).toEqual(undefined);
expect(engine.get('key4')).toEqual(undefined);
});
it('should not affect keys with different namespace', function() {
ls.setItem('diff_namespace', 'val');
engine.clear();
expect(ls.getItem('diff_namespace')).toEqual('val');
});
});
describe('#isExpired', function() {
it('should be false for keys without ttl', function() {
engine.set('key', 'value');
expect(engine.isExpired('key')).toBe(false);
});
it('should be false for fresh keys', function() {
engine.set('key', 'value', 1);
expect(engine.isExpired('key')).toBe(false);
});
it('should be true for stale keys', function() {
engine.set('key', 'value', -1);
expect(engine.isExpired('key')).toBe(true);
});
});
// compatible across browsers
function spyThrough(method) {
return jasmine.createSpy().andCallFake(fake);
function fake() {
return localStorage[method].apply(localStorage, arguments);
}
}
});

View File

@ -1,182 +0,0 @@
describe('Prefetch', function() {
function build(o) {
return new Prefetch(_.mixin({
url: '/prefetch',
ttl: 3600,
cache: true,
thumbprint: '',
cacheKey: 'cachekey',
prepare: function(x) { return x; },
transform: function(x) { return x; },
transport: $.ajax
}, o || {}));
}
beforeEach(function() {
jasmine.PersistentStorage.useMock();
this.prefetch = build();
this.storage = this.prefetch.storage;
this.thumbprint = this.prefetch.thumbprint;
});
describe('#clear', function() {
it('should clear cache storage', function() {
this.prefetch.clear();
expect(this.storage.clear).toHaveBeenCalled();
});
});
describe('#store', function() {
it('should store data in the storage cache', function() {
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set)
.toHaveBeenCalledWith('data', { foo: 'bar' }, 3600);
});
it('should store thumbprint in the storage cache', function() {
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set)
.toHaveBeenCalledWith('thumbprint', jasmine.any(String), 3600);
});
it('should store protocol in the storage cache', function() {
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set)
.toHaveBeenCalledWith('protocol', location.protocol, 3600);
});
it('should be noop if cache option is false', function() {
this.prefetch = build({ cache: false });
this.prefetch.store({ foo: 'bar' });
expect(this.storage.set).not.toHaveBeenCalled();
});
});
describe('#fromCache', function() {
it('should return data if available', function() {
this.storage.get
.andCallFake(fakeStorageGet({ foo: 'bar' }, this.thumbprint));
expect(this.prefetch.fromCache()).toEqual({ foo: 'bar' });
});
it('should return null if data is expired', function() {
this.storage.get
.andCallFake(fakeStorageGet({ foo: 'bar' }, 'foo'));
expect(this.prefetch.fromCache()).toBeNull();
});
it('should return null if data does not exist', function() {
this.storage.get
.andCallFake(fakeStorageGet(null, this.thumbprint));
expect(this.prefetch.fromCache()).toBeNull();
});
it('should return null if cache option is false', function() {
this.prefetch = build({ cache: false });
this.storage.get
.andCallFake(fakeStorageGet({ foo: 'bar' }, this.thumbprint));
expect(this.prefetch.fromCache()).toBeNull();
expect(this.storage.get).not.toHaveBeenCalled();
});
});
describe('#fromNetwork', function() {
it('should have sensible default request settings', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport').andReturn($.Deferred());
this.prefetch.fromNetwork(spy);
expect(this.prefetch.transport).toHaveBeenCalledWith({
url: '/prefetch',
type: 'GET',
dataType: 'json'
});
});
it('should transform request settings with prepare', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'prepare').andReturn({ foo: 'bar' });
spyOn(this.prefetch, 'transport').andReturn($.Deferred());
this.prefetch.fromNetwork(spy);
expect(this.prefetch.transport).toHaveBeenCalledWith({ foo: 'bar' });
});
it('should transform the response using transform', function() {
var spy;
this.prefetch = build({
transform: function() { return { bar: 'foo' }; }
});
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport')
.andReturn($.Deferred().resolve({ foo: 'bar' }));
this.prefetch.fromNetwork(spy);
expect(spy).toHaveBeenCalledWith(null, { bar: 'foo' });
});
it('should invoke callback with data if success', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport')
.andReturn($.Deferred().resolve({ foo: 'bar' }));
this.prefetch.fromNetwork(spy);
expect(spy).toHaveBeenCalledWith(null, { foo: 'bar' });
});
it('should invoke callback with err argument true if failure', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.prefetch, 'transport').andReturn($.Deferred().reject());
this.prefetch.fromNetwork(spy);
expect(spy).toHaveBeenCalledWith(true);
});
});
function fakeStorageGet(data, thumbprint, protocol) {
return function(key) {
var val;
switch (key) {
case 'data':
val = data;
break;
case 'protocol':
val = protocol || location.protocol;
break;
case 'thumbprint':
val = thumbprint;
break;
}
return val;
};
}
});

View File

@ -1,73 +0,0 @@
describe('Remote', function() {
beforeEach(function() {
jasmine.Transport.useMock();
this.remote = new Remote({
url: '/test?q=%QUERY',
prepare: function(x) { return x; },
transform: function(x) { return x; }
});
this.transport = this.remote.transport;
});
describe('#cancelLastRequest', function() {
it('should cancel last request', function() {
this.remote.cancelLastRequest();
expect(this.transport.cancel).toHaveBeenCalled();
});
});
describe('#get', function() {
it('should have sensible default request settings', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.remote, 'prepare');
this.remote.get('foo', spy);
expect(this.remote.prepare).toHaveBeenCalledWith('foo', {
url: '/test?q=%QUERY',
type: 'GET',
dataType: 'json'
});
});
it('should transform request settings with prepare', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.remote, 'prepare').andReturn([{ foo: 'bar' }]);
this.remote.get('foo', spy);
expect(this.transport.get)
.toHaveBeenCalledWith([{ foo: 'bar' }], jasmine.any(Function));
});
it('should transform response with transform', function() {
var spy;
spy = jasmine.createSpy();
spyOn(this.remote, 'transform').andReturn([{ foo: 'bar' }]);
this.transport.get.andCallFake(function(_, cb) { cb(null, {}); });
this.remote.get('foo', spy);
expect(spy).toHaveBeenCalledWith([{ foo: 'bar' }]);
});
it('should return empty array on error', function() {
var spy;
spy = jasmine.createSpy();
this.transport.get.andCallFake(function(_, cb) { cb(true); });
this.remote.get('foo', spy);
expect(spy).toHaveBeenCalledWith([]);
});
});
});

View File

@ -1,79 +0,0 @@
describe('SearchIndex', function() {
function build(o) {
return new SearchIndex(_.mixin({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace
}, o || {}));
}
beforeEach(function() {
this.index = build();
this.index.add(fixtures.data.simple);
});
it('should support serialization/deserialization', function() {
var serialized = this.index.serialize();
this.index.bootstrap(serialized);
expect(this.index.search('smaller')).toEqual([{ value: 'smaller' }]);
});
it('should be able to add data on the fly', function() {
this.index.add({ value: 'new' });
expect(this.index.search('new')).toEqual([{ value: 'new' }]);
});
it('#get should return datums by id', function() {
this.index = build({ identify: function(d) { return d.value; } });
this.index.add(fixtures.data.simple);
expect(this.index.get(['big', 'bigger'])).toEqual([
{ value: 'big' },
{ value: 'bigger' }
]);
});
it('#search should return datums that match the given query', function() {
expect(this.index.search('big')).toEqual([
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' }
]);
expect(this.index.search('small')).toEqual([
{ value: 'small' },
{ value: 'smaller' },
{ value: 'smallest' }
]);
});
it('#search should return an empty array of there are no matches', function() {
expect(this.index.search('wtf')).toEqual([]);
});
it('#search should handle multi-token queries', function() {
this.index.add({ value: 'foo bar' });
expect(this.index.search('foo b')).toEqual([{ value: 'foo bar' }]);
});
it('#search should return results that match ANY query-token when options.matchAnyQueryToken', function() {
this.index = build({matchAnyQueryToken:true});
this.index.add({ value: 'foo bar' });
expect(this.index.search('blah bar')).toEqual([{ value: 'foo bar' }]);
expect(this.index.search('food bark')).toEqual([]);
});
it('#all should return all datums', function() {
expect(this.index.all()).toEqual(fixtures.data.simple);
});
it('#reset should empty the search index', function() {
this.index.reset();
expect(this.index.datums).toEqual([]);
expect(this.index.trie.i).toEqual([]);
expect(this.index.trie.c).toEqual({});
});
});

View File

@ -1,74 +0,0 @@
describe('tokenizers', function() {
it('.whitespace should tokenize on whitespace', function() {
var tokens = tokenizers.whitespace('big-deal ok');
expect(tokens).toEqual(['big-deal', 'ok']);
});
it('.whitespace should treat null as empty string', function() {
var tokens = tokenizers.whitespace(null);
expect(tokens).toEqual([]);
});
it('.whitespace should treat undefined as empty string', function() {
var tokens = tokenizers.whitespace(undefined);
expect(tokens).toEqual([]);
});
it('.nonword should tokenize on non-word characters', function() {
var tokens = tokenizers.nonword('big-deal ok');
expect(tokens).toEqual(['big', 'deal', 'ok']);
});
it('.nonword should treat null as empty string', function() {
var tokens = tokenizers.nonword(null);
expect(tokens).toEqual([]);
});
it('.nonword should treat undefined as empty string', function() {
var tokens = tokenizers.nonword(undefined);
expect(tokens).toEqual([]);
});
it('.obj.whitespace should tokenize on whitespace', function() {
var t = tokenizers.obj.whitespace('val');
var tokens = t({ val: 'big-deal ok' });
expect(tokens).toEqual(['big-deal', 'ok']);
});
it('.obj.whitespace should accept multiple properties', function() {
var t = tokenizers.obj.whitespace('one', 'two');
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big-deal', 'ok', 'buzz']);
});
it('.obj.whitespace should accept array', function() {
var t = tokenizers.obj.whitespace(['one', 'two']);
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big-deal', 'ok', 'buzz']);
});
it('.obj.nonword should tokenize on non-word characters', function() {
var t = tokenizers.obj.nonword('val');
var tokens = t({ val: 'big-deal ok' });
expect(tokens).toEqual(['big', 'deal', 'ok']);
});
it('.obj.nonword should accept multiple properties', function() {
var t = tokenizers.obj.nonword('one', 'two');
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big', 'deal', 'ok', 'buzz']);
});
it('.obj.nonword should accept array', function() {
var t = tokenizers.obj.nonword(['one', 'two']);
var tokens = t({ one: 'big-deal ok', two: 'buzz' });
expect(tokens).toEqual(['big', 'deal', 'ok', 'buzz']);
});
});

View File

@ -1,175 +0,0 @@
describe('Transport', function() {
beforeEach(function() {
jasmine.Ajax.useMock();
jasmine.Clock.useMock();
this.transport = new Transport({ transport: $.ajax });
});
afterEach(function() {
// run twice to flush out on-deck requests
$.each(ajaxRequests, drop);
$.each(ajaxRequests, drop);
clearAjaxRequests();
Transport.resetCache();
function drop(i, req) {
req.readyState !== 4 && req.response(fixtures.ajaxResps.ok);
}
});
it('should use jQuery.ajax as the default transport mechanism', function() {
var req, resp = fixtures.ajaxResps.ok, spy = jasmine.createSpy();
this.transport.get('/test', spy);
req = mostRecentAjaxRequest();
req.response(resp);
expect(req.url).toBe('/test');
expect(spy).toHaveBeenCalledWith(null, resp.parsed);
});
it('should respect maxPendingRequests configuration', function() {
for (var i = 0; i < 10; i++) {
this.transport.get('/test' + i, $.noop);
}
expect(ajaxRequests.length).toBe(6);
});
it('should support rate limiting', function() {
this.transport = new Transport({ transport: $.ajax, limiter: limiter });
for (var i = 0; i < 5; i++) {
this.transport.get('/test' + i, $.noop);
}
jasmine.Clock.tick(100);
expect(ajaxRequests.length).toBe(1);
function limiter(fn) { return _.debounce(fn, 20); }
});
it('should cache most recent requests', function() {
var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy();
this.transport.get('/test1', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
this.transport.get('/test2', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok1);
expect(ajaxRequests.length).toBe(2);
this.transport.get('/test1', spy1);
this.transport.get('/test2', spy2);
jasmine.Clock.tick(0);
// no ajax requests were made on subsequent requests
expect(ajaxRequests.length).toBe(2);
expect(spy1).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed);
expect(spy2).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok1.parsed);
});
it('should not cache requests if cache option is false', function() {
this.transport = new Transport({ transport: $.ajax, cache: false });
this.transport.get('/test1', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
this.transport.get('/test1', $.noop);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(ajaxRequests.length).toBe(2);
});
it('should prevent dog pile', function() {
var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy();
this.transport.get('/test1', spy1);
this.transport.get('/test1', spy2);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(ajaxRequests.length).toBe(1);
waitsFor(function() { return spy1.callCount && spy2.callCount; });
runs(function() {
expect(spy1).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed);
expect(spy2).toHaveBeenCalledWith(null, fixtures.ajaxResps.ok.parsed);
});
});
it('should always make a request for the last call to #get', function() {
var spy = jasmine.createSpy();
for (var i = 0; i < 6; i++) {
this.transport.get('/test' + i, $.noop);
}
this.transport.get('/test' + i, spy);
expect(ajaxRequests.length).toBe(6);
_.each(ajaxRequests, function(req) {
req.response(fixtures.ajaxResps.ok);
});
expect(ajaxRequests.length).toBe(7);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(spy).toHaveBeenCalled();
});
it('should invoke the callback with err set to true on failure', function() {
var req, resp = fixtures.ajaxResps.err, spy = jasmine.createSpy();
this.transport.get('/test', spy);
req = mostRecentAjaxRequest();
req.response(resp);
expect(req.url).toBe('/test');
expect(spy).toHaveBeenCalledWith(true);
});
it('should not send cancelled requests', function() {
this.transport = new Transport({ transport: $.ajax, limiter: limiter });
this.transport.get('/test', $.noop);
this.transport.cancel();
jasmine.Clock.tick(100);
expect(ajaxRequests.length).toBe(0);
function limiter(fn) { return _.debounce(fn, 20); }
});
it('should not send outdated requests', function() {
this.transport = new Transport({ transport: $.ajax, limiter: limiter });
// warm cache
this.transport.get('/test1', $.noop);
jasmine.Clock.tick(100);
mostRecentAjaxRequest().response(fixtures.ajaxResps.ok);
expect(mostRecentAjaxRequest().url).toBe('/test1');
expect(ajaxRequests.length).toBe(1);
// within the same rate-limit cycle, request test2 and test1. test2 becomes
// outdated after test1 is requested and no request is sent for test1
// because it's a cache hit
this.transport.get('/test2', $.noop);
this.transport.get('/test1', $.noop);
jasmine.Clock.tick(100);
expect(ajaxRequests.length).toBe(1);
function limiter(fn) { return _.debounce(fn, 20); }
});
});

View File

@ -1,12 +0,0 @@
#!/bin/bash -x
if [ "$TEST_SUITE" == "unit" ]; then
./node_modules/karma/bin/karma start --single-run --browsers PhantomJS
elif [ "$TRAVIS_SECURE_ENV_VARS" == "true" -a "$TEST_SUITE" == "integration" ]; then
./node_modules/.bin/static -p 8888 &
sleep 3
# integration tests are flaky, don't let them fail the build
./node_modules/mocha/bin/mocha --harmony -R spec ./test/integration/test.js || true
else
echo "Not running any tests"
fi

View File

@ -1,19 +0,0 @@
var fixtures = fixtures || {};
fixtures.ajaxResps = {
ok: {
status: 200,
responseText: '[{ "value": "big" }, { "value": "bigger" }, { "value": "biggest" }, { "value": "small" }, { "value": "smaller" }, { "value": "smallest" }]'
},
ok1: {
status: 200,
responseText: '["dog", "cat", "moose"]'
},
err: {
status: 500
}
};
$.each(fixtures.ajaxResps, function(i, resp) {
resp.responseText && (resp.parsed = $.parseJSON(resp.responseText));
});

View File

@ -1,128 +0,0 @@
var fixtures = fixtures || {};
fixtures.data = {
simple: [
{ value: 'big' },
{ value: 'bigger' },
{ value: 'biggest' },
{ value: 'small' },
{ value: 'smaller' },
{ value: 'smallest' }
],
animals: [
{ value: 'dog' },
{ value: 'cat' },
{ value: 'moose' }
]
};
fixtures.serialized = {
simple: {
"datums": {
"{\"value\":\"big\"}": {
"value": "big"
},
"{\"value\":\"bigger\"}": {
"value": "bigger"
},
"{\"value\":\"biggest\"}": {
"value": "biggest"
},
"{\"value\":\"small\"}": {
"value": "small"
},
"{\"value\":\"smaller\"}": {
"value": "smaller"
},
"{\"value\":\"smallest\"}": {
"value": "smallest"
}
},
"trie": {
"i": [],
"c": {
"b": {
"i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"i": {
"i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"g": {
"i": ["{\"value\":\"big\"}", "{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"g": {
"i": ["{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"e": {
"i": ["{\"value\":\"bigger\"}", "{\"value\":\"biggest\"}"],
"c": {
"r": {
"i": ["{\"value\":\"bigger\"}"],
"c": {}
},
"s": {
"i": ["{\"value\":\"biggest\"}"],
"c": {
"t": {
"i": ["{\"value\":\"biggest\"}"],
"c": {}
}
}
}
}
}
}
}
}
}
}
}
}
},
"s": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"m": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"a": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"l": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"l": {
"i": ["{\"value\":\"small\"}", "{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"e": {
"i": ["{\"value\":\"smaller\"}", "{\"value\":\"smallest\"}"],
"c": {
"r": {
"i": ["{\"value\":\"smaller\"}"],
"c": {}
},
"s": {
"i": ["{\"value\":\"smallest\"}"],
"c": {
"t": {
"i": ["{\"value\":\"smallest\"}"],
"c": {}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}

View File

@ -1,13 +0,0 @@
var fixtures = fixtures || {};
fixtures.html = {
input: '<input class="tt-input" type="text" autocomplete="false" spellcheck="false">',
hint: '<input class="tt-hint" type="text" autocomplete="false" spellcheck="false" disabled>',
dataset: [
'<div class="tt-dataset-test">',
'<div class="tt-selectable"><p>one</p></div>',
'<div class="tt-selectable"><p>two</p></div>',
'<div class="tt-selectable"><p>three</p></div>',
'</div>'
].join('')
};

View File

@ -1,78 +0,0 @@
(function(root) {
var components;
components = [
'Bloodhound',
'Prefetch',
'Remote',
'PersistentStorage',
'Transport',
'SearchIndex',
'Input',
'Dataset',
'Menu'
];
for (var i = 0; i < components.length; i++) {
makeMockable(components[i]);
}
function makeMockable(component) {
var Original, Mock;
Original = root[component];
Mock = mock(Original);
jasmine[component] = { useMock: useMock, uninstallMock: uninstallMock };
function useMock() {
root[component] = Mock;
jasmine.getEnv().currentSpec.after(uninstallMock);
}
function uninstallMock() {
root[component] = Original;
}
}
function mock(Constructor) {
var constructorSpy;
Mock.prototype = Constructor.prototype;
constructorSpy = jasmine.createSpy('mock constructor').andCallFake(Mock);
// copy instance methods
for (var key in Constructor) {
if (typeof Constructor[key] === 'function') {
constructorSpy[key] = Constructor[key];
}
}
return constructorSpy;
function Mock() {
var instance = _.mixin({}, Constructor.prototype);
for (var key in instance) {
if (typeof instance[key] === 'function') {
spyOn(instance, key);
// special case for some components
if (key === 'bind') {
instance[key].andCallFake(function() { return this; });
}
}
}
// have the event emitter methods call through
instance.onSync && instance.onSync.andCallThrough();
instance.onAsync && instance.onAsync.andCallThrough();
instance.off && instance.off.andCallThrough();
instance.trigger && instance.trigger.andCallThrough();
instance.constructor = Constructor;
return instance;
}
}
})(this);

View File

@ -1,108 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../dist/typeahead.bundle.js"></script>
<style>
.container {
width: 800px;
margin: 50px auto;
}
.typeahead-wrapper {
display: block;
margin: 50px 0;
}
.tt-menu {
background-color: #fff;
border: 1px solid #000;
}
.tt-suggestion.tt-cursor {
background-color: #ccc;
}
</style>
</head>
<body>
<div class="container">
<form action="/where" method="GET">
<div class="typeahead-wrapper">
<input id="states" name="states" type="text">
<input type="submit">
</div>
</form>
</div>
<script>
var states = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('val'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: 'Alabama' },
{ val: 'Alaska' },
{ val: 'Arizona' },
{ val: 'Arkansas' },
{ val: 'California' },
{ val: 'Colorado' },
{ val: 'Connecticut' },
{ val: 'Delaware' },
{ val: 'Florida' },
{ val: 'Georgia' },
{ val: 'Hawaii' },
{ val: 'Idaho' },
{ val: 'Illinois' },
{ val: 'Indiana' },
{ val: 'Iowa' },
{ val: 'Kansas' },
{ val: 'Kentucky' },
{ val: 'Louisiana' },
{ val: 'Maine' },
{ val: 'Maryland' },
{ val: 'Massachusetts' },
{ val: 'Michigan' },
{ val: 'Minnesota' },
{ val: 'Mississippi' },
{ val: 'Missouri' },
{ val: 'Montana' },
{ val: 'Nebraska' },
{ val: 'Nevada' },
{ val: 'New Hampshire' },
{ val: 'New Jersey' },
{ val: 'New Mexico' },
{ val: 'New York' },
{ val: 'North Carolina' },
{ val: 'North Dakota' },
{ val: 'Ohio' },
{ val: 'Oklahoma' },
{ val: 'Oregon' },
{ val: 'Pennsylvania' },
{ val: 'Rhode Island' },
{ val: 'South Carolina' },
{ val: 'South Dakota' },
{ val: 'Tennessee' },
{ val: 'Texas' },
{ val: 'Utah' },
{ val: 'Vermont' },
{ val: 'Virginia' },
{ val: 'Washington' },
{ val: 'West Virginia' },
{ val: 'Wisconsin' },
{ val: 'Wyoming' },
{ val: 'this is a very long value so deal with it' }
]
});
$('#states').typeahead({
highlight: true
},
{
display: 'val',
source: states
});
</script>
</body>
</html>

View File

@ -1,395 +0,0 @@
/* jshint esnext: true, evil: true, sub: true */
var wd = require('yiewd'),
colors = require('colors'),
expect = require('chai').expect,
_ = require('underscore'),
f = require('util').format,
env = process.env;
var browser, caps;
browser = (process.env.BROWSER || 'chrome').split(':');
caps = {
name: f('[%s] typeahead.js ui', browser.join(' , ')),
browserName: browser[0]
};
setIf(caps, 'version', browser[1]);
setIf(caps, 'platform', browser[2]);
setIf(caps, 'tunnel-identifier', env['TRAVIS_JOB_NUMBER']);
setIf(caps, 'build', env['TRAVIS_BUILD_NUMBER']);
setIf(caps, 'tags', env['CI'] ? ['CI'] : ['local']);
function setIf(obj, key, val) {
val && (obj[key] = val);
}
describe('jquery-typeahead.js', function() {
var driver, body, input, hint, dropdown, allPassed = true;
this.timeout(300000);
before(function(done) {
var host = 'ondemand.saucelabs.com', port = 80, username, password;
if (env['CI']) {
host = 'localhost';
port = 4445;
username = env['SAUCE_USERNAME'];
password = env['SAUCE_ACCESS_KEY'];
}
driver = wd.remote(host, port, username, password);
driver.configureHttp({
timeout: 30000,
retries: 5,
retryDelay: 200
});
driver.on('status', function(info) {
console.log(info.cyan);
});
driver.on('command', function(meth, path, data) {
console.log(' > ' + meth.yellow, path.grey, data || '');
});
driver.run(function*() {
yield this.init(caps);
yield this.get('http://localhost:8888/test/integration/test.html');
body = yield this.elementByTagName('body');
input = yield this.elementById('states');
hint = yield this.elementByClassName('tt-hint');
dropdown = yield this.elementByClassName('tt-menu');
done();
});
});
afterEach(function(done) {
allPassed = allPassed && (this.currentTest.state === 'passed');
driver.run(function*() {
yield body.click();
yield this.execute('window.jQuery("#states").typeahead("val", "")');
done();
});
});
after(function(done) {
driver.run(function*() {
yield this.quit();
yield driver.sauceJobStatus(allPassed);
done();
});
});
describe('on blur', function() {
it('should close dropdown', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield dropdown.isDisplayed()).to.equal(true);
yield body.click();
expect(yield dropdown.isDisplayed()).to.equal(false);
done();
});
});
it('should clear hint', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield hint.getValue()).to.equal('michigan');
yield body.click();
expect(yield hint.getValue()).to.equal('');
done();
});
});
});
describe('on query change', function() {
it('should open dropdown if suggestions', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield dropdown.isDisplayed()).to.equal(true);
done();
});
});
it('should close dropdown if no suggestions', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('huh?');
expect(yield dropdown.isDisplayed()).to.equal(false);
done();
});
});
it('should render suggestions if suggestions', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
expect(suggestions).to.have.length('4');
expect(yield suggestions[0].text()).to.equal('Michigan');
expect(yield suggestions[1].text()).to.equal('Minnesota');
expect(yield suggestions[2].text()).to.equal('Mississippi');
expect(yield suggestions[3].text()).to.equal('Missouri');
done();
});
});
it('should show hint if top suggestion is a match', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield hint.getValue()).to.equal('michigan');
done();
});
});
it('should match hint to query', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('NeW JE');
expect(yield hint.getValue()).to.equal('NeW JErsey');
done();
});
});
it('should not show hint if top suggestion is not a match', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('ham');
expect(yield hint.getValue()).to.equal('');
done();
});
});
it('should not show hint if there is query overflow', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('this is a very long value so ');
expect(yield hint.getValue()).to.equal('');
done();
});
});
});
describe('on up arrow', function() {
it('should cycle through suggestions', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Missouri');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Mississippi');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Minnesota');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('Michigan');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Up arrow']);
expect(yield input.getValue()).to.equal('mi');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
done();
});
});
});
describe('on down arrow', function() {
it('should cycle through suggestions', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Michigan');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Minnesota');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Mississippi');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('Missouri');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable tt-cursor');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
expect(yield input.getValue()).to.equal('mi');
expect(yield suggestions[0].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[1].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[2].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
expect(yield suggestions[3].getAttribute('class')).to.equal('tt-suggestion tt-selectable');
done();
});
});
});
describe('on escape', function() {
it('should close dropdown', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield dropdown.isDisplayed()).to.equal(true);
yield input.type(wd.SPECIAL_KEYS['Escape']);
expect(yield dropdown.isDisplayed()).to.equal(false);
done();
});
});
it('should clear hint', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
expect(yield hint.getValue()).to.equal('michigan');
yield input.type(wd.SPECIAL_KEYS['Escape']);
expect(yield hint.getValue()).to.equal('');
done();
});
});
});
describe('on tab', function() {
it('should autocomplete if hint is present', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
yield input.type(wd.SPECIAL_KEYS['Tab']);
expect(yield input.getValue()).to.equal('Michigan');
done();
});
});
it('should select if cursor is on suggestion', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Tab']);
expect(yield dropdown.isDisplayed()).to.equal(false);
expect(yield input.getValue()).to.equal('Minnesota');
done();
});
});
});
describe('on right arrow', function() {
it('should autocomplete if hint is present', function(done) {
driver.run(function*() {
yield input.click();
yield input.type('mi');
yield input.type(wd.SPECIAL_KEYS['Right arrow']);
expect(yield input.getValue()).to.equal('Michigan');
done();
});
});
});
describe('on suggestion click', function() {
it('should select suggestion', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield suggestions[1].click();
expect(yield dropdown.isDisplayed()).to.equal(false);
expect(yield input.getValue()).to.equal('Minnesota');
done();
});
});
});
describe('on enter', function() {
it('should select if cursor is on suggestion', function(done) {
driver.run(function*() {
var suggestions;
yield input.click();
yield input.type('mi');
suggestions = yield dropdown.elementsByClassName('tt-suggestion');
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Down arrow']);
yield input.type(wd.SPECIAL_KEYS['Return']);
expect(yield dropdown.isDisplayed()).to.equal(false);
expect(yield input.getValue()).to.equal('Minnesota');
done();
});
});
});
});

View File

@ -1,346 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script src="../bower_components/jquery/jquery.js"></script>
<script src="../dist/typeahead.bundle.js"></script>
<style>
.container {
width: 800px;
margin: 50px auto;
}
.typeahead-wrapper {
display: block;
margin: 50px 0;
}
.tt-dropdown-menu {
background-color: #fff;
border: 1px solid #000;
}
.tt-suggestion.tt-cursor {
background-color: #ccc;
}
.triggered-events {
float: right;
width: 500px;
height: 300px;
}
</style>
</head>
<body>
<div class="container">
<textarea class="triggered-events"></textarea>
<form action="/where" method="GET">
<div class="typeahead-wrapper">
<input class="states" name="states" type="text" placeholder="states" value="Michigan">
<input type="submit">
</div>
</form>
<div class="typeahead-wrapper">
<input class="bad-tokens" type="text" placeholder="bad tokens">
</div>
<div class="typeahead-wrapper">
<input class="regex-symbols" type="text" placeholder="regex symbols">
</div>
<div class="typeahead-wrapper">
<input class="header-footer" type="text" placeholder="header footer">
</div>
<div class="typeahead-wrapper">
<input class="ltr" type="text" placeholder="ltr">
</div>
<div class="typeahead-wrapper">
<input class="rtl" type="text" placeholder="rtl">
</div>
<div class="typeahead-wrapper">
<input class="mixed" type="text" placeholder="mixed">
</div>
</div>
</div>
<script>
var states = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming'
]
});
states.initialize();
$('.states').typeahead({
highlight: true
},
{
source: states
});
var badTokens = new Bloodhound({
datumTokenizer: function(d) { return d.tokens; },
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{
value1: 'all bad',
jake: '111',
tokens: [' ', ' ', null, undefined, false, 'all', 'bad']
},
{
value1: 'whitespace',
jake: '112',
tokens: [' ', ' ', '\t', '\n', 'whitespace']
},
{
value1: 'undefined',
jake: '113',
tokens: [undefined, 'undefined']
},
{
value1: 'null',
jake: '114',
tokens: [null, 'null']
},
{
value1: 'false',
jake: '115',
tokens: [false, 'false']
}
]
});
badTokens.initialize();
$('.bad-tokens').typeahead(null, {
displayKey: 'value1',
source: badTokens
});
var regexSymbols = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: '*.js' },
{ val: '[Tt]ypeahead.js' },
{ val: '^typeahead.js$' },
{ val: 'typeahead.js(0.8.2)' },
{ val: 'typeahead.js(@\\d.\\d.\\d)' },
{ val: 'typeahead.js@0.8.2' }
]
});
regexSymbols.initialize();
$('.regex-symbols').typeahead(null, {
displayKey: 'val',
source: regexSymbols
});
var abc = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: 'a' },
{ val: 'ab' },
{ val: 'abc' },
{ val: 'abcd' },
{ val: 'abcde' }
]
});
abc.initialize();
$('.header-footer').typeahead(null, {
displayKey: 'val',
source: abc,
templates: {
header: '<h3>Header</h3>',
footer: '<h3>Footer</h3>'
}
},
{
displayKey: 'val',
source: abc,
templates: {
header: '<h3>start</h3>',
footer: '<h3>end</h3>',
empty: '<h3>empty</h3>'
}
});
var ltr = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: "one" },
{ val: "two three" },
{ val: "four" },
{ val: "five six" },
{ val: "seven" }
]
});
ltr.initialize();
$('.ltr').typeahead({
highlight: true
},
{
displayKey: 'val',
source: ltr
});
var rtl = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: "שלום" },
{ val: "ערב טוב" },
{ val: "מה שלומך" },
{ val: "רב תודות" },
{ val: "אין דבר" }
]
});
rtl.initialize();
$('.rtl').typeahead({
highlight: true
},
{
displayKey: 'val',
source: rtl
});
var mixed = new Bloodhound({
datumTokenizer: function(d) {
return Bloodhound.tokenizers.whitespace(d.val);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
local: [
{ val: "שלום" },
{ val: "ערב טוב" },
{ val: "מה שלומך" },
{ val: "one" },
{ val: "two three" }
]
});
mixed.initialize();
$('.mixed').typeahead({
highlight: true
},
{
displayKey: 'val',
source: mixed
});
$('input').on([
'typeahead:active',
'typeahead:idle',
'typeahead:open',
'typeahead:close',
'typeahead:change',
'typeahead:render',
'typeahead:select',
'typeahead:autocomplete',
'typeahead:cursorchange',
].join(' '), logTypeaheadEvent);
$('form').on('submit', logSubmitEvent);
function logSubmitEvent($e) {
var text;
$e && $e.preventDefault();
text = JSON.stringify($(this).serializeArray());
writeToTextarea('submit', text);
}
function logTypeaheadEvent($e) {
var args, type, text;
args = [].slice.call(arguments, 1);
type = $e.type;
text = window.JSON ? JSON.stringify(args) : '';
writeToTextarea(type, text);
}
function writeToTextarea(/* lines */) {
var $textarea, val, text;
$textarea = $('.triggered-events');
val = $textarea.val();
text = [].join.call(arguments, '\n');
$textarea.val([val, text, '\n'].join('\n'));
$textarea[0].scrollTop = $textarea[0].scrollHeight;
}
</script>
</body>
</html>

View File

@ -1,496 +0,0 @@
describe('Dataset', function() {
var www = WWW(), mockSuggestions, mockSuggestionsDisplayFn;
mockSuggestions = [
{ value: 'one', raw: { value: 'one' } },
{ value: 'two', raw: { value: 'two' } },
{ value: 'html', raw: { value: '<b>html</b>' } }
];
mockSuggestionsDisplayFn = [
{ display: '4' },
{ display: '5' },
{ display: '6' }
];
beforeEach(function() {
this.dataset = new Dataset({
name: 'test',
node: $('<div>'),
source: this.source = jasmine.createSpy('source')
}, www);
});
it('should throw an error if source is missing', function() {
expect(noSource).toThrow();
function noSource() { new Dataset({}, www); }
});
it('should throw an error if the name is not a valid class name', function() {
expect(fn).toThrow();
function fn() {
var d = new Dataset({
name: 'a space',
node: $('<div>'),
source: $.noop
}, www);
}
});
describe('#getRoot', function() {
it('should return the root element', function() {
var sel = 'div' + www.selectors.dataset + www.selectors.dataset + '-test';
expect(this.dataset.$el).toBe(sel);
});
});
describe('#update', function() {
it('should render suggestions', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('one');
expect(this.dataset.$el).toContainText('two');
expect(this.dataset.$el).toContainText('html');
});
it('should escape html chars from display value when using default template', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('<b>html</b>');
});
it('should respect limit option', function() {
this.dataset.limit = 2;
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('one');
expect(this.dataset.$el).toContainText('two');
expect(this.dataset.$el).not.toContainText('three');
});
it('should allow custom display functions', function() {
this.dataset = new Dataset({
name: 'test',
node: $('<div>'),
display: function(o) { return o.display; },
source: this.source = jasmine.createSpy('source')
}, www);
this.source.andCallFake(syncMockSuggestionsDisplayFn);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('4');
expect(this.dataset.$el).toContainText('5');
expect(this.dataset.$el).toContainText('6');
});
it('should ignore async invocations of sync', function() {
this.source.andCallFake(asyncSync);
this.dataset.update('woah');
expect(this.dataset.$el).not.toContainText('one');
});
it('should ignore subesequent invocations of sync', function() {
this.source.andCallFake(multipleSync);
this.dataset.update('woah');
expect(this.dataset.$el.find('.tt-suggestion')).toHaveLength(3);
});
it('should trigger asyncRequested when needing/expecting backfill', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncRequested', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
expect(spy).toHaveBeenCalled();
});
it('should not trigger asyncRequested when not expecting backfill', function() {
var spy = jasmine.createSpy();
this.dataset.async = false;
this.dataset.onSync('asyncRequested', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
expect(spy).not.toHaveBeenCalled();
});
it('should not trigger asyncRequested when not expecting backfill', function() {
var spy = jasmine.createSpy();
this.dataset.limit = 2;
this.dataset.async = true;
this.dataset.onSync('asyncRequested', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
expect(spy).not.toHaveBeenCalled();
});
it('should trigger asyncCanceled when pending aysnc is canceled', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncCanceled', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.cancel();
waits(100);
runs(function() {
expect(spy).toHaveBeenCalled();
});
});
it('should not trigger asyncCanceled when cancel happens after update', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncCanceled', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
this.dataset.cancel();
expect(spy).not.toHaveBeenCalled();
});
});
it('should trigger asyncReceived when aysnc is received', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncReceived', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
expect(spy).toHaveBeenCalled();
});
});
it('should not trigger asyncReceived if canceled', function() {
var spy = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncReceived', spy);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.cancel();
waits(100);
runs(function() {
expect(spy).not.toHaveBeenCalled();
});
});
it('should not modify sync when async is added', function() {
var $test;
this.dataset.async = true;
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
$test = this.dataset.$el.find('.tt-suggestion').first();
$test.addClass('test');
waits(100);
runs(function() {
expect($test).toHaveClass('test');
});
});
it('should respect limit option in regard to async', function() {
this.dataset.async = true;
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
expect(this.dataset.$el.find('.tt-suggestion')).toHaveLength(5);
});
});
it('should cancel pending async', function() {
var spy1 = jasmine.createSpy(), spy2 = jasmine.createSpy();
this.dataset.async = true;
this.dataset.onSync('asyncCanceled', spy1);
this.dataset.onSync('asyncReceived', spy2);
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.update('woah again');
waits(100);
runs(function() {
expect(spy1.callCount).toBe(1);
expect(spy2.callCount).toBe(1);
});
});
it('should render notFound when no suggestions are available', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
notFound: '<h2>empty</h2>'
}
}, www);
this.source.andCallFake(syncEmptySuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('empty');
});
it('should render pending when no suggestions are available but async is pending', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
async: true,
templates: {
pending: '<h2>pending</h2>'
}
}, www);
this.source.andCallFake(syncEmptySuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('pending');
});
it('should render header when suggestions are rendered', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
header: '<h2>header</h2>'
}
}, www);
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('header');
});
it('should render footer when suggestions are rendered', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
footer: function(c) { return '<p>' + c.query + '</p>'; }
}
}, www);
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).toContainText('woah');
});
it('should not render header/footer if there is no content', function() {
this.dataset = new Dataset({
source: this.source,
node: $('<div>'),
templates: {
header: '<h2>header</h2>',
footer: '<h2>footer</h2>'
}
}, www);
this.source.andCallFake(syncEmptySuggestions);
this.dataset.update('woah');
expect(this.dataset.$el).not.toContainText('header');
expect(this.dataset.$el).not.toContainText('footer');
});
it('should not render stale suggestions', function() {
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('nelly');
waits(100);
runs(function() {
expect(this.dataset.$el).toContainText('one');
expect(this.dataset.$el).toContainText('two');
expect(this.dataset.$el).toContainText('html');
expect(this.dataset.$el).not.toContainText('four');
expect(this.dataset.$el).not.toContainText('five');
});
});
it('should not render async suggestions if update was canceled', function() {
this.source.andCallFake(fakeGetWithAsyncSuggestions);
this.dataset.update('woah');
this.dataset.cancel();
waits(100);
runs(function() {
var rendered = this.dataset.$el.find('.tt-suggestion');
expect(rendered).toHaveLength(3);
});
});
it('should render all async suggestions if sync had no content', function() {
this.source.andCallFake(fakeGetWithEmptySyncAndAsyncSuggestions);
this.dataset.update('woah');
waits(100);
runs(function() {
var rendered = this.dataset.$el.find('.tt-suggestion');
expect(rendered).toHaveLength(5);
});
});
it('should trigger rendered after suggestions are rendered', function() {
var spy;
this.dataset.onSync('rendered', spy = jasmine.createSpy());
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
waitsFor(function() { return spy.callCount; });
});
});
describe('#clear', function() {
it('should clear suggestions', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
this.dataset.clear();
expect(this.dataset.$el).toBeEmpty();
});
it('should cancel pending updates', function() {
var spy;
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
spy = spyOn(this.dataset, 'cancel');
this.dataset.clear();
expect(spy).toHaveBeenCalled();
});
it('should trigger cleared', function() {
var spy;
this.dataset.onSync('cleared', spy = jasmine.createSpy());
this.dataset.clear();
expect(spy).toHaveBeenCalled();
});
});
describe('#isEmpty', function() {
it('should return true when empty', function() {
expect(this.dataset.isEmpty()).toBe(true);
});
it('should return false when not empty', function() {
this.source.andCallFake(syncMockSuggestions);
this.dataset.update('woah');
expect(this.dataset.isEmpty()).toBe(false);
});
});
describe('#destroy', function() {
it('should set dataset element to dummy element', function() {
var $prevEl = this.dataset.$el;
this.dataset.destroy();
expect(this.dataset.$el).not.toBe($prevEl);
});
});
// helper functions
// ----------------
function syncEmptySuggestions(q, sync, async) {
sync([]);
}
function syncMockSuggestions(q, sync, async) {
sync(mockSuggestions);
}
function syncMockSuggestionsDisplayFn(q, sync, async) {
sync(mockSuggestionsDisplayFn);
}
function asyncSync(q, sync, async) {
setTimeout(function() { sync(mockSuggestions); }, 0);
}
function multipleSync(q, sync, async) {
sync(mockSuggestions);
sync(mockSuggestions);
}
function fakeGetWithAsyncSuggestions(query, sync, async) {
sync(mockSuggestions);
setTimeout(function() {
async([
{ value: 'four', raw: { value: 'four' } },
{ value: 'five', raw: { value: 'five' } },
{ value: 'six', raw: { value: 'six' } },
{ value: 'seven', raw: { value: 'seven' } },
{ value: 'eight', raw: { value: 'eight' } },
]);
}, 0);
}
function fakeGetWithEmptySyncAndAsyncSuggestions(query, sync, async) {
sync([]);
setTimeout(function() {
async([
{ value: 'four', raw: { value: 'four' } },
{ value: 'five', raw: { value: 'five' } },
{ value: 'six', raw: { value: 'six' } },
{ value: 'seven', raw: { value: 'seven' } },
{ value: 'eight', raw: { value: 'eight' } },
]);
}, 0);
}
});

View File

@ -1,103 +0,0 @@
describe('DefaultMenu', function() {
var www = WWW();
beforeEach(function() {
var $fixture;
jasmine.Dataset.useMock();
setFixtures('<div id="menu-fixture"></div>');
$fixture = $('#jasmine-fixtures');
this.$node = $fixture.find('#menu-fixture');
this.$node.html(fixtures.html.dataset);
this.view = new DefaultMenu({ node: this.$node, datasets: [{}] }, www).bind();
this.dataset = this.view.datasets[0];
});
describe('when rendered is triggered on a dataset', function() {
it('should hide menu if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.view._show();
this.dataset.trigger('rendered');
expect(this.$node).not.toBeVisible();
});
it('should not show menu if not open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.dataset.trigger('rendered');
expect(this.$node).not.toBeVisible();
});
it('should show menu if not empty and open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.view.open();
this.dataset.trigger('rendered');
expect(this.$node).toBeVisible();
});
});
describe('when cleared is triggered on a dataset', function() {
it('should hide menu if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.view._show();
this.dataset.trigger('cleared');
expect(this.$node).not.toBeVisible();
});
it('should not show menu if not open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.dataset.trigger('cleared');
expect(this.$node).not.toBeVisible();
});
it('should show menu if not empty and open', function() {
this.dataset.isEmpty.andReturn(false);
this.view._hide();
this.view.open();
this.dataset.trigger('cleared');
expect(this.$node).toBeVisible();
});
});
describe('#open', function() {
it('should show menu if not empty', function() {
spyOn(this.view, '_allDatasetsEmpty').andReturn(false);
this.view.open();
expect(this.$node[0].getAttribute('style')).toMatch(/display: block/);
});
it('should not show menu if empty', function() {
spyOn(this.view, '_allDatasetsEmpty').andReturn(true);
this.view.open();
expect(this.$node).not.toHaveAttr('style', 'display: block;');
});
});
describe('#close', function() {
it('should hide menu', function() {
this.view._show();
this.view.close();
expect(this.$node).not.toBeVisible();
});
});
});

View File

@ -1,42 +0,0 @@
describe('EventBus', function() {
beforeEach(function() {
var $fixture;
setFixtures(fixtures.html.input);
$fixture = $('#jasmine-fixtures');
this.$el = $fixture.find('.tt-input');
this.eventBus = new EventBus({ el: this.$el });
});
it('#trigger should trigger event', function() {
var spy = jasmine.createSpy();
this.$el.on('typeahead:fiz', spy);
this.eventBus.trigger('fiz');
expect(spy).toHaveBeenCalled();
});
it('#before should return false if default was not prevented', function() {
var spy = jasmine.createSpy();
this.$el.on('typeahead:beforefiz', spy);
expect(this.eventBus.before('fiz')).toBe(false);
expect(spy).toHaveBeenCalled();
});
it('#before should return true if default was prevented', function() {
var spy = jasmine.createSpy().andCallFake(prevent);
this.$el.on('typeahead:beforefiz', spy);
expect(this.eventBus.before('fiz')).toBe(true);
expect(spy).toHaveBeenCalled();
function prevent($e) { $e.preventDefault(); }
});
});

View File

@ -1,111 +0,0 @@
describe('EventEmitter', function() {
beforeEach(function() {
this.spy = jasmine.createSpy();
this.target = _.mixin({}, EventEmitter);
});
it('methods should be chainable', function() {
expect(this.target.onSync()).toEqual(this.target);
expect(this.target.onAsync()).toEqual(this.target);
expect(this.target.off()).toEqual(this.target);
expect(this.target.trigger()).toEqual(this.target);
});
it('#on should take the context a callback should be called in', function() {
var context = { val: 3 }, cbContext;
this.target.onSync('xevent', setCbContext, context).trigger('xevent');
waitsFor(assertCbContext, 'callback was called in the wrong context');
function setCbContext() { cbContext = this; }
function assertCbContext() { return cbContext === context; }
});
it('#onAsync callbacks should be invoked asynchronously', function() {
this.target.onAsync('event', this.spy).trigger('event');
expect(this.spy.callCount).toBe(0);
waitsFor(assertCallCount(this.spy, 1), 'the callback was not invoked');
});
it('#onSync callbacks should be invoked synchronously', function() {
this.target.onSync('event', this.spy).trigger('event');
expect(this.spy.callCount).toBe(1);
});
it('#off should remove callbacks', function() {
this.target
.onSync('event1 event2', this.spy)
.onAsync('event1 event2', this.spy)
.off('event1 event2')
.trigger('event1 event2');
waits(100);
runs(assertCallCount(this.spy, 0));
});
it('methods should accept multiple event types', function() {
this.target
.onSync('event1 event2', this.spy)
.onAsync('event1 event2', this.spy)
.trigger('event1 event2');
expect(this.spy.callCount).toBe(2);
waitsFor(assertCallCount(this.spy, 4), 'the callback was not invoked');
});
it('the event type should be passed to the callback', function() {
this.target
.onSync('sync', this.spy)
.onAsync('async', this.spy)
.trigger('sync async');
waitsFor(assertArgs(this.spy, 0, ['sync']), 'bad args');
waitsFor(assertArgs(this.spy, 1, ['async']), 'bad args');
});
it('arbitrary args should be passed to the callback', function() {
this.target
.onSync('event', this.spy)
.onAsync('event', this.spy)
.trigger('event', 1, 2);
waitsFor(assertArgs(this.spy, 0, ['event', 1, 2]), 'bad args');
waitsFor(assertArgs(this.spy, 1, ['event', 1, 2]), 'bad args');
});
it('callback execution should be cancellable', function() {
var cancelSpy = jasmine.createSpy().andCallFake(cancel);
this.target
.onSync('one', cancelSpy)
.onSync('one', this.spy)
.onAsync('two', cancelSpy)
.onAsync('two', this.spy)
.onSync('three', cancelSpy)
.onAsync('three', this.spy)
.trigger('one two three');
waitsFor(assertCallCount(cancelSpy, 3));
waitsFor(assertCallCount(this.spy, 0));
function cancel() { return false; }
});
function assertCallCount(spy, expected) {
return function() { return spy.callCount === expected; };
}
function assertArgs(spy, call, expected) {
return function() {
var env = jasmine.getEnv(),
actual = spy.calls[call] ? spy.calls[call].args : undefined;
return env.equals_(actual, expected);
};
}
});

View File

@ -1,117 +0,0 @@
describe('highlight', function() {
it('should allow tagName to be specified', function() {
var before = 'abcde',
after = 'a<span>bcd</span>e',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', tagName: 'span' });
expect(testNode.innerHTML).toEqual(after);
});
it('should allow className to be specified', function() {
var before = 'abcde',
after = 'a<strong class="one two">bcd</strong>e',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', className: 'one two' });
expect(testNode.innerHTML).toEqual(after);
});
it('should be case insensitive by default', function() {
var before = 'ABCDE',
after = 'A<strong>BCD</strong>E',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd' });
expect(testNode.innerHTML).toEqual(after);
});
it('should support case sensitivity', function() {
var before = 'ABCDE',
after = 'ABCDE',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'bcd', caseSensitive: true });
expect(testNode.innerHTML).toEqual(after);
});
it('should support words only matching', function() {
var before = 'tone one phone',
after = 'tone <strong>one</strong> phone',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'one', wordsOnly: true });
expect(testNode.innerHTML).toEqual(after);
});
it('should support matching multiple patterns', function() {
var before = 'tone one phone',
after = '<strong>tone</strong> one <strong>phone</strong>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['tone', 'phone'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should support regex chars in the pattern', function() {
var before = '*.js when?',
after = '<strong>*.</strong>js when<strong>?</strong>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['*.', '?'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should work on complex html structures', function() {
var before = [
'<div>abcde',
'<span>abcde</span>',
'<div><p>abcde</p></div>',
'</div>'
].join(''),
after = [
'<div><strong>abc</strong>de',
'<span><strong>abc</strong>de</span>',
'<div><p><strong>abc</strong>de</p></div>',
'</div>'
].join(''),
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
it('should ignore html tags and attributes', function() {
var before = '<span class="class"></span>',
after = '<span class="class"></span>',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: ['span', 'class'] });
expect(testNode.innerHTML).toEqual(after);
});
it('should not match across tags', function() {
var before = 'a<span>b</span>c',
after = 'a<span>b</span>c',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
it('should ignore html comments', function() {
var before = '<!-- abc -->',
after = '<!-- abc -->',
testNode = buildTestNode(before);
highlight({ node: testNode, pattern: 'abc' });
expect(testNode.innerHTML).toEqual(after);
});
function buildTestNode(content) {
var node = document.createElement('div');
node.innerHTML = content;
return node;
}
});

View File

@ -1,538 +0,0 @@
describe('Input', function() {
var KEYS, www;
KEYS = {
enter: 13,
esc: 27,
tab: 9,
left: 37,
right: 39,
up: 38,
down: 40,
normal: 65 // "A" key
};
www = WWW();
beforeEach(function() {
var $fixture;
setFixtures(fixtures.html.input + fixtures.html.hint);
$fixture = $('#jasmine-fixtures');
this.$input = $fixture.find('.tt-input');
this.$hint = $fixture.find('.tt-hint');
this.view = new Input({ input: this.$input, hint: this.$hint }, www).bind();
});
it('should throw an error if no input is provided', function() {
expect(noInput).toThrow();
function noInput() { new Input({}, www); }
});
describe('when the blur DOM event is triggered', function() {
it('should reset the input value', function() {
this.view.setQuery('wine');
this.view.setInputValue('cheese');
this.$input.blur();
expect(this.$input.val()).toBe('wine');
});
it('should trigger blurred', function() {
var spy;
this.view.onSync('blurred', spy = jasmine.createSpy());
this.$input.blur();
expect(spy).toHaveBeenCalled();
});
});
describe('when the focus DOM event is triggered', function() {
it('should update queryWhenFocused', function() {
this.view.setQuery('hi');
this.$input.focus();
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(false);
this.view.setQuery('bye');
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(true);
});
it('should trigger focused', function() {
var spy;
this.view.onSync('focused', spy = jasmine.createSpy());
this.$input.focus();
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by tab', function() {
it('should trigger tabKeyed if no modifiers were pressed', function() {
var spy;
this.view.onSync('tabKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.tab);
expect(spy).toHaveBeenCalled();
});
it('should not trigger tabKeyed if modifiers were pressed', function() {
var spy;
this.view.onSync('tabKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.tab, true);
expect(spy).not.toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by esc', function() {
it('should trigger escKeyed', function() {
var spy;
this.view.onSync('escKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.esc);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by left', function() {
it('should trigger leftKeyed', function() {
var spy;
this.view.onSync('leftKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.left);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by right', function() {
it('should trigger rightKeyed', function() {
var spy;
this.view.onSync('rightKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.right);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by enter', function() {
it('should trigger enterKeyed', function() {
var spy;
this.view.onSync('enterKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.enter);
expect(spy).toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by up', function() {
it('should trigger upKeyed', function() {
var spy;
this.view.onSync('upKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.up);
expect(spy).toHaveBeenCalled();
});
it('should prevent default if no modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up);
expect($e.preventDefault).toHaveBeenCalled();
});
it('should not prevent default if modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.up, true);
expect($e.preventDefault).not.toHaveBeenCalled();
});
});
describe('when the keydown DOM event is triggered by down', function() {
it('should trigger downKeyed', function() {
var spy;
this.view.onSync('downKeyed', spy = jasmine.createSpy());
simulateKeyEvent(this.$input, 'keydown', KEYS.down);
expect(spy).toHaveBeenCalled();
});
it('should prevent default if no modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down);
expect($e.preventDefault).toHaveBeenCalled();
});
it('should not prevent default if modifers were pressed', function() {
var $e = simulateKeyEvent(this.$input, 'keydown', KEYS.down, true);
expect($e.preventDefault).not.toHaveBeenCalled();
});
});
// NOTE: have to treat these as async because the ie polyfill acts
// in a async manner
describe('when the input DOM event is triggered', function() {
it('should update query', function() {
this.view.setQuery('wine');
this.view.setInputValue('cheese');
simulateInputEvent(this.$input);
waitsFor(function() { return this.view.getQuery() === 'cheese'; });
});
it('should trigger queryChanged if the query changed', function() {
var spy;
this.view.setQuery('wine');
this.view.setInputValue('cheese');
this.view.onSync('queryChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(spy).toHaveBeenCalled();
});
it('should trigger whitespaceChanged if whitespace changed', function() {
var spy;
this.view.setQuery('wine bar');
this.view.setInputValue('wine bar');
this.view.onSync('whitespaceChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(spy).toHaveBeenCalled();
});
it('should clear hint if invalid', function() {
spyOn(this.view, 'clearHintIfInvalid');
simulateInputEvent(this.$input);
expect(this.view.clearHintIfInvalid).toHaveBeenCalled();
});
it('should check lang direction', function() {
var spy;
this.$input.css('direction', 'rtl');
this.view.onSync('langDirChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(this.view.dir).toBe('rtl');
expect(this.$hint).toHaveAttr('dir', 'rtl');
expect(spy).toHaveBeenCalled();
});
});
describe('.normalizeQuery', function() {
it('should strip leading whitespace', function() {
expect(Input.normalizeQuery(' foo')).toBe('foo');
});
it('should condense whitespace', function() {
expect(Input.normalizeQuery('foo bar')).toBe('foo bar');
});
it('should play nice with non-string values', function() {
expect(Input.normalizeQuery(2)).toBe('2');
expect(Input.normalizeQuery([])).toBe('');
expect(Input.normalizeQuery(null)).toBe('');
expect(Input.normalizeQuery(undefined)).toBe('');
expect(Input.normalizeQuery(false)).toBe('false');
});
});
describe('#focus', function() {
it('should focus the input', function() {
this.$input.blur();
this.view.focus();
expect(this.$input).toBeFocused();
});
});
describe('#blur', function() {
it('should blur the input', function() {
this.$input.focus();
this.view.blur();
expect(this.$input).not.toBeFocused();
});
});
describe('#getQuery', function() {
it('should act as getter to the query property', function() {
this.view.setQuery('mouse');
expect(this.view.getQuery()).toBe('mouse');
});
});
describe('#setQuery', function() {
it('should act as setter to the query property', function() {
this.view.setQuery('mouse');
expect(this.view.getQuery()).toBe('mouse');
});
it('should update input value', function() {
this.view.setQuery('mouse');
expect(this.view.getInputValue()).toBe('mouse');
});
it('should trigger queryChanged if the query changed', function() {
var spy;
this.view.setQuery('wine');
this.view.onSync('queryChanged', spy = jasmine.createSpy());
this.view.setQuery('cheese');
expect(spy).toHaveBeenCalled();
});
it('should trigger whitespaceChanged if whitespace changed', function() {
var spy;
this.view.setQuery('wine bar');
this.view.onSync('whitespaceChanged', spy = jasmine.createSpy());
this.view.setQuery('wine bar');
expect(spy).toHaveBeenCalled();
});
it('should clear hint if invalid', function() {
spyOn(this.view, 'clearHintIfInvalid');
simulateInputEvent(this.$input);
expect(this.view.clearHintIfInvalid).toHaveBeenCalled();
});
});
describe('#hasQueryChangedSinceLastFocus', function() {
it('should return true if the query has changed since focus', function() {
this.view.setQuery('hi');
this.$input.focus();
this.view.setQuery('bye');
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(true);
});
it('should return false if the query has not changed since focus', function() {
this.view.setQuery('hi');
this.$input.focus();
expect(this.view.hasQueryChangedSinceLastFocus()).toBe(false);
});
});
describe('#getInputValue', function() {
it('should act as getter to the input value', function() {
this.$input.val('cheese');
expect(this.view.getInputValue()).toBe('cheese');
});
});
describe('#setInputValue', function() {
it('should act as setter to the input value', function() {
this.view.setInputValue('cheese');
expect(this.view.getInputValue()).toBe('cheese');
});
it('should clear hint if invalid', function() {
spyOn(this.view, 'clearHintIfInvalid');
this.view.setInputValue('cheese head');
expect(this.view.clearHintIfInvalid).toHaveBeenCalled();
});
it('should check lang direction', function() {
var spy;
this.$input.css('direction', 'rtl');
this.view.onSync('langDirChanged', spy = jasmine.createSpy());
simulateInputEvent(this.$input);
expect(this.view.dir).toBe('rtl');
expect(this.$hint).toHaveAttr('dir', 'rtl');
expect(spy).toHaveBeenCalled();
});
});
describe('#getHint/#setHint', function() {
it('should act as getter/setter to value of hint', function() {
this.view.setHint('mountain');
expect(this.view.getHint()).toBe('mountain');
});
});
describe('#resetInputValue', function() {
it('should reset input value to last query', function() {
this.view.setQuery('cheese');
this.view.setInputValue('wine');
this.view.resetInputValue();
expect(this.view.getInputValue()).toBe('cheese');
});
});
describe('#clearHint', function() {
it('should set the hint value to the empty string', function() {
this.view.setHint('cheese');
this.view.clearHint();
expect(this.view.getHint()).toBe('');
});
});
describe('#clearHintIfInvalid', function() {
it('should clear hint if input value is empty string', function() {
this.view.setInputValue('');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('');
});
it('should clear hint if input value is not prefix of input', function() {
this.view.setInputValue('milk');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('');
});
it('should clear hint if overflow exists', function() {
spyOn(this.view, 'hasOverflow').andReturn(true);
this.view.setInputValue('che');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('');
});
it('should not clear hint if input value is prefix of input', function() {
this.view.setInputValue('che');
this.view.setHint('cheese');
this.view.clearHintIfInvalid();
expect(this.view.getHint()).toBe('cheese');
});
});
describe('#hasOverflow', function() {
it('should return true if the input has overflow text', function() {
var longStr = new Array(1000).join('a');
this.view.setInputValue(longStr);
expect(this.view.hasOverflow()).toBe(true);
});
it('should return false if the input has no overflow text', function() {
var shortStr = 'aah';
this.view.setInputValue(shortStr);
expect(this.view.hasOverflow()).toBe(false);
});
});
describe('#isCursorAtEnd', function() {
it('should return true if the text cursor is at the end', function() {
this.view.setInputValue('boo');
setCursorPosition(this.$input, 3);
expect(this.view.isCursorAtEnd()).toBe(true);
});
it('should return false if the text cursor is not at the end', function() {
this.view.setInputValue('boo');
setCursorPosition(this.$input, 1);
expect(this.view.isCursorAtEnd()).toBe(false);
});
});
describe('#destroy', function() {
it('should remove event handlers', function() {
var $input, $hint;
$hint = this.view.$hint;
$input = this.view.$input;
spyOn($hint, 'off');
spyOn($input, 'off');
this.view.destroy();
expect($hint.off).toHaveBeenCalledWith('.tt');
expect($input.off).toHaveBeenCalledWith('.tt');
});
it('should set references to DOM elements to dummy element', function() {
var $hint, $input, $overflowHelper;
$hint = this.view.$hint;
$input = this.view.$input;
$overflowHelper = this.view.$overflowHelper;
this.view.destroy();
expect(this.view.$hint).not.toBe($hint);
expect(this.view.$input).not.toBe($input);
expect(this.view.$overflowHelper).not.toBe($overflowHelper);
});
});
// helper functions
// ----------------
function simulateInputEvent($node) {
var $e, type;
type = _.isMsie() ? 'keypress' : 'input';
$e = $.Event(type);
$node.trigger($e);
}
function simulateKeyEvent($node, type, key, withModifier) {
var $e;
$e = $.Event(type, {
keyCode: key,
altKey: !!withModifier,
ctrlKey: !!withModifier,
metaKey: !!withModifier,
shiftKey: !!withModifier
});
spyOn($e, 'preventDefault');
$node.trigger($e);
return $e;
}
function setCursorPosition($input, pos) {
var input = $input[0], range;
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(pos, pos);
}
else if (input.createTextRange) {
range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}
});

View File

@ -1,339 +0,0 @@
describe('Menu', function() {
var www = WWW();
beforeEach(function() {
var $fixture;
jasmine.Dataset.useMock();
setFixtures('<div id="menu-fixture"></div>');
$fixture = $('#jasmine-fixtures');
this.$node = $fixture.find('#menu-fixture');
this.$node.html(fixtures.html.dataset);
this.view = new Menu({ node: this.$node, datasets: [{}] }, www).bind();
this.dataset = this.view.datasets[0];
});
it('should throw an error if node is missing', function() {
expect(noNode).toThrow();
function noNode() { new Menu({ datasets: [{}] }, www); }
});
describe('when click event is triggered on a selectable', function() {
it('should trigger selectableClicked', function() {
var spy;
this.view.onSync('selectableClicked', spy = jasmine.createSpy());
this.$node.find(www.selectors.selectable).first().click();
expect(spy).toHaveBeenCalled();
});
});
describe('when rendered is triggered on a dataset', function() {
it('should add empty class to node if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.$node.removeClass(www.classes.empty);
this.dataset.trigger('rendered');
expect(this.$node).toHaveClass(www.classes.empty);
});
it('should remove empty class from node if not empty', function() {
this.dataset.isEmpty.andReturn(false);
this.$node.addClass(www.classes.empty);
this.dataset.trigger('rendered');
expect(this.$node).not.toHaveClass(www.classes.empty);
});
it('should trigger datasetRendered', function() {
var spy;
this.view.onSync('datasetRendered', spy = jasmine.createSpy());
this.dataset.trigger('rendered');
expect(spy).toHaveBeenCalled();
});
});
describe('when cleared is triggered on a dataset', function() {
it('should add empty class to node if empty', function() {
this.dataset.isEmpty.andReturn(true);
this.$node.removeClass(www.classes.empty);
this.dataset.trigger('cleared');
expect(this.$node).toHaveClass(www.classes.empty);
});
it('should remove empty class from node if not empty', function() {
this.dataset.isEmpty.andReturn(false);
this.$node.addClass(www.classes.empty);
this.dataset.trigger('cleared');
expect(this.$node).not.toHaveClass(www.classes.empty);
});
it('should trigger datasetCleared', function() {
var spy;
this.view.onSync('datasetCleared', spy = jasmine.createSpy());
this.dataset.trigger('cleared');
expect(spy).toHaveBeenCalled();
});
});
describe('when asyncRequested is triggered on a dataset', function() {
it('should propagate event', function() {
var spy = jasmine.createSpy();
this.dataset.onSync('asyncRequested', spy);
this.dataset.trigger('asyncRequested');
expect(spy).toHaveBeenCalled();
});
});
describe('when asyncCanceled is triggered on a dataset', function() {
it('should propagate event', function() {
var spy = jasmine.createSpy();
this.dataset.onSync('asyncCanceled', spy);
this.dataset.trigger('asyncCanceled');
expect(spy).toHaveBeenCalled();
});
});
describe('when asyncReceieved is triggered on a dataset', function() {
it('should propagate event', function() {
var spy = jasmine.createSpy();
this.dataset.onSync('asyncReceived', spy);
this.dataset.trigger('asyncReceived');
expect(spy).toHaveBeenCalled();
});
});
describe('#open', function() {
it('should set scroll top of node to 0', function() {
spyOn(this.view.$node, 'scrollTop');
this.view.open();
expect(this.view.$node.scrollTop).toHaveBeenCalledWith(0);
});
it('should add open class to node', function() {
this.$node.removeClass(www.classes.open);
this.view.open();
expect(this.$node).toHaveClass(www.classes.open);
});
});
describe('#close', function() {
it('should remove open class to node', function() {
this.$node.addClass(www.classes.open);
this.view.close();
expect(this.$node).not.toHaveClass(www.classes.open);
});
it('should remove cursor', function() {
var $selectable;
$selectable = this.view._getSelectables().first();
this.view.setCursor($selectable);
expect($selectable).toHaveClass(www.classes.cursor);
this.view.close();
expect($selectable).not.toHaveClass(www.classes.cursor);
});
});
describe('#setLanguageDirection', function() {
it('should update css for given language direction', function() {
this.view.setLanguageDirection('rtl');
expect(this.$node).toHaveAttr('dir', 'rtl');
this.view.setLanguageDirection('ltr');
expect(this.$node).toHaveAttr('dir', 'ltr');
});
});
describe('#selectableRelativeToCursor', function() {
it('should return selectable delta spots away from cursor', function() {
var $first, $second;
$first = this.view._getSelectables().eq(0);
$second = this.view._getSelectables().eq(1);
this.view.setCursor($first);
expect(this.view.selectableRelativeToCursor(+1)).toBe($second);
});
it('should support negative deltas', function() {
var $first, $second;
$first = this.view._getSelectables().eq(0);
$second = this.view._getSelectables().eq(1);
this.view.setCursor($second);
expect(this.view.selectableRelativeToCursor(-1)).toBe($first);
});
it('should wrap', function() {
var $expected, $actual;
$expected = this.view._getSelectables().eq(-1);
$actual = this.view.selectableRelativeToCursor(-1);
expect($actual).toBe($expected);
});
it('should return null if delta lands on input', function() {
var $first;
$first = this.view._getSelectables().eq(0);
this.view.setCursor($first);
expect(this.view.selectableRelativeToCursor(-1)).toBeNull();
});
});
describe('#setCursor', function() {
it('should remove cursor if null is passed in', function() {
var $selectable;
$selectable = this.view._getSelectables().eq(0);
this.view.setCursor($selectable);
expect(this.view.getActiveSelectable()).toBe($selectable);
this.view.setCursor(null);
expect(this.view.getActiveSelectable()).toBeNull();
});
it('should move cursor to passed in selectable', function() {
var $selectable;
$selectable = this.view._getSelectables().eq(0);
expect(this.view.getActiveSelectable()).toBeNull();
this.view.setCursor($selectable);
expect(this.view.getActiveSelectable()).toBe($selectable);
});
});
describe('#getSelectableData', function() {
it('should extract the data from the selectable element', function() {
var $selectable, datum;
$selectable = $('<div>').data({
'tt-selectable-display': 'one',
'tt-selectable-object': 'two'
});
data = this.view.getSelectableData($selectable);
expect(data).toEqual({ val: 'one', obj: 'two' });
});
it('should return null if no element is given', function() {
expect(this.view.getSelectableData($('notreal'))).toBeNull();
});
});
describe('#getActiveSelectable', function() {
it('should return the selectable the cursor is on', function() {
var $first;
$first = this.view._getSelectables().eq(0);
this.view.setCursor($first);
expect(this.view.getActiveSelectable()).toBe($first);
});
it('should return null if the cursor is off', function() {
expect(this.view.getActiveSelectable()).toBeNull();
});
});
describe('#getTopSelectable', function() {
it('should return the selectable at the top of the menu', function() {
var $first;
$first = this.view._getSelectables().eq(0);
expect(this.view.getTopSelectable()).toBe($first);
});
});
describe('#update', function() {
it('should invoke update on each dataset if valid update', function() {
this.view.update('fiz');
expect(this.dataset.update).toHaveBeenCalled();
});
it('should return true when valid update', function() {
expect(this.view.update('fiz')).toBe(true);
});
it('should return false when invalid update', function() {
this.view.update('fiz');
expect(this.view.update('fiz')).toBe(false);
});
});
describe('#empty', function() {
it('should set query to null', function() {
this.view.query = 'fiz';
this.view.empty();
expect(this.view.query).toBeNull();
});
it('should add empty class to node', function() {
this.$node.removeClass(www.classes.empty);
this.view.empty();
expect(this.$node).toHaveClass(www.classes.empty);
});
it('should invoke clear on each dataset', function() {
this.view.empty();
expect(this.dataset.clear).toHaveBeenCalled();
});
});
describe('#destroy', function() {
it('should remove event handlers', function() {
var $node = this.view.$node;
spyOn($node, 'off');
this.view.destroy();
expect($node.off).toHaveBeenCalledWith('.tt');
});
it('should destroy its datasets', function() {
this.view.destroy();
expect(this.dataset.destroy).toHaveBeenCalled();
});
it('should set node element to dummy element', function() {
var $node = this.view.$node;
this.view.destroy();
expect(this.view.$node).not.toBe($node);
});
});
});

View File

@ -1,205 +0,0 @@
describe('$plugin', function() {
beforeEach(function() {
var $fixture;
setFixtures('<input class="test-input" type="text" autocomplete="on">');
$fixture = $('#jasmine-fixtures');
this.$input = $fixture.find('.test-input');
this.$input.typeahead(null, {
displayKey: 'v',
source: function(q, sync) {
sync([{ v: '1' }, { v: '2' }, { v: '3' }]);
}
});
});
it('#enable should enable the typaahead', function() {
this.$input.typeahead('disable');
expect(this.$input.typeahead('isEnabled')).toBe(false);
this.$input.typeahead('enable');
expect(this.$input.typeahead('isEnabled')).toBe(true);
});
it('#disable should disable the typaahead', function() {
this.$input.typeahead('enable');
expect(this.$input.typeahead('isEnabled')).toBe(true);
this.$input.typeahead('disable');
expect(this.$input.typeahead('isEnabled')).toBe(false);
});
it('#activate should activate the typaahead', function() {
this.$input.typeahead('deactivate');
expect(this.$input.typeahead('isActive')).toBe(false);
this.$input.typeahead('activate');
expect(this.$input.typeahead('isActive')).toBe(true);
});
it('#activate should fail to activate the typaahead if disabled', function() {
this.$input.typeahead('deactivate');
expect(this.$input.typeahead('isActive')).toBe(false);
this.$input.typeahead('disable');
this.$input.typeahead('activate');
expect(this.$input.typeahead('isActive')).toBe(false);
});
it('#deactivate should deactivate the typaahead', function() {
this.$input.typeahead('activate');
expect(this.$input.typeahead('isActive')).toBe(true);
this.$input.typeahead('deactivate');
expect(this.$input.typeahead('isActive')).toBe(false);
});
it('#open should open the menu', function() {
this.$input.typeahead('close');
expect(this.$input.typeahead('isOpen')).toBe(false);
this.$input.typeahead('open');
expect(this.$input.typeahead('isOpen')).toBe(true);
});
it('#close should close the menu', function() {
this.$input.typeahead('open');
expect(this.$input.typeahead('isOpen')).toBe(true);
this.$input.typeahead('close');
expect(this.$input.typeahead('isOpen')).toBe(false);
});
it('#select should select selectable', function() {
var $el;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
$el = $('.tt-selectable').first();
expect(this.$input.typeahead('select', $el)).toBe(true);
expect(this.$input.typeahead('val')).toBe('1');
});
it('#select should return false if not valid selectable', function() {
var body;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
body = document.body;
expect(this.$input.typeahead('select', body)).toBe(false);
});
it('#autocomplete should autocomplete to selectable', function() {
var $el;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
$el = $('.tt-selectable').first();
expect(this.$input.typeahead('autocomplete', $el)).toBe(true);
expect(this.$input.typeahead('val')).toBe('1');
});
it('#autocomplete should return false if not valid selectable', function() {
var body;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
body = document.body;
expect(this.$input.typeahead('autocomplete', body)).toBe(false);
});
it('#moveCursor should move cursor', function() {
var $el;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
$el = $('.tt-selectable').first();
expect($el).not.toHaveClass('tt-cursor');
expect(this.$input.typeahead('moveCursor', 1)).toBe(true);
expect($el).toHaveClass('tt-cursor');
});
it('#select should return false if not valid selectable', function() {
var body;
// activate and set val to render some selectables
this.$input.typeahead('activate');
this.$input.typeahead('val', 'o');
body = document.body;
expect(this.$input.typeahead('select', body)).toBe(false);
});
it('#val() should typeahead value of element', function() {
var $els;
this.$input.typeahead('val', 'foo');
$els = this.$input.add('<div>');
expect($els.typeahead('val')).toBe('foo');
});
it('#val(q) should set query', function() {
this.$input.typeahead('val', 'foo');
expect(this.$input.typeahead('val')).toBe('foo');
});
it('#val(q) should coerce null and undefined to empty string', function() {
this.$input.typeahead('val', null);
expect(this.$input.typeahead('val')).toBe('');
this.$input.typeahead('val', undefined);
expect(this.$input.typeahead('val')).toBe('');
});
it('#destroy should revert modified attributes', function() {
expect(this.$input).toHaveAttr('autocomplete', 'off');
expect(this.$input).toHaveAttr('dir');
expect(this.$input).toHaveAttr('spellcheck');
expect(this.$input).toHaveAttr('style');
this.$input.typeahead('destroy');
expect(this.$input).toHaveAttr('autocomplete', 'on');
expect(this.$input).not.toHaveAttr('dir');
expect(this.$input).not.toHaveAttr('spellcheck');
expect(this.$input).not.toHaveAttr('style');
});
it('#destroy should remove data', function() {
expect(this.$input.data('tt-www')).toBeTruthy();
expect(this.$input.data('tt-attrs')).toBeTruthy();
expect(this.$input.data('tt-typeahead')).toBeTruthy();
this.$input.typeahead('destroy');
expect(this.$input.data('tt-www')).toBeFalsy();
expect(this.$input.data('tt-attrs')).toBeFalsy();
expect(this.$input.data('tt-typeahead')).toBeFalsy();
});
it('#destroy should remove add classes', function() {
expect(this.$input).toHaveClass('tt-input');
this.$input.typeahead('destroy');
expect(this.$input).not.toHaveClass('tt-input');
});
it('#destroy should revert DOM changes', function() {
expect($('.twitter-typeahead')).toExist();
this.$input.typeahead('destroy');
expect($('.twitter-typeahead')).not.toExist();
});
});

File diff suppressed because it is too large Load Diff