├── .cz.json ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .release.json ├── .travis.yml ├── CHANGELOG.md ├── Makefile ├── README.md ├── dist └── index.js ├── examples ├── codeSample.jsx ├── dist │ ├── index.html │ └── index.js ├── example.jsx ├── exampleList.jsx ├── index.jsx ├── index.template.html ├── propTable.jsx ├── samples │ ├── addRemoveToken.sample │ ├── basic.sample │ ├── index.js │ ├── inputChange.sample │ ├── limitToSuggestions.sample │ ├── parse.sample │ ├── processing.sample │ ├── simulateSelect.sample │ ├── singleValue.sample │ └── treshold.sample ├── stateTable.jsx ├── styles │ ├── codemirror.css │ ├── main.css │ └── monokai.css └── webpack.config.js ├── package.json ├── sample.env ├── sample.env-production ├── scripts ├── .pre-push ├── build.sh ├── dev.sh ├── gh-pages.sh ├── install.sh ├── lint.sh ├── report-coverage.sh ├── setup-hooks.sh └── test.sh ├── src ├── TokenAutocomplete │ ├── index.jsx │ ├── options │ │ ├── index.jsx │ │ ├── option │ │ │ ├── index.jsx │ │ │ ├── spec.js │ │ │ └── styles.js │ │ ├── spec.js │ │ └── styles.js │ ├── spec.js │ ├── styles.js │ └── token │ │ ├── index.jsx │ │ ├── spec.js │ │ └── styles.js ├── _tests │ └── utils.js ├── _utils │ └── keyCodes.js └── index.js ├── tests ├── karma.conf.js └── webpack.tests.js └── webpack.config.js /.cz.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "node_modules/cz-conventional-changelog/" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es6": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "parser": "babel-eslint", 11 | "globals": { 12 | "describe": true, 13 | "it": true, 14 | "expect": true, 15 | "jest": true, 16 | "before": true, 17 | "after": true, 18 | "sinon": true, 19 | "beforeEach": true, 20 | "afterEach": true, 21 | "createServerFactory": true 22 | }, 23 | "ecmaFeatures": { 24 | "jsx": true, 25 | "arrowFunctions": true, 26 | "blockBindings": true, 27 | "classes": true, 28 | "defaultParams": true, 29 | "destructuring": true, 30 | "forOf": true, 31 | "generators": true, 32 | "modules": true, 33 | "spread": true, 34 | "templateStrings": true 35 | }, 36 | "settings": { 37 | "ecmascript": 6, 38 | "jsx": true 39 | }, 40 | "rules": { 41 | "comma-dangle": 1, 42 | "no-debugger": 2, 43 | "no-extra-semi": 1, 44 | "no-unreachable": 1, 45 | "no-dupe-keys": 1, 46 | "no-unused-expressions": 0, 47 | "eqeqeq": 1, 48 | "new-cap": 0, 49 | "no-alert": 2, 50 | "no-shadow": 0, 51 | "no-lone-blocks": 1, 52 | "no-loop-func": 1, 53 | "no-multi-spaces": 1, 54 | "no-native-reassign": 1, 55 | "no-redeclare": 1, 56 | "radix": 1, 57 | "no-console": 0, 58 | "handle-callback-err": 0, 59 | "no-undef": 1, 60 | "no-unused-vars": [1, {"vars": "all", "args": "none"}], 61 | "no-use-before-define": 0, 62 | "space-return-throw-case": 1, 63 | "space-infix-ops": 1, 64 | "space-in-parens": [1, "never"], 65 | "space-before-blocks": [1, "always"], 66 | "space-after-keywords": [1, "always"], 67 | "semi": [1, "always"], 68 | "quotes": [1, "single"], 69 | "one-var": [1, "never"], 70 | "no-trailing-spaces": 1, 71 | "semi-spacing": 1, 72 | "no-multiple-empty-lines": 1, 73 | "no-mixed-spaces-and-tabs": 1, 74 | "key-spacing": [1, { 75 | "beforeColon": false, 76 | "afterColon": true 77 | }], 78 | "object-shorthand": [2, "methods"], 79 | "camelcase": 0, 80 | "comma-spacing": [2, {"before": false, "after": true}] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .DS_Store 30 | 31 | .env 32 | .env-production 33 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "changelogCommand": "", 3 | "buildCommand": "make lint && make build", 4 | "github": { 5 | "release": true, 6 | "tokenRef": "GITHUB_TOKEN" 7 | }, 8 | "npm": { 9 | "publish": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | install: 5 | before_install: 6 | - "export DISPLAY=:99.0" 7 | - "sh -e /etc/init.d/xvfb start" 8 | after_success: 9 | - "make coverage" 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterKaleta/react-token-autocomplete/566eab7dbe4424a6871d32ac05e7a0535cc3479e/CHANGELOG.md -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build init setup-hooks lint test test-watch dev ghpages 2 | 3 | clean: 4 | rm -rf ./dist 5 | 6 | init: 7 | make install 8 | make setup-hooks 9 | 10 | install: 11 | ./scripts/install.sh 12 | 13 | setup-hooks: 14 | ./scripts/setup-hooks.sh 15 | 16 | dev: 17 | ./scripts/dev.sh 18 | 19 | dev-examples: 20 | ./scripts/dev-examples.sh 21 | 22 | build: 23 | ./scripts/build.sh 24 | 25 | lint: 26 | ./scripts/lint.sh 27 | 28 | test: 29 | ./scripts/test.sh 30 | 31 | test-watch: 32 | ./scripts/test.sh -w 33 | 34 | ghpages: 35 | ./scripts/gh-pages.sh 36 | 37 | coverage: 38 | ./scripts/report-coverage.sh 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-token-autocomplete 2 | An easily stylable React select / token / autocomplete ready to work with flux flow. 3 | 4 | ## Install 5 | ``` 6 | npm install react-token-autocomplete 7 | ``` 8 | ## Docs & examples 9 | 10 | Check docs and examples at http://peterkaleta.github.io/react-token-autocomplete 11 | -------------------------------------------------------------------------------- /examples/codeSample.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import radium from 'radium'; 3 | import Playground from 'component-playground'; 4 | import TokenAutocomplete from '../src'; 5 | 6 | const styles = { 7 | wrapper: { 8 | } 9 | }; 10 | 11 | @radium 12 | export default class CodeSample extends React.Component { 13 | 14 | static displayName = 'CodeSample'; 15 | 16 | static propTypes = { 17 | sample: PropTypes.string 18 | } 19 | 20 | static defaultProps = { 21 | sample: '' 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 | 32 |
33 | ); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 |
26 | 27 | 28 | 29 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/example.jsx: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react'; 2 | import radium from 'radium'; 3 | import CodeSample from './codeSample'; 4 | 5 | const styles = { 6 | wrapper: { 7 | margin: '30px auto', 8 | background: '#fff', 9 | boxShadow: ' 0 0 6px 0 rgba(0, 0, 0, 0.2)' 10 | }, 11 | contents: { 12 | padding: 10 13 | }, 14 | header: { 15 | margin: 0, 16 | textTransform: 'uppercase' 17 | } 18 | }; 19 | 20 | @radium 21 | export default class Example extends React.Component { 22 | 23 | static displayName = 'Example'; 24 | 25 | static propTypes = { 26 | sample: PropTypes.string, 27 | title: PropTypes.string 28 | } 29 | 30 | static defaultProps = { 31 | sample: '', 32 | title: '' 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 |
39 |

