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:
parent
0e09e09f04
commit
9897b5fbd4
3
Makefile
3
Makefile
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../../lib/typeahead/dist/typeahead.bundle.min.js
|
File diff suppressed because one or more lines are too long
|
@ -1,16 +0,0 @@
|
|||
*.swp
|
||||
.DS_Store
|
||||
|
||||
.grunt
|
||||
_SpecRunner.html
|
||||
test/coverage
|
||||
|
||||
dist_temp
|
||||
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
bower_components
|
||||
|
||||
*.iml
|
||||
.idea
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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');
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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...
|
|
@ -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"
|
||||
}
|
|
@ -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** — check if the issue has already been
|
||||
reported.
|
||||
|
||||
2. **Check if the issue has been fixed** — try to reproduce it using the
|
||||
latest `master` or integration branch in the repository.
|
||||
|
||||
3. **Isolate the problem** — 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
|
|
@ -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
|
@ -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
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
```
|
|
@ -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 :)
|
|
@ -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'
|
||||
]
|
||||
});
|
||||
};
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
|
||||
})();
|
|
@ -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); });
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
})();
|
|
@ -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%';
|
|
@ -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() {}
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
})();
|
|
@ -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)); };
|
||||
}
|
||||
})();
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
})();
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
})();
|
|
@ -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+/); }
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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(); }
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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({});
|
||||
});
|
||||
});
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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); }
|
||||
});
|
||||
});
|
|
@ -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
|
|
@ -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));
|
||||
});
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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('')
|
||||
};
|
|
@ -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);
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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(); }
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
Loading…
Reference in New Issue