├── example ├── youtube │ ├── .gitignore │ ├── js │ │ ├── dispatcher │ │ │ └── AppDispatcher.js │ │ ├── constants │ │ │ └── OptionConstants.js │ │ ├── actions │ │ │ └── OptionActions.js │ │ ├── stores │ │ │ └── OptionStore.js │ │ ├── utils │ │ │ └── OptionsWebAPIUtils.js │ │ ├── components │ │ │ └── OptionTemplate.jsx │ │ └── app.jsx │ ├── index.html │ ├── README.md │ ├── package.json │ └── css │ │ └── youtube.css └── netflix │ ├── index.html │ ├── package.json │ ├── README.md │ ├── js │ ├── components │ │ └── OptionTemplate.jsx │ ├── utils │ │ └── OptionWebAPIUtils.js │ └── app.jsx │ └── css │ └── netflix.css ├── .gitignore ├── dist ├── npm │ ├── index.js │ ├── utils │ │ ├── get_text_direction.js │ │ ├── rtl_chars_regexp.js │ │ └── neutral_chars_regexp.js │ ├── package.json │ ├── components │ │ ├── aria_status.js │ │ ├── input.js │ │ └── typeahead.js │ └── README.md └── react-typeahead-component.min.js ├── src ├── index.js ├── utils │ ├── get_text_direction.js │ ├── rtl_chars_regexp.js │ ├── _browser_unit_ │ │ └── get_text_direction_test.js │ └── neutral_chars_regexp.js └── components │ ├── _browser_unit_ │ ├── aria_status_test.jsx │ ├── input_test.jsx │ └── typeahead_test.jsx │ ├── aria_status.jsx │ ├── input.jsx │ └── typeahead.jsx ├── AUTHORS ├── test └── polyfill │ └── bind.js ├── LICENSE ├── karma.conf.js ├── package.json └── README.md /example/youtube/.gitignore: -------------------------------------------------------------------------------- 1 | js/bundle.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/build/* 3 | coverage 4 | -------------------------------------------------------------------------------- /dist/npm/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./components/typeahead'); 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./components/typeahead.jsx'); 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ezequiel Rodriguez (https://www.github.com/ezequiel) 2 | -------------------------------------------------------------------------------- /example/youtube/js/dispatcher/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require('flux').Dispatcher; 2 | 3 | module.exports = new Dispatcher(); 4 | -------------------------------------------------------------------------------- /example/youtube/js/constants/OptionConstants.js: -------------------------------------------------------------------------------- 1 | var keyMirror = require('keymirror'); 2 | 3 | module.exports = { 4 | ActionTypes: keyMirror({ 5 | GET_OPTIONS_SUCCESS: null 6 | }) 7 | }; 8 | -------------------------------------------------------------------------------- /example/netflix/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Netflix Typeahead 5 | 6 | 7 | 8 |

Netflix Typeahead Widget

9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/netflix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typeahead-netflix-example", 3 | "version": "0.1.0", 4 | "description": "Netflix's autocomplete widget, written using react-typeahead-component.", 5 | "main": "app.jsx", 6 | "scripts": { 7 | "start": "watchify js/app.jsx -t reactify -o js/bundle.js -v -d" 8 | }, 9 | "author": "Ezequiel Rodriguez", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "classnames": "^2.1.1", 13 | "jsonp": "^0.2.0", 14 | "reactify": "^1.1.1", 15 | "rx": "^2.5.3", 16 | "watchify": "^3.2.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/youtube/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | YouTube Typeahead 5 | 6 | 7 | 8 | 9 |

YouTube Autocomplete Widget

10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/utils/get_text_direction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RTLCharactersRegExp = require('./rtl_chars_regexp'), 4 | NeutralCharactersRegExp = require('./neutral_chars_regexp'), 5 | startsWithRTL = new RegExp('^(?:' + NeutralCharactersRegExp + ')*(?:' + RTLCharactersRegExp + ')'), 6 | neutralText = new RegExp('^(?:' + NeutralCharactersRegExp + ')*$'); 7 | 8 | module.exports = function(text) { 9 | var dir = 'ltr'; 10 | 11 | if (startsWithRTL.test(text)) { 12 | dir = 'rtl'; 13 | } else if (neutralText.test(text)) { 14 | dir = null; 15 | } 16 | 17 | return dir; 18 | }; 19 | -------------------------------------------------------------------------------- /dist/npm/utils/get_text_direction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RTLCharactersRegExp = require('./rtl_chars_regexp'), 4 | NeutralCharactersRegExp = require('./neutral_chars_regexp'), 5 | startsWithRTL = new RegExp('^(?:' + NeutralCharactersRegExp + ')*(?:' + RTLCharactersRegExp + ')'), 6 | neutralText = new RegExp('^(?:' + NeutralCharactersRegExp + ')*$'); 7 | 8 | module.exports = function(text) { 9 | var dir = 'ltr'; 10 | 11 | if (startsWithRTL.test(text)) { 12 | dir = 'rtl'; 13 | } else if (neutralText.test(text)) { 14 | dir = null; 15 | } 16 | 17 | return dir; 18 | }; 19 | -------------------------------------------------------------------------------- /example/youtube/js/actions/OptionActions.js: -------------------------------------------------------------------------------- 1 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 2 | var ActionTypes = require('../constants/OptionConstants').ActionTypes; 3 | var OptionWebAPIUtils = require('../utils/OptionsWebAPIUtils'); 4 | 5 | var OptionActions = { 6 | getOptions: function(inputValue) { 7 | OptionWebAPIUtils.fetchOptions(inputValue).then(function(data) { 8 | AppDispatcher.dispatch({ 9 | actionType: ActionTypes.GET_OPTIONS_SUCCESS, 10 | options: data 11 | }); 12 | }); 13 | } 14 | }; 15 | 16 | module.exports = OptionActions; 17 | -------------------------------------------------------------------------------- /example/youtube/README.md: -------------------------------------------------------------------------------- 1 | YouTube's autocomplete widget, written using react-typeahead-component. 2 | ==================================================================== 3 | 4 | ![youtube11](https://cloud.githubusercontent.com/assets/368069/7670388/3ab8d8ae-fc57-11e4-8fc1-7ff020e76bf1.gif) 5 | 6 | This example uses the [Flux application architecture](https://facebook.github.io/flux/) in conjunction with YouTube's API. 7 | 8 | ## Running 9 | 10 | * Install this example's dependecies via `npm i`. 11 | * Run `npm start` to build the bundle. 12 | * This will start the watcher. It will rebuild the bundle upon any file changes. 13 | * Load `index.html` in a browser. 14 | -------------------------------------------------------------------------------- /example/youtube/js/stores/OptionStore.js: -------------------------------------------------------------------------------- 1 | var AppDispatcher = require('../dispatcher/AppDispatcher'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var ActionTypes = require('../constants/OptionConstants').ActionTypes; 4 | var assign = require('object-assign'); 5 | 6 | var OptionStore = assign({}, EventEmitter.prototype, { 7 | emitChange: function(options) { 8 | this.emit('change', options); 9 | } 10 | }); 11 | 12 | AppDispatcher.register(function(action) { 13 | switch(action.actionType) { 14 | case ActionTypes.GET_OPTIONS_SUCCESS: 15 | OptionStore.emitChange(action.options); 16 | break; 17 | } 18 | }); 19 | 20 | module.exports = OptionStore; 21 | -------------------------------------------------------------------------------- /example/netflix/README.md: -------------------------------------------------------------------------------- 1 | Netflix typeahead widget, written using react-typeahead-component. 2 | ==================================================================== 3 | 4 | ![netflix](https://cloud.githubusercontent.com/assets/368069/8123520/ed4419e8-107f-11e5-8134-d13c22fcf5d2.gif) 5 | 6 | It uses the [`RxJs`](http://reactive-extensions.github.io/RxJS/) in conjunction with Netflix's API. 7 | 8 | It also takes from Netflix's newest layout redesign. 9 | 10 | ## Running 11 | 12 | * Install this example's dependecies via `npm i`. 13 | * Run `npm start` to build the bundle. 14 | * This will start the watcher. It will rebuild the bundle upon any file changes. 15 | * Load `index.html` in a browser. 16 | -------------------------------------------------------------------------------- /example/youtube/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typeahead-youtube-example", 3 | "version": "0.1.0", 4 | "description": "YouTube's autocomplete widget, written using react-typeahead-component.", 5 | "main": "app.jsx", 6 | "scripts": { 7 | "start": "watchify js/app.jsx -t reactify -o js/bundle.js -v -d" 8 | }, 9 | "author": "Ezequiel Rodriguez", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "classnames": "^2.1.1", 13 | "events": "^1.0.2", 14 | "flux": "^2.0.3", 15 | "jsonp": "^0.2.0", 16 | "keymirror": "^0.1.1", 17 | "lodash.throttle": "^3.0.2", 18 | "object-assign": "^2.0.0", 19 | "reactify": "^1.1.1", 20 | "watchify": "^3.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /dist/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typeahead-component", 3 | "description": "Typeahead, written using the React.js library.", 4 | "author": "Ezequiel Rodriguez ", 5 | "version": "0.9.0", 6 | "main": "index.js", 7 | "license": "MIT", 8 | "bugs": "https://github.com/ezequiel/react-typeahead-component/issues", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ezequiel/react-typeahead-component.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "reactjs", 16 | "typeahead", 17 | "autocomplete", 18 | "react-component", 19 | "component" 20 | ], 21 | "peerDependencies": { 22 | "react": ">=0.13.1 <1.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/polyfill/bind.js: -------------------------------------------------------------------------------- 1 | // Credit goes to Mozilla: http://mdn.io/bind#Polyfill 2 | if (!Function.prototype.bind) { 3 | Function.prototype.bind = function(oThis) { 4 | if (typeof this !== 'function') { 5 | // closest thing possible to the ECMAScript 5 6 | // internal IsCallable function 7 | throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); 8 | } 9 | 10 | var aArgs = Array.prototype.slice.call(arguments, 1), 11 | fToBind = this, 12 | fNOP = function() {}, 13 | fBound = function() { 14 | return fToBind.apply(this instanceof fNOP 15 | ? this 16 | : oThis, 17 | aArgs.concat(Array.prototype.slice.call(arguments))); 18 | }; 19 | 20 | fNOP.prototype = this.prototype || {}; 21 | fBound.prototype = new fNOP(); 22 | 23 | return fBound; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /example/youtube/css/youtube.css: -------------------------------------------------------------------------------- 1 | #yt-typeahead, h1 { 2 | font-family: Roboto,arial,sans-serif; 3 | } 4 | 5 | #yt-typeahead { 6 | width: 583px 7 | } 8 | 9 | .react-typeahead-input-container { 10 | border: 1px solid #ccc; 11 | box-shadow: inset 0 1px 2px #eee; 12 | } 13 | 14 | .react-typeahead-input-container input { 15 | font-size: 16px; 16 | height: 27px; 17 | padding: 2px 6px; 18 | width: 100%; 19 | border: 0; 20 | outline: none; 21 | } 22 | 23 | .react-typeahead-input-container input:focus { 24 | border: 1px solid #1c62b9; 25 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); 26 | } 27 | 28 | .react-typeahead-options { 29 | margin: 0; 30 | padding: 0; 31 | list-style-type: none; 32 | border: 1px solid #ccc; 33 | border-top: 0; 34 | cursor: default; 35 | box-shadow: 0 2px 4px rgba(0,0,0,0.2) 36 | } 37 | 38 | .yt-option { 39 | padding: 0 6px; 40 | } 41 | 42 | .yt-selected-option { 43 | background-color: #eee; 44 | } 45 | -------------------------------------------------------------------------------- /example/youtube/js/utils/OptionsWebAPIUtils.js: -------------------------------------------------------------------------------- 1 | var JSONP = require('jsonp'); 2 | var YOUTUBE_API_ENDPOINT = "https://clients1.google.com/complete/search?client=youtube&ds=yt"; 3 | 4 | var cache = { 5 | '': [] 6 | }; 7 | 8 | module.exports = { 9 | fetchOptions: function(query) { 10 | return new Promise(function(resolve, reject) { 11 | var result = cache[query], url; 12 | 13 | if (result !== undefined) { 14 | resolve(result); 15 | } else { 16 | url = YOUTUBE_API_ENDPOINT + '&q=' + query; 17 | 18 | JSONP(url, function(error, data) { 19 | if (error) { 20 | reject(error); 21 | } else { 22 | result = data[1].map(function(datum) { 23 | return datum[0]; 24 | }); 25 | 26 | cache[query] = result; 27 | 28 | resolve(result); 29 | } 30 | }); 31 | } 32 | }); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ezequiel Rodriguez 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/netflix/js/components/OptionTemplate.jsx: -------------------------------------------------------------------------------- 1 | var cx = require('classnames'); 2 | 3 | module.exports = React.createClass({ 4 | displayName: 'OptionTemplate', 5 | 6 | propTypes: { 7 | data: React.PropTypes.any, 8 | isSelected: React.PropTypes.bool 9 | }, 10 | 11 | render: function() { 12 | var classes = cx({ 13 | 'option-value': true, 14 | 'selected-option': this.props.isSelected 15 | }), 16 | optionData = this.props.data; 17 | 18 | return ( 19 |
20 | {this.renderHeader(optionData)} 21 |
22 | {optionData.value} 23 |
24 |
25 | ) 26 | }, 27 | 28 | renderHeader: function(option) { 29 | // If this option is the first of its type, 30 | // then render the header. 31 | if (option.index === 0) { 32 | return ( 33 |
34 | {option.type} 35 |
36 | ); 37 | } 38 | 39 | return null; 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /example/youtube/js/components/OptionTemplate.jsx: -------------------------------------------------------------------------------- 1 | var cx = require('classnames'); 2 | 3 | module.exports = React.createClass({ 4 | displayName: 'OptionTemplate', 5 | 6 | propTypes: { 7 | data: React.PropTypes.any, 8 | inputValue: React.PropTypes.string, 9 | isSelected: React.PropTypes.bool 10 | }, 11 | 12 | render: function() { 13 | var classes = cx({ 14 | 'yt-option': true, 15 | 'yt-selected-option': this.props.isSelected 16 | }); 17 | 18 | return ( 19 |
20 | {this.renderOption()} 21 |
22 | ); 23 | }, 24 | 25 | renderOption: function() { 26 | var optionData = this.props.data, 27 | inputValue = this.props.userInputValue; 28 | 29 | if (optionData.indexOf(inputValue) === 0) { 30 | return ( 31 | 32 | {inputValue} 33 | 34 | {optionData.slice(inputValue.length)} 35 | 36 | 37 | ); 38 | } 39 | 40 | return optionData; 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/_browser_unit_/aria_status_test.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'), 4 | TestUtils = React.addons.TestUtils, 5 | AriaStatus = require('../aria_status.jsx'); 6 | 7 | describe('AriaStatus', function() { 8 | describe('#setTextContent', function() { 9 | it('should set the text content of the component', function() { 10 | var ariaStatusInstance = TestUtils.renderIntoDocument( 11 | 12 | ), 13 | text = 'this is a test'; 14 | 15 | ariaStatusInstance.setTextContent(text); 16 | expect(React.findDOMNode(ariaStatusInstance).textContent).to.equal(text); 17 | }); 18 | 19 | it('should set the text content to an empty string when passed null/undefined', function() { 20 | var ariaStatusInstance = TestUtils.renderIntoDocument( 21 | 22 | ); 23 | 24 | [null, undefined].forEach(function(value) { 25 | ariaStatusInstance.setTextContent(value); 26 | expect(React.findDOMNode(ariaStatusInstance).textContent).to.equal(''); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/aria_status.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | module.exports = React.createClass({ 6 | displayName: 'Aria Status', 7 | 8 | propTypes: process.env.NODE_ENV === 'production' ? {} : { 9 | message: React.PropTypes.string 10 | }, 11 | 12 | componentDidMount: function() { 13 | var _this = this; 14 | 15 | // This is needed as `componentDidUpdate` 16 | // does not fire on the initial render. 17 | _this.setTextContent(_this.props.message); 18 | }, 19 | 20 | componentDidUpdate: function() { 21 | var _this = this; 22 | 23 | _this.setTextContent(_this.props.message); 24 | }, 25 | 26 | render: function() { 27 | return ( 28 | 36 | ); 37 | }, 38 | 39 | // We cannot set `textContent` directly in `render`, 40 | // because React adds/deletes text nodes when rendering, 41 | // which confuses screen readers and doesn't cause them to read changes. 42 | setTextContent: function(textContent) { 43 | // We could set `innerHTML`, but it's better to avoid it. 44 | this.getDOMNode().textContent = textContent || ''; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /dist/npm/components/aria_status.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | module.exports = React.createClass({ 6 | displayName: 'Aria Status', 7 | 8 | propTypes: process.env.NODE_ENV === 'production' ? {} : { 9 | message: React.PropTypes.string 10 | }, 11 | 12 | componentDidMount: function() { 13 | var _this = this; 14 | 15 | // This is needed as `componentDidUpdate` 16 | // does not fire on the initial render. 17 | _this.setTextContent(_this.props.message); 18 | }, 19 | 20 | componentDidUpdate: function() { 21 | var _this = this; 22 | 23 | _this.setTextContent(_this.props.message); 24 | }, 25 | 26 | render: function() { 27 | return ( 28 | React.createElement("span", { 29 | role: "status", 30 | "aria-live": "polite", 31 | style: { 32 | left: '-9999px', 33 | position: 'absolute' 34 | }} 35 | ) 36 | ); 37 | }, 38 | 39 | // We cannot set `textContent` directly in `render`, 40 | // because React adds/deletes text nodes when rendering, 41 | // which confuses screen readers and doesn't cause them to read changes. 42 | setTextContent: function(textContent) { 43 | // We could set `innerHTML`, but it's better to avoid it. 44 | this.getDOMNode().textContent = textContent || ''; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /src/utils/rtl_chars_regexp.js: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT! 2 | // THIS FILE IS GENERATED! 3 | 4 | // All bidi characters found in classes 'R', 'AL', 'RLE', 'RLO', and 'RLI' as per Unicode 7.0.0. 5 | 6 | // jshint ignore:start 7 | // jscs:disable maximumLineLength 8 | module.exports = '[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05F0-\u05F4\u0608\u060B\u060D\u061B\u061C\u061E-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u070D\u070F\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0830-\u083E\u0840-\u0858\u085E\u08A0-\u08B2\u200F\u202B\u202E\u2067\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE70-\uFE74\uFE76-\uFEFC]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC57-\uDC9E\uDCA7-\uDCAF\uDD00-\uDD1B\uDD20-\uDD39\uDD3F\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE40-\uDE47\uDE50-\uDE58\uDE60-\uDE9F\uDEC0-\uDEE4\uDEEB-\uDEF6\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDF99-\uDF9C\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]'; 9 | // jscs:enable maximumLineLength 10 | // jshint ignore:end 11 | -------------------------------------------------------------------------------- /dist/npm/utils/rtl_chars_regexp.js: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT! 2 | // THIS FILE IS GENERATED! 3 | 4 | // All bidi characters found in classes 'R', 'AL', 'RLE', 'RLO', and 'RLI' as per Unicode 7.0.0. 5 | 6 | // jshint ignore:start 7 | // jscs:disable maximumLineLength 8 | module.exports = '[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05EA\u05F0-\u05F4\u0608\u060B\u060D\u061B\u061C\u061E-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u070D\u070F\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0830-\u083E\u0840-\u0858\u085E\u08A0-\u08B2\u200F\u202B\u202E\u2067\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBC1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFC\uFE70-\uFE74\uFE76-\uFEFC]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC57-\uDC9E\uDCA7-\uDCAF\uDD00-\uDD1B\uDD20-\uDD39\uDD3F\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE40-\uDE47\uDE50-\uDE58\uDE60-\uDE9F\uDEC0-\uDEE4\uDEEB-\uDEF6\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDF99-\uDF9C\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]'; 9 | // jscs:enable maximumLineLength 10 | // jshint ignore:end 11 | -------------------------------------------------------------------------------- /src/utils/_browser_unit_/get_text_direction_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getTextDirection = require('../get_text_direction.js'); 4 | 5 | describe('getTextDirection', function() { 6 | it('should return `rtl` for the Arabic language', function() { 7 | expect(getTextDirection('مرحبا')).to.equal('rtl') 8 | }); 9 | 10 | it('should return `rtl` for the Hebrew language', function() { 11 | expect(getTextDirection('בראשית')).to.equal('rtl') 12 | }); 13 | 14 | it('should return `ltr` for the Chinese language', function() { 15 | expect(getTextDirection('漢語')).to.equal('ltr'); 16 | }); 17 | 18 | it('should return `ltr` for the English language', function() { 19 | expect(getTextDirection('hello')).to.equal('ltr'); 20 | }); 21 | 22 | it('should return `null` for punctuation (neutral text)', function() { 23 | expect(getTextDirection('! %$,')).to.equal(null); 24 | }); 25 | 26 | it('should return `null` for digits (neutral text)', function() { 27 | expect(getTextDirection('01233456789')).to.equal(null); 28 | }); 29 | 30 | it('should return `ltr` for text which starts in English, but ends in Arabic', function() { 31 | expect(getTextDirection('hello goodbye مرحبا')).to.equal('ltr'); 32 | }); 33 | 34 | it('should return `rtl` for text which starts in Arabic, but ends in English', function() { 35 | expect(getTextDirection('مرحبا hello goodbye')).to.equal('rtl') 36 | }); 37 | 38 | it('should return `rtl` for text which starts and ends in Hebrew', function() { 39 | expect(getTextDirection('בראשית hello בראשית')).to.equal('rtl'); 40 | }); 41 | 42 | it('should return `ltr` for text which starts and ends in English', function() { 43 | expect(getTextDirection('hello בראשית goodbye')).to.equal('ltr'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /example/youtube/js/app.jsx: -------------------------------------------------------------------------------- 1 | var OptionStore = require('./stores/OptionStore'); 2 | var OptionActions = require('./actions/OptionActions'); 3 | var OptionTemplate = require('./components/OptionTemplate.jsx'); 4 | var throttle = require('lodash.throttle'); 5 | 6 | var MyApp = React.createClass({ 7 | getInitialState: function() { 8 | return { 9 | inputValue: '', 10 | options: [] 11 | }; 12 | }, 13 | 14 | componentWillMount: function() { 15 | OptionStore.on('change', this.handleStoreChange); 16 | }, 17 | 18 | componentWillUnmount: function() { 19 | OptionStore.removeListener('change', this.handleStoreChange); 20 | }, 21 | 22 | render: function() { 23 | return ( 24 | 32 | ); 33 | }, 34 | 35 | handleChange: function(event) { 36 | var value = event.target.value; 37 | this.setInputValue(value); 38 | this.getOptions(value); 39 | }, 40 | 41 | getOptions: throttle(OptionActions.getOptions, 300), 42 | 43 | handleOptionChange: function(event, option) { 44 | this.setInputValue(option); 45 | }, 46 | 47 | handleOptionClick: function(event, option) { 48 | this.setInputValue(option); 49 | }, 50 | 51 | setInputValue: function(value) { 52 | this.setState({ 53 | inputValue: value 54 | }); 55 | }, 56 | 57 | handleStoreChange: function(newOptions) { 58 | this.setState({ 59 | options: newOptions 60 | }); 61 | } 62 | }); 63 | 64 | React.render(, document.getElementById('yt-typeahead')); 65 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sun Apr 12 2015 06:41:58 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha', 'sinon-chai', 'browserify'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | './test/polyfill/*.js', 19 | './src/**/*.js?(x)', 20 | './src/**/_browser_unit_/*.js?(x)' 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [ 26 | ], 27 | 28 | 29 | // preprocess matching files before serving them to the browser 30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 31 | preprocessors: { 32 | './src/**/*.js?(x)': ['browserify'] 33 | }, 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | reporters: ['mocha'], 40 | 41 | // web server port 42 | port: 9876, 43 | 44 | 45 | // enable / disable colors in the output (reporters and logs) 46 | colors: true, 47 | 48 | 49 | // level of logging 50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 51 | logLevel: config.LOG_INFO, 52 | 53 | 54 | // enable / disable watching file and executing tests whenever any file changes 55 | autoWatch: true, 56 | 57 | 58 | // start these browsers 59 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 60 | browsers: ['PhantomJS'], 61 | 62 | 63 | // Continuous Integration mode 64 | // if true, Karma captures browsers, runs the tests and exits 65 | singleRun: true, 66 | 67 | mochaReporter: { 68 | output: 'autoWatch' 69 | }, 70 | 71 | browserify: { 72 | debug: true, 73 | transform: [ 74 | 'reactify' 75 | ], 76 | extensions: ['.js', '.jsx'] 77 | } 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /src/components/input.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | module.exports = React.createClass({ 6 | displayName: 'Input', 7 | 8 | propTypes: process.env.NODE_ENV === 'production' ? {} : { 9 | value: React.PropTypes.string, 10 | onChange: React.PropTypes.func 11 | }, 12 | 13 | getDefaultProps: function() { 14 | return { 15 | value: '', 16 | onChange: function() {} 17 | }; 18 | }, 19 | 20 | componentDidUpdate: function() { 21 | var _this = this, 22 | dir = _this.props.dir; 23 | 24 | if (dir === null || dir === undefined) { 25 | // When setting an attribute to null/undefined, 26 | // React instead sets the attribute to an empty string. 27 | 28 | // This is not desired because of a possible bug in Chrome. 29 | // If the page is RTL, and the input's `dir` attribute is set 30 | // to an empty string, Chrome assumes LTR, which isn't what we want. 31 | React.findDOMNode(_this).removeAttribute('dir'); 32 | } 33 | }, 34 | 35 | render: function() { 36 | var _this = this; 37 | 38 | return ( 39 | 43 | ); 44 | }, 45 | 46 | handleChange: function(event) { 47 | var props = this.props; 48 | 49 | // There are several React bugs in IE, 50 | // where the `input`'s `onChange` event is 51 | // fired even when the value didn't change. 52 | // https://github.com/facebook/react/issues/2185 53 | // https://github.com/facebook/react/issues/3377 54 | if (event.target.value !== props.value) { 55 | props.onChange(event); 56 | } 57 | }, 58 | 59 | blur: function() { 60 | React.findDOMNode(this).blur(); 61 | }, 62 | 63 | isCursorAtEnd: function() { 64 | var _this = this, 65 | inputDOMNode = React.findDOMNode(_this), 66 | valueLength = _this.props.value.length; 67 | 68 | return inputDOMNode.selectionStart === valueLength && 69 | inputDOMNode.selectionEnd === valueLength; 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /dist/npm/components/input.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | module.exports = React.createClass({ 6 | displayName: 'Input', 7 | 8 | propTypes: process.env.NODE_ENV === 'production' ? {} : { 9 | value: React.PropTypes.string, 10 | onChange: React.PropTypes.func 11 | }, 12 | 13 | getDefaultProps: function() { 14 | return { 15 | value: '', 16 | onChange: function() {} 17 | }; 18 | }, 19 | 20 | componentDidUpdate: function() { 21 | var _this = this, 22 | dir = _this.props.dir; 23 | 24 | if (dir === null || dir === undefined) { 25 | // When setting an attribute to null/undefined, 26 | // React instead sets the attribute to an empty string. 27 | 28 | // This is not desired because of a possible bug in Chrome. 29 | // If the page is RTL, and the input's `dir` attribute is set 30 | // to an empty string, Chrome assumes LTR, which isn't what we want. 31 | React.findDOMNode(_this).removeAttribute('dir'); 32 | } 33 | }, 34 | 35 | render: function() { 36 | var _this = this; 37 | 38 | return ( 39 | React.createElement("input", React.__spread({}, 40 | _this.props, 41 | {onChange: _this.handleChange}) 42 | ) 43 | ); 44 | }, 45 | 46 | handleChange: function(event) { 47 | var props = this.props; 48 | 49 | // There are several React bugs in IE, 50 | // where the `input`'s `onChange` event is 51 | // fired even when the value didn't change. 52 | // https://github.com/facebook/react/issues/2185 53 | // https://github.com/facebook/react/issues/3377 54 | if (event.target.value !== props.value) { 55 | props.onChange(event); 56 | } 57 | }, 58 | 59 | blur: function() { 60 | React.findDOMNode(this).blur(); 61 | }, 62 | 63 | isCursorAtEnd: function() { 64 | var _this = this, 65 | inputDOMNode = React.findDOMNode(_this), 66 | valueLength = _this.props.value.length; 67 | 68 | return inputDOMNode.selectionStart === valueLength && 69 | inputDOMNode.selectionEnd === valueLength; 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typeahead-component", 3 | "description": "Typeahead, written using the React.js library.", 4 | "author": "Ezequiel Rodriguez ", 5 | "version": "0.9.0", 6 | "main": "./src/index.js", 7 | "license": "MIT", 8 | "bugs": "https://github.com/ezequiel/react-typeahead-component/issues", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/ezequiel/react-typeahead-component.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "reactjs", 16 | "typeahead", 17 | "autocomplete", 18 | "react-component", 19 | "component" 20 | ], 21 | "files": [ 22 | "./dist/npm" 23 | ], 24 | "directories": { 25 | "lib": "./src", 26 | "example": "./example" 27 | }, 28 | "scripts": { 29 | "build": "browserify ./src/index.js -t reactify -t literalify -x react -s Typeahead -d", 30 | "dist:dev": "npm --loglevel=silent run build > dist/react-typeahead-component.dev.js", 31 | "dist:min": "npm --loglevel=silent run build -- -t [envify --NODE_ENV production] | uglifyjs -cmvb beautify=false,ascii_only=true - > dist/react-typeahead-component.min.js", 32 | "test": "karma start", 33 | "test:dev": "karma start --no-single-run --browsers Chrome", 34 | "jsx": "jsx ./src ./dist/npm -x jsx -x js" 35 | }, 36 | "peerDependencies": { 37 | "react": ">=0.13.1 <1.0.0" 38 | }, 39 | "devDependencies": { 40 | "brfs": "^1.4.0", 41 | "browserify": "^9.0.3", 42 | "browserify-css": "^0.6.1", 43 | "browserify-shim": "^3.8.6", 44 | "envify": "^3.4.0", 45 | "insert-css": "^0.2.0", 46 | "karma": "^0.12.31", 47 | "karma-browserify": "^4.1.2", 48 | "karma-chrome-launcher": "^0.1.7", 49 | "karma-cli": "^0.0.4", 50 | "karma-firefox-launcher": "^0.1.4", 51 | "karma-ie-launcher": "^0.1.5", 52 | "karma-mocha": "^0.1.10", 53 | "karma-mocha-reporter": "^1.0.2", 54 | "karma-opera-launcher": "^0.1.0", 55 | "karma-phantomjs-launcher": "^0.1.4", 56 | "karma-safari-launcher": "^0.1.1", 57 | "karma-sinon-chai": "^0.3.0", 58 | "literalify": "^0.4.0", 59 | "mocha": "^2.2.4", 60 | "react-tools": "^0.13.1", 61 | "reactify": "^1.0.0", 62 | "uglify-js": "^2.4.20", 63 | "watchify": "^2.4.0" 64 | }, 65 | "literalify": { 66 | "react": "window.React || require('react')" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/netflix/css/netflix.css: -------------------------------------------------------------------------------- 1 | .netflix-typeahead-container { 2 | display: flex; 3 | display: -webkit-flex; 4 | align-items: center; 5 | -webkit-align-items: center; 6 | background: black; 7 | border: solid 1px rgba(255,255,255,.85); 8 | width: 265px; 9 | position: relative !important; 10 | } 11 | 12 | .netflix-typeahead-container .icon-search { 13 | color: #fff; 14 | padding: 0 6px; 15 | } 16 | 17 | .netflix-typeahead-container .icon-remove { 18 | color: #fff; 19 | cursor: pointer; 20 | margin-right: 6px; 21 | padding: 0; 22 | font-size: 13px; 23 | border: none; 24 | background: none; 25 | outline: none; 26 | 27 | } 28 | 29 | .netflix-typeahead-container .icon-remove:focus, 30 | .netflix-typeahead-container .icon-remove:hover { 31 | color: #e50914; 32 | } 33 | 34 | body { 35 | font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; 36 | -webkit-font-smoothing: antialiased; 37 | } 38 | 39 | .netflix-typeahead-container input { 40 | color: #fff; 41 | background: 0 0; 42 | border: none; 43 | padding: 7px 14px 7px 7px; 44 | font-size: 14px; 45 | width: 212px; 46 | outline: 0; 47 | } 48 | 49 | .react-typeahead-options { 50 | margin: 0; 51 | padding: 0; 52 | list-style-type: none; 53 | border: solid 1px rgba(255,255,255,.85); 54 | cursor: default; 55 | box-shadow: 0 2px 4px rgba(0,0,0,0.2); 56 | background: black !important; 57 | color: #fff; 58 | font-size: 15px; 59 | border-top: none; 60 | margin-top: 1px; 61 | width: 267px !important; 62 | margin-left: -29px; 63 | } 64 | 65 | .react-typeahead-options li { 66 | padding: 4px 14px 4px 7px; 67 | } 68 | 69 | @font-face { 70 | font-family: 'nf-icon'; 71 | src: url('http://cdn.nflximg.com/ffe/siteui/fonts/nf-icon-v1-57.svg#nf-icon-v1-57') format('svg'),url('http://cdn.nflximg.com/ffe/siteui/fonts/nf-icon-v1-57.eot?#iefix') format('embedded-opentype'),url('http://cdn.nflximg.com/ffe/siteui/fonts/nf-icon-v1-57.woff') format('woff'),url('http://cdn.nflximg.com/ffe/siteui/fonts/nf-icon-v1-57.ttf') format('truetype'); 72 | font-weight: 400; 73 | font-style: normal; 74 | } 75 | 76 | [class^="icon-"],[class*=" icon-"] { 77 | font-family: 'nf-icon'; 78 | speak: none; 79 | font-style: normal; 80 | font-weight: 400; 81 | font-variant: normal; 82 | text-transform: none; 83 | line-height: 1; 84 | } 85 | 86 | .icon-remove:before { 87 | content: '\e863' 88 | } 89 | 90 | .icon-search:before { 91 | content: '\e636' 92 | } 93 | 94 | .selected-option { 95 | color: #e50914; 96 | } 97 | 98 | .option-header { 99 | color: #999; 100 | font-weight: 500; 101 | margin-bottom: 3px; 102 | } 103 | 104 | .option-value { 105 | padding-left: 10px; 106 | } 107 | -------------------------------------------------------------------------------- /example/netflix/js/utils/OptionWebAPIUtils.js: -------------------------------------------------------------------------------- 1 | var Rx = require('rx'); 2 | var JSONP = require('jsonp'); 3 | var NETFLIX_API_ENDPOINT = "http://dvd.netflix.com/JSON/AutoCompleteSearch?type=grouped"; 4 | 5 | var cache = { 6 | '': [] 7 | }; 8 | 9 | module.exports = { 10 | fetchOptions: function(query) { 11 | return Rx.Observable.fromPromise(new Promise(function(resolve, reject) { 12 | var result = cache[query], url; 13 | 14 | if (result !== undefined) { 15 | resolve(result); 16 | } else { 17 | url = NETFLIX_API_ENDPOINT + '&prefix=' + query; 18 | 19 | JSONP(url, function(error, data) { 20 | if (error) { 21 | reject(error); 22 | } else { 23 | // Transform the server response 24 | // into a flatter structure. 25 | result = 26 | Object 27 | .keys(data) 28 | // Don't need the total result count. 29 | .filter(function(key) { 30 | return key !== "totalResults"; 31 | }) 32 | // Flatten the result list. 33 | .reduce(function(result, key) { 34 | var values = data[key].values; 35 | 36 | return result.concat(values.map(function(value, index) { 37 | return { 38 | type: key, 39 | index: index, 40 | value: value.pName, 41 | }; 42 | })); 43 | }, []) 44 | .map(function(option) { 45 | // Values contain html, such as: "Hello" 46 | // We will never dangerously set inner html, 47 | // so strip all html tags. 48 | var value = 49 | option.value.replace(/<\/?[^>]+(>|$)/g, "") 50 | .replace(' ', ' '), 51 | type = option.type; 52 | 53 | return { 54 | // Capitalize the type. 55 | type: type.charAt(0).toUpperCase() + type.slice(1), 56 | index: option.index, 57 | value: value 58 | }; 59 | }); 60 | 61 | cache[query] = result; 62 | 63 | resolve(result); 64 | } 65 | }); 66 | } 67 | })); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /example/netflix/js/app.jsx: -------------------------------------------------------------------------------- 1 | var OptionTemplate = require('./components/OptionTemplate.jsx'); 2 | var OptionWebAPIUtils = require('./utils/OptionWebAPIUtils'); 3 | var Rx = require('rx'); 4 | 5 | var MyApp = React.createClass({ 6 | getInitialState: function() { 7 | return { 8 | inputValue: '', 9 | options: [] 10 | }; 11 | }, 12 | 13 | componentWillMount: function() { 14 | var inputChanges = new Rx.Subject(); 15 | 16 | this.inputChanges = inputChanges; 17 | this.handleChange = inputChanges.onNext.bind(inputChanges); 18 | 19 | var inputValues = inputChanges.map(function(event) { 20 | return event.target.value; 21 | }); 22 | 23 | inputValues 24 | .subscribe(this.setInputValue); 25 | 26 | inputValues 27 | .debounce(250) 28 | .flatMapLatest(function(inputValue) { 29 | return ( 30 | OptionWebAPIUtils 31 | .fetchOptions(inputValue) 32 | .retry(2) 33 | // Completes the collection at this point 34 | // if there is a new input value. 35 | .takeUntil(inputChanges) 36 | ); 37 | }) 38 | .subscribe(this.setOptions); 39 | }, 40 | 41 | componentWillUnmount: function() { 42 | this.inputChanges.dispose(); 43 | }, 44 | 45 | render: function() { 46 | return ( 47 |
48 | 52 | 62 | {this.renderRemoveIcon()} 63 |
64 | ); 65 | }, 66 | 67 | renderRemoveIcon: function() { 68 | if (this.state.inputValue.length > 0) { 69 | return ( 70 |