first commit
This commit is contained in:
No files matched your search
@@ -0,0 +1,16 @@
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
.grunt
|
||||
_SpecRunner.html
|
||||
test/coverage
|
||||
|
||||
dist_temp
|
||||
|
||||
node_modules
|
||||
npm-debug.log
|
||||
|
||||
bower_components
|
||||
|
||||
*.iml
|
||||
.idea
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"curly": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
"regexp": true,
|
||||
"trailing": true,
|
||||
|
||||
"boss": true,
|
||||
"eqnull": true,
|
||||
"expr": true,
|
||||
"validthis": true,
|
||||
|
||||
"browser": true,
|
||||
"jquery": true
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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:
|
||||
- 0.11.13
|
||||
before_script:
|
||||
- npm install -g [email protected]
|
||||
- npm install -g [email protected]
|
||||
- npm install -g [email protected]
|
||||
- bower install
|
||||
- grunt build
|
||||
script: test/ci
|
||||
addons:
|
||||
sauce_connect: true
|
||||
@@ -0,0 +1,249 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
For transparency and insight into our release cycle, releases will be numbered
|
||||
with the follow 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 http://semver.org/.
|
||||
|
||||
---
|
||||
|
||||
### 0.11.1 April 26, 2015
|
||||
|
||||
* Add prepare option to prefetch. [#1181]
|
||||
* Handle QuotaExceededError. [#1110]
|
||||
* Escape HTML entities from suggestion display value when rendering with default
|
||||
template. [#964]
|
||||
* List jquery as a dependency in package.json. [#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]
|
||||
|
||||
### 0.10.4 July 13, 2014
|
||||
|
||||
**Hotfix**
|
||||
|
||||
* Fix regression that breaks Bloodhound instances when more than 1 instance is
|
||||
relying on remote data. [#899]
|
||||
|
||||
### 0.10.3 July 10, 2014
|
||||
|
||||
**Bug fixes**
|
||||
|
||||
* `Bloodhound#clearPrefetchCache` now works with cache keys that contain regex
|
||||
characters. [#771]
|
||||
* Prevent outdated network requests from being sent. [#809]
|
||||
* Add support to object tokenizers for multiple property tokenization. [#811]
|
||||
* Fix broken `jQuery#typeahead('val')` method. [#815]
|
||||
* Remove `disabled` attribute from the hint input control. [#839]
|
||||
* Add `tt-highlight` class to highlighted text. [#833]
|
||||
* Handle non-string types that are passed to `jQuery#typeahead('val', val)`. [#881]
|
||||
|
||||
### 0.10.2 March 10, 2014
|
||||
|
||||
* Prevent flickering of dropdown menu when requesting remote suggestions. [#718]
|
||||
* Reduce hint flickering. [#754]
|
||||
* Added `Bloodhound#{clear, clearPrefetchCache, clearRemoteCache}` and made it
|
||||
possible to reinitialize Bloodhound instances. [#703]
|
||||
* Invoke `local` function during initialization. [#687]
|
||||
* In addition to HTML strings, templates can now return DOM nodes. [#742]
|
||||
* Prevent `jQuery#typeahead('val', val)` from opening dropdown menus of
|
||||
non-active typeaheads. [#646]
|
||||
* Fix bug in IE that resulted in dropdown menus with overflow being closed
|
||||
when clicking on the scrollbar. [#705]
|
||||
* Only show dropdown menu if `minLength` is satisfied. [#710]
|
||||
|
||||
### 0.10.1 February 9, 2014
|
||||
|
||||
**Hotfix**
|
||||
|
||||
* Fixed bug that prevented some ajax configs from being respected. [#630]
|
||||
* Event delegation on suggestion clicks is no longer broken. [#118]
|
||||
* Ensure dataset names are valid class name suffixes. [#610]
|
||||
* Added support for `displayKey` to be a function. [#633]
|
||||
* `jQuery#typeahead('val')` now mirrors `jQuery#val()`. [#659]
|
||||
* Datasets can now be passed to jQuery plugin as an array. [#664]
|
||||
* Added a `noConflict` method to the jQuery plugin. [#612]
|
||||
* Bloodhound's `local` property can now be a function. [#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].
|
||||
|
||||
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]
|
||||
* Fixed bug that led to the menu staying open when it should have been closed. [#260]
|
||||
* Private browsing in Safari no longer breaks prefetch. [#270]
|
||||
* Pressing tab while a suggestion is highlighted now results in a selection. [#266]
|
||||
* Dataset name is now passed as an argument for typeahead:selected event. [#207]
|
||||
|
||||
### 0.9.2 April 14, 2013
|
||||
|
||||
* Prefetch usage no longer breaks when cookies are disabled. [#190]
|
||||
* Precompiled templates are now wrapped in the appropriate DOM element. [#172]
|
||||
|
||||
### 0.9.1 April 1, 2013
|
||||
|
||||
* Multiple requests no longer get sent for a query when datasets share a remote source. [#152]
|
||||
* Datasets now support precompiled templates. [#137]
|
||||
* Cached remote suggestions now get rendered immediately. [#156]
|
||||
* Added typeahead:autocompleted event. [#132]
|
||||
* Added a plugin method for programmatically setting the query. Experimental. [#159]
|
||||
* Added minLength option for datasets. Experimental. [#131]
|
||||
* Prefetch objects now support thumbprint option. Experimental. [#157]
|
||||
|
||||
### 0.9.0 March 24, 2013
|
||||
|
||||
**Custom events, no more typeahead.css, and an improved API**
|
||||
|
||||
* Implemented the triggering of custom events. [#106]
|
||||
* Got rid of typeahead.css and now apply styling through JavaScript. [#15]
|
||||
* Made the API more flexible and addressed a handful of remote issues by rewriting the transport component. [#25]
|
||||
* Added support for dataset headers and footers. [#81]
|
||||
* No longer cache unnamed datasets. [#116]
|
||||
* Made the key name of the value property configurable. [#115]
|
||||
* Input values set before initialization of typeaheads are now respected. [#109]
|
||||
* Fixed an input value/hint casing bug. [#108]
|
||||
|
||||
### 0.8.2 March 04, 2013
|
||||
|
||||
* Fixed bug causing error to be thrown when initializing a typeahead on multiple elements. [#51]
|
||||
* Tokens with falsy values are now filtered out – was causing wonky behavior. [#75]
|
||||
* No longer making remote requests for blank queries. [#74]
|
||||
* Datums with regex characters in their value no longer cause errors. [#77]
|
||||
* Now compatible with the Closure Compiler. [#48]
|
||||
* Reference to jQuery is now obtained through window.jQuery, not window.$. [#47]
|
||||
* Added a plugin method for destroying typeaheads. Won't be documented until v0.9 and might change before then. [#59]
|
||||
|
||||
### 0.8.1 February 25, 2013
|
||||
|
||||
* Fixed bug preventing local and prefetch from being used together. [#39]
|
||||
* No longer prevent default browser behavior when up or down arrow is pressed with a modifier. [#6]
|
||||
* Hint is hidden when user entered query is wider than the input. [#26]
|
||||
* Data stored in localStorage now expires properly. [#34]
|
||||
* Normalized search tokens and fixed query tokenization. [#38]
|
||||
* Remote suggestions now are appended, not prepended to suggestions list. [#40]
|
||||
* Fixed some typos through the codebase. [#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...
|
||||
|
||||
[0.10.0 milestone]: https://github.com/twitter/typeahead.js/issues?milestone=8&page=1&state=closed
|
||||
|
||||
[#1181]: https://github.com/twitter/typeahead.js/pull/1181
|
||||
[#1143]: https://github.com/twitter/typeahead.js/pull/1143
|
||||
[#1110]: https://github.com/twitter/typeahead.js/pull/1110
|
||||
[#964]: https://github.com/twitter/typeahead.js/pull/964
|
||||
[#917]: https://github.com/twitter/typeahead.js/pull/917
|
||||
[#899]: https://github.com/twitter/typeahead.js/pull/899
|
||||
[#881]: https://github.com/twitter/typeahead.js/pull/881
|
||||
[#839]: https://github.com/twitter/typeahead.js/pull/839
|
||||
[#833]: https://github.com/twitter/typeahead.js/pull/833
|
||||
[#815]: https://github.com/twitter/typeahead.js/pull/815
|
||||
[#811]: https://github.com/twitter/typeahead.js/pull/811
|
||||
[#809]: https://github.com/twitter/typeahead.js/pull/809
|
||||
[#771]: https://github.com/twitter/typeahead.js/pull/771
|
||||
[#754]: https://github.com/twitter/typeahead.js/pull/754
|
||||
[#742]: https://github.com/twitter/typeahead.js/pull/742
|
||||
[#718]: https://github.com/twitter/typeahead.js/pull/718
|
||||
[#710]: https://github.com/twitter/typeahead.js/pull/710
|
||||
[#705]: https://github.com/twitter/typeahead.js/pull/705
|
||||
[#703]: https://github.com/twitter/typeahead.js/pull/703
|
||||
[#687]: https://github.com/twitter/typeahead.js/pull/687
|
||||
[#664]: https://github.com/twitter/typeahead.js/pull/664
|
||||
[#659]: https://github.com/twitter/typeahead.js/pull/659
|
||||
[#646]: https://github.com/twitter/typeahead.js/pull/646
|
||||
[#633]: https://github.com/twitter/typeahead.js/pull/633
|
||||
[#630]: https://github.com/twitter/typeahead.js/pull/630
|
||||
[#612]: https://github.com/twitter/typeahead.js/pull/612
|
||||
[#610]: https://github.com/twitter/typeahead.js/pull/610
|
||||
[#485]: https://github.com/twitter/typeahead.js/pull/485
|
||||
[#270]: https://github.com/twitter/typeahead.js/pull/270
|
||||
[#266]: https://github.com/twitter/typeahead.js/pull/266
|
||||
[#260]: https://github.com/twitter/typeahead.js/pull/260
|
||||
[#209]: https://github.com/twitter/typeahead.js/pull/209
|
||||
[#207]: https://github.com/twitter/typeahead.js/pull/207
|
||||
[#190]: https://github.com/twitter/typeahead.js/pull/190
|
||||
[#172]: https://github.com/twitter/typeahead.js/pull/172
|
||||
[#159]: https://github.com/twitter/typeahead.js/pull/159
|
||||
[#157]: https://github.com/twitter/typeahead.js/pull/157
|
||||
[#156]: https://github.com/twitter/typeahead.js/pull/156
|
||||
[#152]: https://github.com/twitter/typeahead.js/pull/152
|
||||
[#137]: https://github.com/twitter/typeahead.js/pull/137
|
||||
[#132]: https://github.com/twitter/typeahead.js/pull/132
|
||||
[#131]: https://github.com/twitter/typeahead.js/pull/131
|
||||
[#118]: https://github.com/twitter/typeahead.js/pull/118
|
||||
[#116]: https://github.com/twitter/typeahead.js/pull/116
|
||||
[#115]: https://github.com/twitter/typeahead.js/pull/115
|
||||
[#109]: https://github.com/twitter/typeahead.js/pull/109
|
||||
[#108]: https://github.com/twitter/typeahead.js/pull/108
|
||||
[#106]: https://github.com/twitter/typeahead.js/pull/106
|
||||
[#81]: https://github.com/twitter/typeahead.js/pull/81
|
||||
[#77]: https://github.com/twitter/typeahead.js/pull/77
|
||||
[#75]: https://github.com/twitter/typeahead.js/pull/75
|
||||
[#74]: https://github.com/twitter/typeahead.js/pull/74
|
||||
[#59]: https://github.com/twitter/typeahead.js/pull/59
|
||||
[#51]: https://github.com/twitter/typeahead.js/pull/51
|
||||
[#48]: https://github.com/twitter/typeahead.js/pull/48
|
||||
[#47]: https://github.com/twitter/typeahead.js/pull/47
|
||||
[#40]: https://github.com/twitter/typeahead.js/pull/40
|
||||
[#39]: https://github.com/twitter/typeahead.js/pull/39
|
||||
[#38]: https://github.com/twitter/typeahead.js/pull/38
|
||||
[#34]: https://github.com/twitter/typeahead.js/pull/34
|
||||
[#26]: https://github.com/twitter/typeahead.js/pull/26
|
||||
[#25]: https://github.com/twitter/typeahead.js/pull/25
|
||||
[#15]: https://github.com/twitter/typeahead.js/pull/15
|
||||
[#6]: https://github.com/twitter/typeahead.js/pull/6
|
||||
[#3]: https://github.com/twitter/typeahead.js/pull/3
|
||||
@@ -0,0 +1,120 @@
|
||||
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
|
||||
@@ -0,0 +1,330 @@
|
||||
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',
|
||||
amdModuleId: 'bloodhound',
|
||||
deps: {
|
||||
default: ['$'],
|
||||
amd: ['jquery'],
|
||||
cjs: ['jquery'],
|
||||
global: ['jQuery']
|
||||
}
|
||||
},
|
||||
typeahead: {
|
||||
src: '<%= tempDir %>/typeahead.jquery.js',
|
||||
amdModuleId: 'typeahead.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');
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
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.
|
||||
@@ -0,0 +1,188 @@
|
||||
[](http://travis-ci.org/twitter/typeahead.js)
|
||||
[](http://gruntjs.com/)
|
||||
|
||||
|
||||
[typeahead.js][gh-page]
|
||||
=======================
|
||||
|
||||
Inspired by [twitter.com]'s autocomplete search functionality, typeahead.js is
|
||||
a flexible JavaScript library that provides a strong foundation for building
|
||||
robust typeaheads.
|
||||
|
||||
The typeahead.js library consists of 2 components: the suggestion engine,
|
||||
[Bloodhound], and the UI view, [Typeahead].
|
||||
The suggestion engine is responsible for computing suggestions for a given
|
||||
query. The UI view is responsible for rendering suggestions and handling DOM
|
||||
interactions. Both components can be used separately, but when used together,
|
||||
they can provide a rich typeahead experience.
|
||||
|
||||
<!-- section links -->
|
||||
|
||||
[gh-page]: http://twitter.github.io/typeahead.js/
|
||||
[twitter.com]: https://twitter.com
|
||||
[Bloodhound]: https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md
|
||||
[Typeahead]: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
How you acquire typeahead.js is up to you.
|
||||
|
||||
Preferred method:
|
||||
* Install with [Bower]: `$ bower install typeahead.js`
|
||||
|
||||
Other methods:
|
||||
* [Download zipball of latest release][zipball].
|
||||
* Download the latest dist files individually:
|
||||
* *[bloodhound.js]* (standalone suggestion engine)
|
||||
* *[typeahead.jquery.js]* (standalone UI view)
|
||||
* *[typeahead.bundle.js]* (*bloodhound.js* + *typeahead.jquery.js*)
|
||||
* *[typeahead.bundle.min.js]*
|
||||
|
||||
**Note:** both *bloodhound.js* and *typeahead.jquery.js* have a dependency on
|
||||
[jQuery] 1.9+.
|
||||
|
||||
<!-- section links -->
|
||||
|
||||
[Bower]: http://bower.io/
|
||||
[zipball]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.js.zip
|
||||
[bloodhound.js]: http://twitter.github.com/typeahead.js/releases/latest/bloodhound.js
|
||||
[typeahead.jquery.js]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.jquery.js
|
||||
[typeahead.bundle.js]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.bundle.js
|
||||
[typeahead.bundle.min.js]: http://twitter.github.com/typeahead.js/releases/latest/typeahead.bundle.min.js
|
||||
[jQuery]: http://jquery.com/
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
* [Typeahead Docs]
|
||||
* [Bloodhound Docs]
|
||||
|
||||
[Typeahead Docs]: https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md
|
||||
[Bloodhound Docs]: https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
For some working examples of typeahead.js, visit the [examples page].
|
||||
|
||||
<!-- section links -->
|
||||
|
||||
[examples page]: http://twitter.github.io/typeahead.js/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].
|
||||
|
||||
For technical questions, you should post a question on [Stack Overflow] and tag
|
||||
it with [typeahead.js][so tag].
|
||||
|
||||
<!-- section links -->
|
||||
|
||||
[Stack Overflow]: http://stackoverflow.com/
|
||||
[@typeahead]: https://twitter.com/typeahead
|
||||
[so tag]: http://stackoverflow.com/questions/tagged/typeahead.js
|
||||
|
||||
Issues
|
||||
------
|
||||
|
||||
Discovered a bug? Please create an issue here on GitHub!
|
||||
|
||||
https://github.com/twitter/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 http://semver.org/.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
Tests are written using [Jasmine] and ran with [Karma]. To run
|
||||
the test suite with PhantomJS, run `$ npm test`.
|
||||
|
||||
<!-- section links -->
|
||||
|
||||
[Jasmine]: http://jasmine.github.io/
|
||||
[Karma]: http://karma-runner.github.io/
|
||||
|
||||
Developers
|
||||
----------
|
||||
|
||||
If you plan on contributing to typeahead.js, be sure to read the
|
||||
[contributing guidelines]. A good starting place for new contributors are issues
|
||||
labeled with [entry-level]. 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]
|
||||
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.
|
||||
|
||||
<!-- section links -->
|
||||
|
||||
[contributing guidelines]: https://github.com/twitter/typeahead.js/blob/master/CONTRIBUTING.md
|
||||
[entry-level]: https://github.com/twitter/typeahead.js/issues?&labels=entry-level&state=open
|
||||
[grunt-cli]: https://github.com/gruntjs/grunt-cli
|
||||
|
||||
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
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "typeahead.js",
|
||||
"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.5.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
+918
@@ -0,0 +1,918 @@
|
||||
/*!
|
||||
* 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("bloodhound", [ "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.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) {
|
||||
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 {
|
||||
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.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,
|
||||
sufficient: 5,
|
||||
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.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;
|
||||
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);
|
||||
});
|
||||
async && 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;
|
||||
});
|
||||
+7
File diff suppressed because one or more lines are too long
+2610
File diff suppressed because it is too large
Load diff
+2453
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
+96
@@ -0,0 +1,96 @@
|
||||
.typeahead,
|
||||
.tt-query,
|
||||
.tt-hint {
|
||||
width: 100%;
|
||||
height: calc(2.25rem + 2px);
|
||||
padding: .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border: 2px solid #ccc;
|
||||
-webkit-border-radius: 8px;
|
||||
-moz-border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.typeahead {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.typeahead:focus {
|
||||
border: 2px solid #0097cf;
|
||||
}
|
||||
|
||||
.tt-query {
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.tt-hint {
|
||||
color: #999
|
||||
}
|
||||
|
||||
.tt-menu {
|
||||
width: 100%;
|
||||
margin: 12px 0;
|
||||
padding: 8px 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
-webkit-border-radius: 8px;
|
||||
-moz-border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
.tt-suggestion {
|
||||
padding: 3px 20px;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.tt-suggestion:hover {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
background-color: #0097cf;
|
||||
}
|
||||
|
||||
.tt-suggestion.tt-cursor {
|
||||
color: #fff;
|
||||
background-color: #0097cf;
|
||||
|
||||
}
|
||||
|
||||
.tt-suggestion p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gist {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* example specific styles */
|
||||
/* ----------------------- */
|
||||
|
||||
#custom-templates .empty-message {
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#multiple-datasets .league-name {
|
||||
margin: 0 20px 5px 20px;
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
#scrollable-dropdown-menu .tt-menu {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#rtl-support .tt-menu {
|
||||
text-align: right;
|
||||
}
|
||||
+1538
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,277 @@
|
||||
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.get(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**.
|
||||
|
||||
* `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).
|
||||
|
||||
<!-- 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
|
||||
@@ -0,0 +1,288 @@
|
||||
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'
|
||||
}
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,234 @@
|
||||
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 :)
|
||||
@@ -0,0 +1,416 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Page not found</title>
|
||||
<style>
|
||||
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
|
||||
body {margin:0}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
nav,
|
||||
section,
|
||||
summary {display:block}
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {display:inline-block;vertical-align:baseline}
|
||||
audio:not([controls]) {display:none;height:0}
|
||||
[hidden],
|
||||
template {display:none}
|
||||
a {background:transparent}
|
||||
a:active,
|
||||
a:hover {outline:0}
|
||||
abbr[title] {border-bottom:1px dotted}
|
||||
b,
|
||||
strong {font-weight:bold}
|
||||
dfn {font-style:italic}
|
||||
h1 {font-size:2em;margin:0.67em 0}
|
||||
mark {background:#ff0;color:#000}
|
||||
small {font-size:80%}
|
||||
sub,
|
||||
sup {font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||
sup {top:-0.5em}
|
||||
sub {bottom:-0.25em}
|
||||
img {border:0}
|
||||
svg:not(:root) {overflow:hidden}
|
||||
figure {margin:1em 40px}
|
||||
hr {-moz-box-sizing:content-box;box-sizing:content-box;height:0}
|
||||
pre {overflow:auto}
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {font-family:monospace,monospace;font-size:1em}
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {color:inherit;font:inherit;margin:0}
|
||||
button {overflow:visible}
|
||||
button,
|
||||
select {text-transform:none}
|
||||
button,
|
||||
html input[type="button"],
|
||||
input[type="reset"],
|
||||
input[type="submit"] {-webkit-appearance:button;cursor:pointer}
|
||||
button[disabled],
|
||||
html input[disabled] {cursor:default}
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {border:0;padding:0}
|
||||
input {line-height:normal}
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {box-sizing:border-box;padding:0}
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {height:auto}
|
||||
input[type="search"] {-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}
|
||||
fieldset {border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}
|
||||
legend {border:0;padding:0}
|
||||
textarea {overflow:auto}
|
||||
optgroup {font-weight:bold}
|
||||
table {border-collapse:collapse;border-spacing:0;table-layout:auto;word-wrap:break-word;word-break:break-all}
|
||||
td,
|
||||
th {padding:0}
|
||||
*,
|
||||
*:before,
|
||||
*:after {-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}
|
||||
html {font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}
|
||||
body {font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:14px;line-height:1.42857143;color:#333;background-color:#f9f9f9}
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
textarea {font-family:inherit;font-size:inherit;line-height:inherit}
|
||||
button,
|
||||
input,
|
||||
select[multiple],
|
||||
textarea {background-image:none}
|
||||
a {color:#0181b9;text-decoration:none}
|
||||
a:hover,
|
||||
a:focus {color:#001721;text-decoration:underline}
|
||||
a:focus {outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}
|
||||
img {vertical-align:middle}
|
||||
.img-responsive {display:block;max-width:100%;height:auto}
|
||||
.img-rounded {-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}
|
||||
.img-circle {border-radius:50%}
|
||||
hr {margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}
|
||||
.sr-only {position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);border:0}
|
||||
@media print {* {text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important }a,a:visited {text-decoration:underline }a[href]:after {content:" (" attr(href) ")" }abbr[title]:after {content:" (" attr(title) ")" }a[href^="javascript:"]:after,a[href^="#"]:after {content:"" }pre,blockquote {border:1px solid #999;page-break-inside:avoid }thead {display:table-header-group }tr,img {page-break-inside:avoid }img {max-width:100% !important }p,h2,h3 {orphans:3;widows:3 }h2,h3 {page-break-after:avoid }select {background:#fff !important }.navbar {display:none }.table td,.table th {background-color:#fff !important }.btn >.caret,.dropup >.btn >.caret {border-top-color:#000 !important }.label {border:1px solid #000 }.table {border-collapse:collapse !important }.table-bordered th,.table-bordered td {border:1px solid #ddd !important }}
|
||||
.container {margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}
|
||||
@media (min-width:768px) {.container {width:750px }}
|
||||
@media (min-width:992px) {.container {width:970px }}
|
||||
@media (min-width:1200px) {.container {width:1170px }}
|
||||
.container-fluid {margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}
|
||||
.row {margin-left:-15px;margin-right:-15px}
|
||||
.row-flush {margin-left:0;margin-right:0}
|
||||
.row-flush [class*="col-"] {padding-left:0 !important;padding-right:0 !important}
|
||||
.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12 {position:relative;min-height:1px;padding-left:15px;padding-right:15px}
|
||||
.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12 {float:left}
|
||||
.col-xs-12 {width:100%}
|
||||
.col-xs-11 {width:91.66666667%}
|
||||
.col-xs-10 {width:83.33333333%}
|
||||
.col-xs-9 {width:75%}
|
||||
.col-xs-8 {width:66.66666667%}
|
||||
.col-xs-7 {width:58.33333333%}
|
||||
.col-xs-6 {width:50%}
|
||||
.col-xs-5 {width:41.66666667%}
|
||||
.col-xs-4 {width:33.33333333%}
|
||||
.col-xs-3 {width:25%}
|
||||
.col-xs-2 {width:16.66666667%}
|
||||
.col-xs-1 {width:8.33333333%}
|
||||
.col-xs-pull-12 {right:100%}
|
||||
.col-xs-pull-11 {right:91.66666667%}
|
||||
.col-xs-pull-10 {right:83.33333333%}
|
||||
.col-xs-pull-9 {right:75%}
|
||||
.col-xs-pull-8 {right:66.66666667%}
|
||||
.col-xs-pull-7 {right:58.33333333%}
|
||||
.col-xs-pull-6 {right:50%}
|
||||
.col-xs-pull-5 {right:41.66666667%}
|
||||
.col-xs-pull-4 {right:33.33333333%}
|
||||
.col-xs-pull-3 {right:25%}
|
||||
.col-xs-pull-2 {right:16.66666667%}
|
||||
.col-xs-pull-1 {right:8.33333333%}
|
||||
.col-xs-pull-0 {right:0%}
|
||||
.col-xs-push-12 {left:100%}
|
||||
.col-xs-push-11 {left:91.66666667%}
|
||||
.col-xs-push-10 {left:83.33333333%}
|
||||
.col-xs-push-9 {left:75%}
|
||||
.col-xs-push-8 {left:66.66666667%}
|
||||
.col-xs-push-7 {left:58.33333333%}
|
||||
.col-xs-push-6 {left:50%}
|
||||
.col-xs-push-5 {left:41.66666667%}
|
||||
.col-xs-push-4 {left:33.33333333%}
|
||||
.col-xs-push-3 {left:25%}
|
||||
.col-xs-push-2 {left:16.66666667%}
|
||||
.col-xs-push-1 {left:8.33333333%}
|
||||
.col-xs-push-0 {left:0%}
|
||||
.col-xs-offset-12 {margin-left:100%}
|
||||
.col-xs-offset-11 {margin-left:91.66666667%}
|
||||
.col-xs-offset-10 {margin-left:83.33333333%}
|
||||
.col-xs-offset-9 {margin-left:75%}
|
||||
.col-xs-offset-8 {margin-left:66.66666667%}
|
||||
.col-xs-offset-7 {margin-left:58.33333333%}
|
||||
.col-xs-offset-6 {margin-left:50%}
|
||||
.col-xs-offset-5 {margin-left:41.66666667%}
|
||||
.col-xs-offset-4 {margin-left:33.33333333%}
|
||||
.col-xs-offset-3 {margin-left:25%}
|
||||
.col-xs-offset-2 {margin-left:16.66666667%}
|
||||
.col-xs-offset-1 {margin-left:8.33333333%}
|
||||
.col-xs-offset-0 {margin-left:0%}
|
||||
@media (min-width:768px) {.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12 {float:left }.col-sm-12 {width:100% }.col-sm-11 {width:91.66666667% }.col-sm-10 {width:83.33333333% }.col-sm-9 {width:75% }.col-sm-8 {width:66.66666667% }.col-sm-7 {width:58.33333333% }.col-sm-6 {width:50% }.col-sm-5 {width:41.66666667% }.col-sm-4 {width:33.33333333% }.col-sm-3 {width:25% }.col-sm-2 {width:16.66666667% }.col-sm-1 {width:8.33333333% }.col-sm-pull-12 {right:100% }.col-sm-pull-11 {right:91.66666667% }.col-sm-pull-10 {right:83.33333333% }.col-sm-pull-9 {right:75% }.col-sm-pull-8 {right:66.66666667% }.col-sm-pull-7 {right:58.33333333% }.col-sm-pull-6 {right:50% }.col-sm-pull-5 {right:41.66666667% }.col-sm-pull-4 {right:33.33333333% }.col-sm-pull-3 {right:25% }.col-sm-pull-2 {right:16.66666667% }.col-sm-pull-1 {right:8.33333333% }.col-sm-pull-0 {right:0% }.col-sm-push-12 {left:100% }.col-sm-push-11 {left:91.66666667% }.col-sm-push-10 {left:83.33333333% }.col-sm-push-9 {left:75% }.col-sm-push-8 {left:66.66666667% }.col-sm-push-7 {left:58.33333333% }.col-sm-push-6 {left:50% }.col-sm-push-5 {left:41.66666667% }.col-sm-push-4 {left:33.33333333% }.col-sm-push-3 {left:25% }.col-sm-push-2 {left:16.66666667% }.col-sm-push-1 {left:8.33333333% }.col-sm-push-0 {left:0% }.col-sm-offset-12 {margin-left:100% }.col-sm-offset-11 {margin-left:91.66666667% }.col-sm-offset-10 {margin-left:83.33333333% }.col-sm-offset-9 {margin-left:75% }.col-sm-offset-8 {margin-left:66.66666667% }.col-sm-offset-7 {margin-left:58.33333333% }.col-sm-offset-6 {margin-left:50% }.col-sm-offset-5 {margin-left:41.66666667% }.col-sm-offset-4 {margin-left:33.33333333% }.col-sm-offset-3 {margin-left:25% }.col-sm-offset-2 {margin-left:16.66666667% }.col-sm-offset-1 {margin-left:8.33333333% }.col-sm-offset-0 {margin-left:0% }}
|
||||
@media (min-width:992px) {.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12 {float:left }.col-md-12 {width:100% }.col-md-11 {width:91.66666667% }.col-md-10 {width:83.33333333% }.col-md-9 {width:75% }.col-md-8 {width:66.66666667% }.col-md-7 {width:58.33333333% }.col-md-6 {width:50% }.col-md-5 {width:41.66666667% }.col-md-4 {width:33.33333333% }.col-md-3 {width:25% }.col-md-2 {width:16.66666667% }.col-md-1 {width:8.33333333% }.col-md-pull-12 {right:100% }.col-md-pull-11 {right:91.66666667% }.col-md-pull-10 {right:83.33333333% }.col-md-pull-9 {right:75% }.col-md-pull-8 {right:66.66666667% }.col-md-pull-7 {right:58.33333333% }.col-md-pull-6 {right:50% }.col-md-pull-5 {right:41.66666667% }.col-md-pull-4 {right:33.33333333% }.col-md-pull-3 {right:25% }.col-md-pull-2 {right:16.66666667% }.col-md-pull-1 {right:8.33333333% }.col-md-pull-0 {right:0% }.col-md-push-12 {left:100% }.col-md-push-11 {left:91.66666667% }.col-md-push-10 {left:83.33333333% }.col-md-push-9 {left:75% }.col-md-push-8 {left:66.66666667% }.col-md-push-7 {left:58.33333333% }.col-md-push-6 {left:50% }.col-md-push-5 {left:41.66666667% }.col-md-push-4 {left:33.33333333% }.col-md-push-3 {left:25% }.col-md-push-2 {left:16.66666667% }.col-md-push-1 {left:8.33333333% }.col-md-push-0 {left:0% }.col-md-offset-12 {margin-left:100% }.col-md-offset-11 {margin-left:91.66666667% }.col-md-offset-10 {margin-left:83.33333333% }.col-md-offset-9 {margin-left:75% }.col-md-offset-8 {margin-left:66.66666667% }.col-md-offset-7 {margin-left:58.33333333% }.col-md-offset-6 {margin-left:50% }.col-md-offset-5 {margin-left:41.66666667% }.col-md-offset-4 {margin-left:33.33333333% }.col-md-offset-3 {margin-left:25% }.col-md-offset-2 {margin-left:16.66666667% }.col-md-offset-1 {margin-left:8.33333333% }.col-md-offset-0 {margin-left:0% }}
|
||||
@media (min-width:1200px) {.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12 {float:left }.col-lg-12 {width:100% }.col-lg-11 {width:91.66666667% }.col-lg-10 {width:83.33333333% }.col-lg-9 {width:75% }.col-lg-8 {width:66.66666667% }.col-lg-7 {width:58.33333333% }.col-lg-6 {width:50% }.col-lg-5 {width:41.66666667% }.col-lg-4 {width:33.33333333% }.col-lg-3 {width:25% }.col-lg-2 {width:16.66666667% }.col-lg-1 {width:8.33333333% }.col-lg-pull-12 {right:100% }.col-lg-pull-11 {right:91.66666667% }.col-lg-pull-10 {right:83.33333333% }.col-lg-pull-9 {right:75% }.col-lg-pull-8 {right:66.66666667% }.col-lg-pull-7 {right:58.33333333% }.col-lg-pull-6 {right:50% }.col-lg-pull-5 {right:41.66666667% }.col-lg-pull-4 {right:33.33333333% }.col-lg-pull-3 {right:25% }.col-lg-pull-2 {right:16.66666667% }.col-lg-pull-1 {right:8.33333333% }.col-lg-pull-0 {right:0% }.col-lg-push-12 {left:100% }.col-lg-push-11 {left:91.66666667% }.col-lg-push-10 {left:83.33333333% }.col-lg-push-9 {left:75% }.col-lg-push-8 {left:66.66666667% }.col-lg-push-7 {left:58.33333333% }.col-lg-push-6 {left:50% }.col-lg-push-5 {left:41.66666667% }.col-lg-push-4 {left:33.33333333% }.col-lg-push-3 {left:25% }.col-lg-push-2 {left:16.66666667% }.col-lg-push-1 {left:8.33333333% }.col-lg-push-0 {left:0% }.col-lg-offset-12 {margin-left:100% }.col-lg-offset-11 {margin-left:91.66666667% }.col-lg-offset-10 {margin-left:83.33333333% }.col-lg-offset-9 {margin-left:75% }.col-lg-offset-8 {margin-left:66.66666667% }.col-lg-offset-7 {margin-left:58.33333333% }.col-lg-offset-6 {margin-left:50% }.col-lg-offset-5 {margin-left:41.66666667% }.col-lg-offset-4 {margin-left:33.33333333% }.col-lg-offset-3 {margin-left:25% }.col-lg-offset-2 {margin-left:16.66666667% }.col-lg-offset-1 {margin-left:8.33333333% }.col-lg-offset-0 {margin-left:0% }}
|
||||
.clearfix:before,
|
||||
.clearfix:after,
|
||||
.container:before,
|
||||
.container:after,
|
||||
.container-fluid:before,
|
||||
.container-fluid:after,
|
||||
.row:before,
|
||||
.row:after {content:" ";display:table}
|
||||
.clearfix:after,
|
||||
.container:after,
|
||||
.container-fluid:after,
|
||||
.row:after {clear:both}
|
||||
.center-block {display:block;margin-left:auto;margin-right:auto}
|
||||
.pull-right {float:right !important}
|
||||
.pull-left {float:left !important}
|
||||
.hide {display:none !important}
|
||||
.show {display:block !important}
|
||||
.invisible {visibility:hidden}
|
||||
.text-hide {font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}
|
||||
.hidden {display:none !important;visibility:hidden !important}
|
||||
.affix {position:fixed}
|
||||
@-ms-viewport {width:device-width}
|
||||
.visible-xs,
|
||||
.visible-sm,
|
||||
.visible-md,
|
||||
.visible-lg {display:none !important}
|
||||
@media (max-width:767px) {.visible-xs {display:block !important }table.visible-xs {display:table }tr.visible-xs {display:table-row !important }th.visible-xs,td.visible-xs {display:table-cell !important }}
|
||||
@media (min-width:768px) and (max-width:991px) {.visible-sm {display:block !important }table.visible-sm {display:table }tr.visible-sm {display:table-row !important }th.visible-sm,td.visible-sm {display:table-cell !important }}
|
||||
@media (min-width:992px) and (max-width:1199px) {.visible-md {display:block !important }table.visible-md {display:table }tr.visible-md {display:table-row !important }th.visible-md,td.visible-md {display:table-cell !important }}
|
||||
@media (min-width:1200px) {.visible-lg {display:block !important }table.visible-lg {display:table }tr.visible-lg {display:table-row !important }th.visible-lg,td.visible-lg {display:table-cell !important }}
|
||||
@media (max-width:767px) {.hidden-xs {display:none !important }}
|
||||
@media (min-width:768px) and (max-width:991px) {.hidden-sm {display:none !important }}
|
||||
@media (min-width:992px) and (max-width:1199px) {.hidden-md {display:none !important }}
|
||||
@media (min-width:1200px) {.hidden-lg {display:none !important }}
|
||||
.visible-print {display:none !important}
|
||||
@media print {.visible-print {display:block !important }table.visible-print {display:table }tr.visible-print {display:table-row !important }th.visible-print,td.visible-print {display:table-cell !important }}
|
||||
@media print {.hidden-print {display:none !important }}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.h1,
|
||||
.h2,
|
||||
.h3,
|
||||
.h4,
|
||||
.h5,
|
||||
.h6 {font-family:inherit;font-weight:400;line-height:1.1;color:inherit}
|
||||
h1 small,
|
||||
h2 small,
|
||||
h3 small,
|
||||
h4 small,
|
||||
h5 small,
|
||||
h6 small,
|
||||
.h1 small,
|
||||
.h2 small,
|
||||
.h3 small,
|
||||
.h4 small,
|
||||
.h5 small,
|
||||
.h6 small,
|
||||
h1 .small,
|
||||
h2 .small,
|
||||
h3 .small,
|
||||
h4 .small,
|
||||
h5 .small,
|
||||
h6 .small,
|
||||
.h1 .small,
|
||||
.h2 .small,
|
||||
.h3 .small,
|
||||
.h4 .small,
|
||||
.h5 .small,
|
||||
.h6 .small {font-weight:normal;line-height:1;color:#999}
|
||||
h1,
|
||||
.h1,
|
||||
h2,
|
||||
.h2,
|
||||
h3,
|
||||
.h3 {margin-top:20px;margin-bottom:10px}
|
||||
h1 small,
|
||||
.h1 small,
|
||||
h2 small,
|
||||
.h2 small,
|
||||
h3 small,
|
||||
.h3 small,
|
||||
h1 .small,
|
||||
.h1 .small,
|
||||
h2 .small,
|
||||
.h2 .small,
|
||||
h3 .small,
|
||||
.h3 .small {font-size:65%}
|
||||
h4,
|
||||
.h4,
|
||||
h5,
|
||||
.h5,
|
||||
h6,
|
||||
.h6 {margin-top:10px;margin-bottom:10px}
|
||||
h4 small,
|
||||
.h4 small,
|
||||
h5 small,
|
||||
.h5 small,
|
||||
h6 small,
|
||||
.h6 small,
|
||||
h4 .small,
|
||||
.h4 .small,
|
||||
h5 .small,
|
||||
.h5 .small,
|
||||
h6 .small,
|
||||
.h6 .small {font-size:75%}
|
||||
h1,
|
||||
.h1 {font-size:36px}
|
||||
h2,
|
||||
.h2 {font-size:30px}
|
||||
h3,
|
||||
.h3 {font-size:24px}
|
||||
h4,
|
||||
.h4 {font-size:18px}
|
||||
h5,
|
||||
.h5 {font-size:14px}
|
||||
h6,
|
||||
.h6 {font-size:12px}
|
||||
p {margin:0 0 10px}
|
||||
.lead {margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}
|
||||
@media (min-width:768px) {.lead {font-size:21px }}
|
||||
small,
|
||||
.small {font-size:85%}
|
||||
cite {font-style:normal}
|
||||
.text-left {text-align:left}
|
||||
.text-right {text-align:right}
|
||||
.text-center {text-align:center}
|
||||
.text-justify {text-align:justify}
|
||||
.text-muted {color:#999}
|
||||
.text-primary {color:#34495e}
|
||||
a.text-primary:hover {color:#222f3d}
|
||||
.text-success {color:#3c763d}
|
||||
a.text-success:hover {color:#2b542c}
|
||||
.text-info {color:#31708f}
|
||||
a.text-info:hover {color:#245269}
|
||||
.text-warning {color:#8a6d3b}
|
||||
a.text-warning:hover {color:#66512c}
|
||||
.text-danger {color:#a94442}
|
||||
a.text-danger:hover {color:#843534}
|
||||
.bg-primary {color:#fff;background-color:#34495e}
|
||||
a.bg-primary:hover {background-color:#222f3d}
|
||||
.bg-success {background-color:#dff0d8}
|
||||
a.bg-success:hover {background-color:#c1e2b3}
|
||||
.bg-info {background-color:#d9edf7}
|
||||
a.bg-info:hover {background-color:#afd9ee}
|
||||
.bg-warning {background-color:#fcf8e3}
|
||||
a.bg-warning:hover {background-color:#f7ecb5}
|
||||
.bg-danger {background-color:#f2dede}
|
||||
a.bg-danger:hover {background-color:#e4b9b9}
|
||||
.page-header {padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}
|
||||
ul,
|
||||
ol {margin-top:0;margin-bottom:10px}
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol,
|
||||
ol ol {margin-bottom:0}
|
||||
.list-unstyled {padding-left:0;list-style:none}
|
||||
.list-inline {padding-left:0;list-style:none;margin-left:-5px}
|
||||
.list-inline >li {display:inline-block;padding-left:5px;padding-right:5px}
|
||||
dl {margin-top:0;margin-bottom:20px}
|
||||
dt,
|
||||
dd {line-height:1.42857143}
|
||||
dt {font-weight:bold}
|
||||
dd {margin-left:0}
|
||||
@media (min-width:768px) {.dl-horizontal dt {float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap }.dl-horizontal dd {margin-left:180px }}
|
||||
abbr[title],
|
||||
abbr[data-original-title] {cursor:help;border-bottom:1px dotted #999}
|
||||
.initialism {font-size:90%;text-transform:uppercase}
|
||||
blockquote {padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}
|
||||
blockquote p:last-child,
|
||||
blockquote ul:last-child,
|
||||
blockquote ol:last-child {margin-bottom:0}
|
||||
blockquote footer,
|
||||
blockquote small,
|
||||
blockquote .small {display:block;font-size:80%;line-height:1.42857143;color:#999}
|
||||
blockquote footer:before,
|
||||
blockquote small:before,
|
||||
blockquote .small:before {content:'\2014 \00A0'}
|
||||
.blockquote-reverse,
|
||||
blockquote.pull-right {padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}
|
||||
.blockquote-reverse footer:before,
|
||||
blockquote.pull-right footer:before,
|
||||
.blockquote-reverse small:before,
|
||||
blockquote.pull-right small:before,
|
||||
.blockquote-reverse .small:before,
|
||||
blockquote.pull-right .small:before {content:''}
|
||||
.blockquote-reverse footer:after,
|
||||
blockquote.pull-right footer:after,
|
||||
.blockquote-reverse small:after,
|
||||
blockquote.pull-right small:after,
|
||||
.blockquote-reverse .small:after,
|
||||
blockquote.pull-right .small:after {content:'\00A0 \2014'}
|
||||
blockquote:before,
|
||||
blockquote:after {content:""}
|
||||
address {margin-bottom:20px;font-style:normal;line-height:1.42857143}
|
||||
|
||||
.oc-icon-chain:before,
|
||||
.icon-chain:before,
|
||||
|
||||
.oc-icon-chain-broken:before,
|
||||
.icon-chain-broken:before {content:"\f127"}
|
||||
|
||||
.close {float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;font-family:sans-serif;opacity:0.2;filter:alpha(opacity=20)}
|
||||
.close:hover,
|
||||
.close:focus {color:#000;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}
|
||||
button.close {padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}
|
||||
@font-face {font-family:'FontAwesome';src:url('../library/font-awesome-4.7.0/fonts/fontawesome-webfont.eot?v=1.0.1');src:url('../library/font-awesome-4.7.0/fonts/fontawesome-webfont.eot?#iefix&v=1.0.1') format('embedded-opentype'),url('../library/font-awesome-4.7.0/fonts/fontawesome-webfont.woff?v=1.0.1') format('woff'),url('../ui/font/fontawesome-webfont.ttf?v=1.0.1') format('truetype'),url('../library/font-awesome-4.7.0/fonts/fontawesome-webfont.svg#fontawesomeregular?v=1.0.1') format('svg');font-weight:normal;font-style:normal}
|
||||
[class^="icon-"],
|
||||
[class*=" icon-"] {font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em;display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0% 0%;background-repeat:repeat;margin-top:0}
|
||||
[class^="icon-"]:before,
|
||||
[class*=" icon-"]:before {text-decoration:inherit;display:inline-block;speak:none}
|
||||
[class^="icon-"].pull-left,
|
||||
[class*=" icon-"].pull-left {margin-right:.3em}
|
||||
[class^="icon-"].pull-right,
|
||||
[class*=" icon-"].pull-right {margin-left:.3em}
|
||||
[class^="oc-icon-"]:before,
|
||||
[class*=" oc-icon-"]:before {display:inline-block;margin-right:8px;font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em;vertical-align:baseline}
|
||||
[class^="oc-icon-"].empty:before,
|
||||
[class*=" oc-icon-"].empty:before {margin-right:0}
|
||||
.icon-lg {font-size:1.33333333em;line-height:0.75em;vertical-align:-15%}
|
||||
.icon-2x {font-size:2em}
|
||||
.icon-3x {font-size:3em}
|
||||
.icon-4x {font-size:4em}
|
||||
.icon-5x {font-size:5em}
|
||||
body {padding-top:20px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";background:#f3f3f3;color:#405261}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";text-transform:uppercase}
|
||||
h1 {font-weight:300;font-size:50px;margin-bottom:15px}
|
||||
h1 i[class^="icon-"]:before {font-size:46px}
|
||||
i[class^="icon-"].warning {color:#c84530}
|
||||
h3 {font-size:24px;font-weight:300}
|
||||
p.lead {font-size:16px;font-weight:300}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1><i class="icon-chain-broken warning"></i> Page not found</h1>
|
||||
<p class="lead">The requested page cannot be found.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,50 @@
|
||||
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'
|
||||
]
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "typeahead.js",
|
||||
"description": "fast and fully-featured autocomplete library",
|
||||
"keywords": [
|
||||
"typeahead",
|
||||
"autocomplete"
|
||||
],
|
||||
"homepage": "http://twitter.github.com/typeahead.js",
|
||||
"bugs": "https://github.com/twitter/typeahead.js/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twitter/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"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": ">=1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^1.9.1",
|
||||
"colors": "^0.6.2",
|
||||
"grunt": "~0.4",
|
||||
"grunt-concurrent": "^0.5.0",
|
||||
"grunt-contrib-clean": "~0.4.0",
|
||||
"grunt-contrib-concat": "~0.1",
|
||||
"grunt-contrib-connect": "~0.1",
|
||||
"grunt-contrib-jshint": "~0.8",
|
||||
"grunt-contrib-uglify": "~0.2.6",
|
||||
"grunt-contrib-watch": "~0.2",
|
||||
"grunt-exec": "~0.4.5",
|
||||
"grunt-sed": "~0.1",
|
||||
"grunt-step": "~0.2.0",
|
||||
"grunt-umd": "^2.3.3",
|
||||
"karma": "^0.12.22",
|
||||
"karma-chrome-launcher": "^0.1.4",
|
||||
"karma-coverage": "^0.2.6",
|
||||
"karma-firefox-launcher": "^0.1.3",
|
||||
"karma-jasmine": "^0.1.5",
|
||||
"karma-opera-launcher": "^0.1.0",
|
||||
"karma-phantomjs-launcher": "^0.1.4",
|
||||
"karma-safari-launcher": "^0.1.1",
|
||||
"mocha": "^1.20.1",
|
||||
"semver": "~1.1.3",
|
||||
"underscore": "^1.6.0",
|
||||
"yiewd": "^0.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/karma/bin/karma start --single-run --browsers PhantomJS"
|
||||
},
|
||||
"version": "0.11.1",
|
||||
"main": "dist/typeahead.bundle.js"
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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.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;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
async && 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;
|
||||
})();
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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,
|
||||
sufficient: 5,
|
||||
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); });
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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;
|
||||
})();
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.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;
|
||||
})();
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.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) {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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;
|
||||
})();
|
||||
@@ -0,0 +1,7 @@
|
||||
/*
|
||||
* typeahead.js
|
||||
* https://github.com/twitter/typeahead.js
|
||||
* Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
|
||||
*/
|
||||
|
||||
var VERSION = '%VERSION%';
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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() {}
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* 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;
|
||||
rendered += suggestions.length;
|
||||
that._append(query, suggestions.slice(0, that.limit - rendered));
|
||||
|
||||
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);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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;
|
||||
})();
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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;
|
||||
})();
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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)); };
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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);
|
||||
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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);
|
||||
|
||||
_.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.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;
|
||||
})();
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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 opearte 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(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 intialized 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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* 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.getTopSelectable());
|
||||
}
|
||||
},
|
||||
|
||||
_onRightKeyed: function onRightKeyed() {
|
||||
if (this.dir === 'ltr' && this.input.isCursorAtEnd()) {
|
||||
this.autocomplete(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);
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,299 @@
|
||||
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); }
|
||||
});
|
||||
});
|
||||
|
||||
// helper functions
|
||||
// ----------------
|
||||
|
||||
function datumTokenizer(d) { return $.trim(d.value).split(/\s+/); }
|
||||
function queryTokenizer(s) { return $.trim(s).split(/\s+/); }
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
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(); }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
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('#serach should handle multi-token queries', function() {
|
||||
this.index.add({ value: 'foo bar' });
|
||||
expect(this.index.search('foo b')).toEqual([{ value: 'foo bar' }]);
|
||||
});
|
||||
|
||||
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({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
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); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/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
|
||||
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
|
||||
@@ -0,0 +1,19 @@
|
||||
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));
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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('')
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
(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);
|
||||
@@ -0,0 +1,108 @@
|
||||
<!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>
|
||||
@@ -0,0 +1,395 @@
|
||||
/* 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,346 @@
|
||||
<!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: '[email protected]' }
|
||||
]
|
||||
});
|
||||
|
||||
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: '<h5>Header</h5>',
|
||||
footer: '<h5>Footer</h5>'
|
||||
}
|
||||
},
|
||||
{
|
||||
displayKey: 'val',
|
||||
source: abc,
|
||||
templates: {
|
||||
header: '<h5>start</h5>',
|
||||
footer: '<h5>end</h5>',
|
||||
empty: '<h5>empty</h5>'
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,469 @@
|
||||
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 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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
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(); }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,538 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
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('#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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,332 @@
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"licenses": [
|
||||
{
|
||||
"url": "https://github.com/twitter/typeahead.js/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": ">=1.7"
|
||||
},
|
||||
"docs": "https://github.com/twitter/typeahead.js",
|
||||
"demo": "http://twitter.github.com/typeahead.js/examples",
|
||||
"name": "typeahead.js",
|
||||
"title": "typeahead.js",
|
||||
"author": {
|
||||
"name": "Twitter, Inc.",
|
||||
"url": "https://twitter.com/twitteross"
|
||||
},
|
||||
"description": "fast and fully-featured autocomplete library",
|
||||
"keywords": [
|
||||
"typeahead",
|
||||
"autocomplete"
|
||||
],
|
||||
"homepage": "http://twitter.github.com/typeahead.js",
|
||||
"bugs": "https://github.com/twitter/typeahead.js/issues",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Jake Harding",
|
||||
"url": "https://twitter.com/JakeHarding"
|
||||
},
|
||||
{
|
||||
"name": "Tim Trueman",
|
||||
"url": "https://twitter.com/timtrueman"
|
||||
},
|
||||
{
|
||||
"name": "Veljko Skarich",
|
||||
"url": "https://twitter.com/vskarich"
|
||||
}
|
||||
],
|
||||
"version": "0.11.1"
|
||||
}
|
||||
Reference in New Issue
Block a user