{this.props.title}

40 |

{this.props.children}

41 |
42 | 43 | 44 |
45 | ); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /examples/exampleList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import radium from 'radium'; 3 | import Samples from './samples'; 4 | import Example from './example'; 5 | import PropsTable from './propTable'; 6 | import StateTable from './stateTable'; 7 | 8 | const styles = { 9 | wrapper: { 10 | maxWidth: 600, 11 | margin: 'auto', 12 | fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', 13 | fontWeight: 200 14 | }, 15 | code: { 16 | margin: 10, 17 | padding: 10, 18 | background: '#fff' 19 | }, 20 | badge: { 21 | marginRight: 15, 22 | maxWidth: 90, 23 | display: 'inline-block' 24 | } 25 | }; 26 | 27 | 28 | @radium 29 | export default class Examples extends React.Component { 30 | 31 | static displayName = 'examples'; 32 | 33 | 34 | render() { 35 | return ( 36 |
37 | 38 |

react-token-autocomplete

39 | 40 |

41 | An easily stylable React select / token / autocomplete ready to 42 | work with flux flow. 43 |
44 | All examples below were created with the awesome component playground so feel free to change the code around and play with it in real time. 45 | If you are not really into examples go directly to props listing and component state. 46 |

47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 | 58 |
59 | 60 | 61 |

Install me, please

62 | 63 |
64 | 65 | npm install react-token-autocomplete 66 | 67 |
68 | 69 |

Basic config

70 | 71 | 74 | Dropdown with options to add custom tags and filter suggestions on the fly. 75 | 76 | 77 | 80 | You can also limit user choices to suggestions. 81 | 82 | 83 | 86 | It can work as a simple select (with or without filtering). 87 | 88 | 89 | 90 |

Additional fun

91 | 92 | 95 | Set minimal term length user needs to type before showing suggestions 96 | 97 | 98 | 101 | Show users when you are processing stuff 102 | 103 | 104 |

Callbacks

105 | 106 | 109 | React to input changes 110 | 111 | 112 | 115 | Callback when user is either adding or removing value 116 | 117 | 118 |

Parsing

119 | 120 | 123 | Callback when user is either adding or removing value 124 | 125 | 126 | 127 |

Component state

128 | 129 | 130 | 131 |

Available props

132 | 133 | 134 | 135 |

have fun!

136 | 137 | 138 | Fork me on GitHub 139 | 140 | 141 |
142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /examples/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import './styles/codemirror.css'; 3 | import './styles/monokai.css'; 4 | import './styles/main.css'; 5 | 6 | import React from 'react'; 7 | import Examples from './exampleList.jsx'; 8 | 9 | let wrapper = document.createElement('div'); 10 | document.body.appendChild(wrapper); 11 | React.render(, wrapper); 12 | -------------------------------------------------------------------------------- /examples/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 |
26 | 27 | 28 | 29 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/propTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import radium from 'radium'; 3 | import {map} from 'lodash'; 4 | 5 | const styles = { 6 | wrapper: { 7 | background: '#fff', 8 | width: '100%' 9 | } 10 | }; 11 | 12 | const PROPS_DESCRIPTION = [ 13 | [ 14 | 'options', 15 | 'array', 16 | '[]', 17 | 'suggestions to be displayed in autocomplete' 18 | ], 19 | [ 20 | 'placeholder', 21 | 'string', 22 | '"add new tag"', 23 | 'placeholder for empty input' 24 | ], 25 | [ 26 | 'defaultValues', 27 | 'array', 28 | '[]', 29 | 'values already selected' 30 | ], 31 | [ 32 | 'treshold', 33 | 'number', 34 | '0', 35 | 'minimal input length to display suggestions' 36 | ], 37 | [ 38 | 'focus', 39 | 'boolean', 40 | 'false', 41 | 'should input be focused by default' 42 | ], 43 | [ 44 | 'filterOptions', 45 | 'boolean', 46 | 'true', 47 | 'allows user to filter options by typing into the input' 48 | ], 49 | [ 50 | 'processing', 51 | 'boolean', 52 | 'false', 53 | 'display processing indicator' 54 | ], 55 | [ 56 | 'limitToOptions', 57 | 'boolean', 58 | 'false', 59 | 'allow/disable custom tokens added by user' 60 | ], 61 | [ 62 | 'onInputChange', 63 | 'function', 64 | 'noop', 65 | 'callback when input changes' 66 | ], 67 | [ 68 | 'onAdd', 69 | 'function', 70 | 'noop', 71 | 'callback when new token is selected' 72 | ], 73 | [ 74 | 'onRemove', 75 | 'function', 76 | 'noop', 77 | 'callback when token is removed' 78 | ], 79 | [ 80 | 'parseToken', 81 | 'function', 82 | 'identity', 83 | 'parse value to token label' 84 | ], 85 | [ 86 | 'parseOption', 87 | 'function', 88 | 'identity', 89 | 'parse value to suggestion label' 90 | ], 91 | [ 92 | 'parseCustom', 93 | 'function', 94 | 'identity', 95 | 'parse user entered values before adding to values' 96 | ], 97 | [ 98 | 'simulateSelect', 99 | 'boolean', 100 | 'false', 101 | 'transforms token autocomplete into a standard dropdown' 102 | ] 103 | 104 | ]; 105 | 106 | @radium 107 | export default class PropTable extends React.Component { 108 | 109 | static displayName = 'PropTable'; 110 | 111 | renderTableContents () { 112 | return map(PROPS_DESCRIPTION, (row, index) => { 113 | return ( 114 | 115 | {row[0]} 116 | {row[1]} 117 | {row[2]} 118 | {row[3]} 119 | 120 | ); 121 | }); 122 | } 123 | 124 | render() { 125 | return ( 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | { this.renderTableContents() } 134 |
propstypedefaultdescription
135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /examples/samples/addRemoveToken.sample: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /examples/samples/basic.sample: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/samples/index.js: -------------------------------------------------------------------------------- 1 | 2 | import limitToSuggestions from 'raw!./limitToSuggestions.sample'; 3 | import basic from 'raw!./basic.sample'; 4 | import inputChange from 'raw!./inputChange.sample'; 5 | import processing from 'raw!./processing.sample'; 6 | import addRemoveToken from 'raw!./addRemoveToken.sample'; 7 | import treshold from 'raw!./treshold.sample'; 8 | import parse from 'raw!./parse.sample'; 9 | import simulateSelect from 'raw!./simulateSelect.sample'; 10 | 11 | export default { 12 | basic, 13 | limitToSuggestions, 14 | processing, 15 | inputChange, 16 | addRemoveToken, 17 | treshold, 18 | parse, 19 | simulateSelect 20 | }; 21 | -------------------------------------------------------------------------------- /examples/samples/inputChange.sample: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/samples/limitToSuggestions.sample: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/samples/parse.sample: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /examples/samples/processing.sample: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /examples/samples/simulateSelect.sample: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/samples/singleValue.sample: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/samples/treshold.sample: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /examples/stateTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import radium from 'radium'; 3 | import {map} from 'lodash'; 4 | 5 | const styles = { 6 | wrapper: { 7 | background: '#fff', 8 | width: '100%' 9 | } 10 | }; 11 | 12 | const PROPS_DESCRIPTION = [ 13 | [ 14 | 'values', 15 | 'array', 16 | '[] or props.defaultValues', 17 | 'stores currently selected tokens,' 18 | ], 19 | [ 20 | 'focus', 21 | 'boolean', 22 | 'false or props.focos', 23 | 'is input currently focused' 24 | ], 25 | [ 26 | 'inputValue', 27 | 'string', 28 | 'empty or props.defaultInputValue', 29 | 'value currently entered in input' 30 | ] 31 | ]; 32 | 33 | @radium 34 | export default class StateTable extends React.Component { 35 | 36 | static displayName = 'StateTable'; 37 | 38 | renderTableContents () { 39 | return map(PROPS_DESCRIPTION, (row, index) => { 40 | return ( 41 | 42 | {row[0]} 43 | {row[1]} 44 | {row[2]} 45 | {row[3]} 46 | 47 | ); 48 | }); 49 | } 50 | 51 | render() { 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | { this.renderTableContents() } 61 |
statetypedefaultdescription
62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/styles/codemirror.css: -------------------------------------------------------------------------------- 1 | 2 | .CodeMirror { 3 | /* Set height, width, borders, and global font properties here */ 4 | font-family: monospace; 5 | height: 300px; 6 | color: black; 7 | } 8 | 9 | /* PADDING */ 10 | 11 | .CodeMirror-lines { 12 | padding: 4px 0; /* Vertical padding around content */ 13 | } 14 | .CodeMirror pre { 15 | padding: 0 4px; /* Horizontal padding of content */ 16 | } 17 | 18 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 19 | background-color: white; /* The little square between H and V scrollbars */ 20 | } 21 | 22 | /* GUTTER */ 23 | 24 | .CodeMirror-gutters { 25 | border-right: 1px solid #ddd; 26 | background-color: #f7f7f7; 27 | white-space: nowrap; 28 | } 29 | .CodeMirror-linenumbers {} 30 | .CodeMirror-linenumber { 31 | padding: 0 3px 0 5px; 32 | min-width: 20px; 33 | text-align: right; 34 | color: #999; 35 | white-space: nowrap; 36 | } 37 | 38 | .CodeMirror-guttermarker { color: black; } 39 | .CodeMirror-guttermarker-subtle { color: #999; } 40 | 41 | /* CURSOR */ 42 | 43 | .CodeMirror-cursor { 44 | border-left: 1px solid black; 45 | border-right: none; 46 | width: 0; 47 | } 48 | /* Shown when moving in bi-directional text */ 49 | .CodeMirror div.CodeMirror-secondarycursor { 50 | border-left: 1px solid silver; 51 | } 52 | .cm-fat-cursor .CodeMirror-cursor { 53 | width: auto; 54 | border: 0; 55 | background: #7e7; 56 | } 57 | .cm-fat-cursor div.CodeMirror-cursors { 58 | z-index: 1; 59 | } 60 | 61 | .cm-animate-fat-cursor { 62 | width: auto; 63 | border: 0; 64 | -webkit-animation: blink 1.06s steps(1) infinite; 65 | -moz-animation: blink 1.06s steps(1) infinite; 66 | animation: blink 1.06s steps(1) infinite; 67 | background-color: #7e7; 68 | } 69 | @-moz-keyframes blink { 70 | 0% {} 71 | 50% { background-color: transparent; } 72 | 100% {} 73 | } 74 | @-webkit-keyframes blink { 75 | 0% {} 76 | 50% { background-color: transparent; } 77 | 100% {} 78 | } 79 | @keyframes blink { 80 | 0% {} 81 | 50% { background-color: transparent; } 82 | 100% {} 83 | } 84 | 85 | /* Can style cursor different in overwrite (non-insert) mode */ 86 | .CodeMirror-overwrite .CodeMirror-cursor {} 87 | 88 | .cm-tab { display: inline-block; text-decoration: inherit; } 89 | 90 | .CodeMirror-ruler { 91 | border-left: 1px solid #ccc; 92 | position: absolute; 93 | } 94 | 95 | /* DEFAULT THEME */ 96 | 97 | .cm-s-default .cm-header {color: blue;} 98 | .cm-s-default .cm-quote {color: #090;} 99 | .cm-negative {color: #d44;} 100 | .cm-positive {color: #292;} 101 | .cm-header, .cm-strong {font-weight: bold;} 102 | .cm-em {font-style: italic;} 103 | .cm-link {text-decoration: underline;} 104 | .cm-strikethrough {text-decoration: line-through;} 105 | 106 | .cm-s-default .cm-keyword {color: #708;} 107 | .cm-s-default .cm-atom {color: #219;} 108 | .cm-s-default .cm-number {color: #164;} 109 | .cm-s-default .cm-def {color: #00f;} 110 | .cm-s-default .cm-variable, 111 | .cm-s-default .cm-punctuation, 112 | .cm-s-default .cm-property, 113 | .cm-s-default .cm-operator {} 114 | .cm-s-default .cm-variable-2 {color: #05a;} 115 | .cm-s-default .cm-variable-3 {color: #085;} 116 | .cm-s-default .cm-comment {color: #a50;} 117 | .cm-s-default .cm-string {color: #a11;} 118 | .cm-s-default .cm-string-2 {color: #f50;} 119 | .cm-s-default .cm-meta {color: #555;} 120 | .cm-s-default .cm-qualifier {color: #555;} 121 | .cm-s-default .cm-builtin {color: #30a;} 122 | .cm-s-default .cm-bracket {color: #997;} 123 | .cm-s-default .cm-tag {color: #170;} 124 | .cm-s-default .cm-attribute {color: #00c;} 125 | .cm-s-default .cm-hr {color: #999;} 126 | .cm-s-default .cm-link {color: #00c;} 127 | 128 | .cm-s-default .cm-error {color: #f00;} 129 | .cm-invalidchar {color: #f00;} 130 | 131 | .CodeMirror-composing { border-bottom: 2px solid; } 132 | 133 | /* Default styles for common addons */ 134 | 135 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 136 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 137 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 138 | .CodeMirror-activeline-background {background: #e8f2ff;} 139 | 140 | /* STOP */ 141 | 142 | /* The rest of this file contains styles related to the mechanics of 143 | the editor. You probably shouldn't touch them. */ 144 | 145 | .CodeMirror { 146 | position: relative; 147 | overflow: hidden; 148 | background: white; 149 | } 150 | 151 | .CodeMirror-scroll { 152 | overflow: scroll !important; /* Things will break if this is overridden */ 153 | /* 30px is the magic margin used to hide the element's real scrollbars */ 154 | /* See overflow: hidden in .CodeMirror */ 155 | margin-bottom: -30px; margin-right: -30px; 156 | padding-bottom: 30px; 157 | height: 100%; 158 | outline: none; /* Prevent dragging from highlighting the element */ 159 | position: relative; 160 | } 161 | .CodeMirror-sizer { 162 | position: relative; 163 | border-right: 30px solid transparent; 164 | } 165 | 166 | /* The fake, visible scrollbars. Used to force redraw during scrolling 167 | before actuall scrolling happens, thus preventing shaking and 168 | flickering artifacts. */ 169 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 170 | position: absolute; 171 | z-index: 6; 172 | display: none; 173 | } 174 | .CodeMirror-vscrollbar { 175 | right: 0; top: 0; 176 | overflow-x: hidden; 177 | overflow-y: scroll; 178 | } 179 | .CodeMirror-hscrollbar { 180 | bottom: 0; left: 0; 181 | overflow-y: hidden; 182 | overflow-x: scroll; 183 | } 184 | .CodeMirror-scrollbar-filler { 185 | right: 0; bottom: 0; 186 | } 187 | .CodeMirror-gutter-filler { 188 | left: 0; bottom: 0; 189 | } 190 | 191 | .CodeMirror-gutters { 192 | position: absolute; left: 0; top: 0; 193 | z-index: 3; 194 | } 195 | .CodeMirror-gutter { 196 | white-space: normal; 197 | height: 100%; 198 | display: inline-block; 199 | margin-bottom: -30px; 200 | /* Hack to make IE7 behave */ 201 | *zoom:1; 202 | *display:inline; 203 | } 204 | .CodeMirror-gutter-wrapper { 205 | position: absolute; 206 | z-index: 4; 207 | background: none !important; 208 | border: none !important; 209 | } 210 | .CodeMirror-gutter-background { 211 | position: absolute; 212 | top: 0; bottom: 0; 213 | z-index: 4; 214 | } 215 | .CodeMirror-gutter-elt { 216 | position: absolute; 217 | cursor: default; 218 | z-index: 4; 219 | } 220 | .CodeMirror-gutter-wrapper { 221 | -webkit-user-select: none; 222 | -moz-user-select: none; 223 | user-select: none; 224 | } 225 | 226 | .CodeMirror-lines { 227 | cursor: text; 228 | min-height: 1px; /* prevents collapsing before first draw */ 229 | } 230 | .CodeMirror pre { 231 | /* Reset some styles that the rest of the page might have set */ 232 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 233 | border-width: 0; 234 | background: transparent; 235 | font-family: inherit; 236 | font-size: inherit; 237 | margin: 0; 238 | white-space: pre; 239 | word-wrap: normal; 240 | line-height: inherit; 241 | color: inherit; 242 | z-index: 2; 243 | position: relative; 244 | overflow: visible; 245 | -webkit-tap-highlight-color: transparent; 246 | } 247 | .CodeMirror-wrap pre { 248 | word-wrap: break-word; 249 | white-space: pre-wrap; 250 | word-break: normal; 251 | } 252 | 253 | .CodeMirror-linebackground { 254 | position: absolute; 255 | left: 0; right: 0; top: 0; bottom: 0; 256 | z-index: 0; 257 | } 258 | 259 | .CodeMirror-linewidget { 260 | position: relative; 261 | z-index: 2; 262 | overflow: auto; 263 | } 264 | 265 | .CodeMirror-widget {} 266 | 267 | .CodeMirror-code { 268 | outline: none; 269 | } 270 | 271 | /* Force content-box sizing for the elements where we expect it */ 272 | .CodeMirror-scroll, 273 | .CodeMirror-sizer, 274 | .CodeMirror-gutter, 275 | .CodeMirror-gutters, 276 | .CodeMirror-linenumber { 277 | -moz-box-sizing: content-box; 278 | box-sizing: content-box; 279 | } 280 | 281 | .CodeMirror-measure { 282 | position: absolute; 283 | width: 100%; 284 | height: 0; 285 | overflow: hidden; 286 | visibility: hidden; 287 | } 288 | 289 | .CodeMirror-cursor { position: absolute; } 290 | .CodeMirror-measure pre { position: static; } 291 | 292 | div.CodeMirror-cursors { 293 | visibility: hidden; 294 | position: relative; 295 | z-index: 3; 296 | } 297 | div.CodeMirror-dragcursors { 298 | visibility: visible; 299 | } 300 | 301 | .CodeMirror-focused div.CodeMirror-cursors { 302 | visibility: visible; 303 | } 304 | 305 | .CodeMirror-selected { background: #d9d9d9; } 306 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 307 | .CodeMirror-crosshair { cursor: crosshair; } 308 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 309 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 310 | 311 | .cm-searching { 312 | background: #ffa; 313 | background: rgba(255, 255, 0, .4); 314 | } 315 | 316 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 317 | .CodeMirror span { *vertical-align: text-bottom; } 318 | 319 | /* Used to force a border model for a node */ 320 | .cm-force-border { padding-right: .1px; } 321 | 322 | @media print { 323 | /* Hide the cursor when printing */ 324 | .CodeMirror div.CodeMirror-cursors { 325 | visibility: hidden; 326 | } 327 | } 328 | 329 | /* See issue #2901 */ 330 | .cm-tab-wrap-hack:after { content: ''; } 331 | 332 | /* Help users use markselection to safely style text background */ 333 | span.CodeMirror-selectedtext { background: none; } 334 | -------------------------------------------------------------------------------- /examples/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #444; 3 | background: #fff; 4 | } 5 | 6 | p{ 7 | margin: 6px 0 10px 0; 8 | font-size: 17px; 9 | line-height: 1.4em; 10 | } 11 | table{ 12 | border-spacing: 0; 13 | border-collapse: collapse; 14 | } 15 | 16 | a, a:visited{ 17 | color: rgb(191, 129, 22); 18 | } 19 | 20 | iframe { 21 | border: 0; 22 | } 23 | 24 | 25 | 26 | .githubRibbon{ 27 | position: fixed; 28 | top: 0; 29 | right: 0; 30 | border: 0; 31 | 32 | } 33 | 34 | 35 | td, th{ 36 | border: 1px solid #888; 37 | padding: 5px; 38 | } 39 | td{ 40 | font-size:13px; 41 | } 42 | 43 | th{ 44 | text-align: left; 45 | font-weight:400; 46 | text-transform: capitalize; 47 | } 48 | 49 | h1, h2, h3{ 50 | font-weight: 200; 51 | } 52 | 53 | .playgroundCode .CodeMirror{ 54 | 55 | } 56 | 57 | .playgroundToggleCodeBar{ 58 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 59 | } 60 | 61 | .playground.collapsableCode .playgroundCode { 62 | height: 0; 63 | overflow: hidden; 64 | } 65 | 66 | .playground.collapsableCode .playgroundCode.expandedCode { 67 | height: auto; 68 | } 69 | 70 | .playgroundToggleCodeLink { 71 | cursor: pointer; 72 | font-family: monospace; 73 | position: relative; 74 | right: 0; 75 | display: inline-block; 76 | padding: 5px; 77 | } 78 | 79 | .playgroundToggleCodeLink:hover { 80 | color: #aaa; 81 | } 82 | 83 | .CodeMirror { 84 | padding: 10px; 85 | height: auto; 86 | } 87 | 88 | .playgroundPreview { 89 | background: white; 90 | padding: 10px; 91 | } 92 | 93 | .playgroundError { 94 | padding: 10px; 95 | font-size: 12px; 96 | color: white; 97 | background: #F2777A; 98 | } 99 | -------------------------------------------------------------------------------- /examples/styles/monokai.css: -------------------------------------------------------------------------------- 1 | .cm-s-mbo.CodeMirror{background:#2c2c2c;color:#ffffec}.cm-s-mbo div.CodeMirror-selected{background:#716C62!important}.cm-s-mbo.CodeMirror ::selection{background:rgba(113,108,98,.99)}.cm-s-mbo.CodeMirror ::-moz-selection{background:rgba(113,108,98,.99)}.cm-s-mbo .CodeMirror-gutters{background:#4e4e4e;border-right:0}.cm-s-mbo .CodeMirror-guttermarker{color:#fff}.cm-s-mbo .CodeMirror-guttermarker-subtle{color:grey}.cm-s-mbo .CodeMirror-linenumber{color:#dadada}.cm-s-mbo .CodeMirror-cursor{border-left:1px solid #ffffec!important}.cm-s-mbo span.cm-comment{color:#95958a}.cm-s-mbo span.cm-atom,.cm-s-mbo span.cm-number{color:#00a8c6}.cm-s-mbo span.cm-attribute,.cm-s-mbo span.cm-property{color:#9ddfe9}.cm-s-mbo span.cm-keyword{color:#ffb928}.cm-s-mbo span.cm-string{color:#ffcf6c}.cm-s-mbo span.cm-string.cm-property,.cm-s-mbo span.cm-variable{color:#ffffec}.cm-s-mbo span.cm-variable-2{color:#00a8c6}.cm-s-mbo span.cm-def{color:#ffffec}.cm-s-mbo span.cm-bracket{color:#fffffc;font-weight:700}.cm-s-mbo span.cm-tag{color:#9ddfe9}.cm-s-mbo span.cm-link{color:#f54b07}.cm-s-mbo span.cm-error{border-bottom:#636363;color:#ffffec}.cm-s-mbo span.cm-qualifier{color:#ffffec}.cm-s-mbo .CodeMirror-activeline-background{background:#494b41!important}.cm-s-mbo .CodeMirror-matchingbracket{color:#222!important}.cm-s-mbo .CodeMirror-matchingtag{background:rgba(255,255,255,.37)} 2 | -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | var webpackConf = require('peters-toolbelt').webpack; 4 | 5 | var plugins = []; 6 | 7 | 8 | var outputPath; 9 | var addTracking; 10 | if (process.env.BRANCH === 'gh-pages') { 11 | outputPath = path.join(__dirname, '../'); 12 | addTracking = true; 13 | } else { 14 | outputPath = path.join(__dirname, '/dist'); 15 | addTracking = false; 16 | } 17 | 18 | plugins.push(new HtmlWebpackPlugin({ 19 | filename: 'index.html', 20 | template: './examples/index.template.html', 21 | tracking: addTracking 22 | })); 23 | 24 | var conf = new webpackConf({ 25 | entry: path.join(__dirname, '/index.jsx'), 26 | output: { 27 | path: outputPath, 28 | filename: 'index.js' 29 | }, 30 | resolve: { 31 | alias: { 32 | utils: path.join(__dirname, '../src/_utils') 33 | } 34 | }, 35 | plugins: plugins 36 | }) 37 | .iNeedReact() 38 | .iNeedWebFonts() 39 | .iNeedSCSS() 40 | .iNeedHotDevServer() 41 | .getConfig(); 42 | 43 | module.exports = conf; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-token-autocomplete", 3 | "version": "0.5.3", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "make lint && make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/peterKaleta/react-token-autocomplete.git" 12 | }, 13 | "author": "mail@peterkaleta.com", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/peterKaleta/react-token-autocomplete/issues" 17 | }, 18 | "homepage": "https://github.com/peterKaleta/react-token-autocomplete#readme", 19 | "dependencies": { 20 | "immutable": "^3.7.5", 21 | "lodash": "^3.10.1", 22 | "radium": "^0.13.8", 23 | "react": "^0.13.3", 24 | "underscore.string": "^3.2.1" 25 | }, 26 | "devDependencies": { 27 | "babel": "^5.8.23", 28 | "babel-core": "^5.8.23", 29 | "babel-eslint": "^4.1.1", 30 | "babel-loader": "^5.3.2", 31 | "babel-plugin-rewire": "0.1.21", 32 | "babel-runtime": "^5.8.20", 33 | "chai": "^3.2.0", 34 | "codecov.io": "^0.1.6", 35 | "component-playground": "^0.1.1", 36 | "css-loader": "^0.17.0", 37 | "cz-conventional-changelog": "^1.1.2", 38 | "eslint": "^1.3.1", 39 | "eslint-plugin-react": "^3.3.1", 40 | "exports-loader": "^0.6.2", 41 | "html-webpack-plugin": "^1.6.1", 42 | "image-webpack-loader": "^1.6.1", 43 | "inert": "^3.0.1", 44 | "isparta": "^3.0.4", 45 | "isparta-loader": "^0.2.0", 46 | "karma": "^0.13.9", 47 | "karma-chai": "^0.1.0", 48 | "karma-chrome-launcher": "^0.2.0", 49 | "karma-cli": "^0.1.0", 50 | "karma-coverage": "^0.5.1", 51 | "karma-firefox-launcher": "^0.1.6", 52 | "karma-mocha": "^0.2.0", 53 | "karma-mocha-reporter": "^1.1.1", 54 | "karma-phantomjs2-launcher": "^0.3.2", 55 | "karma-sinon": "^1.0.4", 56 | "karma-sourcemap-loader": "^0.3.5", 57 | "karma-webpack": "^1.7.0", 58 | "l": "0.0.1", 59 | "mocha": "^2.3.0", 60 | "mocha-babel": "^3.0.0", 61 | "path": "^0.11.14", 62 | "peters-toolbelt": "^0.3.9", 63 | "raw-loader": "^0.5.1", 64 | "react-hot-loader": "^1.3.0", 65 | "rewire": "^2.3.4", 66 | "rewire-webpack": "^1.0.0", 67 | "sass": "^0.5.0", 68 | "sass-loader": "^2.0.1", 69 | "script-loader": "^0.6.1", 70 | "sinon": "^1.16.1", 71 | "sinon-chai": "^2.8.0", 72 | "style-loader": "^0.12.3", 73 | "url-loader": "^0.5.6", 74 | "webpack": "^1.12.1", 75 | "webpack-dev-server": "^1.10.1" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | GITHUB_TOKEN=SOME_GIT_TOKENN 3 | -------------------------------------------------------------------------------- /sample.env-production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | -------------------------------------------------------------------------------- /scripts/.pre-push: -------------------------------------------------------------------------------- 1 | make lint 2 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $( cat .env-production|xargs) 3 | export NODE_ENV=production 4 | 5 | ./node_modules/.bin/webpack \ 6 | --colors \ 7 | --verbose \ 8 | --devtool eval \ 9 | --progress \ 10 | --display-chunks \ 11 | --optimize-occurence-order \ 12 | --bail 13 | 14 | ./node_modules/.bin/webpack \ 15 | --config ./examples/webpack.config.js \ 16 | --colors \ 17 | --verbose \ 18 | --devtool inline-source-map \ 19 | --progress \ 20 | --display-chunks \ 21 | --optimize-occurence-order \ 22 | --bail 23 | -------------------------------------------------------------------------------- /scripts/dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $( cat .env|xargs) 3 | ./node_modules/.bin/webpack-dev-server \ 4 | --config ./examples/webpack.config.js \ 5 | --hot \ 6 | --debug \ 7 | --colors \ 8 | --verbose \ 9 | --devtool inline-source-map \ 10 | --display-chunks \ 11 | --progress \ 12 | --history-api-fallback \ 13 | --output-pathinfo 14 | -------------------------------------------------------------------------------- /scripts/gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $( cat .env|xargs) 3 | git checkout gh-pages 4 | branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') 5 | export BRANCH=$branch 6 | git merge master --no-edit 7 | make build 8 | git add -A 9 | git commit -m "new docs" 10 | git push origin gh-pages 11 | git checkout master 12 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $( cat .env-production|xargs) 3 | npm install 4 | cp sample.env .env 5 | cp sample.env-production .env-production 6 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./node_modules/.bin/eslint -c .eslintrc --ext .js src examples/*.jsx examples/*.js 3 | -------------------------------------------------------------------------------- /scripts/report-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $( cat .env|xargs) 3 | cat ./tests/coverage/cobertura/cobertura-coverage.xml | ./node_modules/.bin/codecov 4 | -------------------------------------------------------------------------------- /scripts/setup-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ln scripts/.pre-push .git/hooks/pre-push 3 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export $( cat .env|xargs) 3 | 4 | while getopts 'w' FLAG 5 | do 6 | case $FLAG in 7 | w) export WATCH_TESTS=true ;; 8 | esac 9 | done 10 | 11 | ./node_modules/karma/bin/karma start ./tests/karma.conf.js 12 | -------------------------------------------------------------------------------- /src/TokenAutocomplete/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import radium from 'radium'; 3 | import OptionList from './options'; 4 | import Token from './token'; 5 | import {include, difference, filter, noop, identity, isArray, isUndefined, isEmpty} from 'lodash'; 6 | import {contains} from 'underscore.string'; 7 | import Immutable from 'immutable'; 8 | import styles from './styles'; 9 | import keyCodes from 'utils/keyCodes'; 10 | import {decorators} from 'peters-toolbelt'; 11 | const {StyleDefaults} = decorators; 12 | 13 | 14 | function defaultValuesPropType(props, propName, component) { 15 | const prop = props[propName]; 16 | 17 | if (props.simulateSelect && isArray(prop) && prop.length > 1) { 18 | return new Error( 19 | 'when props.simulateSelect is set to TRUE, you should pass more than a single value in props.defaultValues' 20 | ); 21 | } 22 | 23 | return React.PropTypes.array(props, propName, component); 24 | } 25 | 26 | function tresholdPropType(props, propName, component) { 27 | const prop = props[propName]; 28 | 29 | if (props.simulateSelect && prop > 0) { 30 | return new Error( 31 | 'when props.simulateSelect is set to TRUE, you should not pass non-zero treshold' 32 | ); 33 | } 34 | 35 | return React.PropTypes.number(props, propName, component); 36 | } 37 | 38 | @radium 39 | @StyleDefaults(styles) 40 | export default class TokenAutocomplete extends React.Component { 41 | 42 | static displayName = 'TokenAutocomplete'; 43 | 44 | static propTypes = { 45 | //initial state 46 | options: React.PropTypes.array, 47 | placeholder: React.PropTypes.string, 48 | treshold: tresholdPropType, 49 | defaultValues: defaultValuesPropType, 50 | processing: React.PropTypes.bool, 51 | focus: React.PropTypes.bool, 52 | //behaviour 53 | filterOptions: React.PropTypes.bool, 54 | simulateSelect: React.PropTypes.bool, 55 | limitToOptions: React.PropTypes.bool, 56 | parseOption: React.PropTypes.func, 57 | parseToken: React.PropTypes.func, 58 | parseCustom: React.PropTypes.func, 59 | //handles 60 | onInputChange: React.PropTypes.func, 61 | onAdd: React.PropTypes.func, 62 | onRemove: React.PropTypes.func 63 | } 64 | 65 | static contextTypes = { 66 | } 67 | 68 | static defaultProps = { 69 | //initial state 70 | options: [], 71 | defaultValues: [], 72 | placeholder: 'add new tag', 73 | treshold: 0, 74 | focus: false, 75 | processing: false, 76 | //behaviour 77 | filterOptions: true, 78 | simulateSelect: false, 79 | limitToOptions: false, 80 | parseOption: identity, 81 | parseToken: identity, 82 | parseCustom: identity, 83 | //handles 84 | onInputChange: noop, 85 | onAdd: noop, 86 | onRemove: noop 87 | } 88 | 89 | state = { 90 | focused: false, 91 | inputValue: '', 92 | values: Immutable.List([]) 93 | } 94 | 95 | 96 | //LIFECYCLE 97 | 98 | componentDidMount() { 99 | let values = Immutable.List(this.props.defaultValues); 100 | this.setState({values}); 101 | 102 | if (!isUndefined(this.props.focus)) { 103 | this.setState({focused: this.props.focus}); 104 | } 105 | 106 | if (this.props.focus) { 107 | this.bindListeners(); 108 | } 109 | 110 | if (window) { 111 | window.addEventListener('click', this.handleClick); 112 | } 113 | 114 | } 115 | 116 | componentWillUnmount() { 117 | if (window) { 118 | window.removeEventListener('click', this.handleClick); 119 | } 120 | } 121 | 122 | bindListeners() { 123 | if (!this.keyDownListener) { 124 | this.keyDownListener = window.addEventListener('keydown', this.onKeyDown); 125 | } 126 | } 127 | 128 | unbindListeners() { 129 | window.removeEventListener('keydown', this.onKeyDown); 130 | delete this.keyDownListener; 131 | } 132 | 133 | //EVENT HANDLERS 134 | 135 | onInputChange = e => { 136 | 137 | this.props.onInputChange(e.target.value); 138 | this.setState({ 139 | inputValue: e.target.value 140 | }); 141 | 142 | } 143 | 144 | onKeyDown = e => { 145 | 146 | switch (e.keyCode) { 147 | case keyCodes.ESC: 148 | this.blur(); 149 | break; 150 | case keyCodes.ENTER: 151 | this.addSelectedValue(); 152 | break; 153 | case keyCodes.BACKSPACE: 154 | if (!this.state.inputValue.length) { 155 | this.setState({ 156 | inputValue: this.state.inputValue.slice(0, -1) 157 | }); 158 | this.deleteValue(this.state.values.size - 1); 159 | e.preventDefault(); 160 | 161 | } 162 | break; 163 | } 164 | } 165 | 166 | handleClick = e => { 167 | const clickedOutside = !React.findDOMNode(this).contains(e.target); 168 | 169 | if (clickedOutside && this.state.focused) { 170 | this.blur(); 171 | } 172 | 173 | if (!clickedOutside && !this.state.focused) { 174 | this.focus(); 175 | } 176 | } 177 | 178 | //ACTIONS 179 | 180 | focus = () => { 181 | if (this.refs.input) { 182 | React.findDOMNode(this.refs.input).focus(); 183 | } 184 | this.bindListeners(); 185 | this.setState({focused: true}); 186 | } 187 | 188 | blur = () => { 189 | if (this.refs.input) { 190 | React.findDOMNode(this.refs.input).blur(); 191 | } 192 | 193 | this.unbindListeners(); 194 | this.setState({focused: false}); 195 | } 196 | 197 | deleteValue = index => { 198 | 199 | const valueRemoved = this.state.values.get(index); 200 | const values = this.state.values.delete(index); 201 | this.props.onRemove(valueRemoved, values); 202 | 203 | this.setState({values}); 204 | this.focus(); 205 | } 206 | 207 | addSelectedValue = () => { 208 | 209 | const areOptionsAvailable = this.getAvailableOptions().length; 210 | const newValue = areOptionsAvailable ? this.refs.options.getSelected() : void 0; 211 | const isAlreadySelected = include(this.state.values.toArray(), newValue); 212 | const shouldAddValue = !!newValue && !isAlreadySelected; 213 | 214 | if (shouldAddValue) { 215 | 216 | let values = this.props.simulateSelect 217 | ? Immutable.List([newValue]) 218 | : this.state.values.push(newValue); 219 | 220 | this.props.onAdd(newValue, values); 221 | this.setState({ 222 | values, 223 | inputValue: '' 224 | }); 225 | 226 | } 227 | 228 | if (this.props.simulateSelect) { 229 | this.blur(); 230 | } else { 231 | this.focus(); 232 | } 233 | 234 | } 235 | 236 | //HELPERS 237 | 238 | getAvailableOptions = () => { 239 | 240 | let availableOptions = []; 241 | 242 | if (this.isTresholdReached()) { 243 | //notselected if not simulating select 244 | if (this.props.simulateSelect) { 245 | availableOptions = this.props.options; 246 | } else { 247 | availableOptions = difference(this.props.options, this.state.values.toArray()); 248 | } 249 | 250 | //filter 251 | availableOptions = filter(availableOptions, option => { 252 | return contains(option, this.state.inputValue); 253 | }); 254 | 255 | } 256 | 257 | if (this.shouldAllowCustomValue() && 258 | !isEmpty(this.state.inputValue) && 259 | !include(availableOptions, this.state.inputValue)) { 260 | availableOptions.unshift(this.props.parseCustom(this.state.inputValue)); 261 | } 262 | 263 | return availableOptions; 264 | 265 | } 266 | 267 | shouldAllowCustomValue = () => { 268 | return !this.props.limitToOptions && !this.props.simulateSelect; 269 | } 270 | 271 | shouldShowOptions = () => { 272 | return this.isTresholdReached() && this.state.focused; 273 | } 274 | 275 | shouldShowInput = () => { 276 | return this.props.filterOptions && (!this.props.simulateSelect || !this.state.values.size); 277 | } 278 | 279 | shouldShowFakePlaceholder = () => { 280 | return !this.shouldShowInput() && !this.state.values.size; 281 | } 282 | 283 | isTresholdReached = () => { 284 | return this.state.inputValue.length >= this.props.treshold; 285 | } 286 | 287 | //RENDERERS 288 | 289 | renderOptionsDropdown = () => { 290 | if (this.shouldShowOptions()) { 291 | let passProps = { 292 | options: this.getAvailableOptions(), 293 | term: this.state.inputValue, 294 | handleAddSelected: this.addSelectedValue, 295 | parseOption: this.props.parseOption 296 | }; 297 | return ; 298 | } else { 299 | return null; 300 | } 301 | 302 | } 303 | 304 | renderTokens = () => { 305 | return this.state.values.map((value, key) => { 306 | return ( 307 | 315 | ); 316 | }); 317 | } 318 | 319 | renderProcessing = () => { 320 | return this.props.processing ?
: null; 321 | } 322 | 323 | renderFakePlaceholder = () => { 324 | return this.shouldShowFakePlaceholder() 325 | ? (
328 | {this.props.placeholder} 329 |
) 330 | : null; 331 | } 332 | 333 | renderInput = () => { 334 | return this.shouldShowInput() 335 | ? () 342 | : this.renderFakePlaceholder(); 343 | } 344 | 345 | renderDropdownIndicator = () => { 346 | return this.props.simulateSelect 347 | ?
348 | : null; 349 | } 350 | 351 | render() { 352 | return ( 353 |
354 |
355 | {this.renderTokens()} 356 | {this.renderInput()} 357 | {this.renderProcessing()} 358 | {this.renderDropdownIndicator()} 359 |
360 | {this.renderOptionsDropdown()} 361 |
362 | ); 363 | 364 | } 365 | 366 | } 367 | -------------------------------------------------------------------------------- /src/TokenAutocomplete/options/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import radium from 'radium'; 3 | import styles from './styles'; 4 | import {noop, map} from 'lodash'; 5 | import keyCodes from 'utils/keyCodes'; 6 | import Option from './option'; 7 | import {decorators} from 'peters-toolbelt'; 8 | const {StyleDefaults} = decorators; 9 | 10 | @radium 11 | @StyleDefaults(styles) 12 | export default class OptionList extends React.Component { 13 | 14 | static displayName = 'Option List'; 15 | 16 | static propTypes = { 17 | options: React.PropTypes.array, 18 | alreadySelected: React.PropTypes.array, 19 | term: React.PropTypes.string 20 | } 21 | 22 | static defaultProps = { 23 | options: [], 24 | term: '', 25 | emptyInfo: 'no suggestions', 26 | handleAddSelected: noop 27 | } 28 | 29 | state = { 30 | selected: 0, 31 | suggestions: [] 32 | } 33 | 34 | componentDidMount() { 35 | if (window) { 36 | window.addEventListener('keydown', this.onKeyDown); 37 | } 38 | } 39 | 40 | componentWillUnmount() { 41 | if (window) { 42 | window.removeEventListener('keydown', this.onKeyDown); 43 | } 44 | } 45 | 46 | componentWillReceiveProps(newProps) { 47 | if (newProps.options.length <= this.state.selected) { 48 | this.setState({selected: newProps.options.length - 1}); 49 | } 50 | 51 | if (!newProps.options.length) { 52 | this.setState({selected: 0}); 53 | } 54 | } 55 | 56 | onKeyDown = e => { 57 | switch (e.keyCode) { 58 | case keyCodes.UP : 59 | this.selectPrev(); 60 | e.preventDefault(); 61 | break; 62 | case keyCodes.DOWN : 63 | this.selectNext(); 64 | e.preventDefault(); 65 | break; 66 | } 67 | } 68 | 69 | renderOption = (option, index) => { 70 | return ( 71 |