├── .gitignore ├── .jscsrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── assets └── react-typeahead-animation.gif ├── bower.json ├── dist └── lib │ └── js │ └── react-typeahead.js ├── example.js ├── example ├── dist │ └── example.js ├── index.html └── typeahead-custom-template.css ├── lib ├── css │ └── typeahead.css └── js │ └── react-typeahead.js ├── package.json └── vendor ├── jquery └── jquery.js └── typeahead.js └── typeahead.bundle.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | bower_components 29 | 30 | dist/js/* -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowKeywords": [ 4 | "with" 5 | ], 6 | "disallowKeywordsOnNewLine": [ 7 | "else" 8 | ], 9 | "disallowMixedSpacesAndTabs": true, 10 | "disallowMultipleLineBreaks": true, 11 | "disallowNewlineBeforeBlockStatements": true, 12 | "disallowPaddingNewlinesInBlocks": true, 13 | "disallowQuotedKeysInObjects": true, 14 | "disallowSpaceAfterObjectKeys": true, 15 | "disallowSpaceAfterPrefixUnaryOperators": true, 16 | "disallowSpaceBeforeBinaryOperators": [ 17 | "," 18 | ], 19 | "disallowSpaceBeforePostfixUnaryOperators": true, 20 | "disallowSpacesInFunctionDeclaration": { 21 | "beforeOpeningRoundBrace": true 22 | }, 23 | "disallowSpacesInNamedFunctionExpression": { 24 | "beforeOpeningRoundBrace": true 25 | }, 26 | "disallowSpacesInsideArrayBrackets": true, 27 | "disallowSpacesInsideObjectBrackets": "all", 28 | "disallowSpacesInsideParentheses": true, 29 | "disallowTrailingComma": true, 30 | "disallowTrailingWhitespace": true, 31 | "disallowYodaConditions": true, 32 | "requireBlocksOnNewline": 1, 33 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 34 | "requireCapitalizedConstructors": true, 35 | "requireCommaBeforeLineBreak": true, 36 | "requireCurlyBraces": [ 37 | "if", 38 | "else", 39 | "for", 40 | "while", 41 | "do", 42 | "try", 43 | "catch" 44 | ], 45 | "requireDotNotation": true, 46 | "requireLineFeedAtFileEnd": true, 47 | "requireMultipleVarDecl": "onevar", 48 | "requireParenthesesAroundIIFE": true, 49 | "requireSpaceAfterBinaryOperators": true, 50 | "requireSpaceAfterKeywords": [ 51 | "if", 52 | "else", 53 | "for", 54 | "while", 55 | "do", 56 | "switch", 57 | "case", 58 | "return", 59 | "try", 60 | "catch", 61 | "function", 62 | "typeof" 63 | ], 64 | "requireSpaceAfterLineComment": true, 65 | "requireSpaceBeforeBinaryOperators": true, 66 | "requireSpaceBeforeBlockStatements": true, 67 | "requireSpacesInAnonymousFunctionExpression": { 68 | "beforeOpeningRoundBrace": true 69 | }, 70 | "requireSpacesInConditionalExpression": true, 71 | "safeContextKeyword": [ 72 | "self" 73 | ], 74 | "validateIndentation": 4, 75 | "validateJSDoc": { 76 | "checkParamNames": true, 77 | "checkRedundantParams": true, 78 | "requireParamTypes": true 79 | }, 80 | "validateQuoteMarks": "'" 81 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "iojs" 7 | 8 | notifications: 9 | email: 10 | recipients: 11 | - "erik.schlegel@gmail.com" 12 | on_success: never 13 | on_failure: always 14 | template: 15 | - "Build by %{author} has failed:" 16 | - "Git Info: %{branch} - %{commit}" 17 | - "Travis states: %{message}" 18 | - "Build Details: %{build_url}" 19 | - "Change View: %{compare_url}" 20 | skip_join: true 21 | 22 | branches: 23 | only: 24 | - master 25 | 26 | matrix: 27 | fast_finish: true -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var pkg = require('./package.json'); 2 | 3 | module.exports = function (grunt) { 4 | var exampleFile = grunt.option('src-file'); 5 | exampleFile = (!exampleFile)?'':exampleFile; 6 | console.log('ex:'+exampleFile); 7 | exampleFileDest = exampleFile.lastIndexOf('/')>-1?exampleFile.substring(exampleFile.lastIndexOf('/'), exampleFile.length):exampleFile; 8 | console.log('ex:'+exampleFileDest); 9 | 10 | grunt.initConfig({ 11 | watch: { 12 | browserify: { 13 | files: ['lib/**/*.js'], 14 | tasks: ['browserify:lib'] 15 | }, 16 | example: { 17 | files: [exampleFile], 18 | tasks: ['browserify:example'] 19 | }, 20 | options: { 21 | nospawn: true, 22 | livereload: true 23 | } 24 | }, 25 | 26 | bower: { 27 | install: { 28 | options: { 29 | targetDir: 'vendor', 30 | cleanTargetDir: true, 31 | cleanBowerDir: true 32 | } 33 | } 34 | }, 35 | 36 | browserify: { 37 | lib: { 38 | src: pkg.componentJSX, 39 | dest: './dist/'+pkg.componentJSX, 40 | options: { 41 | debug: true, 42 | extensions: ['.js'], 43 | transform: [ 44 | ['babelify', { 45 | loose: 'all' 46 | }] 47 | ] 48 | } 49 | }, 50 | example: { 51 | src: './example.js', 52 | dest: './example/dist/example.js', 53 | cwd: __dirname, 54 | options: { 55 | debug: true, 56 | cwd: __dirname, 57 | extensions: ['.js'], 58 | transform: [ 59 | ['babelify', { 60 | loose: 'all' 61 | }] 62 | ] 63 | } 64 | } 65 | }, 66 | 67 | jscs: { 68 | files: { 69 | src: ['src/**/*.js'] 70 | }, 71 | options: { 72 | config: '.jscsrc', 73 | esprima: 'esprima-fb', 74 | esnext: true 75 | } 76 | }, 77 | 78 | test: { 79 | options: { 80 | reporter: 'spec', 81 | captureFile: pkg.testResultMain, // Optionally capture the reporter output to a file 82 | quiet: false, // Optionally suppress output to standard out (defaults to false) 83 | clearRequireCache: false // Optionally clear the require cache before running tests (defaults to false) 84 | }, 85 | src: ['test/**/*.js'] 86 | } 87 | }); 88 | 89 | grunt.loadNpmTasks('grunt-browserify'); 90 | grunt.loadNpmTasks('grunt-contrib-watch'); 91 | grunt.loadNpmTasks("grunt-jscs"); 92 | grunt.loadNpmTasks('grunt-mocha-test'); 93 | grunt.loadNpmTasks('grunt-bower-task'); 94 | 95 | grunt.registerTask('default', ['bower', 'test', 'browserify:lib', 'watch']); 96 | grunt.registerTask('build', ['bower', 'browserify:lib']); 97 | grunt.registerTask('test', ['jscs']); 98 | }; 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Schlegel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/erikschlegel/React-Twitter-Typeahead?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/erikschlegel/React-Twitter-Typeahead.svg)](https://travis-ci.org/erikschlegel/React-Twitter-Typeahead) 2 | 3 | ## React-Twitter-Typeahead 4 | A stylish and flexible reactJS autosuggest component that integrates Twitter's typeahead.js with ReactJS. Typeahead.js was built by Twitter and is one of the most frequently used and trusted solutions for a battle-tested autosuggest control. 5 | 6 | The preview below showcases configuring this component for searching against google books using a custom template. 7 | 8 | ![](https://raw.githubusercontent.com/erikschlegel/React-Twitter-Typeahead/master/assets/react-typeahead-animation.gif) 9 | 10 | [See some examples on our Azure site](http://reactypeahead.azurewebsites.net/example/) 11 | 12 | ## Installation 13 | ```js 14 | git clone https://github.com/erikschlegel/React-Twitter-Typeahead.git 15 | cd React-Twitter-Typeahead 16 | npm install 17 | npm run build 18 | ``` 19 | ## Usage 20 | Let's start off creating a basic typeahead by customizing the bloodhound config object. Bloodhound is typeahead.js's powerful suggestion engine. The API docs that explain the available options in the bloodhound config object are [here](https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md#options). 21 | ```js 22 | var React = require('react'); 23 | var ReactTypeahead = require('./lib/js/react-typeahead'); 24 | var states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California';//.... 25 | 26 | var bloodhoundConfig = { 27 | local: states 28 | }; 29 | 30 | React.render( 31 | , 33 | document.getElementById('#typeaheadDiv') 34 | ); 35 | ``` 36 | 37 | You can also configure the component to make a JSONP remote call and dress up the results by using a handlebar custom template. 38 | 39 | **Configuring the remote call** 40 | 41 | Bloodhound allows you to transform the returned response prior to typeahead.js processing(var responseTransformation). In the example below we're extracting the data points from the response that are relevant for rendering. The URL call can be configured in the 'remote' object of the bloodhound config. All other available options are listed in Twitter's API [docs](https://github.com/twitter/typeahead.js/blob/master/doc/bloodhound.md#remote). 42 | ```js 43 | var responseTransformation = function(rsp){ 44 | var initRsp = rsp.items, maxCharacterTitleLgth = 29, maxDescLength = 80; 45 | var finalResult = []; 46 | 47 | initRsp.map(function(item){ 48 | var title = item.volumeInfo.title; 49 | finalResult.push({value: title.length>maxCharacterTitleLgth?title.substring(0, maxCharacterTitleLgth):title, 50 | thumbnail: item.volumeInfo.imageLinks.thumbnail, 51 | id: item.id, 52 | description:(item.volumeInfo.description)?item.volumeInfo.description.substring(0, maxDescLength):''}); 53 | }); 54 | 55 | return finalResult; 56 | }; 57 | 58 | var bloodhoundRemoteConfig = { 59 | remote: { 60 | url: 'https://www.googleapis.com/books/v1/volumes?q=%QUERY', 61 | wildcard: '%QUERY',/*typeahead.js will replace the specified wildcard with the inputted value in the GET call*/ 62 | transform: responseTransformation 63 | } 64 | }; 65 | ``` 66 | **Adding some style** 67 | 68 | You can customize the presentation of the remote dataset by overriding the dataset config. All available options are listed [here](https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#datasets). This project comes packaged with handlebars, but you're free to use your template library of choice. 69 | ```js 70 | var Handlebars = require('handlebars'); 71 | 72 | var datasetConfig = { 73 | name: 'books-to-buy', 74 | display: 'value', 75 | limit: 8, 76 | templates: { 77 | header: header, 78 | pending: '
Processing...
', 79 | empty: '
unable to find any books that matched your query
', 80 | suggestion: Handlebars.compile(handlerbarTemplate) 81 | } 82 | }; 83 | ``` 84 | **Binding Custom Events** 85 | 86 | Custom callbacks can be provided in the customEvents config. This sample callback is invoked when you select an option in the dropdowan. 'id' is a property on the returning dataset. All other optional callback functions can be found in the [docs](https://github.com/twitter/typeahead.js/blob/master/doc/jquery_typeahead.md#custom-events). 87 | ```js 88 | var selectedFunc = function(e, datum){alert('Selected book: ' + datum['id']);}; 89 | var customEvents = { 90 | 'typeahead:selected typeahead:autocompleted': selectedFunc 91 | }; 92 | ``` 93 | 94 | **Brining it all together with some additional typeahead configuring** 95 | 96 | ```js 97 | var typeaheadConfig = {highlight:false}; 98 | 99 | React.render( 100 | , 105 | document.getElementById('#typeaheadDivRpc') 106 | ); 107 | ``` 108 | 109 | ## Dependencies 110 | This requires NPM. Also, the underlying typeahead.js library uses jquery to hook some initial events to the control, so you'll need to include the following scripts towards the end of your html page. 111 | ```html 112 | 113 | 114 | ``` 115 | 116 | ## License 117 | MIT Licensed -------------------------------------------------------------------------------- /assets/react-typeahead-animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erikschlegel/React-Twitter-Typeahead/d0fe9fb751ae113b44a3a8dcd5d29efbf2476713/assets/react-typeahead-animation.gif -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React-Twitter-Typeahead", 3 | "version": "0.0.0", 4 | "maintainers": [ 5 | { 6 | "name": "Erik Schlegel", 7 | "email": "erik.schlegel@gmail.com", 8 | "web": "erikschlegel.com" 9 | } 10 | ], 11 | "moduleType": [ 12 | "globals" 13 | ], 14 | "license": "MIT", 15 | "dependencies": { 16 | "typeahead.js": "~0.11.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var ReactTypeahead = require('./lib/js/react-typeahead'); 5 | var Handlebars = require('handlebars'); 6 | 7 | var states = ['Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 8 | 'Colorado', 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 9 | 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 10 | 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 11 | 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 12 | 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', 13 | 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania', 'Rhode Island', 14 | 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 15 | 'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming' 16 | ]; 17 | 18 | var handlerbarTemplate = '
' + 19 | ' ' + 20 | ' ' + 21 | ' ' + 22 | ' ' + 23 | '

{{value}}

' + 24 | '

{{description}}

' + 25 | '
' + 26 | '
'; 27 | 28 | var header = '
Book CoverBook Title/Description' 29 | 30 | var bloodhoundConfig = { 31 | local: states 32 | }; 33 | 34 | React.render( 35 | , 37 | document.getElementById('#typeaheadDiv') 38 | ); 39 | 40 | var responseTransformation = function(rsp){ 41 | var initRsp = rsp.items, maxCharacterTitleLgth = 29, maxDescLength = 80; 42 | var finalResult = []; 43 | 44 | initRsp.map(function(item){ 45 | var title = item.volumeInfo.title; 46 | finalResult.push({value: title.length>maxCharacterTitleLgth?title.substring(0, maxCharacterTitleLgth):title, 47 | thumbnail: item.volumeInfo.imageLinks.thumbnail, 48 | id: item.id, 49 | description:(item.volumeInfo.description)?item.volumeInfo.description.substring(0, maxDescLength):''}); 50 | }); 51 | 52 | return finalResult; 53 | }; 54 | 55 | var bloodhoundRemoteConfig = { 56 | prefetch: 'https://www.googleapis.com/books/v1/volumes?q=reactjs', 57 | remote: { 58 | url: 'https://www.googleapis.com/books/v1/volumes?q=%QUERY', 59 | wildcard: '%QUERY', 60 | transform: responseTransformation 61 | } 62 | }; 63 | 64 | var dsRemote = { 65 | name: 'books-to-buy', 66 | display: 'value', 67 | limit: 8, 68 | templates: { 69 | header: header, 70 | pending: '
Processing...
', 71 | empty: '
unable to find any books that matched your query
', 72 | suggestion: Handlebars.compile(handlerbarTemplate) 73 | } 74 | }; 75 | 76 | var selectedFunc = function(e, datum){alert('Selected book: ' + datum['id']);}; 77 | 78 | var customEvents = { 79 | 'typeahead:selected typeahead:autocompleted': selectedFunc 80 | }; 81 | 82 | var typeaheadConfig = {highlight:false}; 83 | 84 | React.render( 85 | , 90 | document.getElementById('#typeaheadDivRpc') 91 | ); 92 | 93 | var remoteTransformation = function(rsp){ 94 | var initRsp = rsp.items, maxCharacterLgth = 100; 95 | var finalResult = []; 96 | 97 | initRsp.map(function(item){ 98 | finalResult.push({value: item.volumeInfo.title}); 99 | }); 100 | 101 | return finalResult; 102 | }; 103 | 104 | var bloodhoundRPCConfig = { 105 | prefetch: 'https://www.googleapis.com/books/v1/volumes?q=reactjs', 106 | remote: { 107 | url: 'https://www.googleapis.com/books/v1/volumes?q=%QUERY', 108 | wildcard: '%QUERY', 109 | transform: remoteTransformation 110 | } 111 | }; 112 | 113 | var remoteDS = { 114 | name: 'best-books', 115 | display: 'value' 116 | }; 117 | 118 | React.render( 119 | , 122 | document.getElementById('#typeaheadDivRemote') 123 | ); 124 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Typeahead Example 6 | 7 | 8 | 9 | 10 |
11 |

React-Twitter Typeahead

12 |

These are some lightweight examples to demonstrate a reactified version of Twitter's typeahead control.

13 |
14 |

The basics(suggestion engine)

15 |

This is a vanilla example of pre-defined dropdown options using the default features of the react component

16 |
17 |
18 | 19 |

Remote

20 |

Using a JSONP remote service as the data source for the react typeahead control

21 |
22 |
23 | 24 |

Remote Data Call with a Custom Display Template

25 |

This is a vanilla example of pre-defined dropdown options using the default features of the react component

26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/typeahead-custom-template.css: -------------------------------------------------------------------------------- 1 | .tt-custom-row{ 2 | font-family: Arial, Helvetica, sans-serif; 3 | font-stretch: normal; 4 | font-style: normal; 5 | font-weight: 500; 6 | font-size: 14px; 7 | max-height: 70px!important; 8 | min-height: 2.143em; 9 | line-height: 2.143em; 10 | border-width: 1px; 11 | border-style: solid; 12 | color: #444; 13 | background-color: #ebebeb; 14 | border-color: #e5e5e5; 15 | } 16 | 17 | .tt-custom-thumbnail{ 18 | width: 77px; 19 | border-left-width: 0; 20 | display: inline!important; 21 | padding: .6em 0 0 0; 22 | } 23 | 24 | .tt-custom-thumbnail img{ 25 | width: 60px; 26 | height: 60px; 27 | border-radius: 50%; 28 | } 29 | 30 | .tt-custom-cell h3{ 31 | min-height: 80px; 32 | border-width: 0 0 1px 0; 33 | font-size: 1.2em; 34 | display:inline; 35 | vertical-align: top; 36 | } 37 | 38 | .tt-custom-cell p{ 39 | margin: 0; 40 | padding: 0; 41 | display: block; 42 | line-height: 1.8em; 43 | font-size: 12px; 44 | } 45 | 46 | .tt-custom-cell{ 47 | min-height: 80px; 48 | border-width: 0 0 1px 0; 49 | -webkit-box-sizing: border-box; 50 | -moz-box-sizing: border-box; 51 | box-sizing: border-box; 52 | display: inline-block; 53 | vertical-align: top; 54 | min-height: 95px; 55 | width: 79%; 56 | padding: .6em 0 0 .6em; 57 | } 58 | 59 | .tt-custom-header{ 60 | background-image: none; 61 | width: 50%; 62 | background-position: 50% 50%; 63 | background-color: #343C37; 64 | color: #fff; 65 | font-size: 0.9em; 66 | font-family: Arial, Helvetica, sans-serif; 67 | font-weight: 500; 68 | box-sizing: border-box; 69 | text-align: left; 70 | display: inline-block; 71 | padding: .3em .6em; 72 | } -------------------------------------------------------------------------------- /lib/css/typeahead.css: -------------------------------------------------------------------------------- 1 | .typeahead, 2 | .tt-query, 3 | .tt-hint { 4 | width: 396px; 5 | height: 30px; 6 | padding: 8px 12px; 7 | font-size: 24px; 8 | line-height: 30px; 9 | border: 2px solid #ccc; 10 | -webkit-border-radius: 8px; 11 | -moz-border-radius: 8px; 12 | border-radius: 8px; 13 | outline: none; 14 | } 15 | 16 | .typeahead { 17 | background-color: #fff; 18 | } 19 | 20 | .typeahead:focus { 21 | border: 2px solid #0097cf; 22 | } 23 | 24 | .tt-query { 25 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 26 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 27 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 28 | } 29 | 30 | .tt-hint { 31 | color: #999 32 | } 33 | 34 | .tt-menu { 35 | width: 422px; 36 | margin: 12px 0; 37 | padding: 8px 0; 38 | background-color: #fff; 39 | border: 1px solid #ccc; 40 | border: 1px solid rgba(0, 0, 0, 0.2); 41 | -webkit-border-radius: 8px; 42 | -moz-border-radius: 8px; 43 | border-radius: 8px; 44 | -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); 45 | -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); 46 | box-shadow: 0 5px 10px rgba(0,0,0,.2); 47 | } 48 | 49 | .tt-suggestion { 50 | padding: 3px 20px; 51 | font-size: 18px; 52 | line-height: 24px; 53 | } 54 | 55 | .tt-suggestion:hover { 56 | cursor: pointer; 57 | color: #fff; 58 | background-color: #0097cf; 59 | } 60 | 61 | .tt-suggestion.tt-cursor { 62 | color: #fff; 63 | background-color: #0097cf; 64 | 65 | } 66 | 67 | .tt-suggestion p { 68 | margin: 0; 69 | } 70 | -------------------------------------------------------------------------------- /lib/js/react-typeahead.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | extend = require("extend"), 3 | Bloodhound = require('typeahead.js/dist/bloodhound'), 4 | Typeahead = require('typeahead.js/dist/typeahead.jquery'), 5 | ReactTypeahead; 6 | 7 | ReactTypeahead = React.createClass({displayName: "ReactTypeahead", 8 | /** 9 | * 'initOptions' method 10 | * This method sets up the typeahead with initial config parameters. The first set is default 11 | * and the other set is defined by the 12 | */ 13 | initOptions: function () { 14 | var defaultMinLength = 2, config = {}; 15 | 16 | if(!this.props.bloodhound) 17 | this.props.bloodhound = {}; 18 | if(!this.props.typeahead) 19 | this.props.typeahead = {}; 20 | if(!this.props.datasource) 21 | this.props.datasource = {}; 22 | 23 | var defaults = { 24 | bloodhound: { 25 | datumTokenizer: Bloodhound.tokenizers.whitespace, 26 | queryTokenizer: Bloodhound.tokenizers.whitespace 27 | }, 28 | typeahead: { 29 | minLength: defaultMinLength, 30 | hint: true, 31 | highlight: true 32 | }, 33 | datasource: { 34 | displayProperty: 'value', 35 | queryStr: '%QUERY' 36 | } 37 | }; 38 | 39 | config.bloodhound = extend(true, {}, defaults.bloodhound, this.props.bloodhound); 40 | config.typeahead = extend(true, {}, defaults.typeahead, this.props.typeahead); 41 | config.datasource = extend(true, {}, defaults.datasource, this.props.datasource); 42 | 43 | return config; 44 | }, 45 | 46 | loadScript: function(scriptURL){ 47 | script = document.createElement('script'); 48 | script.src = scriptURL; 49 | script.type = 'text/javascript'; 50 | script.async = true; 51 | document.body.appendChild(script); 52 | }, 53 | 54 | /** 55 | * 'getInitialState' method 56 | * We want to make sure that the jquery and typeahead libraries are loaded into the DOM 57 | */ 58 | getInitialState: function(){ 59 | return {data: []}; 60 | }, 61 | /** 62 | * 'componentDidMount' method 63 | * Initializes react with the typeahead component. 64 | */ 65 | componentDidMount: function () { 66 | var self = this, 67 | options = this.initOptions(); 68 | 69 | var remoteCall = new Bloodhound(options.bloodhound); 70 | options.datasource.source = remoteCall; 71 | var typeaheadInput = React.findDOMNode(self); 72 | if(typeaheadInput) 73 | this.typeahead = $(typeaheadInput).typeahead(options.typeahead, options.datasource); 74 | 75 | this.bindCustomEvents(); 76 | }, 77 | 78 | render: function () { 79 | let className = "typeahead"; 80 | 81 | if(this.props.className) 82 | className += ' ' + this.props.className; 83 | 84 | return ( 85 | 86 | ); 87 | }, 88 | 89 | bindCustomEvents: function(){ 90 | var customEvents = this.props.customEvents; 91 | 92 | if (!customEvents) 93 | return; 94 | 95 | for (var event in customEvents) 96 | this.typeahead.on(event, customEvents[event]); 97 | } 98 | }); 99 | 100 | module.exports = ReactTypeahead; 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-twitter-typeahead", 3 | "description": "A reactJS component that integrates Twitter's typeahead autosuggest control", 4 | "version": "1.1.10", 5 | "license": "MIT", 6 | "componentJSX": "lib/js/react-typeahead.js", 7 | "main": "dist/lib/js/react-typeahead.js", 8 | "repository": "erikschlegel/React-Twitter-Typeahead", 9 | "maintainers": [ 10 | { 11 | "name": "Erik Schlegel", 12 | "email": "erik.schlegel@gmail.com", 13 | "web": "erikschlegel.com" 14 | } 15 | ], 16 | "devDependencies": { 17 | "browserify": "^11.0.1", 18 | "babel": "^5.8.21", 19 | "babelify": "^6.1.3", 20 | "browserify-shim": "^3.8.3", 21 | "envify": "^3.2.0", 22 | "grunt": "^0.4.5", 23 | "grunt-bower-task": "^0.4.0", 24 | "esprima-fb": "^15001.1001.0-dev-harmony-fb", 25 | "grunt-browserify": "^4.0.0", 26 | "grunt-cli": "^0.1.13", 27 | "grunt-contrib-watch": "^0.6.1", 28 | "grunt-jscs": "^2.1.0", 29 | "grunt-mocha-test": "*", 30 | "grunt-react": "^0.12.2", 31 | "grunt-watchify": "^0.1.0", 32 | "jscs": "^2.1.0", 33 | "react": ">=0.13.0" 34 | }, 35 | "dependencies": { 36 | "extend": "^3.0.0", 37 | "handlebars": "^3.0.3", 38 | "jquery": "^2.1", 39 | "typeahead.js": "^0.11.1" 40 | }, 41 | "browserify": { 42 | "transform": [ 43 | "babelify" 44 | ] 45 | }, 46 | "scripts": { 47 | "test": "grunt build", 48 | "build": "grunt build", 49 | "prepublish": "grunt build" 50 | }, 51 | "testResultMain": "testResults.txt" 52 | } 53 | -------------------------------------------------------------------------------- /vendor/typeahead.js/typeahead.bundle.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * typeahead.js 0.11.1 3 | * https://github.com/twitter/typeahead.js 4 | * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT 5 | */ 6 | 7 | (function(root, factory) { 8 | if (typeof define === "function" && define.amd) { 9 | define("bloodhound", [ "jquery" ], function(a0) { 10 | return root["Bloodhound"] = factory(a0); 11 | }); 12 | } else if (typeof exports === "object") { 13 | module.exports = factory(require("jquery")); 14 | } else { 15 | root["Bloodhound"] = factory(jQuery); 16 | } 17 | })(this, function($) { 18 | var _ = function() { 19 | "use strict"; 20 | return { 21 | isMsie: function() { 22 | return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; 23 | }, 24 | isBlankString: function(str) { 25 | return !str || /^\s*$/.test(str); 26 | }, 27 | escapeRegExChars: function(str) { 28 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 29 | }, 30 | isString: function(obj) { 31 | return typeof obj === "string"; 32 | }, 33 | isNumber: function(obj) { 34 | return typeof obj === "number"; 35 | }, 36 | isArray: $.isArray, 37 | isFunction: $.isFunction, 38 | isObject: $.isPlainObject, 39 | isUndefined: function(obj) { 40 | return typeof obj === "undefined"; 41 | }, 42 | isElement: function(obj) { 43 | return !!(obj && obj.nodeType === 1); 44 | }, 45 | isJQuery: function(obj) { 46 | return obj instanceof $; 47 | }, 48 | toStr: function toStr(s) { 49 | return _.isUndefined(s) || s === null ? "" : s + ""; 50 | }, 51 | bind: $.proxy, 52 | each: function(collection, cb) { 53 | $.each(collection, reverseArgs); 54 | function reverseArgs(index, value) { 55 | return cb(value, index); 56 | } 57 | }, 58 | map: $.map, 59 | filter: $.grep, 60 | every: function(obj, test) { 61 | var result = true; 62 | if (!obj) { 63 | return result; 64 | } 65 | $.each(obj, function(key, val) { 66 | if (!(result = test.call(null, val, key, obj))) { 67 | return false; 68 | } 69 | }); 70 | return !!result; 71 | }, 72 | some: function(obj, test) { 73 | var result = false; 74 | if (!obj) { 75 | return result; 76 | } 77 | $.each(obj, function(key, val) { 78 | if (result = test.call(null, val, key, obj)) { 79 | return false; 80 | } 81 | }); 82 | return !!result; 83 | }, 84 | mixin: $.extend, 85 | identity: function(x) { 86 | return x; 87 | }, 88 | clone: function(obj) { 89 | return $.extend(true, {}, obj); 90 | }, 91 | getIdGenerator: function() { 92 | var counter = 0; 93 | return function() { 94 | return counter++; 95 | }; 96 | }, 97 | templatify: function templatify(obj) { 98 | return $.isFunction(obj) ? obj : template; 99 | function template() { 100 | return String(obj); 101 | } 102 | }, 103 | defer: function(fn) { 104 | setTimeout(fn, 0); 105 | }, 106 | debounce: function(func, wait, immediate) { 107 | var timeout, result; 108 | return function() { 109 | var context = this, args = arguments, later, callNow; 110 | later = function() { 111 | timeout = null; 112 | if (!immediate) { 113 | result = func.apply(context, args); 114 | } 115 | }; 116 | callNow = immediate && !timeout; 117 | clearTimeout(timeout); 118 | timeout = setTimeout(later, wait); 119 | if (callNow) { 120 | result = func.apply(context, args); 121 | } 122 | return result; 123 | }; 124 | }, 125 | throttle: function(func, wait) { 126 | var context, args, timeout, result, previous, later; 127 | previous = 0; 128 | later = function() { 129 | previous = new Date(); 130 | timeout = null; 131 | result = func.apply(context, args); 132 | }; 133 | return function() { 134 | var now = new Date(), remaining = wait - (now - previous); 135 | context = this; 136 | args = arguments; 137 | if (remaining <= 0) { 138 | clearTimeout(timeout); 139 | timeout = null; 140 | previous = now; 141 | result = func.apply(context, args); 142 | } else if (!timeout) { 143 | timeout = setTimeout(later, remaining); 144 | } 145 | return result; 146 | }; 147 | }, 148 | stringify: function(val) { 149 | return _.isString(val) ? val : JSON.stringify(val); 150 | }, 151 | noop: function() {} 152 | }; 153 | }(); 154 | var VERSION = "0.11.1"; 155 | var tokenizers = function() { 156 | "use strict"; 157 | return { 158 | nonword: nonword, 159 | whitespace: whitespace, 160 | obj: { 161 | nonword: getObjTokenizer(nonword), 162 | whitespace: getObjTokenizer(whitespace) 163 | } 164 | }; 165 | function whitespace(str) { 166 | str = _.toStr(str); 167 | return str ? str.split(/\s+/) : []; 168 | } 169 | function nonword(str) { 170 | str = _.toStr(str); 171 | return str ? str.split(/\W+/) : []; 172 | } 173 | function getObjTokenizer(tokenizer) { 174 | return function setKey(keys) { 175 | keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); 176 | return function tokenize(o) { 177 | var tokens = []; 178 | _.each(keys, function(k) { 179 | tokens = tokens.concat(tokenizer(_.toStr(o[k]))); 180 | }); 181 | return tokens; 182 | }; 183 | }; 184 | } 185 | }(); 186 | var LruCache = function() { 187 | "use strict"; 188 | function LruCache(maxSize) { 189 | this.maxSize = _.isNumber(maxSize) ? maxSize : 100; 190 | this.reset(); 191 | if (this.maxSize <= 0) { 192 | this.set = this.get = $.noop; 193 | } 194 | } 195 | _.mixin(LruCache.prototype, { 196 | set: function set(key, val) { 197 | var tailItem = this.list.tail, node; 198 | if (this.size >= this.maxSize) { 199 | this.list.remove(tailItem); 200 | delete this.hash[tailItem.key]; 201 | this.size--; 202 | } 203 | if (node = this.hash[key]) { 204 | node.val = val; 205 | this.list.moveToFront(node); 206 | } else { 207 | node = new Node(key, val); 208 | this.list.add(node); 209 | this.hash[key] = node; 210 | this.size++; 211 | } 212 | }, 213 | get: function get(key) { 214 | var node = this.hash[key]; 215 | if (node) { 216 | this.list.moveToFront(node); 217 | return node.val; 218 | } 219 | }, 220 | reset: function reset() { 221 | this.size = 0; 222 | this.hash = {}; 223 | this.list = new List(); 224 | } 225 | }); 226 | function List() { 227 | this.head = this.tail = null; 228 | } 229 | _.mixin(List.prototype, { 230 | add: function add(node) { 231 | if (this.head) { 232 | node.next = this.head; 233 | this.head.prev = node; 234 | } 235 | this.head = node; 236 | this.tail = this.tail || node; 237 | }, 238 | remove: function remove(node) { 239 | node.prev ? node.prev.next = node.next : this.head = node.next; 240 | node.next ? node.next.prev = node.prev : this.tail = node.prev; 241 | }, 242 | moveToFront: function(node) { 243 | this.remove(node); 244 | this.add(node); 245 | } 246 | }); 247 | function Node(key, val) { 248 | this.key = key; 249 | this.val = val; 250 | this.prev = this.next = null; 251 | } 252 | return LruCache; 253 | }(); 254 | var PersistentStorage = function() { 255 | "use strict"; 256 | var LOCAL_STORAGE; 257 | try { 258 | LOCAL_STORAGE = window.localStorage; 259 | LOCAL_STORAGE.setItem("~~~", "!"); 260 | LOCAL_STORAGE.removeItem("~~~"); 261 | } catch (err) { 262 | LOCAL_STORAGE = null; 263 | } 264 | function PersistentStorage(namespace, override) { 265 | this.prefix = [ "__", namespace, "__" ].join(""); 266 | this.ttlKey = "__ttl__"; 267 | this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); 268 | this.ls = override || LOCAL_STORAGE; 269 | !this.ls && this._noop(); 270 | } 271 | _.mixin(PersistentStorage.prototype, { 272 | _prefix: function(key) { 273 | return this.prefix + key; 274 | }, 275 | _ttlKey: function(key) { 276 | return this._prefix(key) + this.ttlKey; 277 | }, 278 | _noop: function() { 279 | this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; 280 | }, 281 | _safeSet: function(key, val) { 282 | try { 283 | this.ls.setItem(key, val); 284 | } catch (err) { 285 | if (err.name === "QuotaExceededError") { 286 | this.clear(); 287 | this._noop(); 288 | } 289 | } 290 | }, 291 | get: function(key) { 292 | if (this.isExpired(key)) { 293 | this.remove(key); 294 | } 295 | return decode(this.ls.getItem(this._prefix(key))); 296 | }, 297 | set: function(key, val, ttl) { 298 | if (_.isNumber(ttl)) { 299 | this._safeSet(this._ttlKey(key), encode(now() + ttl)); 300 | } else { 301 | this.ls.removeItem(this._ttlKey(key)); 302 | } 303 | return this._safeSet(this._prefix(key), encode(val)); 304 | }, 305 | remove: function(key) { 306 | this.ls.removeItem(this._ttlKey(key)); 307 | this.ls.removeItem(this._prefix(key)); 308 | return this; 309 | }, 310 | clear: function() { 311 | var i, keys = gatherMatchingKeys(this.keyMatcher); 312 | for (i = keys.length; i--; ) { 313 | this.remove(keys[i]); 314 | } 315 | return this; 316 | }, 317 | isExpired: function(key) { 318 | var ttl = decode(this.ls.getItem(this._ttlKey(key))); 319 | return _.isNumber(ttl) && now() > ttl ? true : false; 320 | } 321 | }); 322 | return PersistentStorage; 323 | function now() { 324 | return new Date().getTime(); 325 | } 326 | function encode(val) { 327 | return JSON.stringify(_.isUndefined(val) ? null : val); 328 | } 329 | function decode(val) { 330 | return $.parseJSON(val); 331 | } 332 | function gatherMatchingKeys(keyMatcher) { 333 | var i, key, keys = [], len = LOCAL_STORAGE.length; 334 | for (i = 0; i < len; i++) { 335 | if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { 336 | keys.push(key.replace(keyMatcher, "")); 337 | } 338 | } 339 | return keys; 340 | } 341 | }(); 342 | var Transport = function() { 343 | "use strict"; 344 | var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); 345 | function Transport(o) { 346 | o = o || {}; 347 | this.cancelled = false; 348 | this.lastReq = null; 349 | this._send = o.transport; 350 | this._get = o.limiter ? o.limiter(this._get) : this._get; 351 | this._cache = o.cache === false ? new LruCache(0) : sharedCache; 352 | } 353 | Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { 354 | maxPendingRequests = num; 355 | }; 356 | Transport.resetCache = function resetCache() { 357 | sharedCache.reset(); 358 | }; 359 | _.mixin(Transport.prototype, { 360 | _fingerprint: function fingerprint(o) { 361 | o = o || {}; 362 | return o.url + o.type + $.param(o.data || {}); 363 | }, 364 | _get: function(o, cb) { 365 | var that = this, fingerprint, jqXhr; 366 | fingerprint = this._fingerprint(o); 367 | if (this.cancelled || fingerprint !== this.lastReq) { 368 | return; 369 | } 370 | if (jqXhr = pendingRequests[fingerprint]) { 371 | jqXhr.done(done).fail(fail); 372 | } else if (pendingRequestsCount < maxPendingRequests) { 373 | pendingRequestsCount++; 374 | pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); 375 | } else { 376 | this.onDeckRequestArgs = [].slice.call(arguments, 0); 377 | } 378 | function done(resp) { 379 | cb(null, resp); 380 | that._cache.set(fingerprint, resp); 381 | } 382 | function fail() { 383 | cb(true); 384 | } 385 | function always() { 386 | pendingRequestsCount--; 387 | delete pendingRequests[fingerprint]; 388 | if (that.onDeckRequestArgs) { 389 | that._get.apply(that, that.onDeckRequestArgs); 390 | that.onDeckRequestArgs = null; 391 | } 392 | } 393 | }, 394 | get: function(o, cb) { 395 | var resp, fingerprint; 396 | cb = cb || $.noop; 397 | o = _.isString(o) ? { 398 | url: o 399 | } : o || {}; 400 | fingerprint = this._fingerprint(o); 401 | this.cancelled = false; 402 | this.lastReq = fingerprint; 403 | if (resp = this._cache.get(fingerprint)) { 404 | cb(null, resp); 405 | } else { 406 | this._get(o, cb); 407 | } 408 | }, 409 | cancel: function() { 410 | this.cancelled = true; 411 | } 412 | }); 413 | return Transport; 414 | }(); 415 | var SearchIndex = window.SearchIndex = function() { 416 | "use strict"; 417 | var CHILDREN = "c", IDS = "i"; 418 | function SearchIndex(o) { 419 | o = o || {}; 420 | if (!o.datumTokenizer || !o.queryTokenizer) { 421 | $.error("datumTokenizer and queryTokenizer are both required"); 422 | } 423 | this.identify = o.identify || _.stringify; 424 | this.datumTokenizer = o.datumTokenizer; 425 | this.queryTokenizer = o.queryTokenizer; 426 | this.reset(); 427 | } 428 | _.mixin(SearchIndex.prototype, { 429 | bootstrap: function bootstrap(o) { 430 | this.datums = o.datums; 431 | this.trie = o.trie; 432 | }, 433 | add: function(data) { 434 | var that = this; 435 | data = _.isArray(data) ? data : [ data ]; 436 | _.each(data, function(datum) { 437 | var id, tokens; 438 | that.datums[id = that.identify(datum)] = datum; 439 | tokens = normalizeTokens(that.datumTokenizer(datum)); 440 | _.each(tokens, function(token) { 441 | var node, chars, ch; 442 | node = that.trie; 443 | chars = token.split(""); 444 | while (ch = chars.shift()) { 445 | node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); 446 | node[IDS].push(id); 447 | } 448 | }); 449 | }); 450 | }, 451 | get: function get(ids) { 452 | var that = this; 453 | return _.map(ids, function(id) { 454 | return that.datums[id]; 455 | }); 456 | }, 457 | search: function search(query) { 458 | var that = this, tokens, matches; 459 | tokens = normalizeTokens(this.queryTokenizer(query)); 460 | _.each(tokens, function(token) { 461 | var node, chars, ch, ids; 462 | if (matches && matches.length === 0) { 463 | return false; 464 | } 465 | node = that.trie; 466 | chars = token.split(""); 467 | while (node && (ch = chars.shift())) { 468 | node = node[CHILDREN][ch]; 469 | } 470 | if (node && chars.length === 0) { 471 | ids = node[IDS].slice(0); 472 | matches = matches ? getIntersection(matches, ids) : ids; 473 | } else { 474 | matches = []; 475 | return false; 476 | } 477 | }); 478 | return matches ? _.map(unique(matches), function(id) { 479 | return that.datums[id]; 480 | }) : []; 481 | }, 482 | all: function all() { 483 | var values = []; 484 | for (var key in this.datums) { 485 | values.push(this.datums[key]); 486 | } 487 | return values; 488 | }, 489 | reset: function reset() { 490 | this.datums = {}; 491 | this.trie = newNode(); 492 | }, 493 | serialize: function serialize() { 494 | return { 495 | datums: this.datums, 496 | trie: this.trie 497 | }; 498 | } 499 | }); 500 | return SearchIndex; 501 | function normalizeTokens(tokens) { 502 | tokens = _.filter(tokens, function(token) { 503 | return !!token; 504 | }); 505 | tokens = _.map(tokens, function(token) { 506 | return token.toLowerCase(); 507 | }); 508 | return tokens; 509 | } 510 | function newNode() { 511 | var node = {}; 512 | node[IDS] = []; 513 | node[CHILDREN] = {}; 514 | return node; 515 | } 516 | function unique(array) { 517 | var seen = {}, uniques = []; 518 | for (var i = 0, len = array.length; i < len; i++) { 519 | if (!seen[array[i]]) { 520 | seen[array[i]] = true; 521 | uniques.push(array[i]); 522 | } 523 | } 524 | return uniques; 525 | } 526 | function getIntersection(arrayA, arrayB) { 527 | var ai = 0, bi = 0, intersection = []; 528 | arrayA = arrayA.sort(); 529 | arrayB = arrayB.sort(); 530 | var lenArrayA = arrayA.length, lenArrayB = arrayB.length; 531 | while (ai < lenArrayA && bi < lenArrayB) { 532 | if (arrayA[ai] < arrayB[bi]) { 533 | ai++; 534 | } else if (arrayA[ai] > arrayB[bi]) { 535 | bi++; 536 | } else { 537 | intersection.push(arrayA[ai]); 538 | ai++; 539 | bi++; 540 | } 541 | } 542 | return intersection; 543 | } 544 | }(); 545 | var Prefetch = function() { 546 | "use strict"; 547 | var keys; 548 | keys = { 549 | data: "data", 550 | protocol: "protocol", 551 | thumbprint: "thumbprint" 552 | }; 553 | function Prefetch(o) { 554 | this.url = o.url; 555 | this.ttl = o.ttl; 556 | this.cache = o.cache; 557 | this.prepare = o.prepare; 558 | this.transform = o.transform; 559 | this.transport = o.transport; 560 | this.thumbprint = o.thumbprint; 561 | this.storage = new PersistentStorage(o.cacheKey); 562 | } 563 | _.mixin(Prefetch.prototype, { 564 | _settings: function settings() { 565 | return { 566 | url: this.url, 567 | type: "GET", 568 | dataType: "json" 569 | }; 570 | }, 571 | store: function store(data) { 572 | if (!this.cache) { 573 | return; 574 | } 575 | this.storage.set(keys.data, data, this.ttl); 576 | this.storage.set(keys.protocol, location.protocol, this.ttl); 577 | this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); 578 | }, 579 | fromCache: function fromCache() { 580 | var stored = {}, isExpired; 581 | if (!this.cache) { 582 | return null; 583 | } 584 | stored.data = this.storage.get(keys.data); 585 | stored.protocol = this.storage.get(keys.protocol); 586 | stored.thumbprint = this.storage.get(keys.thumbprint); 587 | isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; 588 | return stored.data && !isExpired ? stored.data : null; 589 | }, 590 | fromNetwork: function(cb) { 591 | var that = this, settings; 592 | if (!cb) { 593 | return; 594 | } 595 | settings = this.prepare(this._settings()); 596 | this.transport(settings).fail(onError).done(onResponse); 597 | function onError() { 598 | cb(true); 599 | } 600 | function onResponse(resp) { 601 | cb(null, that.transform(resp)); 602 | } 603 | }, 604 | clear: function clear() { 605 | this.storage.clear(); 606 | return this; 607 | } 608 | }); 609 | return Prefetch; 610 | }(); 611 | var Remote = function() { 612 | "use strict"; 613 | function Remote(o) { 614 | this.url = o.url; 615 | this.prepare = o.prepare; 616 | this.transform = o.transform; 617 | this.transport = new Transport({ 618 | cache: o.cache, 619 | limiter: o.limiter, 620 | transport: o.transport 621 | }); 622 | } 623 | _.mixin(Remote.prototype, { 624 | _settings: function settings() { 625 | return { 626 | url: this.url, 627 | type: "GET", 628 | dataType: "json" 629 | }; 630 | }, 631 | get: function get(query, cb) { 632 | var that = this, settings; 633 | if (!cb) { 634 | return; 635 | } 636 | query = query || ""; 637 | settings = this.prepare(query, this._settings()); 638 | return this.transport.get(settings, onResponse); 639 | function onResponse(err, resp) { 640 | err ? cb([]) : cb(that.transform(resp)); 641 | } 642 | }, 643 | cancelLastRequest: function cancelLastRequest() { 644 | this.transport.cancel(); 645 | } 646 | }); 647 | return Remote; 648 | }(); 649 | var oParser = function() { 650 | "use strict"; 651 | return function parse(o) { 652 | var defaults, sorter; 653 | defaults = { 654 | initialize: true, 655 | identify: _.stringify, 656 | datumTokenizer: null, 657 | queryTokenizer: null, 658 | sufficient: 5, 659 | sorter: null, 660 | local: [], 661 | prefetch: null, 662 | remote: null 663 | }; 664 | o = _.mixin(defaults, o || {}); 665 | !o.datumTokenizer && $.error("datumTokenizer is required"); 666 | !o.queryTokenizer && $.error("queryTokenizer is required"); 667 | sorter = o.sorter; 668 | o.sorter = sorter ? function(x) { 669 | return x.sort(sorter); 670 | } : _.identity; 671 | o.local = _.isFunction(o.local) ? o.local() : o.local; 672 | o.prefetch = parsePrefetch(o.prefetch); 673 | o.remote = parseRemote(o.remote); 674 | return o; 675 | }; 676 | function parsePrefetch(o) { 677 | var defaults; 678 | if (!o) { 679 | return null; 680 | } 681 | defaults = { 682 | url: null, 683 | ttl: 24 * 60 * 60 * 1e3, 684 | cache: true, 685 | cacheKey: null, 686 | thumbprint: "", 687 | prepare: _.identity, 688 | transform: _.identity, 689 | transport: null 690 | }; 691 | o = _.isString(o) ? { 692 | url: o 693 | } : o; 694 | o = _.mixin(defaults, o); 695 | !o.url && $.error("prefetch requires url to be set"); 696 | o.transform = o.filter || o.transform; 697 | o.cacheKey = o.cacheKey || o.url; 698 | o.thumbprint = VERSION + o.thumbprint; 699 | o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; 700 | return o; 701 | } 702 | function parseRemote(o) { 703 | var defaults; 704 | if (!o) { 705 | return; 706 | } 707 | defaults = { 708 | url: null, 709 | cache: true, 710 | prepare: null, 711 | replace: null, 712 | wildcard: null, 713 | limiter: null, 714 | rateLimitBy: "debounce", 715 | rateLimitWait: 300, 716 | transform: _.identity, 717 | transport: null 718 | }; 719 | o = _.isString(o) ? { 720 | url: o 721 | } : o; 722 | o = _.mixin(defaults, o); 723 | !o.url && $.error("remote requires url to be set"); 724 | o.transform = o.filter || o.transform; 725 | o.prepare = toRemotePrepare(o); 726 | o.limiter = toLimiter(o); 727 | o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; 728 | delete o.replace; 729 | delete o.wildcard; 730 | delete o.rateLimitBy; 731 | delete o.rateLimitWait; 732 | return o; 733 | } 734 | function toRemotePrepare(o) { 735 | var prepare, replace, wildcard; 736 | prepare = o.prepare; 737 | replace = o.replace; 738 | wildcard = o.wildcard; 739 | if (prepare) { 740 | return prepare; 741 | } 742 | if (replace) { 743 | prepare = prepareByReplace; 744 | } else if (o.wildcard) { 745 | prepare = prepareByWildcard; 746 | } else { 747 | prepare = idenityPrepare; 748 | } 749 | return prepare; 750 | function prepareByReplace(query, settings) { 751 | settings.url = replace(settings.url, query); 752 | return settings; 753 | } 754 | function prepareByWildcard(query, settings) { 755 | settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); 756 | return settings; 757 | } 758 | function idenityPrepare(query, settings) { 759 | return settings; 760 | } 761 | } 762 | function toLimiter(o) { 763 | var limiter, method, wait; 764 | limiter = o.limiter; 765 | method = o.rateLimitBy; 766 | wait = o.rateLimitWait; 767 | if (!limiter) { 768 | limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); 769 | } 770 | return limiter; 771 | function debounce(wait) { 772 | return function debounce(fn) { 773 | return _.debounce(fn, wait); 774 | }; 775 | } 776 | function throttle(wait) { 777 | return function throttle(fn) { 778 | return _.throttle(fn, wait); 779 | }; 780 | } 781 | } 782 | function callbackToDeferred(fn) { 783 | return function wrapper(o) { 784 | var deferred = $.Deferred(); 785 | fn(o, onSuccess, onError); 786 | return deferred; 787 | function onSuccess(resp) { 788 | _.defer(function() { 789 | deferred.resolve(resp); 790 | }); 791 | } 792 | function onError(err) { 793 | _.defer(function() { 794 | deferred.reject(err); 795 | }); 796 | } 797 | }; 798 | } 799 | }(); 800 | var Bloodhound = function() { 801 | "use strict"; 802 | var old; 803 | old = window && window.Bloodhound; 804 | function Bloodhound(o) { 805 | o = oParser(o); 806 | this.sorter = o.sorter; 807 | this.identify = o.identify; 808 | this.sufficient = o.sufficient; 809 | this.local = o.local; 810 | this.remote = o.remote ? new Remote(o.remote) : null; 811 | this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; 812 | this.index = new SearchIndex({ 813 | identify: this.identify, 814 | datumTokenizer: o.datumTokenizer, 815 | queryTokenizer: o.queryTokenizer 816 | }); 817 | o.initialize !== false && this.initialize(); 818 | } 819 | Bloodhound.noConflict = function noConflict() { 820 | window && (window.Bloodhound = old); 821 | return Bloodhound; 822 | }; 823 | Bloodhound.tokenizers = tokenizers; 824 | _.mixin(Bloodhound.prototype, { 825 | __ttAdapter: function ttAdapter() { 826 | var that = this; 827 | return this.remote ? withAsync : withoutAsync; 828 | function withAsync(query, sync, async) { 829 | return that.search(query, sync, async); 830 | } 831 | function withoutAsync(query, sync) { 832 | return that.search(query, sync); 833 | } 834 | }, 835 | _loadPrefetch: function loadPrefetch() { 836 | var that = this, deferred, serialized; 837 | deferred = $.Deferred(); 838 | if (!this.prefetch) { 839 | deferred.resolve(); 840 | } else if (serialized = this.prefetch.fromCache()) { 841 | this.index.bootstrap(serialized); 842 | deferred.resolve(); 843 | } else { 844 | this.prefetch.fromNetwork(done); 845 | } 846 | return deferred.promise(); 847 | function done(err, data) { 848 | if (err) { 849 | return deferred.reject(); 850 | } 851 | that.add(data); 852 | that.prefetch.store(that.index.serialize()); 853 | deferred.resolve(); 854 | } 855 | }, 856 | _initialize: function initialize() { 857 | var that = this, deferred; 858 | this.clear(); 859 | (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); 860 | return this.initPromise; 861 | function addLocalToIndex() { 862 | that.add(that.local); 863 | } 864 | }, 865 | initialize: function initialize(force) { 866 | return !this.initPromise || force ? this._initialize() : this.initPromise; 867 | }, 868 | add: function add(data) { 869 | this.index.add(data); 870 | return this; 871 | }, 872 | get: function get(ids) { 873 | ids = _.isArray(ids) ? ids : [].slice.call(arguments); 874 | return this.index.get(ids); 875 | }, 876 | search: function search(query, sync, async) { 877 | var that = this, local; 878 | local = this.sorter(this.index.search(query)); 879 | sync(this.remote ? local.slice() : local); 880 | if (this.remote && local.length < this.sufficient) { 881 | this.remote.get(query, processRemote); 882 | } else if (this.remote) { 883 | this.remote.cancelLastRequest(); 884 | } 885 | return this; 886 | function processRemote(remote) { 887 | var nonDuplicates = []; 888 | _.each(remote, function(r) { 889 | !_.some(local, function(l) { 890 | return that.identify(r) === that.identify(l); 891 | }) && nonDuplicates.push(r); 892 | }); 893 | async && async(nonDuplicates); 894 | } 895 | }, 896 | all: function all() { 897 | return this.index.all(); 898 | }, 899 | clear: function clear() { 900 | this.index.reset(); 901 | return this; 902 | }, 903 | clearPrefetchCache: function clearPrefetchCache() { 904 | this.prefetch && this.prefetch.clear(); 905 | return this; 906 | }, 907 | clearRemoteCache: function clearRemoteCache() { 908 | Transport.resetCache(); 909 | return this; 910 | }, 911 | ttAdapter: function ttAdapter() { 912 | return this.__ttAdapter(); 913 | } 914 | }); 915 | return Bloodhound; 916 | }(); 917 | return Bloodhound; 918 | }); 919 | 920 | (function(root, factory) { 921 | if (typeof define === "function" && define.amd) { 922 | define("typeahead.js", [ "jquery" ], function(a0) { 923 | return factory(a0); 924 | }); 925 | } else if (typeof exports === "object") { 926 | module.exports = factory(require("jquery")); 927 | } else { 928 | factory(jQuery); 929 | } 930 | })(this, function($) { 931 | var _ = function() { 932 | "use strict"; 933 | return { 934 | isMsie: function() { 935 | return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; 936 | }, 937 | isBlankString: function(str) { 938 | return !str || /^\s*$/.test(str); 939 | }, 940 | escapeRegExChars: function(str) { 941 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 942 | }, 943 | isString: function(obj) { 944 | return typeof obj === "string"; 945 | }, 946 | isNumber: function(obj) { 947 | return typeof obj === "number"; 948 | }, 949 | isArray: $.isArray, 950 | isFunction: $.isFunction, 951 | isObject: $.isPlainObject, 952 | isUndefined: function(obj) { 953 | return typeof obj === "undefined"; 954 | }, 955 | isElement: function(obj) { 956 | return !!(obj && obj.nodeType === 1); 957 | }, 958 | isJQuery: function(obj) { 959 | return obj instanceof $; 960 | }, 961 | toStr: function toStr(s) { 962 | return _.isUndefined(s) || s === null ? "" : s + ""; 963 | }, 964 | bind: $.proxy, 965 | each: function(collection, cb) { 966 | $.each(collection, reverseArgs); 967 | function reverseArgs(index, value) { 968 | return cb(value, index); 969 | } 970 | }, 971 | map: $.map, 972 | filter: $.grep, 973 | every: function(obj, test) { 974 | var result = true; 975 | if (!obj) { 976 | return result; 977 | } 978 | $.each(obj, function(key, val) { 979 | if (!(result = test.call(null, val, key, obj))) { 980 | return false; 981 | } 982 | }); 983 | return !!result; 984 | }, 985 | some: function(obj, test) { 986 | var result = false; 987 | if (!obj) { 988 | return result; 989 | } 990 | $.each(obj, function(key, val) { 991 | if (result = test.call(null, val, key, obj)) { 992 | return false; 993 | } 994 | }); 995 | return !!result; 996 | }, 997 | mixin: $.extend, 998 | identity: function(x) { 999 | return x; 1000 | }, 1001 | clone: function(obj) { 1002 | return $.extend(true, {}, obj); 1003 | }, 1004 | getIdGenerator: function() { 1005 | var counter = 0; 1006 | return function() { 1007 | return counter++; 1008 | }; 1009 | }, 1010 | templatify: function templatify(obj) { 1011 | return $.isFunction(obj) ? obj : template; 1012 | function template() { 1013 | return String(obj); 1014 | } 1015 | }, 1016 | defer: function(fn) { 1017 | setTimeout(fn, 0); 1018 | }, 1019 | debounce: function(func, wait, immediate) { 1020 | var timeout, result; 1021 | return function() { 1022 | var context = this, args = arguments, later, callNow; 1023 | later = function() { 1024 | timeout = null; 1025 | if (!immediate) { 1026 | result = func.apply(context, args); 1027 | } 1028 | }; 1029 | callNow = immediate && !timeout; 1030 | clearTimeout(timeout); 1031 | timeout = setTimeout(later, wait); 1032 | if (callNow) { 1033 | result = func.apply(context, args); 1034 | } 1035 | return result; 1036 | }; 1037 | }, 1038 | throttle: function(func, wait) { 1039 | var context, args, timeout, result, previous, later; 1040 | previous = 0; 1041 | later = function() { 1042 | previous = new Date(); 1043 | timeout = null; 1044 | result = func.apply(context, args); 1045 | }; 1046 | return function() { 1047 | var now = new Date(), remaining = wait - (now - previous); 1048 | context = this; 1049 | args = arguments; 1050 | if (remaining <= 0) { 1051 | clearTimeout(timeout); 1052 | timeout = null; 1053 | previous = now; 1054 | result = func.apply(context, args); 1055 | } else if (!timeout) { 1056 | timeout = setTimeout(later, remaining); 1057 | } 1058 | return result; 1059 | }; 1060 | }, 1061 | stringify: function(val) { 1062 | return _.isString(val) ? val : JSON.stringify(val); 1063 | }, 1064 | noop: function() {} 1065 | }; 1066 | }(); 1067 | var WWW = function() { 1068 | "use strict"; 1069 | var defaultClassNames = { 1070 | wrapper: "twitter-typeahead", 1071 | input: "tt-input", 1072 | hint: "tt-hint", 1073 | menu: "tt-menu", 1074 | dataset: "tt-dataset", 1075 | suggestion: "tt-suggestion", 1076 | selectable: "tt-selectable", 1077 | empty: "tt-empty", 1078 | open: "tt-open", 1079 | cursor: "tt-cursor", 1080 | highlight: "tt-highlight" 1081 | }; 1082 | return build; 1083 | function build(o) { 1084 | var www, classes; 1085 | classes = _.mixin({}, defaultClassNames, o); 1086 | www = { 1087 | css: buildCss(), 1088 | classes: classes, 1089 | html: buildHtml(classes), 1090 | selectors: buildSelectors(classes) 1091 | }; 1092 | return { 1093 | css: www.css, 1094 | html: www.html, 1095 | classes: www.classes, 1096 | selectors: www.selectors, 1097 | mixin: function(o) { 1098 | _.mixin(o, www); 1099 | } 1100 | }; 1101 | } 1102 | function buildHtml(c) { 1103 | return { 1104 | wrapper: '', 1105 | menu: '
' 1106 | }; 1107 | } 1108 | function buildSelectors(classes) { 1109 | var selectors = {}; 1110 | _.each(classes, function(v, k) { 1111 | selectors[k] = "." + v; 1112 | }); 1113 | return selectors; 1114 | } 1115 | function buildCss() { 1116 | var css = { 1117 | wrapper: { 1118 | position: "relative", 1119 | display: "inline-block" 1120 | }, 1121 | hint: { 1122 | position: "absolute", 1123 | top: "0", 1124 | left: "0", 1125 | borderColor: "transparent", 1126 | boxShadow: "none", 1127 | opacity: "1" 1128 | }, 1129 | input: { 1130 | position: "relative", 1131 | verticalAlign: "top", 1132 | backgroundColor: "transparent" 1133 | }, 1134 | inputWithNoHint: { 1135 | position: "relative", 1136 | verticalAlign: "top" 1137 | }, 1138 | menu: { 1139 | position: "absolute", 1140 | top: "100%", 1141 | left: "0", 1142 | zIndex: "100", 1143 | display: "none" 1144 | }, 1145 | ltr: { 1146 | left: "0", 1147 | right: "auto" 1148 | }, 1149 | rtl: { 1150 | left: "auto", 1151 | right: " 0" 1152 | } 1153 | }; 1154 | if (_.isMsie()) { 1155 | _.mixin(css.input, { 1156 | backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" 1157 | }); 1158 | } 1159 | return css; 1160 | } 1161 | }(); 1162 | var EventBus = function() { 1163 | "use strict"; 1164 | var namespace, deprecationMap; 1165 | namespace = "typeahead:"; 1166 | deprecationMap = { 1167 | render: "rendered", 1168 | cursorchange: "cursorchanged", 1169 | select: "selected", 1170 | autocomplete: "autocompleted" 1171 | }; 1172 | function EventBus(o) { 1173 | if (!o || !o.el) { 1174 | $.error("EventBus initialized without el"); 1175 | } 1176 | this.$el = $(o.el); 1177 | } 1178 | _.mixin(EventBus.prototype, { 1179 | _trigger: function(type, args) { 1180 | var $e; 1181 | $e = $.Event(namespace + type); 1182 | (args = args || []).unshift($e); 1183 | this.$el.trigger.apply(this.$el, args); 1184 | return $e; 1185 | }, 1186 | before: function(type) { 1187 | var args, $e; 1188 | args = [].slice.call(arguments, 1); 1189 | $e = this._trigger("before" + type, args); 1190 | return $e.isDefaultPrevented(); 1191 | }, 1192 | trigger: function(type) { 1193 | var deprecatedType; 1194 | this._trigger(type, [].slice.call(arguments, 1)); 1195 | if (deprecatedType = deprecationMap[type]) { 1196 | this._trigger(deprecatedType, [].slice.call(arguments, 1)); 1197 | } 1198 | } 1199 | }); 1200 | return EventBus; 1201 | }(); 1202 | var EventEmitter = function() { 1203 | "use strict"; 1204 | var splitter = /\s+/, nextTick = getNextTick(); 1205 | return { 1206 | onSync: onSync, 1207 | onAsync: onAsync, 1208 | off: off, 1209 | trigger: trigger 1210 | }; 1211 | function on(method, types, cb, context) { 1212 | var type; 1213 | if (!cb) { 1214 | return this; 1215 | } 1216 | types = types.split(splitter); 1217 | cb = context ? bindContext(cb, context) : cb; 1218 | this._callbacks = this._callbacks || {}; 1219 | while (type = types.shift()) { 1220 | this._callbacks[type] = this._callbacks[type] || { 1221 | sync: [], 1222 | async: [] 1223 | }; 1224 | this._callbacks[type][method].push(cb); 1225 | } 1226 | return this; 1227 | } 1228 | function onAsync(types, cb, context) { 1229 | return on.call(this, "async", types, cb, context); 1230 | } 1231 | function onSync(types, cb, context) { 1232 | return on.call(this, "sync", types, cb, context); 1233 | } 1234 | function off(types) { 1235 | var type; 1236 | if (!this._callbacks) { 1237 | return this; 1238 | } 1239 | types = types.split(splitter); 1240 | while (type = types.shift()) { 1241 | delete this._callbacks[type]; 1242 | } 1243 | return this; 1244 | } 1245 | function trigger(types) { 1246 | var type, callbacks, args, syncFlush, asyncFlush; 1247 | if (!this._callbacks) { 1248 | return this; 1249 | } 1250 | types = types.split(splitter); 1251 | args = [].slice.call(arguments, 1); 1252 | while ((type = types.shift()) && (callbacks = this._callbacks[type])) { 1253 | syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); 1254 | asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); 1255 | syncFlush() && nextTick(asyncFlush); 1256 | } 1257 | return this; 1258 | } 1259 | function getFlush(callbacks, context, args) { 1260 | return flush; 1261 | function flush() { 1262 | var cancelled; 1263 | for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { 1264 | cancelled = callbacks[i].apply(context, args) === false; 1265 | } 1266 | return !cancelled; 1267 | } 1268 | } 1269 | function getNextTick() { 1270 | var nextTickFn; 1271 | if (window.setImmediate) { 1272 | nextTickFn = function nextTickSetImmediate(fn) { 1273 | setImmediate(function() { 1274 | fn(); 1275 | }); 1276 | }; 1277 | } else { 1278 | nextTickFn = function nextTickSetTimeout(fn) { 1279 | setTimeout(function() { 1280 | fn(); 1281 | }, 0); 1282 | }; 1283 | } 1284 | return nextTickFn; 1285 | } 1286 | function bindContext(fn, context) { 1287 | return fn.bind ? fn.bind(context) : function() { 1288 | fn.apply(context, [].slice.call(arguments, 0)); 1289 | }; 1290 | } 1291 | }(); 1292 | var highlight = function(doc) { 1293 | "use strict"; 1294 | var defaults = { 1295 | node: null, 1296 | pattern: null, 1297 | tagName: "strong", 1298 | className: null, 1299 | wordsOnly: false, 1300 | caseSensitive: false 1301 | }; 1302 | return function hightlight(o) { 1303 | var regex; 1304 | o = _.mixin({}, defaults, o); 1305 | if (!o.node || !o.pattern) { 1306 | return; 1307 | } 1308 | o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; 1309 | regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); 1310 | traverse(o.node, hightlightTextNode); 1311 | function hightlightTextNode(textNode) { 1312 | var match, patternNode, wrapperNode; 1313 | if (match = regex.exec(textNode.data)) { 1314 | wrapperNode = doc.createElement(o.tagName); 1315 | o.className && (wrapperNode.className = o.className); 1316 | patternNode = textNode.splitText(match.index); 1317 | patternNode.splitText(match[0].length); 1318 | wrapperNode.appendChild(patternNode.cloneNode(true)); 1319 | textNode.parentNode.replaceChild(wrapperNode, patternNode); 1320 | } 1321 | return !!match; 1322 | } 1323 | function traverse(el, hightlightTextNode) { 1324 | var childNode, TEXT_NODE_TYPE = 3; 1325 | for (var i = 0; i < el.childNodes.length; i++) { 1326 | childNode = el.childNodes[i]; 1327 | if (childNode.nodeType === TEXT_NODE_TYPE) { 1328 | i += hightlightTextNode(childNode) ? 1 : 0; 1329 | } else { 1330 | traverse(childNode, hightlightTextNode); 1331 | } 1332 | } 1333 | } 1334 | }; 1335 | function getRegex(patterns, caseSensitive, wordsOnly) { 1336 | var escapedPatterns = [], regexStr; 1337 | for (var i = 0, len = patterns.length; i < len; i++) { 1338 | escapedPatterns.push(_.escapeRegExChars(patterns[i])); 1339 | } 1340 | regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; 1341 | return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); 1342 | } 1343 | }(window.document); 1344 | var Input = function() { 1345 | "use strict"; 1346 | var specialKeyCodeMap; 1347 | specialKeyCodeMap = { 1348 | 9: "tab", 1349 | 27: "esc", 1350 | 37: "left", 1351 | 39: "right", 1352 | 13: "enter", 1353 | 38: "up", 1354 | 40: "down" 1355 | }; 1356 | function Input(o, www) { 1357 | o = o || {}; 1358 | if (!o.input) { 1359 | $.error("input is missing"); 1360 | } 1361 | www.mixin(this); 1362 | this.$hint = $(o.hint); 1363 | this.$input = $(o.input); 1364 | this.query = this.$input.val(); 1365 | this.queryWhenFocused = this.hasFocus() ? this.query : null; 1366 | this.$overflowHelper = buildOverflowHelper(this.$input); 1367 | this._checkLanguageDirection(); 1368 | if (this.$hint.length === 0) { 1369 | this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; 1370 | } 1371 | } 1372 | Input.normalizeQuery = function(str) { 1373 | return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); 1374 | }; 1375 | _.mixin(Input.prototype, EventEmitter, { 1376 | _onBlur: function onBlur() { 1377 | this.resetInputValue(); 1378 | this.trigger("blurred"); 1379 | }, 1380 | _onFocus: function onFocus() { 1381 | this.queryWhenFocused = this.query; 1382 | this.trigger("focused"); 1383 | }, 1384 | _onKeydown: function onKeydown($e) { 1385 | var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; 1386 | this._managePreventDefault(keyName, $e); 1387 | if (keyName && this._shouldTrigger(keyName, $e)) { 1388 | this.trigger(keyName + "Keyed", $e); 1389 | } 1390 | }, 1391 | _onInput: function onInput() { 1392 | this._setQuery(this.getInputValue()); 1393 | this.clearHintIfInvalid(); 1394 | this._checkLanguageDirection(); 1395 | }, 1396 | _managePreventDefault: function managePreventDefault(keyName, $e) { 1397 | var preventDefault; 1398 | switch (keyName) { 1399 | case "up": 1400 | case "down": 1401 | preventDefault = !withModifier($e); 1402 | break; 1403 | 1404 | default: 1405 | preventDefault = false; 1406 | } 1407 | preventDefault && $e.preventDefault(); 1408 | }, 1409 | _shouldTrigger: function shouldTrigger(keyName, $e) { 1410 | var trigger; 1411 | switch (keyName) { 1412 | case "tab": 1413 | trigger = !withModifier($e); 1414 | break; 1415 | 1416 | default: 1417 | trigger = true; 1418 | } 1419 | return trigger; 1420 | }, 1421 | _checkLanguageDirection: function checkLanguageDirection() { 1422 | var dir = (this.$input.css("direction") || "ltr").toLowerCase(); 1423 | if (this.dir !== dir) { 1424 | this.dir = dir; 1425 | this.$hint.attr("dir", dir); 1426 | this.trigger("langDirChanged", dir); 1427 | } 1428 | }, 1429 | _setQuery: function setQuery(val, silent) { 1430 | var areEquivalent, hasDifferentWhitespace; 1431 | areEquivalent = areQueriesEquivalent(val, this.query); 1432 | hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; 1433 | this.query = val; 1434 | if (!silent && !areEquivalent) { 1435 | this.trigger("queryChanged", this.query); 1436 | } else if (!silent && hasDifferentWhitespace) { 1437 | this.trigger("whitespaceChanged", this.query); 1438 | } 1439 | }, 1440 | bind: function() { 1441 | var that = this, onBlur, onFocus, onKeydown, onInput; 1442 | onBlur = _.bind(this._onBlur, this); 1443 | onFocus = _.bind(this._onFocus, this); 1444 | onKeydown = _.bind(this._onKeydown, this); 1445 | onInput = _.bind(this._onInput, this); 1446 | this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); 1447 | if (!_.isMsie() || _.isMsie() > 9) { 1448 | this.$input.on("input.tt", onInput); 1449 | } else { 1450 | this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { 1451 | if (specialKeyCodeMap[$e.which || $e.keyCode]) { 1452 | return; 1453 | } 1454 | _.defer(_.bind(that._onInput, that, $e)); 1455 | }); 1456 | } 1457 | return this; 1458 | }, 1459 | focus: function focus() { 1460 | this.$input.focus(); 1461 | }, 1462 | blur: function blur() { 1463 | this.$input.blur(); 1464 | }, 1465 | getLangDir: function getLangDir() { 1466 | return this.dir; 1467 | }, 1468 | getQuery: function getQuery() { 1469 | return this.query || ""; 1470 | }, 1471 | setQuery: function setQuery(val, silent) { 1472 | this.setInputValue(val); 1473 | this._setQuery(val, silent); 1474 | }, 1475 | hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { 1476 | return this.query !== this.queryWhenFocused; 1477 | }, 1478 | getInputValue: function getInputValue() { 1479 | return this.$input.val(); 1480 | }, 1481 | setInputValue: function setInputValue(value) { 1482 | this.$input.val(value); 1483 | this.clearHintIfInvalid(); 1484 | this._checkLanguageDirection(); 1485 | }, 1486 | resetInputValue: function resetInputValue() { 1487 | this.setInputValue(this.query); 1488 | }, 1489 | getHint: function getHint() { 1490 | return this.$hint.val(); 1491 | }, 1492 | setHint: function setHint(value) { 1493 | this.$hint.val(value); 1494 | }, 1495 | clearHint: function clearHint() { 1496 | this.setHint(""); 1497 | }, 1498 | clearHintIfInvalid: function clearHintIfInvalid() { 1499 | var val, hint, valIsPrefixOfHint, isValid; 1500 | val = this.getInputValue(); 1501 | hint = this.getHint(); 1502 | valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; 1503 | isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); 1504 | !isValid && this.clearHint(); 1505 | }, 1506 | hasFocus: function hasFocus() { 1507 | return this.$input.is(":focus"); 1508 | }, 1509 | hasOverflow: function hasOverflow() { 1510 | var constraint = this.$input.width() - 2; 1511 | this.$overflowHelper.text(this.getInputValue()); 1512 | return this.$overflowHelper.width() >= constraint; 1513 | }, 1514 | isCursorAtEnd: function() { 1515 | var valueLength, selectionStart, range; 1516 | valueLength = this.$input.val().length; 1517 | selectionStart = this.$input[0].selectionStart; 1518 | if (_.isNumber(selectionStart)) { 1519 | return selectionStart === valueLength; 1520 | } else if (document.selection) { 1521 | range = document.selection.createRange(); 1522 | range.moveStart("character", -valueLength); 1523 | return valueLength === range.text.length; 1524 | } 1525 | return true; 1526 | }, 1527 | destroy: function destroy() { 1528 | this.$hint.off(".tt"); 1529 | this.$input.off(".tt"); 1530 | this.$overflowHelper.remove(); 1531 | this.$hint = this.$input = this.$overflowHelper = $("
"); 1532 | } 1533 | }); 1534 | return Input; 1535 | function buildOverflowHelper($input) { 1536 | return $('').css({ 1537 | position: "absolute", 1538 | visibility: "hidden", 1539 | whiteSpace: "pre", 1540 | fontFamily: $input.css("font-family"), 1541 | fontSize: $input.css("font-size"), 1542 | fontStyle: $input.css("font-style"), 1543 | fontVariant: $input.css("font-variant"), 1544 | fontWeight: $input.css("font-weight"), 1545 | wordSpacing: $input.css("word-spacing"), 1546 | letterSpacing: $input.css("letter-spacing"), 1547 | textIndent: $input.css("text-indent"), 1548 | textRendering: $input.css("text-rendering"), 1549 | textTransform: $input.css("text-transform") 1550 | }).insertAfter($input); 1551 | } 1552 | function areQueriesEquivalent(a, b) { 1553 | return Input.normalizeQuery(a) === Input.normalizeQuery(b); 1554 | } 1555 | function withModifier($e) { 1556 | return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; 1557 | } 1558 | }(); 1559 | var Dataset = function() { 1560 | "use strict"; 1561 | var keys, nameGenerator; 1562 | keys = { 1563 | val: "tt-selectable-display", 1564 | obj: "tt-selectable-object" 1565 | }; 1566 | nameGenerator = _.getIdGenerator(); 1567 | function Dataset(o, www) { 1568 | o = o || {}; 1569 | o.templates = o.templates || {}; 1570 | o.templates.notFound = o.templates.notFound || o.templates.empty; 1571 | if (!o.source) { 1572 | $.error("missing source"); 1573 | } 1574 | if (!o.node) { 1575 | $.error("missing node"); 1576 | } 1577 | if (o.name && !isValidName(o.name)) { 1578 | $.error("invalid dataset name: " + o.name); 1579 | } 1580 | www.mixin(this); 1581 | this.highlight = !!o.highlight; 1582 | this.name = o.name || nameGenerator(); 1583 | this.limit = o.limit || 5; 1584 | this.displayFn = getDisplayFn(o.display || o.displayKey); 1585 | this.templates = getTemplates(o.templates, this.displayFn); 1586 | this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; 1587 | this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; 1588 | this._resetLastSuggestion(); 1589 | this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); 1590 | } 1591 | Dataset.extractData = function extractData(el) { 1592 | var $el = $(el); 1593 | if ($el.data(keys.obj)) { 1594 | return { 1595 | val: $el.data(keys.val) || "", 1596 | obj: $el.data(keys.obj) || null 1597 | }; 1598 | } 1599 | return null; 1600 | }; 1601 | _.mixin(Dataset.prototype, EventEmitter, { 1602 | _overwrite: function overwrite(query, suggestions) { 1603 | suggestions = suggestions || []; 1604 | if (suggestions.length) { 1605 | this._renderSuggestions(query, suggestions); 1606 | } else if (this.async && this.templates.pending) { 1607 | this._renderPending(query); 1608 | } else if (!this.async && this.templates.notFound) { 1609 | this._renderNotFound(query); 1610 | } else { 1611 | this._empty(); 1612 | } 1613 | this.trigger("rendered", this.name, suggestions, false); 1614 | }, 1615 | _append: function append(query, suggestions) { 1616 | suggestions = suggestions || []; 1617 | if (suggestions.length && this.$lastSuggestion.length) { 1618 | this._appendSuggestions(query, suggestions); 1619 | } else if (suggestions.length) { 1620 | this._renderSuggestions(query, suggestions); 1621 | } else if (!this.$lastSuggestion.length && this.templates.notFound) { 1622 | this._renderNotFound(query); 1623 | } 1624 | this.trigger("rendered", this.name, suggestions, true); 1625 | }, 1626 | _renderSuggestions: function renderSuggestions(query, suggestions) { 1627 | var $fragment; 1628 | $fragment = this._getSuggestionsFragment(query, suggestions); 1629 | this.$lastSuggestion = $fragment.children().last(); 1630 | this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); 1631 | }, 1632 | _appendSuggestions: function appendSuggestions(query, suggestions) { 1633 | var $fragment, $lastSuggestion; 1634 | $fragment = this._getSuggestionsFragment(query, suggestions); 1635 | $lastSuggestion = $fragment.children().last(); 1636 | this.$lastSuggestion.after($fragment); 1637 | this.$lastSuggestion = $lastSuggestion; 1638 | }, 1639 | _renderPending: function renderPending(query) { 1640 | var template = this.templates.pending; 1641 | this._resetLastSuggestion(); 1642 | template && this.$el.html(template({ 1643 | query: query, 1644 | dataset: this.name 1645 | })); 1646 | }, 1647 | _renderNotFound: function renderNotFound(query) { 1648 | var template = this.templates.notFound; 1649 | this._resetLastSuggestion(); 1650 | template && this.$el.html(template({ 1651 | query: query, 1652 | dataset: this.name 1653 | })); 1654 | }, 1655 | _empty: function empty() { 1656 | this.$el.empty(); 1657 | this._resetLastSuggestion(); 1658 | }, 1659 | _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { 1660 | var that = this, fragment; 1661 | fragment = document.createDocumentFragment(); 1662 | _.each(suggestions, function getSuggestionNode(suggestion) { 1663 | var $el, context; 1664 | context = that._injectQuery(query, suggestion); 1665 | $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); 1666 | fragment.appendChild($el[0]); 1667 | }); 1668 | this.highlight && highlight({ 1669 | className: this.classes.highlight, 1670 | node: fragment, 1671 | pattern: query 1672 | }); 1673 | return $(fragment); 1674 | }, 1675 | _getFooter: function getFooter(query, suggestions) { 1676 | return this.templates.footer ? this.templates.footer({ 1677 | query: query, 1678 | suggestions: suggestions, 1679 | dataset: this.name 1680 | }) : null; 1681 | }, 1682 | _getHeader: function getHeader(query, suggestions) { 1683 | return this.templates.header ? this.templates.header({ 1684 | query: query, 1685 | suggestions: suggestions, 1686 | dataset: this.name 1687 | }) : null; 1688 | }, 1689 | _resetLastSuggestion: function resetLastSuggestion() { 1690 | this.$lastSuggestion = $(); 1691 | }, 1692 | _injectQuery: function injectQuery(query, obj) { 1693 | return _.isObject(obj) ? _.mixin({ 1694 | _query: query 1695 | }, obj) : obj; 1696 | }, 1697 | update: function update(query) { 1698 | var that = this, canceled = false, syncCalled = false, rendered = 0; 1699 | this.cancel(); 1700 | this.cancel = function cancel() { 1701 | canceled = true; 1702 | that.cancel = $.noop; 1703 | that.async && that.trigger("asyncCanceled", query); 1704 | }; 1705 | this.source(query, sync, async); 1706 | !syncCalled && sync([]); 1707 | function sync(suggestions) { 1708 | if (syncCalled) { 1709 | return; 1710 | } 1711 | syncCalled = true; 1712 | suggestions = (suggestions || []).slice(0, that.limit); 1713 | rendered = suggestions.length; 1714 | that._overwrite(query, suggestions); 1715 | if (rendered < that.limit && that.async) { 1716 | that.trigger("asyncRequested", query); 1717 | } 1718 | } 1719 | function async(suggestions) { 1720 | suggestions = suggestions || []; 1721 | if (!canceled && rendered < that.limit) { 1722 | that.cancel = $.noop; 1723 | rendered += suggestions.length; 1724 | that._append(query, suggestions.slice(0, that.limit - rendered)); 1725 | that.async && that.trigger("asyncReceived", query); 1726 | } 1727 | } 1728 | }, 1729 | cancel: $.noop, 1730 | clear: function clear() { 1731 | this._empty(); 1732 | this.cancel(); 1733 | this.trigger("cleared"); 1734 | }, 1735 | isEmpty: function isEmpty() { 1736 | return this.$el.is(":empty"); 1737 | }, 1738 | destroy: function destroy() { 1739 | this.$el = $("
"); 1740 | } 1741 | }); 1742 | return Dataset; 1743 | function getDisplayFn(display) { 1744 | display = display || _.stringify; 1745 | return _.isFunction(display) ? display : displayFn; 1746 | function displayFn(obj) { 1747 | return obj[display]; 1748 | } 1749 | } 1750 | function getTemplates(templates, displayFn) { 1751 | return { 1752 | notFound: templates.notFound && _.templatify(templates.notFound), 1753 | pending: templates.pending && _.templatify(templates.pending), 1754 | header: templates.header && _.templatify(templates.header), 1755 | footer: templates.footer && _.templatify(templates.footer), 1756 | suggestion: templates.suggestion || suggestionTemplate 1757 | }; 1758 | function suggestionTemplate(context) { 1759 | return $("
").text(displayFn(context)); 1760 | } 1761 | } 1762 | function isValidName(str) { 1763 | return /^[_a-zA-Z0-9-]+$/.test(str); 1764 | } 1765 | }(); 1766 | var Menu = function() { 1767 | "use strict"; 1768 | function Menu(o, www) { 1769 | var that = this; 1770 | o = o || {}; 1771 | if (!o.node) { 1772 | $.error("node is required"); 1773 | } 1774 | www.mixin(this); 1775 | this.$node = $(o.node); 1776 | this.query = null; 1777 | this.datasets = _.map(o.datasets, initializeDataset); 1778 | function initializeDataset(oDataset) { 1779 | var node = that.$node.find(oDataset.node).first(); 1780 | oDataset.node = node.length ? node : $("
").appendTo(that.$node); 1781 | return new Dataset(oDataset, www); 1782 | } 1783 | } 1784 | _.mixin(Menu.prototype, EventEmitter, { 1785 | _onSelectableClick: function onSelectableClick($e) { 1786 | this.trigger("selectableClicked", $($e.currentTarget)); 1787 | }, 1788 | _onRendered: function onRendered(type, dataset, suggestions, async) { 1789 | this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); 1790 | this.trigger("datasetRendered", dataset, suggestions, async); 1791 | }, 1792 | _onCleared: function onCleared() { 1793 | this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); 1794 | this.trigger("datasetCleared"); 1795 | }, 1796 | _propagate: function propagate() { 1797 | this.trigger.apply(this, arguments); 1798 | }, 1799 | _allDatasetsEmpty: function allDatasetsEmpty() { 1800 | return _.every(this.datasets, isDatasetEmpty); 1801 | function isDatasetEmpty(dataset) { 1802 | return dataset.isEmpty(); 1803 | } 1804 | }, 1805 | _getSelectables: function getSelectables() { 1806 | return this.$node.find(this.selectors.selectable); 1807 | }, 1808 | _removeCursor: function _removeCursor() { 1809 | var $selectable = this.getActiveSelectable(); 1810 | $selectable && $selectable.removeClass(this.classes.cursor); 1811 | }, 1812 | _ensureVisible: function ensureVisible($el) { 1813 | var elTop, elBottom, nodeScrollTop, nodeHeight; 1814 | elTop = $el.position().top; 1815 | elBottom = elTop + $el.outerHeight(true); 1816 | nodeScrollTop = this.$node.scrollTop(); 1817 | nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); 1818 | if (elTop < 0) { 1819 | this.$node.scrollTop(nodeScrollTop + elTop); 1820 | } else if (nodeHeight < elBottom) { 1821 | this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); 1822 | } 1823 | }, 1824 | bind: function() { 1825 | var that = this, onSelectableClick; 1826 | onSelectableClick = _.bind(this._onSelectableClick, this); 1827 | this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); 1828 | _.each(this.datasets, function(dataset) { 1829 | 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); 1830 | }); 1831 | return this; 1832 | }, 1833 | isOpen: function isOpen() { 1834 | return this.$node.hasClass(this.classes.open); 1835 | }, 1836 | open: function open() { 1837 | this.$node.addClass(this.classes.open); 1838 | }, 1839 | close: function close() { 1840 | this.$node.removeClass(this.classes.open); 1841 | this._removeCursor(); 1842 | }, 1843 | setLanguageDirection: function setLanguageDirection(dir) { 1844 | this.$node.attr("dir", dir); 1845 | }, 1846 | selectableRelativeToCursor: function selectableRelativeToCursor(delta) { 1847 | var $selectables, $oldCursor, oldIndex, newIndex; 1848 | $oldCursor = this.getActiveSelectable(); 1849 | $selectables = this._getSelectables(); 1850 | oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; 1851 | newIndex = oldIndex + delta; 1852 | newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; 1853 | newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; 1854 | return newIndex === -1 ? null : $selectables.eq(newIndex); 1855 | }, 1856 | setCursor: function setCursor($selectable) { 1857 | this._removeCursor(); 1858 | if ($selectable = $selectable && $selectable.first()) { 1859 | $selectable.addClass(this.classes.cursor); 1860 | this._ensureVisible($selectable); 1861 | } 1862 | }, 1863 | getSelectableData: function getSelectableData($el) { 1864 | return $el && $el.length ? Dataset.extractData($el) : null; 1865 | }, 1866 | getActiveSelectable: function getActiveSelectable() { 1867 | var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); 1868 | return $selectable.length ? $selectable : null; 1869 | }, 1870 | getTopSelectable: function getTopSelectable() { 1871 | var $selectable = this._getSelectables().first(); 1872 | return $selectable.length ? $selectable : null; 1873 | }, 1874 | update: function update(query) { 1875 | var isValidUpdate = query !== this.query; 1876 | if (isValidUpdate) { 1877 | this.query = query; 1878 | _.each(this.datasets, updateDataset); 1879 | } 1880 | return isValidUpdate; 1881 | function updateDataset(dataset) { 1882 | dataset.update(query); 1883 | } 1884 | }, 1885 | empty: function empty() { 1886 | _.each(this.datasets, clearDataset); 1887 | this.query = null; 1888 | this.$node.addClass(this.classes.empty); 1889 | function clearDataset(dataset) { 1890 | dataset.clear(); 1891 | } 1892 | }, 1893 | destroy: function destroy() { 1894 | this.$node.off(".tt"); 1895 | this.$node = $("
"); 1896 | _.each(this.datasets, destroyDataset); 1897 | function destroyDataset(dataset) { 1898 | dataset.destroy(); 1899 | } 1900 | } 1901 | }); 1902 | return Menu; 1903 | }(); 1904 | var DefaultMenu = function() { 1905 | "use strict"; 1906 | var s = Menu.prototype; 1907 | function DefaultMenu() { 1908 | Menu.apply(this, [].slice.call(arguments, 0)); 1909 | } 1910 | _.mixin(DefaultMenu.prototype, Menu.prototype, { 1911 | open: function open() { 1912 | !this._allDatasetsEmpty() && this._show(); 1913 | return s.open.apply(this, [].slice.call(arguments, 0)); 1914 | }, 1915 | close: function close() { 1916 | this._hide(); 1917 | return s.close.apply(this, [].slice.call(arguments, 0)); 1918 | }, 1919 | _onRendered: function onRendered() { 1920 | if (this._allDatasetsEmpty()) { 1921 | this._hide(); 1922 | } else { 1923 | this.isOpen() && this._show(); 1924 | } 1925 | return s._onRendered.apply(this, [].slice.call(arguments, 0)); 1926 | }, 1927 | _onCleared: function onCleared() { 1928 | if (this._allDatasetsEmpty()) { 1929 | this._hide(); 1930 | } else { 1931 | this.isOpen() && this._show(); 1932 | } 1933 | return s._onCleared.apply(this, [].slice.call(arguments, 0)); 1934 | }, 1935 | setLanguageDirection: function setLanguageDirection(dir) { 1936 | this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); 1937 | return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); 1938 | }, 1939 | _hide: function hide() { 1940 | this.$node.hide(); 1941 | }, 1942 | _show: function show() { 1943 | this.$node.css("display", "block"); 1944 | } 1945 | }); 1946 | return DefaultMenu; 1947 | }(); 1948 | var Typeahead = function() { 1949 | "use strict"; 1950 | function Typeahead(o, www) { 1951 | var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; 1952 | o = o || {}; 1953 | if (!o.input) { 1954 | $.error("missing input"); 1955 | } 1956 | if (!o.menu) { 1957 | $.error("missing menu"); 1958 | } 1959 | if (!o.eventBus) { 1960 | $.error("missing event bus"); 1961 | } 1962 | www.mixin(this); 1963 | this.eventBus = o.eventBus; 1964 | this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; 1965 | this.input = o.input; 1966 | this.menu = o.menu; 1967 | this.enabled = true; 1968 | this.active = false; 1969 | this.input.hasFocus() && this.activate(); 1970 | this.dir = this.input.getLangDir(); 1971 | this._hacks(); 1972 | 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); 1973 | onFocused = c(this, "activate", "open", "_onFocused"); 1974 | onBlurred = c(this, "deactivate", "_onBlurred"); 1975 | onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); 1976 | onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); 1977 | onEscKeyed = c(this, "isActive", "_onEscKeyed"); 1978 | onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); 1979 | onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); 1980 | onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); 1981 | onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); 1982 | onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); 1983 | onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); 1984 | 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); 1985 | } 1986 | _.mixin(Typeahead.prototype, { 1987 | _hacks: function hacks() { 1988 | var $input, $menu; 1989 | $input = this.input.$input || $("
"); 1990 | $menu = this.menu.$node || $("
"); 1991 | $input.on("blur.tt", function($e) { 1992 | var active, isActive, hasActive; 1993 | active = document.activeElement; 1994 | isActive = $menu.is(active); 1995 | hasActive = $menu.has(active).length > 0; 1996 | if (_.isMsie() && (isActive || hasActive)) { 1997 | $e.preventDefault(); 1998 | $e.stopImmediatePropagation(); 1999 | _.defer(function() { 2000 | $input.focus(); 2001 | }); 2002 | } 2003 | }); 2004 | $menu.on("mousedown.tt", function($e) { 2005 | $e.preventDefault(); 2006 | }); 2007 | }, 2008 | _onSelectableClicked: function onSelectableClicked(type, $el) { 2009 | this.select($el); 2010 | }, 2011 | _onDatasetCleared: function onDatasetCleared() { 2012 | this._updateHint(); 2013 | }, 2014 | _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { 2015 | this._updateHint(); 2016 | this.eventBus.trigger("render", suggestions, async, dataset); 2017 | }, 2018 | _onAsyncRequested: function onAsyncRequested(type, dataset, query) { 2019 | this.eventBus.trigger("asyncrequest", query, dataset); 2020 | }, 2021 | _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { 2022 | this.eventBus.trigger("asynccancel", query, dataset); 2023 | }, 2024 | _onAsyncReceived: function onAsyncReceived(type, dataset, query) { 2025 | this.eventBus.trigger("asyncreceive", query, dataset); 2026 | }, 2027 | _onFocused: function onFocused() { 2028 | this._minLengthMet() && this.menu.update(this.input.getQuery()); 2029 | }, 2030 | _onBlurred: function onBlurred() { 2031 | if (this.input.hasQueryChangedSinceLastFocus()) { 2032 | this.eventBus.trigger("change", this.input.getQuery()); 2033 | } 2034 | }, 2035 | _onEnterKeyed: function onEnterKeyed(type, $e) { 2036 | var $selectable; 2037 | if ($selectable = this.menu.getActiveSelectable()) { 2038 | this.select($selectable) && $e.preventDefault(); 2039 | } 2040 | }, 2041 | _onTabKeyed: function onTabKeyed(type, $e) { 2042 | var $selectable; 2043 | if ($selectable = this.menu.getActiveSelectable()) { 2044 | this.select($selectable) && $e.preventDefault(); 2045 | } else if ($selectable = this.menu.getTopSelectable()) { 2046 | this.autocomplete($selectable) && $e.preventDefault(); 2047 | } 2048 | }, 2049 | _onEscKeyed: function onEscKeyed() { 2050 | this.close(); 2051 | }, 2052 | _onUpKeyed: function onUpKeyed() { 2053 | this.moveCursor(-1); 2054 | }, 2055 | _onDownKeyed: function onDownKeyed() { 2056 | this.moveCursor(+1); 2057 | }, 2058 | _onLeftKeyed: function onLeftKeyed() { 2059 | if (this.dir === "rtl" && this.input.isCursorAtEnd()) { 2060 | this.autocomplete(this.menu.getTopSelectable()); 2061 | } 2062 | }, 2063 | _onRightKeyed: function onRightKeyed() { 2064 | if (this.dir === "ltr" && this.input.isCursorAtEnd()) { 2065 | this.autocomplete(this.menu.getTopSelectable()); 2066 | } 2067 | }, 2068 | _onQueryChanged: function onQueryChanged(e, query) { 2069 | this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); 2070 | }, 2071 | _onWhitespaceChanged: function onWhitespaceChanged() { 2072 | this._updateHint(); 2073 | }, 2074 | _onLangDirChanged: function onLangDirChanged(e, dir) { 2075 | if (this.dir !== dir) { 2076 | this.dir = dir; 2077 | this.menu.setLanguageDirection(dir); 2078 | } 2079 | }, 2080 | _openIfActive: function openIfActive() { 2081 | this.isActive() && this.open(); 2082 | }, 2083 | _minLengthMet: function minLengthMet(query) { 2084 | query = _.isString(query) ? query : this.input.getQuery() || ""; 2085 | return query.length >= this.minLength; 2086 | }, 2087 | _updateHint: function updateHint() { 2088 | var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; 2089 | $selectable = this.menu.getTopSelectable(); 2090 | data = this.menu.getSelectableData($selectable); 2091 | val = this.input.getInputValue(); 2092 | if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { 2093 | query = Input.normalizeQuery(val); 2094 | escapedQuery = _.escapeRegExChars(query); 2095 | frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); 2096 | match = frontMatchRegEx.exec(data.val); 2097 | match && this.input.setHint(val + match[1]); 2098 | } else { 2099 | this.input.clearHint(); 2100 | } 2101 | }, 2102 | isEnabled: function isEnabled() { 2103 | return this.enabled; 2104 | }, 2105 | enable: function enable() { 2106 | this.enabled = true; 2107 | }, 2108 | disable: function disable() { 2109 | this.enabled = false; 2110 | }, 2111 | isActive: function isActive() { 2112 | return this.active; 2113 | }, 2114 | activate: function activate() { 2115 | if (this.isActive()) { 2116 | return true; 2117 | } else if (!this.isEnabled() || this.eventBus.before("active")) { 2118 | return false; 2119 | } else { 2120 | this.active = true; 2121 | this.eventBus.trigger("active"); 2122 | return true; 2123 | } 2124 | }, 2125 | deactivate: function deactivate() { 2126 | if (!this.isActive()) { 2127 | return true; 2128 | } else if (this.eventBus.before("idle")) { 2129 | return false; 2130 | } else { 2131 | this.active = false; 2132 | this.close(); 2133 | this.eventBus.trigger("idle"); 2134 | return true; 2135 | } 2136 | }, 2137 | isOpen: function isOpen() { 2138 | return this.menu.isOpen(); 2139 | }, 2140 | open: function open() { 2141 | if (!this.isOpen() && !this.eventBus.before("open")) { 2142 | this.menu.open(); 2143 | this._updateHint(); 2144 | this.eventBus.trigger("open"); 2145 | } 2146 | return this.isOpen(); 2147 | }, 2148 | close: function close() { 2149 | if (this.isOpen() && !this.eventBus.before("close")) { 2150 | this.menu.close(); 2151 | this.input.clearHint(); 2152 | this.input.resetInputValue(); 2153 | this.eventBus.trigger("close"); 2154 | } 2155 | return !this.isOpen(); 2156 | }, 2157 | setVal: function setVal(val) { 2158 | this.input.setQuery(_.toStr(val)); 2159 | }, 2160 | getVal: function getVal() { 2161 | return this.input.getQuery(); 2162 | }, 2163 | select: function select($selectable) { 2164 | var data = this.menu.getSelectableData($selectable); 2165 | if (data && !this.eventBus.before("select", data.obj)) { 2166 | this.input.setQuery(data.val, true); 2167 | this.eventBus.trigger("select", data.obj); 2168 | this.close(); 2169 | return true; 2170 | } 2171 | return false; 2172 | }, 2173 | autocomplete: function autocomplete($selectable) { 2174 | var query, data, isValid; 2175 | query = this.input.getQuery(); 2176 | data = this.menu.getSelectableData($selectable); 2177 | isValid = data && query !== data.val; 2178 | if (isValid && !this.eventBus.before("autocomplete", data.obj)) { 2179 | this.input.setQuery(data.val); 2180 | this.eventBus.trigger("autocomplete", data.obj); 2181 | return true; 2182 | } 2183 | return false; 2184 | }, 2185 | moveCursor: function moveCursor(delta) { 2186 | var query, $candidate, data, payload, cancelMove; 2187 | query = this.input.getQuery(); 2188 | $candidate = this.menu.selectableRelativeToCursor(delta); 2189 | data = this.menu.getSelectableData($candidate); 2190 | payload = data ? data.obj : null; 2191 | cancelMove = this._minLengthMet() && this.menu.update(query); 2192 | if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { 2193 | this.menu.setCursor($candidate); 2194 | if (data) { 2195 | this.input.setInputValue(data.val); 2196 | } else { 2197 | this.input.resetInputValue(); 2198 | this._updateHint(); 2199 | } 2200 | this.eventBus.trigger("cursorchange", payload); 2201 | return true; 2202 | } 2203 | return false; 2204 | }, 2205 | destroy: function destroy() { 2206 | this.input.destroy(); 2207 | this.menu.destroy(); 2208 | } 2209 | }); 2210 | return Typeahead; 2211 | function c(ctx) { 2212 | var methods = [].slice.call(arguments, 1); 2213 | return function() { 2214 | var args = [].slice.call(arguments); 2215 | _.each(methods, function(method) { 2216 | return ctx[method].apply(ctx, args); 2217 | }); 2218 | }; 2219 | } 2220 | }(); 2221 | (function() { 2222 | "use strict"; 2223 | var old, keys, methods; 2224 | old = $.fn.typeahead; 2225 | keys = { 2226 | www: "tt-www", 2227 | attrs: "tt-attrs", 2228 | typeahead: "tt-typeahead" 2229 | }; 2230 | methods = { 2231 | initialize: function initialize(o, datasets) { 2232 | var www; 2233 | datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); 2234 | o = o || {}; 2235 | www = WWW(o.classNames); 2236 | return this.each(attach); 2237 | function attach() { 2238 | var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; 2239 | _.each(datasets, function(d) { 2240 | d.highlight = !!o.highlight; 2241 | }); 2242 | $input = $(this); 2243 | $wrapper = $(www.html.wrapper); 2244 | $hint = $elOrNull(o.hint); 2245 | $menu = $elOrNull(o.menu); 2246 | defaultHint = o.hint !== false && !$hint; 2247 | defaultMenu = o.menu !== false && !$menu; 2248 | defaultHint && ($hint = buildHintFromInput($input, www)); 2249 | defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); 2250 | $hint && $hint.val(""); 2251 | $input = prepInput($input, www); 2252 | if (defaultHint || defaultMenu) { 2253 | $wrapper.css(www.css.wrapper); 2254 | $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); 2255 | $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); 2256 | } 2257 | MenuConstructor = defaultMenu ? DefaultMenu : Menu; 2258 | eventBus = new EventBus({ 2259 | el: $input 2260 | }); 2261 | input = new Input({ 2262 | hint: $hint, 2263 | input: $input 2264 | }, www); 2265 | menu = new MenuConstructor({ 2266 | node: $menu, 2267 | datasets: datasets 2268 | }, www); 2269 | typeahead = new Typeahead({ 2270 | input: input, 2271 | menu: menu, 2272 | eventBus: eventBus, 2273 | minLength: o.minLength 2274 | }, www); 2275 | $input.data(keys.www, www); 2276 | $input.data(keys.typeahead, typeahead); 2277 | } 2278 | }, 2279 | isEnabled: function isEnabled() { 2280 | var enabled; 2281 | ttEach(this.first(), function(t) { 2282 | enabled = t.isEnabled(); 2283 | }); 2284 | return enabled; 2285 | }, 2286 | enable: function enable() { 2287 | ttEach(this, function(t) { 2288 | t.enable(); 2289 | }); 2290 | return this; 2291 | }, 2292 | disable: function disable() { 2293 | ttEach(this, function(t) { 2294 | t.disable(); 2295 | }); 2296 | return this; 2297 | }, 2298 | isActive: function isActive() { 2299 | var active; 2300 | ttEach(this.first(), function(t) { 2301 | active = t.isActive(); 2302 | }); 2303 | return active; 2304 | }, 2305 | activate: function activate() { 2306 | ttEach(this, function(t) { 2307 | t.activate(); 2308 | }); 2309 | return this; 2310 | }, 2311 | deactivate: function deactivate() { 2312 | ttEach(this, function(t) { 2313 | t.deactivate(); 2314 | }); 2315 | return this; 2316 | }, 2317 | isOpen: function isOpen() { 2318 | var open; 2319 | ttEach(this.first(), function(t) { 2320 | open = t.isOpen(); 2321 | }); 2322 | return open; 2323 | }, 2324 | open: function open() { 2325 | ttEach(this, function(t) { 2326 | t.open(); 2327 | }); 2328 | return this; 2329 | }, 2330 | close: function close() { 2331 | ttEach(this, function(t) { 2332 | t.close(); 2333 | }); 2334 | return this; 2335 | }, 2336 | select: function select(el) { 2337 | var success = false, $el = $(el); 2338 | ttEach(this.first(), function(t) { 2339 | success = t.select($el); 2340 | }); 2341 | return success; 2342 | }, 2343 | autocomplete: function autocomplete(el) { 2344 | var success = false, $el = $(el); 2345 | ttEach(this.first(), function(t) { 2346 | success = t.autocomplete($el); 2347 | }); 2348 | return success; 2349 | }, 2350 | moveCursor: function moveCursoe(delta) { 2351 | var success = false; 2352 | ttEach(this.first(), function(t) { 2353 | success = t.moveCursor(delta); 2354 | }); 2355 | return success; 2356 | }, 2357 | val: function val(newVal) { 2358 | var query; 2359 | if (!arguments.length) { 2360 | ttEach(this.first(), function(t) { 2361 | query = t.getVal(); 2362 | }); 2363 | return query; 2364 | } else { 2365 | ttEach(this, function(t) { 2366 | t.setVal(newVal); 2367 | }); 2368 | return this; 2369 | } 2370 | }, 2371 | destroy: function destroy() { 2372 | ttEach(this, function(typeahead, $input) { 2373 | revert($input); 2374 | typeahead.destroy(); 2375 | }); 2376 | return this; 2377 | } 2378 | }; 2379 | $.fn.typeahead = function(method) { 2380 | if (methods[method]) { 2381 | return methods[method].apply(this, [].slice.call(arguments, 1)); 2382 | } else { 2383 | return methods.initialize.apply(this, arguments); 2384 | } 2385 | }; 2386 | $.fn.typeahead.noConflict = function noConflict() { 2387 | $.fn.typeahead = old; 2388 | return this; 2389 | }; 2390 | function ttEach($els, fn) { 2391 | $els.each(function() { 2392 | var $input = $(this), typeahead; 2393 | (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); 2394 | }); 2395 | } 2396 | function buildHintFromInput($input, www) { 2397 | return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ 2398 | autocomplete: "off", 2399 | spellcheck: "false", 2400 | tabindex: -1 2401 | }); 2402 | } 2403 | function prepInput($input, www) { 2404 | $input.data(keys.attrs, { 2405 | dir: $input.attr("dir"), 2406 | autocomplete: $input.attr("autocomplete"), 2407 | spellcheck: $input.attr("spellcheck"), 2408 | style: $input.attr("style") 2409 | }); 2410 | $input.addClass(www.classes.input).attr({ 2411 | autocomplete: "off", 2412 | spellcheck: false 2413 | }); 2414 | try { 2415 | !$input.attr("dir") && $input.attr("dir", "auto"); 2416 | } catch (e) {} 2417 | return $input; 2418 | } 2419 | function getBackgroundStyles($el) { 2420 | return { 2421 | backgroundAttachment: $el.css("background-attachment"), 2422 | backgroundClip: $el.css("background-clip"), 2423 | backgroundColor: $el.css("background-color"), 2424 | backgroundImage: $el.css("background-image"), 2425 | backgroundOrigin: $el.css("background-origin"), 2426 | backgroundPosition: $el.css("background-position"), 2427 | backgroundRepeat: $el.css("background-repeat"), 2428 | backgroundSize: $el.css("background-size") 2429 | }; 2430 | } 2431 | function revert($input) { 2432 | var www, $wrapper; 2433 | www = $input.data(keys.www); 2434 | $wrapper = $input.parent().filter(www.selectors.wrapper); 2435 | _.each($input.data(keys.attrs), function(val, key) { 2436 | _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); 2437 | }); 2438 | $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); 2439 | if ($wrapper.length) { 2440 | $input.detach().insertAfter($wrapper); 2441 | $wrapper.remove(); 2442 | } 2443 | } 2444 | function $elOrNull(obj) { 2445 | var isValid, $el; 2446 | isValid = _.isJQuery(obj) || _.isElement(obj); 2447 | $el = isValid ? $(obj).first() : []; 2448 | return $el.length ? $el : null; 2449 | } 2450 | })(); 2451 | }); --------------------------------------------------------------------------------