├── .eslintrc ├── .gitignore ├── .jscs.json ├── .jshintrc ├── LICENSE ├── README.md ├── dist ├── humanize-string.js ├── icon.js ├── index.js ├── multiple.js ├── option-mixin.js ├── option-wrapper.js ├── option.js ├── options.js ├── search-mixin.js ├── single.js └── text-highlight.js ├── example ├── base.jsx ├── build │ └── .gitkeep ├── code-snippets │ ├── custom-render.jsx │ ├── multiple.jsx │ └── single.jsx ├── components │ ├── code-snippet.jsx │ ├── custom-render.jsx │ ├── features.jsx │ ├── footer.jsx │ ├── github-ribbon.jsx │ ├── header.jsx │ └── install.jsx ├── css │ ├── _variables.scss │ ├── choice │ │ ├── _icon.scss │ │ ├── _input.scss │ │ ├── _multiple.scss │ │ ├── _option.scss │ │ ├── _options.scss │ │ ├── _single.scss │ │ ├── _text-highlight.scss │ │ ├── _value.scss │ │ ├── _wrapper.scss │ │ ├── index.css │ │ └── index.scss │ ├── components │ │ ├── _code-snippet.scss │ │ ├── _features.scss │ │ ├── _footer.scss │ │ ├── _header.scss │ │ ├── _ofs-credit.scss │ │ ├── _pokemon.scss │ │ └── _tutorial.scss │ └── index.scss ├── data │ ├── countries.js │ └── pokemon.js ├── img │ └── logo.png ├── index.jsx └── js │ └── index.js ├── gulpfile.js ├── package.json ├── src ├── humanize-string.js ├── icon.jsx ├── index.js ├── multiple.jsx ├── option-mixin.js ├── option-wrapper.jsx ├── option.jsx ├── options.jsx ├── search-mixin.js ├── single.jsx └── text-highlight.jsx └── webpack.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "arrowFunctions": true, 4 | "binaryLiterals": false, 5 | "blockBindings": true, 6 | "classes": true, 7 | "defaultParams": true, 8 | "destructuring": true, 9 | "forOf": false, 10 | "generators": true, 11 | "modules": true, 12 | "objectLiteralComputedProperties": true, 13 | "objectLiteralDuplicateProperties": false, 14 | "objectLiteralShorthandMethods": true, 15 | "objectLiteralShorthandProperties": true, 16 | "octalLiterals": true, 17 | "regexUFlag": true, 18 | "regexYFlag": true, 19 | "superInFunctions": false, 20 | "templateStrings": true, 21 | "unicodeCodePointEscapes": false, 22 | "globalReturn": false, 23 | "jsx": true 24 | }, 25 | 26 | "parser": "babel-eslint", 27 | 28 | "plugins": [ 29 | "react" 30 | ], 31 | 32 | "env": { 33 | "browser": true, 34 | "node": true, 35 | "es6": true 36 | }, 37 | 38 | "rules": { 39 | "no-alert": 2, 40 | "no-array-constructor": 2, 41 | "no-bitwise": 0, 42 | "no-caller": 2, 43 | "no-catch-shadow": 0, 44 | "no-cond-assign": 2, 45 | "no-console": 1, 46 | "no-constant-condition": 2, 47 | "no-control-regex": 2, 48 | "no-debugger": 2, 49 | "no-delete-var": 2, 50 | "no-div-regex": 0, 51 | "no-dupe-keys": 2, 52 | "no-dupe-args": 2, 53 | "no-else-return": 1, 54 | "no-empty": 2, 55 | "no-empty-class": 2, 56 | "no-empty-label": 2, 57 | "no-eq-null": 0, 58 | "no-eval": 2, 59 | "no-ex-assign": 2, 60 | "no-extend-native": 2, 61 | "no-extra-bind": 2, 62 | "no-extra-boolean-cast": 2, 63 | "no-extra-parens": 0, 64 | "no-extra-semi": 2, 65 | "no-extra-strict": 2, 66 | "no-fallthrough": 2, 67 | "no-floating-decimal": 0, 68 | "no-func-assign": 2, 69 | "no-implied-eval": 2, 70 | "no-inline-comments": 0, 71 | "no-inner-declarations": [2, "functions"], 72 | "no-invalid-regexp": 2, 73 | "no-irregular-whitespace": 2, 74 | "no-iterator": 2, 75 | "no-label-var": 2, 76 | "no-labels": 2, 77 | "no-lone-blocks": 2, 78 | "no-lonely-if": 0, 79 | "no-loop-func": 2, 80 | "no-mixed-requires": [1, true], 81 | "no-mixed-spaces-and-tabs": [2, false], 82 | "no-multi-spaces": [2, { exceptions: { "VariableDeclarator": true } }], 83 | "no-multi-str": 2, 84 | "no-multiple-empty-lines": [0, {"max": 2}], 85 | "no-native-reassign": 2, 86 | "no-negated-in-lhs": 2, 87 | "no-nested-ternary": 1, 88 | "no-new": 2, 89 | "no-new-func": 2, 90 | "no-new-object": 2, 91 | "no-new-require": 0, 92 | "no-new-wrappers": 2, 93 | "no-obj-calls": 2, 94 | "no-octal": 2, 95 | "no-octal-escape": 2, 96 | "no-path-concat": 0, 97 | "no-plusplus": 0, 98 | "no-process-env": 0, 99 | "no-process-exit": 2, 100 | "no-proto": 2, 101 | "no-redeclare": 2, 102 | "no-regex-spaces": 2, 103 | "no-reserved-keys": 0, 104 | "no-restricted-modules": 0, 105 | "no-return-assign": 2, 106 | "no-script-url": 2, 107 | "no-self-compare": 0, 108 | "no-sequences": 2, 109 | "no-shadow": 2, 110 | "no-shadow-restricted-names": 2, 111 | "no-space-before-semi": 0, 112 | "no-spaced-func": 2, 113 | "no-sparse-arrays": 2, 114 | "no-sync": 0, 115 | "no-ternary": 0, 116 | "no-trailing-spaces": 2, 117 | "no-throw-literal": 0, 118 | "no-undef": 2, 119 | "no-undef-init": 2, 120 | "no-undefined": 0, 121 | "no-underscore-dangle": 0, 122 | "no-unreachable": 2, 123 | "no-unused-expressions": 2, 124 | "no-unused-vars": [1, {"vars": "all", "args": "after-used"}], 125 | "no-use-before-define": 2, 126 | "no-void": 0, 127 | "no-var": 0, 128 | "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], 129 | "no-with": 2, 130 | "no-wrap-func": 2, 131 | 132 | "block-scoped-var": 0, 133 | "brace-style": [0, "1tbs"], 134 | "camelcase": 2, 135 | "comma-spacing": 2, 136 | "comma-style": 0, 137 | "comma-dangle": 0, 138 | "complexity": [0, 11], 139 | "consistent-return": 2, 140 | "consistent-this": [0, "that"], 141 | "curly": [2, "all"], 142 | "default-case": 0, 143 | "dot-notation": [2, { "allowKeywords": true, "allowPattern": "^[a-zA-Z/d]+(_[a-zA-Z/d]+)+$" }], 144 | "eol-last": 2, 145 | "eqeqeq": 2, 146 | "func-names": 0, 147 | "func-style": [0, "declaration"], 148 | "generator-star": 0, 149 | "global-strict": [0, "never"], 150 | "guard-for-in": 0, 151 | "handle-callback-err": 0, 152 | "indent": [2, 2], 153 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 154 | "max-depth": [0, 4], 155 | "max-len": [0, 80, 4], 156 | "max-nested-callbacks": [0, 2], 157 | "max-params": [0, 3], 158 | "max-statements": [0, 10], 159 | "new-cap": 0, 160 | "new-parens": 2, 161 | "one-var": 0, 162 | "operator-assignment": [0, "always"], 163 | "padded-blocks": 0, 164 | "quote-props": 0, 165 | "quotes": [0, "double"], 166 | "radix": 0, 167 | "semi": 2, 168 | "semi-spacing": [2, {"before": false, "after": true}], 169 | "sort-vars": 0, 170 | "space-after-function-name": [2, "never"], 171 | "space-after-keywords": [2, "always"], 172 | "space-before-blocks": [0, "always"], 173 | "space-before-function-parentheses": [0, "always"], 174 | "space-in-brackets": [0, "never"], 175 | "space-in-parens": [0, "never"], 176 | "space-infix-ops": 2, 177 | "space-return-throw-case": 2, 178 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 179 | "spaced-line-comment": [0, "always"], 180 | "strict": 2, 181 | "use-isnan": 2, 182 | "valid-jsdoc": 0, 183 | "valid-typeof": 2, 184 | "vars-on-top": 0, 185 | "wrap-iife": 0, 186 | "wrap-regex": 0, 187 | "yoda": [2, "never"], 188 | 189 | // eslint-plugin-react rules 190 | "react/jsx-boolean-value": [1, "always"], 191 | "react/jsx-uses-react": 1, 192 | "react/jsx-uses-vars": 1, 193 | "react/jsx-no-undef": 2, 194 | "react/no-did-mount-set-state": 1, 195 | "react/no-did-update-set-state": 1, 196 | "react/no-multi-comp": 1, 197 | "react/no-unknown-property": 1, 198 | "react/prop-types": 1, 199 | "react/react-in-jsx-scope": 2, 200 | "react/self-closing-comp": 1, 201 | "react/wrap-multilines": 2 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache/ 2 | node_modules/ 3 | .module-cache/ 4 | example/index.html 5 | example/build/ 6 | example/css/*.css 7 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], 3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 4 | "requireSpaceBeforeBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 5 | "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=", ","], 6 | "requireSpaceBeforePostfixUnaryOperators": ["+", "-", "~", "!"], 7 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 8 | "disallowSpaceAfterBinaryOperators": ["!"], 9 | "requireSpacesInConditionalExpression": true, 10 | "disallowSpaceBeforeBinaryOperators": [","], 11 | "disallowImplicitTypeConversion": ["string"], 12 | "disallowKeywords": ["with"], 13 | "disallowMultipleLineBreaks": true, 14 | "disallowKeywordsOnNewLine": ["else"], 15 | "disallowMixedSpacesAndTabs": true, 16 | "disallowTrailingWhitespace": true, 17 | "requireLineFeedAtFileEnd": true, 18 | "requireSpacesInFunctionExpression": { 19 | "beforeOpeningCurlyBrace": true 20 | }, 21 | "disallowSpacesInFunctionExpression": { 22 | "beforeOpeningRoundBrace": true 23 | }, 24 | "validateIndentation": 2 25 | } 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "newcap": false, 3 | "globalstrict": true, 4 | "node": true, 5 | "browser": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Likealike, Ltd. DBA onefinestay 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React Choice 2 | ====================== 3 | 4 | A React based customisable select box. 5 | 6 | [Demo](http://onefinestay.github.io/react-choice/) 7 | 8 | ## Features 9 | 10 | * Search text highlighting 11 | * Single or multiple selection 12 | * Custom results rendering 13 | 14 | ## Contribute 15 | 16 | Please feel to contribute to this project by making pull requests. You can see a 17 | list of tasks that can be worked on in the [issues list](https://github.com/onefinestay/react-choice/issues). 18 | 19 | ### Building example page 20 | 21 | Once you have the repository cloned run the following commands to get started: 22 | 23 | ```shell 24 | npm install 25 | gulp develop 26 | ``` 27 | 28 | This will start a local server at `http://localhost:9989` where you can see the 29 | example page. It will also watch for any files changes and rebuild. 30 | -------------------------------------------------------------------------------- /dist/humanize-string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (string, upperCase) { 4 | if (typeof string == 'string') { 5 | var firstCharacter = string.charAt(0); 6 | 7 | if (typeof upperCase === 'undefined' || upperCase === true) { 8 | firstCharacter = firstCharacter.toUpperCase(); 9 | } 10 | 11 | var display = firstCharacter + string.slice(1); 12 | return display.replace(/_|-/g, ' '); 13 | } else { 14 | return string; 15 | } 16 | }; -------------------------------------------------------------------------------- /dist/icon.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | var Icon = React.createClass({ 7 | displayName: 'Icon', 8 | 9 | propTypes: { 10 | focused: React.PropTypes.bool.isRequired 11 | }, 12 | 13 | render: function render() { 14 | var arrowClasses = cx({ 15 | 'react-choice-icon__arrow': true, 16 | 'react-choice-icon__arrow--up': this.props.focused, 17 | 'react-choice-icon__arrow--down': !this.props.focused 18 | }); 19 | 20 | return React.createElement('div', { className: arrowClasses }); 21 | } 22 | }); 23 | 24 | module.exports = Icon; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | exports['default'] = { 7 | 'Select': require('./single'), 8 | 'SelectMultiple': require('./multiple'), 9 | 'Option': require('./option'), 10 | 'OptionMixin': require('./option-mixin'), 11 | 'TextHighlight': require('./text-highlight') 12 | }; 13 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/multiple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var _ = require('lodash'); 5 | var cx = React.addons.classSet; 6 | var cloneWithProps = React.addons.cloneWithProps; 7 | 8 | var Options = require('./options'); 9 | var OptionWrapper = require('./option-wrapper'); 10 | 11 | var SearchMixin = require('./search-mixin'); 12 | 13 | var ValueWrapper = React.createClass({ 14 | displayName: 'ValueWrapper', 15 | 16 | propTypes: { 17 | onClick: React.PropTypes.func.isRequired, 18 | onDeleteClick: React.PropTypes.func.isRequired 19 | }, 20 | 21 | onDeleteClick: function onDeleteClick(event) { 22 | event.stopPropagation(); 23 | this.props.onDeleteClick(event); 24 | }, 25 | 26 | render: function render() { 27 | var classes = cx({ 28 | 'react-choice-value': true, 29 | 'react-choice-value--is-selected': this.props.selected 30 | }); 31 | 32 | return React.createElement( 33 | 'div', 34 | { className: classes, onClick: this.props.onClick }, 35 | React.createElement( 36 | 'div', 37 | { className: 'react-choice-value__children' }, 38 | this.props.children 39 | ), 40 | React.createElement( 41 | 'a', 42 | { className: 'react-choice-value__delete', onClick: this.onDeleteClick }, 43 | 'x' 44 | ) 45 | ); 46 | } 47 | }); 48 | 49 | var MultipleChoice = React.createClass({ 50 | displayName: 'MultipleChoice', 51 | 52 | mixins: [SearchMixin], 53 | 54 | propTypes: { 55 | name: React.PropTypes.string, // name of input 56 | placeholder: React.PropTypes.string, // input placeholder 57 | values: React.PropTypes.array, // initial values 58 | 59 | children: React.PropTypes.array.isRequired, 60 | 61 | valueField: React.PropTypes.string, // value field name 62 | labelField: React.PropTypes.string, // label field name 63 | 64 | searchField: React.PropTypes.array, // array of search fields 65 | 66 | onSelect: React.PropTypes.func, // function called when option is selected 67 | onDelete: React.PropTypes.func, // function called when option is deleted 68 | allowDuplicates: React.PropTypes.bool // if true, the same values can be added multiple times 69 | }, 70 | 71 | getDefaultProps: function getDefaultProps() { 72 | return { 73 | values: [], 74 | valueField: 'value', 75 | labelField: 'children', 76 | searchField: ['children'], 77 | allowDuplicates: false 78 | }; 79 | }, 80 | 81 | getInitialState: function getInitialState() { 82 | var props = this.props.searchField; 83 | props.push(this.props.valueField); 84 | props.push(this.props.searchField); 85 | props = _.uniq(props); 86 | 87 | var options = _.map(this.props.children, function (child) { 88 | // TODO Validation ? 89 | return _.pick(child.props, props); 90 | }, this); 91 | 92 | return { 93 | focus: false, 94 | searchResults: this._sort(options), 95 | values: this.props.values, 96 | initialOptions: options, 97 | highlighted: null, 98 | selected: null, 99 | selectedIndex: -1, 100 | searchTokens: [] 101 | }; 102 | }, 103 | 104 | _handleContainerInput: function _handleContainerInput(event) { 105 | var keys = { 106 | 37: this._moveLeft, 107 | 39: this._moveRight, 108 | 8: this._removeSelectedContainer 109 | }; 110 | 111 | if (typeof keys[event.keyCode] === 'function') { 112 | keys[event.keyCode](event); 113 | } 114 | }, 115 | 116 | _handleContainerBlur: function _handleContainerBlur() { 117 | if (this.state.selectedIndex) { 118 | this.setState({ 119 | selectedIndex: -1 120 | }); 121 | } 122 | }, 123 | 124 | _selectOption: function _selectOption(option) { 125 | if (option) { 126 | var values = this.state.values.slice(0); // copy 127 | var options = this._getAvailableOptions(values); 128 | 129 | // determine which item to highlight 130 | var valueField = this.props.valueField; 131 | var optionIndex = _.findIndex(options, function (o) { 132 | return option[valueField] === o[valueField]; 133 | }); 134 | 135 | values.push(option); 136 | 137 | options = this._getAvailableOptions(values); 138 | var state = this._resetSearch(options); 139 | state.values = values; 140 | 141 | var nextOption = options[optionIndex]; 142 | if (_.isUndefined(nextOption)) { 143 | // at the end of the list so select previous one 144 | nextOption = options[optionIndex - 1]; 145 | if (_.isUndefined(nextOption)) { 146 | // bail out 147 | nextOption = _.first(options); 148 | } 149 | } 150 | 151 | state.highlighted = nextOption; 152 | 153 | this.setState(state); 154 | 155 | if (typeof this.props.onSelect === 'function') { 156 | this.props.onSelect(option, values); 157 | } 158 | } 159 | }, 160 | 161 | _getAvailableOptions: function _getAvailableOptions(values) { 162 | var options = this.state.initialOptions; 163 | var valueField = this.props.valueField; 164 | 165 | if (this.props.allowDuplicates === false && values) { 166 | options = _.filter(options, function (option) { 167 | var found = _.find(values, function (value) { 168 | return value[valueField] === option[valueField]; 169 | }); 170 | 171 | return typeof found === 'undefined'; 172 | }); 173 | } 174 | 175 | return this._sort(options); 176 | }, 177 | 178 | _moveLeft: function _moveLeft(event) { 179 | var input = this.refs.input.getDOMNode(); 180 | 181 | if (!this.state.values.length) { 182 | return false; 183 | } 184 | 185 | if (event.target === input && event.target.selectionStart === 0) { 186 | event.preventDefault(); 187 | 188 | // select stage 189 | this.setState({ 190 | selectedIndex: this.state.values.length - 1 191 | }); 192 | 193 | // focus on container 194 | this.refs.container.getDOMNode().focus(); 195 | } else if (this.state.selectedIndex !== -1) { 196 | var nextIndex = this.state.selectedIndex - 1; 197 | if (nextIndex > -1) { 198 | this.setState({ 199 | selectedIndex: nextIndex 200 | }); 201 | } 202 | } 203 | }, 204 | 205 | _moveRight: function _moveRight() { 206 | var input = this.refs.input.getDOMNode(); 207 | 208 | if (!this.state.values.length) { 209 | return false; 210 | } 211 | 212 | if (this.state.selectedIndex !== -1) { 213 | var nextIndex = this.state.selectedIndex + 1; 214 | if (nextIndex < this.state.values.length) { 215 | this.setState({ 216 | selectedIndex: nextIndex 217 | }); 218 | } else { 219 | // focus input box 220 | input.focus(); 221 | this.setState({ 222 | selectedIndex: -1 223 | }); 224 | } 225 | } 226 | }, 227 | 228 | _removeValue: function _removeValue(index) { 229 | var values = this.state.values.slice(0); // copy 230 | var removedOption = values.splice(index, 1); 231 | 232 | var options = this._getAvailableOptions(values); 233 | 234 | var state = this._resetSearch(options); 235 | state.values = values; 236 | 237 | this.setState(state); 238 | 239 | if (typeof this.props.onDelete === 'function') { 240 | this.props.onDelete(removedOption, values); 241 | } 242 | }, 243 | 244 | // removes last element 245 | _remove: function _remove(event) { 246 | if (!this.state.value) { 247 | event.preventDefault(); 248 | 249 | // remove last stage 250 | if (this.state.values.length) { 251 | this._removeValue(this.state.values.length - 1); 252 | } 253 | } 254 | }, 255 | 256 | // called from within, removes selected element 257 | _removeSelectedContainer: function _removeSelectedContainer(event) { 258 | if (this.state.selectedIndex !== -1) { 259 | event.preventDefault(); 260 | 261 | // move selection to the element before the removed one (gmail behavior) 262 | this.setState({ 263 | selectedIndex: this.state.selectedIndex - 1 264 | }); 265 | 266 | this._removeValue(this.state.selectedIndex); 267 | } 268 | }, 269 | 270 | _removeDeletedContainer: function _removeDeletedContainer(index) { 271 | this._removeValue(index); 272 | }, 273 | 274 | _selectValue: function _selectValue(index, event) { 275 | if (event) { 276 | event.preventDefault(); 277 | event.stopPropagation(); 278 | } 279 | 280 | this.setState({ 281 | selectedIndex: index 282 | }); 283 | 284 | this.refs.container.getDOMNode().focus(); 285 | }, 286 | 287 | _handleBlur: function _handleBlur(event) { 288 | if (this._optionsMouseDown === true) { 289 | this._optionsMouseDown = false; 290 | this.refs.input.getDOMNode().focus(); 291 | event.preventDefault(); 292 | event.stopPropagation(); 293 | } else { 294 | event.preventDefault(); 295 | this.setState({ 296 | focus: false 297 | }); 298 | } 299 | }, 300 | 301 | _handleOptionsMouseDown: function _handleOptionsMouseDown() { 302 | this._optionsMouseDown = true; 303 | }, 304 | 305 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 306 | if (_.isEqual(nextProps.values, this.props.values)) { 307 | var options = this._getAvailableOptions(nextProps.values); 308 | 309 | var state = this._resetSearch(options); 310 | state.values = nextProps.values; 311 | state.selected = null; 312 | 313 | this.setState(state); 314 | } 315 | }, 316 | 317 | componentDidUpdate: function componentDidUpdate() { 318 | this._updateScrollPosition(); 319 | }, 320 | 321 | render: function render() { 322 | var values = _.map(this.state.values, function (v, i) { 323 | var key = v[this.props.valueField]; 324 | 325 | var selected = i === this.state.selectedIndex; 326 | 327 | var label = v[this.props.labelField]; 328 | 329 | return React.createElement( 330 | ValueWrapper, 331 | { key: i, 332 | onClick: this._selectValue.bind(null, i), 333 | onDeleteClick: this._removeDeletedContainer.bind(null, i), 334 | selected: selected }, 335 | React.createElement( 336 | 'div', 337 | null, 338 | label 339 | ) 340 | ); 341 | }, this); 342 | 343 | var options = _.map(this.state.searchResults, function (option) { 344 | var valueField = this.props.valueField; 345 | var v = option[valueField]; 346 | 347 | var child = _.find(this.props.children, function (c) { 348 | return c.props[valueField] === v; 349 | }); 350 | 351 | var highlighted = this.state.highlighted && v === this.state.highlighted[valueField]; 352 | 353 | child = cloneWithProps(child, { tokens: this.state.searchTokens }); 354 | 355 | return React.createElement( 356 | OptionWrapper, 357 | { key: v, 358 | selected: highlighted, 359 | ref: highlighted ? 'highlighted' : null, 360 | option: option, 361 | onHover: this._handleOptionHover, 362 | onClick: this._handleOptionClick }, 363 | child 364 | ); 365 | }, this); 366 | 367 | var value = this.state.value; 368 | 369 | var wrapperClasses = cx({ 370 | 'react-choice-wrapper': true, 371 | 'react-choice-multiple': true, 372 | 'react-choice-multiple--in-focus': this.state.focus, 373 | 'react-choice-multiple--not-in-focus': !this.state.focus 374 | }); 375 | 376 | return React.createElement( 377 | 'div', 378 | { className: 'react-choice' }, 379 | React.createElement( 380 | 'div', 381 | { className: wrapperClasses, onClick: this._handleClick, 382 | tabIndex: '-1', ref: 'container', onKeyDown: this._handleContainerInput, 383 | onBlur: this._handleContainerBlur }, 384 | values, 385 | React.createElement('input', { type: 'text', 386 | placeholder: this.props.placeholder, 387 | value: value, 388 | className: 'react-choice-input react-choice-multiple__input', 389 | 390 | onKeyDown: this._handleInput, 391 | onChange: this._handleChange, 392 | onFocus: this._handleFocus, 393 | onBlur: this._handleBlur, 394 | 395 | autoComplete: 'off', 396 | role: 'combobox', 397 | 'aria-autocomplete': 'list', 398 | 'aria-expanded': this.state.focus, 399 | ref: 'input' }) 400 | ), 401 | this.state.focus ? React.createElement( 402 | Options, 403 | { onMouseDown: this._handleOptionsMouseDown, ref: 'options' }, 404 | options 405 | ) : null 406 | ); 407 | } 408 | }); 409 | 410 | module.exports = MultipleChoice; -------------------------------------------------------------------------------- /dist/option-mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react/addons"); 4 | 5 | var OptionMixin = { 6 | propTypes: { 7 | tokens: React.PropTypes.array.isRequired, 8 | children: React.PropTypes.string.isRequired 9 | } 10 | }; 11 | 12 | module.exports = OptionMixin; -------------------------------------------------------------------------------- /dist/option-wrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | function isArray(test) { 7 | return Object.prototype.toString.call(test) === '[object Array]'; 8 | } 9 | 10 | var OptionWrapper = React.createClass({ 11 | displayName: 'OptionWrapper', 12 | 13 | render: function render() { 14 | var classes = cx({ 15 | 'react-choice-option': true, 16 | 'react-choice-option--selected': !!this.props.selected 17 | }); 18 | 19 | return React.createElement( 20 | 'li', 21 | { className: classes, 22 | onMouseEnter: this.props.onHover.bind(null, this.props.option), 23 | onMouseDown: this.props.onClick.bind(null, this.props.option), 24 | onTouchStart: this.props.onClick.bind(null, this.props.option) }, 25 | this.props.children 26 | ); 27 | } 28 | }); 29 | 30 | module.exports = OptionWrapper; -------------------------------------------------------------------------------- /dist/option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | var OptionMixin = require('./option-mixin'); 7 | var TextHighlight = require('./text-highlight'); 8 | 9 | // 10 | // Select option 11 | // 12 | var SelectOption = React.createClass({ 13 | displayName: 'SelectOption', 14 | 15 | mixins: [OptionMixin], 16 | 17 | render: function render() { 18 | return React.createElement( 19 | 'div', 20 | null, 21 | React.createElement( 22 | TextHighlight, 23 | { tokens: this.props.tokens }, 24 | this.props.children 25 | ) 26 | ); 27 | } 28 | }); 29 | 30 | module.exports = SelectOption; -------------------------------------------------------------------------------- /dist/options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require("react/addons"); 4 | 5 | var Options = React.createClass({ 6 | displayName: "Options", 7 | 8 | propTypes: { 9 | children: React.PropTypes.array.isRequired, 10 | onMouseDown: React.PropTypes.func.isRequired 11 | }, 12 | 13 | _handleMouseDown: function _handleMouseDown(event) { 14 | this.props.onMouseDown(event); 15 | }, 16 | 17 | render: function render() { 18 | return React.createElement( 19 | "div", 20 | { className: "react-choice-options", 21 | onMouseDown: this._handleMouseDown, 22 | onMouseUp: this._handleMouseUp }, 23 | React.createElement( 24 | "ul", 25 | { className: "react-choice-options__list" }, 26 | this.props.children 27 | ) 28 | ); 29 | } 30 | }); 31 | 32 | module.exports = Options; -------------------------------------------------------------------------------- /dist/search-mixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Sifter = require('sifter'); 5 | 6 | var SearchMixin = { 7 | // 8 | // Public methods 9 | // 10 | focus: function focus(openOptions) { 11 | this.refs.input.getDOMNode().focus(); 12 | }, 13 | 14 | _sort: function _sort(list) { 15 | if (typeof this.props.sorter === 'function') { 16 | return this.props.sorter(list); 17 | } 18 | return _.sortBy(list, this.props.labelField); 19 | }, 20 | 21 | _handleClick: function _handleClick(event) { 22 | this.refs.input.getDOMNode().focus(); 23 | }, 24 | 25 | _handleInput: function _handleInput(event) { 26 | var keys = { 27 | 13: this._enter, 28 | 37: this._moveLeft, 29 | 38: this._moveUp, 30 | 39: this._moveRight, 31 | 40: this._moveDown, 32 | 8: this._remove 33 | }; 34 | 35 | if (typeof keys[event.keyCode] == 'function') { 36 | keys[event.keyCode](event); 37 | } 38 | }, 39 | 40 | _handleChange: function _handleChange(event) { 41 | event.preventDefault(); 42 | 43 | var query = event.target.value; 44 | 45 | var options = this._getAvailableOptions(); 46 | 47 | var searcher = new Sifter(options); 48 | 49 | var result = searcher.search(query, { 50 | fields: this.props.searchField 51 | }); 52 | 53 | var searchResults = _.map(result.items, function (res) { 54 | return options[res.id]; 55 | }); 56 | 57 | var highlighted = _.first(searchResults); 58 | 59 | this.setState({ 60 | value: query, 61 | searchResults: searchResults, 62 | searchTokens: result.tokens, 63 | highlighted: highlighted, 64 | selected: null }); 65 | }, 66 | 67 | _handleFocus: function _handleFocus(event) { 68 | event.preventDefault(); 69 | 70 | var highlighted; 71 | if (this.state.selected) { 72 | highlighted = _.find(this.state.searchResults, function (option) { 73 | return option[this.props.valueField] == this.state.selected[this.props.valueField]; 74 | }, this); 75 | } else { 76 | highlighted = _.first(this.state.searchResults); 77 | } 78 | 79 | this.setState({ 80 | focus: true, 81 | highlighted: highlighted 82 | }); 83 | }, 84 | 85 | _handleOptionHover: function _handleOptionHover(option, event) { 86 | event.preventDefault(); 87 | this.setState({ 88 | highlighted: option 89 | }); 90 | }, 91 | 92 | _handleOptionClick: function _handleOptionClick(option, event) { 93 | event.preventDefault(); 94 | event.stopPropagation(); 95 | this._selectOption(option); 96 | }, 97 | 98 | _moveUp: function _moveUp(event) { 99 | var options = this.state.searchResults; 100 | if (options.length > 0) { 101 | event.preventDefault(); 102 | var index = _.indexOf(options, this.state.highlighted); 103 | if (!_.isUndefined(options[index - 1])) { 104 | this.setState({ 105 | highlighted: options[index - 1] 106 | }); 107 | } 108 | } 109 | }, 110 | 111 | _moveDown: function _moveDown(event) { 112 | var options = this.state.searchResults; 113 | if (options.length > 0) { 114 | event.preventDefault(); 115 | var index = _.indexOf(options, this.state.highlighted); 116 | if (!_.isUndefined(options[index + 1])) { 117 | this.setState({ 118 | highlighted: options[index + 1] 119 | }); 120 | } 121 | } 122 | }, 123 | 124 | _enter: function _enter(event) { 125 | event.preventDefault(); 126 | this._selectOption(this.state.highlighted); 127 | }, 128 | 129 | _updateScrollPosition: function _updateScrollPosition() { 130 | var highlighted = this.refs.highlighted; 131 | if (highlighted) { 132 | // find if highlighted option is not visible 133 | var el = highlighted.getDOMNode(); 134 | var parent = this.refs.options.getDOMNode(); 135 | var offsetTop = el.offsetTop + el.clientHeight - parent.scrollTop; 136 | 137 | // scroll down 138 | if (offsetTop > parent.clientHeight) { 139 | var diff = el.offsetTop + el.clientHeight - parent.clientHeight; 140 | parent.scrollTop = diff; 141 | } else if (offsetTop - el.clientHeight < 0) { 142 | // scroll up 143 | parent.scrollTop = el.offsetTop; 144 | } 145 | } 146 | }, 147 | 148 | _resetSearch: function _resetSearch(options) { 149 | return { 150 | value: '', 151 | searchResults: options, 152 | searchTokens: [], 153 | highlighted: _.first(options) 154 | }; 155 | } }; 156 | 157 | module.exports = SearchMixin; -------------------------------------------------------------------------------- /dist/single.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var _ = require('lodash'); 5 | var cx = React.addons.classSet; 6 | var cloneWithProps = React.addons.cloneWithProps; 7 | 8 | var Icon = require('./icon'); 9 | var Options = require('./options'); 10 | var OptionWrapper = require('./option-wrapper'); 11 | 12 | var SearchMixin = require('./search-mixin'); 13 | 14 | // 15 | // Auto complete select box 16 | // 17 | var SingleChoice = React.createClass({ 18 | displayName: 'SingleChoice', 19 | 20 | mixins: [SearchMixin], 21 | 22 | propTypes: { 23 | name: React.PropTypes.string, // name of input 24 | placeholder: React.PropTypes.string, // input placeholder 25 | value: React.PropTypes.string, // initial value for input field 26 | children: React.PropTypes.array.isRequired, 27 | 28 | valueField: React.PropTypes.string, // value field name 29 | labelField: React.PropTypes.string, // label field name 30 | 31 | searchField: React.PropTypes.array, // array of search fields 32 | 33 | icon: React.PropTypes.func, // icon render 34 | 35 | onSelect: React.PropTypes.func // function called when option is selected 36 | }, 37 | 38 | getDefaultProps: function getDefaultProps() { 39 | return { 40 | valueField: 'value', 41 | labelField: 'children', 42 | searchField: ['children'] 43 | }; 44 | }, 45 | 46 | _getAvailableOptions: function _getAvailableOptions() { 47 | var options = this.state.initialOptions; 48 | 49 | return this._sort(options); 50 | }, 51 | 52 | getInitialState: function getInitialState() { 53 | var selected = null; 54 | 55 | var props = this.props.searchField; 56 | props.push(this.props.valueField); 57 | props.push(this.props.searchField); 58 | props = _.uniq(props); 59 | 60 | var options = _.map(this.props.children, function (child) { 61 | // TODO Validation ? 62 | return _.pick(child.props, props); 63 | }, this); 64 | 65 | if (this.props.value) { 66 | // find selected value 67 | selected = _.find(options, function (option) { 68 | return option[this.props.valueField] === this.props.value; 69 | }, this); 70 | } 71 | 72 | return { 73 | value: selected ? selected[this.props.labelField] : this.props.value, 74 | focus: false, 75 | searchResults: this._sort(options), 76 | initialOptions: options, 77 | highlighted: null, 78 | selected: selected, 79 | searchTokens: [] 80 | }; 81 | }, 82 | 83 | // 84 | // Public methods 85 | // 86 | getValue: function getValue() { 87 | return this.state.selected ? this.state.selected[this.props.valueField] : null; 88 | }, 89 | 90 | // 91 | // Events 92 | // 93 | _handleArrowClick: function _handleArrowClick(event) { 94 | if (this.state.focus) { 95 | this._handleBlur(event); 96 | this.refs.input.getDOMNode().blur(); 97 | } else { 98 | this._handleFocus(event); 99 | this.refs.input.getDOMNode().focus(); 100 | } 101 | }, 102 | 103 | _remove: function _remove(event) { 104 | if (this.state.selected) { 105 | event.preventDefault(); 106 | 107 | var state = this._resetSearch(this.state.initialOptions); 108 | state.selected = null; 109 | 110 | this.setState(state); 111 | } 112 | }, 113 | 114 | _selectOption: function _selectOption(option) { 115 | this._optionsMouseDown = false; 116 | this.refs.input.getDOMNode().blur(); 117 | this.setState({ 118 | focus: false 119 | }); 120 | 121 | if (option) { 122 | var options = this._getAvailableOptions(); 123 | var state = this._resetSearch(options); 124 | state.selected = option; 125 | 126 | this.setState(state); 127 | 128 | if (typeof this.props.onSelect === 'function') { 129 | this.props.onSelect(option); 130 | } 131 | } 132 | }, 133 | 134 | _handleBlur: function _handleBlur(event) { 135 | event.preventDefault(); 136 | if (this._optionsMouseDown === true) { 137 | this._optionsMouseDown = false; 138 | this.refs.input.getDOMNode().focus(); 139 | event.stopPropagation(); 140 | } else { 141 | this.setState({ 142 | focus: false 143 | }); 144 | } 145 | }, 146 | 147 | _handleOptionsMouseDown: function _handleOptionsMouseDown() { 148 | this._optionsMouseDown = true; 149 | }, 150 | 151 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 152 | if (nextProps.value !== this.props.value) { 153 | var options = this._getAvailableOptions(); 154 | 155 | var selected = _.find(options, function (option) { 156 | return option[this.props.valueField] === nextProps.value; 157 | }, this); 158 | 159 | var state = this._resetSearch(options); 160 | state.value = selected ? selected[this.props.labelField] : nextProps.value; 161 | state.selected = selected; 162 | 163 | this.setState(state); 164 | } 165 | }, 166 | 167 | componentDidUpdate: function componentDidUpdate(prevProps, prevState) { 168 | if (prevState.focus === false && this.state.focus === true) { 169 | this._updateScrollPosition(); 170 | } 171 | 172 | // select selected text in input box 173 | if (this.state.selected && this.state.focus) { 174 | setTimeout((function () { 175 | if (this.isMounted()) { 176 | this.refs.input.getDOMNode().select(); 177 | } 178 | }).bind(this), 50); 179 | } 180 | }, 181 | 182 | render: function render() { 183 | var options = _.map(this.state.searchResults, function (option) { 184 | var valueField = this.props.valueField; 185 | var v = option[valueField]; 186 | 187 | var child = _.find(this.props.children, function (c) { 188 | return c.props[valueField] === v; 189 | }); 190 | 191 | var highlighted = this.state.highlighted && v === this.state.highlighted[valueField]; 192 | 193 | child = cloneWithProps(child, { tokens: this.state.searchTokens }); 194 | 195 | return React.createElement( 196 | OptionWrapper, 197 | { key: v, 198 | selected: highlighted, 199 | ref: highlighted ? 'highlighted' : null, 200 | option: option, 201 | onHover: this._handleOptionHover, 202 | onClick: this._handleOptionClick }, 203 | child 204 | ); 205 | }, this); 206 | 207 | var value = this.state.selected ? this.state.selected[this.props.valueField] : null; 208 | var label = this.state.selected ? this.state.selected[this.props.labelField] : this.state.value; 209 | 210 | var wrapperClasses = cx({ 211 | 'react-choice-wrapper': true, 212 | 'react-choice-single': true, 213 | 'react-choice-single--in-focus': this.state.focus, 214 | 'react-choice-single--not-in-focus': !this.state.focus 215 | }); 216 | 217 | var IconRenderer = this.props.icon || Icon; 218 | 219 | return React.createElement( 220 | 'div', 221 | { className: 'react-choice' }, 222 | React.createElement('input', { type: 'hidden', name: this.props.name, value: value }), 223 | React.createElement( 224 | 'div', 225 | { className: wrapperClasses, onClick: this._handleClick }, 226 | React.createElement('input', { type: 'text', 227 | className: 'react-choice-input react-choice-single__input', 228 | placeholder: this.props.placeholder, 229 | value: label, 230 | 231 | onKeyDown: this._handleInput, 232 | onChange: this._handleChange, 233 | onFocus: this._handleFocus, 234 | onBlur: this._handleBlur, 235 | 236 | autoComplete: 'off', 237 | role: 'combobox', 238 | 'aria-autocomplete': 'list', 239 | 'aria-expanded': this.state.focus, 240 | ref: 'input' }) 241 | ), 242 | React.createElement( 243 | 'div', 244 | { className: 'react-choice-icon', onMouseDown: this._handleArrowClick }, 245 | React.createElement(IconRenderer, { focused: this.state.focus }) 246 | ), 247 | this.state.focus ? React.createElement( 248 | Options, 249 | { onMouseDown: this._handleOptionsMouseDown, ref: 'options' }, 250 | options 251 | ) : null 252 | ); 253 | } 254 | }); 255 | 256 | module.exports = SingleChoice; -------------------------------------------------------------------------------- /dist/text-highlight.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var _ = require('lodash'); 5 | 6 | var humanizeString = require('./humanize-string'); 7 | 8 | var TextHighlight = React.createClass({ 9 | displayName: 'TextHighlight', 10 | 11 | propTypes: { 12 | tokens: React.PropTypes.array.isRequired, // array of search tokens 13 | children: React.PropTypes.string.isRequired // text to highlight 14 | }, 15 | 16 | shouldComponentUpdate: function shouldComponentUpdate(nextProps) { 17 | if (!_.isEqual(nextProps, this.props)) { 18 | return true; 19 | } 20 | return false; 21 | }, 22 | 23 | splitText: function splitText(splits, regex) { 24 | var _splits = []; 25 | _.each(splits, function (split) { 26 | if (split.match === false) { 27 | var match = split.text.match(regex); 28 | if (match) { 29 | var s = split.text.split(regex); 30 | 31 | _.each(s, function (_s, index) { 32 | _splits.push({ 33 | text: _s, 34 | match: false 35 | }); 36 | 37 | if (index !== s.length - 1) { 38 | var matchCharacter = match[0]; 39 | 40 | var i = _splits.length - 1; 41 | if (_.isEmpty(_s) && i === 0 || _s.slice(-1) === ' ') { 42 | matchCharacter = humanizeString(matchCharacter); 43 | } else { 44 | matchCharacter = matchCharacter.toLowerCase(); 45 | } 46 | 47 | _splits.push({ 48 | text: matchCharacter, 49 | match: true 50 | }); 51 | } 52 | }); 53 | } else { 54 | _splits.push(split); 55 | } 56 | } else { 57 | _splits.push(split); 58 | } 59 | }); 60 | return _splits; 61 | }, 62 | 63 | render: function render() { 64 | var label = this.props.children; 65 | var tokens = this.props.tokens; 66 | 67 | var splits = [{ 68 | text: label, 69 | match: false 70 | }]; 71 | 72 | _.each(tokens, function (token) { 73 | splits = this.splitText(splits, token.regex); 74 | }, this); 75 | 76 | var output = _.map(splits, function (split, i) { 77 | var key = [split.text, split.match, i].join('.'); 78 | if (split.match) { 79 | return React.createElement( 80 | 'span', 81 | { className: 'text-highlight__match', key: key }, 82 | split.text 83 | ); 84 | } else { 85 | return React.createElement( 86 | 'span', 87 | { key: key }, 88 | split.text 89 | ); 90 | } 91 | }); 92 | 93 | return React.createElement( 94 | 'span', 95 | { className: 'text-highlight' }, 96 | output 97 | ); 98 | } 99 | }); 100 | 101 | module.exports = TextHighlight; -------------------------------------------------------------------------------- /example/base.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | export default class Base extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | React Choice Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onefinestay/react-choice/5855ae446334d22a92669d1758d7d42a30a445b4/example/build/.gitkeep -------------------------------------------------------------------------------- /example/code-snippets/custom-render.jsx: -------------------------------------------------------------------------------- 1 | var Choice = require('react-choice'); 2 | 3 | var PokemonRenderer = React.createClass({ 4 | // The option mixin provides proptypes that the component requires 5 | mixins: [Choice.OptionMixin], 6 | 7 | render: function() { 8 | var pokemon = this.props.pokemon; 9 | 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | 17 | #{pokemon.national_id} 18 | 19 | 20 | {pokemon.name} 21 | 22 |
23 | HP: {pokemon.hp} | Height: {pokemon.height} m | Weight: {pokemon.weight} kg 24 |
25 |
26 |
27 | ); 28 | } 29 | }); 30 | 31 | var options = POKEMON.map(function(pokemon) { 32 | return ( 33 | 34 | {pokemon.name} 35 | 36 | ); 37 | }); 38 | 39 | // Render component 40 | 41 | {options} 42 | 43 | -------------------------------------------------------------------------------- /example/code-snippets/multiple.jsx: -------------------------------------------------------------------------------- 1 | var Choice = require('react-choice'); 2 | 3 | var countries = [ 4 | {"label": "Afghanistan", "value": "AF"}, 5 | {"label": "Albania", "value": "AL"}, 6 | {"label": "Algeria", "value": "DZ"}, 7 | // etc... 8 | ]; 9 | 10 | var options = countries.map(function(country) { 11 | return ( 12 | 13 | {country.label} 14 | 15 | ); 16 | }); 17 | 18 | // Render component 19 | 20 | {options} 21 | 22 | -------------------------------------------------------------------------------- /example/code-snippets/single.jsx: -------------------------------------------------------------------------------- 1 | var Choice = require('react-choice'); 2 | 3 | var countries = [ 4 | {"label": "Afghanistan", "value": "AF"}, 5 | {"label": "Albania", "value": "AL"}, 6 | {"label": "Algeria", "value": "DZ"}, 7 | // etc... 8 | ]; 9 | 10 | var options = countries.map(function(country) { 11 | return ( 12 | 13 | {country.label} 14 | 15 | ); 16 | }); 17 | 18 | // Render component 19 | 20 | {options} 21 | 22 | -------------------------------------------------------------------------------- /example/components/code-snippet.jsx: -------------------------------------------------------------------------------- 1 | /* global hljs */ 2 | "use strict"; 3 | 4 | var React = require('react/addons'); 5 | var cx = React.addons.classSet; 6 | 7 | var CodeSnippet = React.createClass({ 8 | propTypes: { 9 | language: React.PropTypes.string.isRequired, 10 | toggle: React.PropTypes.bool, 11 | visible: React.PropTypes.bool 12 | }, 13 | 14 | getDefaultProps: function() { 15 | return { 16 | toggle: true, 17 | visible: false 18 | }; 19 | }, 20 | 21 | getInitialState: function() { 22 | return { 23 | visible: this.props.visible || !this.props.toggle 24 | }; 25 | }, 26 | 27 | handleClick: function(event) { 28 | event.preventDefault(); 29 | var value = !this.state.visible; 30 | 31 | var self = this; 32 | 33 | this.setState({ 34 | visible: value 35 | }, function() { 36 | if (value) { 37 | var el = self.refs.codeBlock.getDOMNode(); 38 | hljs.highlightBlock(el); 39 | } 40 | }); 41 | }, 42 | 43 | componentDidMount: function() { 44 | if (this.state.visible) { 45 | var el = this.refs.codeBlock.getDOMNode(); 46 | hljs.highlightBlock(el); 47 | } 48 | }, 49 | 50 | render: function() { 51 | var arrowClasses = cx({ 52 | 'code-snippet__arrow': true, 53 | 'code-snippet__arrow--right': !this.state.visible, 54 | 'code-snippet__arrow--up': this.state.visible, 55 | }); 56 | 57 | return ( 58 |
59 | {this.props.toggle ? 60 | 61 | 62 | {!this.state.visible ? "Show code" : "Hide code"} 63 | : null} 64 | {this.state.visible ? 65 |
66 |             
67 |               {this.props.children}
68 |             
69 |           
: null} 70 |
71 | ); 72 | } 73 | }); 74 | 75 | module.exports = CodeSnippet; 76 | -------------------------------------------------------------------------------- /example/components/custom-render.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | var React = require('react/addons'); 5 | var CodeSnippet = require('./code-snippet'); 6 | var Choice = require('../../'); 7 | var _ = require('lodash'); 8 | 9 | var POKEMON = require('../data/pokemon.js'); 10 | 11 | var PokemonRenderer = React.createClass({ 12 | mixins: [Choice.OptionMixin], 13 | 14 | render: function() { 15 | var pokemon = this.props.pokemon; 16 | 17 | var weight = pokemon.weight / 10; 18 | var height = pokemon.height / 10; 19 | 20 | return ( 21 |
22 |
23 | 24 |
25 |
26 | 27 | #{pokemon.national_id} 28 | 29 | 30 | {pokemon.name} 31 | 32 |
33 | HP: {pokemon.hp} | Height: {height} m | Weight: {weight} kg 34 |
35 |
36 |
37 | ); 38 | } 39 | }); 40 | 41 | var customExample = fs.readFileSync(__dirname + '/../code-snippets/custom-render.jsx', 'utf8'); 42 | 43 | var CustomRender = React.createClass({ 44 | render: function() { 45 | var options = _.map(POKEMON, function(pokemon) { 46 | var value = pokemon.national_id; 47 | return ( 48 | 49 | {pokemon.name} 50 | 51 | ); 52 | }); 53 | 54 | var sorter = function(list) { 55 | return _.sortBy(list, 'national_id'); 56 | }; 57 | 58 | return ( 59 |
60 |

Custom Renderer

61 |
62 | 63 | {options} 64 | 65 |
66 |
67 |

Creating a custom renderer for the options is easy:

68 | 69 | {customExample} 70 | 71 |

TextHighlight is a component that contains the logic 72 | to highlight the text from the search tokens that Sifter returns.

73 |

Pokemon data provided by Pokéapi

74 |
75 |
76 | ); 77 | } 78 | }); 79 | 80 | module.exports = CustomRender; 81 | -------------------------------------------------------------------------------- /example/components/features.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Features = React.createClass({ 6 | render: function() { 7 | return ( 8 |
9 |

Features

10 | 21 |
22 | ); 23 | } 24 | }); 25 | 26 | module.exports = Features; 27 | -------------------------------------------------------------------------------- /example/components/footer.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Footer = React.createClass({ 6 | render: function() { 7 | return ( 8 | 16 | ); 17 | } 18 | }); 19 | 20 | var OFSCredit = React.createClass({ 21 | render: function() { 22 | return ( 23 |
24 |

Built by

25 | 26 | 27 | 28 |
29 | ); 30 | } 31 | }); 32 | 33 | module.exports = Footer; 34 | -------------------------------------------------------------------------------- /example/components/github-ribbon.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | 5 | var GithubRibbon = React.createClass({ 6 | render: function() { 7 | var style = { 8 | position: 'absolute', 9 | top: 0, 10 | right: 0, 11 | border: 0 12 | }; 13 | 14 | return ( 15 | 16 | Fork me on GitHub 17 | 18 | ); 19 | } 20 | }); 21 | 22 | module.exports = GithubRibbon; 23 | -------------------------------------------------------------------------------- /example/components/header.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Header = React.createClass({ 6 | render: function() { 7 | return ( 8 |
9 |

React Choice

10 |
11 | ); 12 | } 13 | }); 14 | 15 | module.exports = Header; 16 | -------------------------------------------------------------------------------- /example/components/install.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var CodeSnippet = require('./code-snippet'); 5 | 6 | var Install = React.createClass({ 7 | render: function() { 8 | return ( 9 |
10 |

Install

11 | 12 | npm install react-choice 13 | 14 |
15 | ); 16 | } 17 | }); 18 | 19 | module.exports = Install; 20 | -------------------------------------------------------------------------------- /example/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $ofs-pink: #ff2558; 2 | $ofs-red: #d80033; 3 | -------------------------------------------------------------------------------- /example/css/choice/_icon.scss: -------------------------------------------------------------------------------- 1 | .react-choice-icon { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | width: 30px; 7 | cursor: pointer; 8 | 9 | $arrow-size: 6px; 10 | $arrow-colour: #222; 11 | 12 | &__arrow { 13 | display: inline-block; 14 | width: 0; 15 | height: 0; 16 | margin-left: 5px; 17 | margin-top: 50%; 18 | 19 | &--down { 20 | border-top: $arrow-size solid $arrow-colour; 21 | border-bottom: $arrow-size solid transparent; 22 | 23 | border-left: $arrow-size solid transparent; 24 | border-right: $arrow-size solid transparent; 25 | } 26 | 27 | &--up { 28 | margin-top: 30%; 29 | 30 | border-top: $arrow-size solid transparent; 31 | border-bottom: $arrow-size solid $arrow-colour; 32 | 33 | border-left: $arrow-size solid transparent; 34 | border-right: $arrow-size solid transparent; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/css/choice/_input.scss: -------------------------------------------------------------------------------- 1 | .react-choice-input { 2 | cursor: inherit; 3 | 4 | -webkit-appearance: none; 5 | -webkit-border-radius: 0; 6 | border-radius: 0; 7 | background-color: transparent; 8 | 9 | background-color: transparent; 10 | font-family: inherit; 11 | border: none; 12 | box-shadow: none; 13 | padding: 0; 14 | margin: 0; 15 | 16 | font-size: .875rem; 17 | 18 | &:focus { 19 | background: transparent; 20 | border: none; 21 | box-shadow: none; 22 | outline: none; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/css/choice/_multiple.scss: -------------------------------------------------------------------------------- 1 | .react-choice-multiple { 2 | cursor: text; 3 | padding-bottom: 3px; 4 | 5 | &:focus { 6 | outline: none; 7 | } 8 | 9 | &__input { 10 | float: left; 11 | margin-top: 5px; 12 | margin-bottom: 10px; 13 | width: 110px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/css/choice/_option.scss: -------------------------------------------------------------------------------- 1 | .react-choice-option { 2 | cursor: pointer; 3 | padding: 6px 10px; 4 | font-size: 0.875rem; 5 | 6 | &--selected { 7 | background: #eee; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/css/choice/_options.scss: -------------------------------------------------------------------------------- 1 | .react-choice-options { 2 | position: absolute; 3 | z-index: 2; 4 | width: 100%; 5 | box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); 6 | max-height: 305px; 7 | overflow: auto; 8 | 9 | &__list { 10 | background: #fff; 11 | border: 1px solid #D0D0D0; 12 | border-top: none; 13 | list-style-type: none; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/css/choice/_single.scss: -------------------------------------------------------------------------------- 1 | .react-choice-single { 2 | height: 2.3125rem; 3 | 4 | &--not-in-focus { 5 | cursor: pointer; 6 | 7 | background-color: #f9f9f9; 8 | background-image: -moz-linear-gradient(top, #fefefe, #f2f2f2); 9 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2)); 10 | background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2); 11 | background-image: -o-linear-gradient(top, #fefefe, #f2f2f2); 12 | background-image: linear-gradient(to bottom, #fefefe, #f2f2f2); 13 | background-repeat: repeat-x; 14 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0); 15 | -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.8); 16 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.8); 17 | } 18 | 19 | &--in-focus { 20 | background: #fafafa; 21 | border-color: #999; 22 | outline: 0; 23 | 24 | -webkit-box-shadow: 0 0 5px #999; 25 | -moz-box-shadow: 0 0 5px #999; 26 | box-shadow: 0 0 5px #999; 27 | border-color: #999; 28 | } 29 | 30 | &__input { 31 | width: 100%; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/css/choice/_text-highlight.scss: -------------------------------------------------------------------------------- 1 | .text-highlight { 2 | &__match { 3 | font-weight: bold; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example/css/choice/_value.scss: -------------------------------------------------------------------------------- 1 | .react-choice-value { 2 | float: left; 3 | border: 1px solid #eee; 4 | margin-right: 5px; 5 | cursor: pointer; 6 | padding: 4px; 7 | margin-bottom: 5px; 8 | 9 | &--is-selected { 10 | background: #eee; 11 | } 12 | 13 | &__delete { 14 | padding-left: 5px; 15 | font-weight: bold; 16 | } 17 | 18 | &__children { 19 | display: inline-block; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/css/choice/_wrapper.scss: -------------------------------------------------------------------------------- 1 | .react-choice-wrapper { 2 | overflow: hidden; 3 | font-family: inherit; 4 | border: 1px solid #ccc; 5 | 6 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 7 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 8 | 9 | color: rgba(0, 0, 0, 0.75); 10 | display: block; 11 | font-size: .875rem; 12 | margin: 0; 13 | padding: .5rem; 14 | width: 100%; 15 | -moz-box-sizing: border-box; 16 | -webkit-box-sizing: border-box; 17 | box-sizing: border-box; 18 | -webkit-transition: -webkit-box-shadow .45s, border-color .45s ease-in-out; 19 | -moz-transition: -moz-box-shadow .45s, border-color .45s ease-in-out; 20 | transition: box-shadow .45s, border-color .45s ease-in-out; 21 | } 22 | -------------------------------------------------------------------------------- /example/css/choice/index.css: -------------------------------------------------------------------------------- 1 | .react-choice-wrapper { 2 | overflow: hidden; 3 | font-family: inherit; 4 | border: 1px solid #ccc; 5 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); 6 | color: rgba(0, 0, 0, 0.75); 7 | display: block; 8 | font-size: .875rem; 9 | margin: 0; 10 | padding: .5rem; 11 | width: 100%; 12 | box-sizing: border-box; 13 | -webkit-transition: box-shadow .45s, border-color .45s ease-in-out; 14 | transition: box-shadow .45s, border-color .45s ease-in-out; } 15 | 16 | .react-choice-input { 17 | cursor: inherit; 18 | -webkit-appearance: none; 19 | border-radius: 0; 20 | background-color: transparent; 21 | background-color: transparent; 22 | font-family: inherit; 23 | border: none; 24 | box-shadow: none; 25 | padding: 0; 26 | margin: 0; 27 | font-size: .875rem; } 28 | .react-choice-input:focus { 29 | background: transparent; 30 | border: none; 31 | box-shadow: none; 32 | outline: none; } 33 | 34 | .react-choice-single { 35 | height: 2.3125rem; } 36 | .react-choice-single--not-in-focus { 37 | cursor: pointer; 38 | background-color: #f9f9f9; 39 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fefefe), to(#f2f2f2)); 40 | background-image: -webkit-linear-gradient(top, #fefefe, #f2f2f2); 41 | background-image: linear-gradient(to bottom, #fefefe, #f2f2f2); 42 | background-repeat: repeat-x; 43 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffefefe', endColorstr='#fff2f2f2', GradientType=0); 44 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.8); } 45 | .react-choice-single--in-focus { 46 | background: #fafafa; 47 | border-color: #999; 48 | outline: 0; 49 | box-shadow: 0 0 5px #999; 50 | border-color: #999; } 51 | .react-choice-single__input { 52 | width: 100%; } 53 | 54 | .react-choice-multiple { 55 | cursor: text; 56 | padding-bottom: 3px; } 57 | .react-choice-multiple:focus { 58 | outline: none; } 59 | .react-choice-multiple__input { 60 | float: left; 61 | margin-top: 5px; 62 | margin-bottom: 10px; 63 | width: 110px; } 64 | 65 | .react-choice-icon { 66 | position: absolute; 67 | top: 0; 68 | bottom: 0; 69 | right: 0; 70 | width: 30px; 71 | cursor: pointer; } 72 | .react-choice-icon__arrow { 73 | display: inline-block; 74 | width: 0; 75 | height: 0; 76 | margin-left: 5px; 77 | margin-top: 50%; } 78 | .react-choice-icon__arrow--down { 79 | border-top: 6px solid #222; 80 | border-bottom: 6px solid transparent; 81 | border-left: 6px solid transparent; 82 | border-right: 6px solid transparent; } 83 | .react-choice-icon__arrow--up { 84 | margin-top: 30%; 85 | border-top: 6px solid transparent; 86 | border-bottom: 6px solid #222; 87 | border-left: 6px solid transparent; 88 | border-right: 6px solid transparent; } 89 | 90 | .react-choice-options { 91 | position: absolute; 92 | z-index: 2; 93 | width: 100%; 94 | box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); 95 | max-height: 305px; 96 | overflow: auto; } 97 | .react-choice-options__list { 98 | background: #fff; 99 | border: 1px solid #D0D0D0; 100 | border-top: none; 101 | list-style-type: none; 102 | margin: 0; 103 | padding: 0; } 104 | 105 | .react-choice-option { 106 | cursor: pointer; 107 | padding: 6px 10px; 108 | font-size: 0.875rem; } 109 | .react-choice-option--selected { 110 | background: #eee; } 111 | 112 | .text-highlight__match { 113 | font-weight: bold; } 114 | 115 | .react-choice-value { 116 | float: left; 117 | border: 1px solid #eee; 118 | margin-right: 5px; 119 | cursor: pointer; 120 | padding: 4px; 121 | margin-bottom: 5px; } 122 | .react-choice-value--is-selected { 123 | background: #eee; } 124 | .react-choice-value__delete { 125 | padding-left: 5px; 126 | font-weight: bold; } 127 | .react-choice-value__children { 128 | display: inline-block; } 129 | 130 | .react-choice { 131 | position: relative; } 132 | -------------------------------------------------------------------------------- /example/css/choice/index.scss: -------------------------------------------------------------------------------- 1 | @import "wrapper"; 2 | @import "input"; 3 | @import "single"; 4 | @import "multiple"; 5 | @import "icon"; 6 | @import "options"; 7 | @import "option"; 8 | @import "text-highlight"; 9 | @import "value"; 10 | 11 | .react-choice { 12 | position: relative; 13 | } 14 | -------------------------------------------------------------------------------- /example/css/components/_code-snippet.scss: -------------------------------------------------------------------------------- 1 | .code-snippet { 2 | margin-top: 1rem; 3 | font-size: 0.9rem; 4 | 5 | $arrow-size: 6px; 6 | 7 | &__arrow { 8 | display: inline-block; 9 | width: 0; 10 | height: 0; 11 | 12 | &--right { 13 | border-top: $arrow-size solid transparent; 14 | border-bottom: $arrow-size solid transparent; 15 | 16 | border-left: $arrow-size solid #AAAAAA; 17 | border-right: $arrow-size solid transparent; 18 | } 19 | 20 | &--up { 21 | margin-right: 6px; 22 | margin-bottom: 3px; 23 | 24 | border-top: $arrow-size solid transparent; 25 | border-bottom: $arrow-size solid #AAAAAA; 26 | 27 | border-left: $arrow-size solid transparent; 28 | border-right: $arrow-size solid transparent; 29 | } 30 | } 31 | 32 | &__toggle-button { 33 | color: $ofs-pink; 34 | text-decoration: none; 35 | 36 | &:hover { 37 | color: $ofs-red; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/css/components/_features.scss: -------------------------------------------------------------------------------- 1 | .features { 2 | overflow: hidden; 3 | 4 | &__point { 5 | margin-bottom: 0.5rem; 6 | font-weight: 200; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/css/components/_footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | margin: 20px 0; 3 | padding: 20px 0; 4 | border-top: 1px solid #ccc; 5 | 6 | &__links { 7 | text-align: center; 8 | } 9 | 10 | &__link { 11 | text-decoration: none; 12 | display: inline-block; 13 | margin-right: 10px; 14 | color: $ofs-pink; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/css/components/_header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | margin-top: 50px; 3 | 4 | &__logo { 5 | width: 200px; 6 | margin: 0 auto; 7 | display: block; 8 | } 9 | 10 | &__title { 11 | text-align: center; 12 | margin-bottom: 3rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/css/components/_ofs-credit.scss: -------------------------------------------------------------------------------- 1 | .ofs-credit { 2 | margin-bottom: 1rem; 3 | 4 | &__text { 5 | text-align: center; 6 | margin: 0; 7 | color: #999; 8 | margin-bottom: 0.5rem; 9 | } 10 | 11 | &__logo { 12 | display: block; 13 | width: 200px; 14 | margin: 0 auto; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/css/components/_pokemon.scss: -------------------------------------------------------------------------------- 1 | .pokemon { 2 | &__image { 3 | width: 40px; 4 | height: 40px; 5 | display: inline-block; 6 | 7 | img { 8 | width: 100%; 9 | } 10 | } 11 | 12 | &__number { 13 | color: #777; 14 | margin-right: 5px; 15 | } 16 | 17 | &__name { 18 | display: inline-block; 19 | padding-left: 20px; 20 | margin-top: 5px; 21 | vertical-align: top; 22 | } 23 | 24 | &__attributes { 25 | color: #777; 26 | font-size: 0.8rem; 27 | margin-top: 5px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/css/components/_tutorial.scss: -------------------------------------------------------------------------------- 1 | .tutorial { 2 | &__example { 3 | margin-bottom: 20px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /example/css/index.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | // Components 4 | @import "components/header"; 5 | @import "components/footer"; 6 | @import "components/code-snippet"; 7 | @import "components/features"; 8 | @import "components/ofs-credit"; 9 | @import "components/tutorial"; 10 | @import "components/pokemon"; 11 | 12 | // Base 13 | body { 14 | font-family: "Helvetica Neue","Helvetica",Helvetica,Arial,sans-serif; 15 | font-size: 16px; 16 | width: 940px; 17 | margin: 0 auto; 18 | } 19 | 20 | h1, 21 | h2, 22 | h3, 23 | h4 { 24 | font-weight: lighter; 25 | letter-spacing: 1.2px; 26 | text-rendering: optimizeLegibility; 27 | color: $ofs-pink; 28 | } 29 | 30 | .example-single, 31 | .example-multiple { 32 | width: 450px; 33 | float: left; 34 | 35 | h2 { 36 | text-align: center; 37 | } 38 | } 39 | 40 | .example-single { 41 | padding-right: 9px; 42 | border-right: 1px solid #ccc; 43 | margin-right: 10px; 44 | padding-bottom: 20px; 45 | 46 | .code-snippet { 47 | margin-top: 23px; 48 | } 49 | } 50 | 51 | p.info { 52 | font-weight: 0.8rem; 53 | color: #777; 54 | } 55 | -------------------------------------------------------------------------------- /example/data/countries.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | {"label": "Afghanistan", "value": "AF"}, 3 | {"label": "Albania", "value": "AL"}, 4 | {"label": "Algeria", "value": "DZ"}, 5 | {"label": "American Samoa", "value": "AS"}, 6 | {"label": "Andorra", "value": "AD"}, 7 | {"label": "Angola", "value": "AO"}, 8 | {"label": "Anguilla", "value": "AI"}, 9 | {"label": "Antarctica", "value": "AQ"}, 10 | {"label": "Antigua and Barbuda", "value": "AG"}, 11 | {"label": "Argentina", "value": "AR"}, 12 | {"label": "Armenia", "value": "AM"}, 13 | {"label": "Aruba", "value": "AW"}, 14 | {"label": "Australia", "value": "AU"}, 15 | {"label": "Austria", "value": "AT"}, 16 | {"label": "Azerbaijan", "value": "AZ"}, 17 | {"label": "Bahamas", "value": "BS"}, 18 | {"label": "Bahrain", "value": "BH"}, 19 | {"label": "Bangladesh", "value": "BD"}, 20 | {"label": "Barbados", "value": "BB"}, 21 | {"label": "Belarus", "value": "BY"}, 22 | {"label": "Belgium", "value": "BE"}, 23 | {"label": "Belize", "value": "BZ"}, 24 | {"label": "Benin", "value": "BJ"}, 25 | {"label": "Bermuda", "value": "BM"}, 26 | {"label": "Bhutan", "value": "BT"}, 27 | {"label": "Bolivia", "value": "BO"}, 28 | {"label": "Bosnia and Herzegovina", "value": "BA"}, 29 | {"label": "Botswana", "value": "BW"}, 30 | {"label": "Bouvet Island", "value": "BV"}, 31 | {"label": "Brazil", "value": "BR"}, 32 | {"label": "British Indian Ocean Territory", "value": "IO"}, 33 | {"label": "Brunei Darussalam", "value": "BN"}, 34 | {"label": "Bulgaria", "value": "BG"}, 35 | {"label": "Burkina Faso", "value": "BF"}, 36 | {"label": "Burundi", "value": "BI"}, 37 | {"label": "Cambodia", "value": "KH"}, 38 | {"label": "Cameroon", "value": "CM"}, 39 | {"label": "Canada", "value": "CA"}, 40 | {"label": "Cape Verde", "value": "CV"}, 41 | {"label": "Cayman Islands", "value": "KY"}, 42 | {"label": "Central African Republic", "value": "CF"}, 43 | {"label": "Chad", "value": "TD"}, 44 | {"label": "Chile", "value": "CL"}, 45 | {"label": "China", "value": "CN"}, 46 | {"label": "Christmas Island", "value": "CX"}, 47 | {"label": "Cocos (Keeling) Islands", "value": "CC"}, 48 | {"label": "Colombia", "value": "CO"}, 49 | {"label": "Comoros", "value": "KM"}, 50 | {"label": "Congo", "value": "CG"}, 51 | {"label": "Congo, The Democratic Republic of the", "value": "CD"}, 52 | {"label": "Cook Islands", "value": "CK"}, 53 | {"label": "Costa Rica", "value": "CR"}, 54 | {"label": "Cote D\"Ivoire", "value": "CI"}, 55 | {"label": "Croatia", "value": "HR"}, 56 | {"label": "Cuba", "value": "CU"}, 57 | {"label": "Cyprus", "value": "CY"}, 58 | {"label": "Czech Republic", "value": "CZ"}, 59 | {"label": "Denmark", "value": "DK"}, 60 | {"label": "Djibouti", "value": "DJ"}, 61 | {"label": "Dominica", "value": "DM"}, 62 | {"label": "Dominican Republic", "value": "DO"}, 63 | {"label": "Ecuador", "value": "EC"}, 64 | {"label": "Egypt", "value": "EG"}, 65 | {"label": "El Salvador", "value": "SV"}, 66 | {"label": "Equatorial Guinea", "value": "GQ"}, 67 | {"label": "Eritrea", "value": "ER"}, 68 | {"label": "Estonia", "value": "EE"}, 69 | {"label": "Ethiopia", "value": "ET"}, 70 | {"label": "Falkland Islands (Malvinas)", "value": "FK"}, 71 | {"label": "Faroe Islands", "value": "FO"}, 72 | {"label": "Fiji", "value": "FJ"}, 73 | {"label": "Finland", "value": "FI"}, 74 | {"label": "France", "value": "FR"}, 75 | {"label": "French Guiana", "value": "GF"}, 76 | {"label": "French Polynesia", "value": "PF"}, 77 | {"label": "French Southern Territories", "value": "TF"}, 78 | {"label": "Gabon", "value": "GA"}, 79 | {"label": "Gambia", "value": "GM"}, 80 | {"label": "Georgia", "value": "GE"}, 81 | {"label": "Germany", "value": "DE"}, 82 | {"label": "Ghana", "value": "GH"}, 83 | {"label": "Gibraltar", "value": "GI"}, 84 | {"label": "Greece", "value": "GR"}, 85 | {"label": "Greenland", "value": "GL"}, 86 | {"label": "Grenada", "value": "GD"}, 87 | {"label": "Guadeloupe", "value": "GP"}, 88 | {"label": "Guam", "value": "GU"}, 89 | {"label": "Guatemala", "value": "GT"}, 90 | {"label": "Guernsey", "value": "GG"}, 91 | {"label": "Guinea", "value": "GN"}, 92 | {"label": "Guinea-Bissau", "value": "GW"}, 93 | {"label": "Guyana", "value": "GY"}, 94 | {"label": "Haiti", "value": "HT"}, 95 | {"label": "Heard Island and Mcdonald Islands", "value": "HM"}, 96 | {"label": "Holy See (Vatican City State)", "value": "VA"}, 97 | {"label": "Honduras", "value": "HN"}, 98 | {"label": "Hong Kong", "value": "HK"}, 99 | {"label": "Hungary", "value": "HU"}, 100 | {"label": "Iceland", "value": "IS"}, 101 | {"label": "India", "value": "IN"}, 102 | {"label": "Indonesia", "value": "ID"}, 103 | {"label": "Iran, Islamic Republic Of", "value": "IR"}, 104 | {"label": "Iraq", "value": "IQ"}, 105 | {"label": "Ireland", "value": "IE"}, 106 | {"label": "Isle of Man", "value": "IM"}, 107 | {"label": "Israel", "value": "IL"}, 108 | {"label": "Italy", "value": "IT"}, 109 | {"label": "Jamaica", "value": "JM"}, 110 | {"label": "Japan", "value": "JP"}, 111 | {"label": "Jersey", "value": "JE"}, 112 | {"label": "Jordan", "value": "JO"}, 113 | {"label": "Kazakhstan", "value": "KZ"}, 114 | {"label": "Kenya", "value": "KE"}, 115 | {"label": "Kiribati", "value": "KI"}, 116 | {"label": "Korea, Democratic People\"S Republic of", "value": "KP"}, 117 | {"label": "Korea, Republic of", "value": "KR"}, 118 | {"label": "Kuwait", "value": "KW"}, 119 | {"label": "Kyrgyzstan", "value": "KG"}, 120 | {"label": "Lao People\"S Democratic Republic", "value": "LA"}, 121 | {"label": "Latvia", "value": "LV"}, 122 | {"label": "Lebanon", "value": "LB"}, 123 | {"label": "Lesotho", "value": "LS"}, 124 | {"label": "Liberia", "value": "LR"}, 125 | {"label": "Libyan Arab Jamahiriya", "value": "LY"}, 126 | {"label": "Liechtenstein", "value": "LI"}, 127 | {"label": "Lithuania", "value": "LT"}, 128 | {"label": "Luxembourg", "value": "LU"}, 129 | {"label": "Macao", "value": "MO"}, 130 | {"label": "Macedonia, The Former Yugoslav Republic of", "value": "MK"}, 131 | {"label": "Madagascar", "value": "MG"}, 132 | {"label": "Malawi", "value": "MW"}, 133 | {"label": "Malaysia", "value": "MY"}, 134 | {"label": "Maldives", "value": "MV"}, 135 | {"label": "Mali", "value": "ML"}, 136 | {"label": "Malta", "value": "MT"}, 137 | {"label": "Marshall Islands", "value": "MH"}, 138 | {"label": "Martinique", "value": "MQ"}, 139 | {"label": "Mauritania", "value": "MR"}, 140 | {"label": "Mauritius", "value": "MU"}, 141 | {"label": "Mayotte", "value": "YT"}, 142 | {"label": "Mexico", "value": "MX"}, 143 | {"label": "Micronesia, Federated States of", "value": "FM"}, 144 | {"label": "Moldova, Republic of", "value": "MD"}, 145 | {"label": "Monaco", "value": "MC"}, 146 | {"label": "Mongolia", "value": "MN"}, 147 | {"label": "Montserrat", "value": "MS"}, 148 | {"label": "Morocco", "value": "MA"}, 149 | {"label": "Mozambique", "value": "MZ"}, 150 | {"label": "Myanmar", "value": "MM"}, 151 | {"label": "Namibia", "value": "NA"}, 152 | {"label": "Nauru", "value": "NR"}, 153 | {"label": "Nepal", "value": "NP"}, 154 | {"label": "Netherlands", "value": "NL"}, 155 | {"label": "Netherlands Antilles", "value": "AN"}, 156 | {"label": "New Caledonia", "value": "NC"}, 157 | {"label": "New Zealand", "value": "NZ"}, 158 | {"label": "Nicaragua", "value": "NI"}, 159 | {"label": "Niger", "value": "NE"}, 160 | {"label": "Nigeria", "value": "NG"}, 161 | {"label": "Niue", "value": "NU"}, 162 | {"label": "Norfolk Island", "value": "NF"}, 163 | {"label": "Northern Mariana Islands", "value": "MP"}, 164 | {"label": "Norway", "value": "NO"}, 165 | {"label": "Oman", "value": "OM"}, 166 | {"label": "Pakistan", "value": "PK"}, 167 | {"label": "Palau", "value": "PW"}, 168 | {"label": "Palestinian Territory, Occupied", "value": "PS"}, 169 | {"label": "Panama", "value": "PA"}, 170 | {"label": "Papua New Guinea", "value": "PG"}, 171 | {"label": "Paraguay", "value": "PY"}, 172 | {"label": "Peru", "value": "PE"}, 173 | {"label": "Philippines", "value": "PH"}, 174 | {"label": "Pitcairn", "value": "PN"}, 175 | {"label": "Poland", "value": "PL"}, 176 | {"label": "Portugal", "value": "PT"}, 177 | {"label": "Puerto Rico", "value": "PR"}, 178 | {"label": "Qatar", "value": "QA"}, 179 | {"label": "Reunion", "value": "RE"}, 180 | {"label": "Romania", "value": "RO"}, 181 | {"label": "Russian Federation", "value": "RU"}, 182 | {"label": "RWANDA", "value": "RW"}, 183 | {"label": "Saint Helena", "value": "SH"}, 184 | {"label": "Saint Kitts and Nevis", "value": "KN"}, 185 | {"label": "Saint Lucia", "value": "LC"}, 186 | {"label": "Saint Pierre and Miquelon", "value": "PM"}, 187 | {"label": "Saint Vincent and the Grenadines", "value": "VC"}, 188 | {"label": "Samoa", "value": "WS"}, 189 | {"label": "San Marino", "value": "SM"}, 190 | {"label": "Sao Tome and Principe", "value": "ST"}, 191 | {"label": "Saudi Arabia", "value": "SA"}, 192 | {"label": "Senegal", "value": "SN"}, 193 | {"label": "Serbia and Montenegro", "value": "CS"}, 194 | {"label": "Seychelles", "value": "SC"}, 195 | {"label": "Sierra Leone", "value": "SL"}, 196 | {"label": "Singapore", "value": "SG"}, 197 | {"label": "Slovakia", "value": "SK"}, 198 | {"label": "Slovenia", "value": "SI"}, 199 | {"label": "Solomon Islands", "value": "SB"}, 200 | {"label": "Somalia", "value": "SO"}, 201 | {"label": "South Africa", "value": "ZA"}, 202 | {"label": "South Georgia and the South Sandwich Islands", "value": "GS"}, 203 | {"label": "Spain", "value": "ES"}, 204 | {"label": "Sri Lanka", "value": "LK"}, 205 | {"label": "Sudan", "value": "SD"}, 206 | {"label": "Suriname", "value": "SR"}, 207 | {"label": "Svalbard and Jan Mayen", "value": "SJ"}, 208 | {"label": "Swaziland", "value": "SZ"}, 209 | {"label": "Sweden", "value": "SE"}, 210 | {"label": "Switzerland", "value": "CH"}, 211 | {"label": "Syrian Arab Republic", "value": "SY"}, 212 | {"label": "Taiwan, Province of China", "value": "TW"}, 213 | {"label": "Tajikistan", "value": "TJ"}, 214 | {"label": "Tanzania, United Republic of", "value": "TZ"}, 215 | {"label": "Thailand", "value": "TH"}, 216 | {"label": "Timor-Leste", "value": "TL"}, 217 | {"label": "Togo", "value": "TG"}, 218 | {"label": "Tokelau", "value": "TK"}, 219 | {"label": "Tonga", "value": "TO"}, 220 | {"label": "Trinidad and Tobago", "value": "TT"}, 221 | {"label": "Tunisia", "value": "TN"}, 222 | {"label": "Turkey", "value": "TR"}, 223 | {"label": "Turkmenistan", "value": "TM"}, 224 | {"label": "Turks and Caicos Islands", "value": "TC"}, 225 | {"label": "Tuvalu", "value": "TV"}, 226 | {"label": "Uganda", "value": "UG"}, 227 | {"label": "Ukraine", "value": "UA"}, 228 | {"label": "United Arab Emirates", "value": "AE"}, 229 | {"label": "United Kingdom", "value": "GB"}, 230 | {"label": "United States", "value": "US"}, 231 | {"label": "United States Minor Outlying Islands", "value": "UM"}, 232 | {"label": "Uruguay", "value": "UY"}, 233 | {"label": "Uzbekistan", "value": "UZ"}, 234 | {"label": "Vanuatu", "value": "VU"}, 235 | {"label": "Venezuela", "value": "VE"}, 236 | {"label": "Viet Nam", "value": "VN"}, 237 | {"label": "Virgin Islands, British", "value": "VG"}, 238 | {"label": "Virgin Islands, U.S.", "value": "VI"}, 239 | {"label": "Wallis and Futuna", "value": "WF"}, 240 | {"label": "Western Sahara", "value": "EH"}, 241 | {"label": "Yemen", "value": "YE"}, 242 | {"label": "Zambia", "value": "ZM"}, 243 | {"label": "Zimbabwe", "value": "ZW"} 244 | ]; 245 | -------------------------------------------------------------------------------- /example/data/pokemon.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | {"name":"Bulbasaur","national_id":1,"hp":45,"attack":49,"sp_atk":65,"sp_def":65,"speed":45,"height":"7","weight":"69","image":"http://pokeapi.co/media/img/1.png"}, 3 | {"name":"Ivysaur","national_id":2,"hp":60,"attack":62,"sp_atk":80,"sp_def":80,"speed":60,"height":"10","weight":"130","image":"http://pokeapi.co/media/img/2.png"}, 4 | {"name":"Venusaur","national_id":3,"hp":80,"attack":82,"sp_atk":100,"sp_def":100,"speed":80,"height":"20","weight":"1000","image":"http://pokeapi.co/media/img/3.png"}, 5 | {"name":"Charmander","national_id":4,"hp":39,"attack":52,"sp_atk":60,"sp_def":50,"speed":65,"height":"6","weight":"85","image":"http://pokeapi.co/media/img/4.png"}, 6 | {"name":"Charmeleon","national_id":5,"hp":58,"attack":64,"sp_atk":80,"sp_def":65,"speed":80,"height":"11","weight":"190","image":"http://pokeapi.co/media/img/5.png"}, 7 | {"name":"Charizard","national_id":6,"hp":78,"attack":84,"sp_atk":109,"sp_def":85,"speed":100,"height":"17","weight":"905","image":"http://pokeapi.co/media/img/6.png"}, 8 | {"name":"Squirtle","national_id":7,"hp":44,"attack":48,"sp_atk":50,"sp_def":64,"speed":43,"height":"5","weight":"90","image":"http://pokeapi.co/media/img/7.png"}, 9 | {"name":"Wartortle","national_id":8,"hp":59,"attack":63,"sp_atk":65,"sp_def":80,"speed":58,"height":"10","weight":"225","image":"http://pokeapi.co/media/img/8.png"}, 10 | {"name":"Blastoise","national_id":9,"hp":79,"attack":83,"sp_atk":85,"sp_def":105,"speed":78,"height":"16","weight":"855","image":"http://pokeapi.co/media/img/9.png"}, 11 | {"name":"Caterpie","national_id":10,"hp":45,"attack":30,"sp_atk":20,"sp_def":20,"speed":45,"height":"3","weight":"29","image":"http://pokeapi.co/media/img/10.png"}, 12 | {"name":"Metapod","national_id":11,"hp":50,"attack":20,"sp_atk":25,"sp_def":25,"speed":30,"height":"7","weight":"99","image":"http://pokeapi.co/media/img/11.png"}, 13 | {"name":"Butterfree","national_id":12,"hp":60,"attack":45,"sp_atk":90,"sp_def":80,"speed":70,"height":"11","weight":"320","image":"http://pokeapi.co/media/img/12.png"}, 14 | {"name":"Weedle","national_id":13,"hp":40,"attack":35,"sp_atk":20,"sp_def":20,"speed":50,"height":"3","weight":"32","image":"http://pokeapi.co/media/img/13.png"}, 15 | {"name":"Kakuna","national_id":14,"hp":45,"attack":25,"sp_atk":25,"sp_def":25,"speed":35,"height":"6","weight":"100","image":"http://pokeapi.co/media/img/14.png"}, 16 | {"name":"Beedrill","national_id":15,"hp":65,"attack":90,"sp_atk":45,"sp_def":80,"speed":75,"height":"10","weight":"295","image":"http://pokeapi.co/media/img/15.png"}, 17 | {"name":"Pidgey","national_id":16,"hp":40,"attack":45,"sp_atk":35,"sp_def":35,"speed":56,"height":"3","weight":"18","image":"http://pokeapi.co/media/img/16.png"}, 18 | {"name":"Pidgeotto","national_id":17,"hp":63,"attack":60,"sp_atk":50,"sp_def":50,"speed":71,"height":"11","weight":"300","image":"http://pokeapi.co/media/img/17.png"}, 19 | {"name":"Pidgeot","national_id":18,"hp":83,"attack":80,"sp_atk":70,"sp_def":70,"speed":101,"height":"15","weight":"395","image":"http://pokeapi.co/media/img/18.png"}, 20 | {"name":"Rattata","national_id":19,"hp":30,"attack":56,"sp_atk":25,"sp_def":35,"speed":72,"height":"3","weight":"35","image":"http://pokeapi.co/media/img/19.png"}, 21 | {"name":"Raticate","national_id":20,"hp":55,"attack":81,"sp_atk":50,"sp_def":70,"speed":97,"height":"7","weight":"185","image":"http://pokeapi.co/media/img/20.png"}, 22 | {"name":"Spearow","national_id":21,"hp":40,"attack":60,"sp_atk":31,"sp_def":31,"speed":70,"height":"3","weight":"20","image":"http://pokeapi.co/media/img/21.png"}, 23 | {"name":"Fearow","national_id":22,"hp":65,"attack":90,"sp_atk":61,"sp_def":61,"speed":100,"height":"12","weight":"380","image":"http://pokeapi.co/media/img/22.png"}, 24 | {"name":"Ekans","national_id":23,"hp":35,"attack":60,"sp_atk":40,"sp_def":54,"speed":55,"height":"20","weight":"69","image":"http://pokeapi.co/media/img/23.png"}, 25 | {"name":"Arbok","national_id":24,"hp":60,"attack":85,"sp_atk":65,"sp_def":79,"speed":80,"height":"35","weight":"650","image":"http://pokeapi.co/media/img/24.png"}, 26 | {"name":"Pikachu","national_id":25,"hp":35,"attack":55,"sp_atk":50,"sp_def":50,"speed":90,"height":"4","weight":"60","image":"http://pokeapi.co/media/img/25.png"}, 27 | {"name":"Raichu","national_id":26,"hp":60,"attack":90,"sp_atk":90,"sp_def":80,"speed":110,"height":"8","weight":"300","image":"http://pokeapi.co/media/img/26.png"}, 28 | {"name":"Sandshrew","national_id":27,"hp":50,"attack":75,"sp_atk":20,"sp_def":30,"speed":40,"height":"6","weight":"120","image":"http://pokeapi.co/media/img/27.png"}, 29 | {"name":"Sandslash","national_id":28,"hp":75,"attack":100,"sp_atk":45,"sp_def":55,"speed":65,"height":"10","weight":"295","image":"http://pokeapi.co/media/img/28.png"}, 30 | {"name":"Nidoran-f","national_id":29,"hp":55,"attack":47,"sp_atk":40,"sp_def":40,"speed":41,"height":"4","weight":"70","image":"http://pokeapi.co/media/img/29.png"}, 31 | {"name":"Nidorina","national_id":30,"hp":70,"attack":62,"sp_atk":55,"sp_def":55,"speed":56,"height":"8","weight":"200","image":"http://pokeapi.co/media/img/30.png"}, 32 | {"name":"Nidoqueen","national_id":31,"hp":90,"attack":92,"sp_atk":75,"sp_def":85,"speed":76,"height":"13","weight":"600","image":"http://pokeapi.co/media/img/31.png"}, 33 | {"name":"Nidoran-m","national_id":32,"hp":46,"attack":57,"sp_atk":40,"sp_def":40,"speed":50,"height":"5","weight":"90","image":"http://pokeapi.co/media/img/32.png"}, 34 | {"name":"Nidorino","national_id":33,"hp":61,"attack":72,"sp_atk":55,"sp_def":55,"speed":65,"height":"9","weight":"195","image":"http://pokeapi.co/media/img/33.png"}, 35 | {"name":"Nidoking","national_id":34,"hp":81,"attack":102,"sp_atk":85,"sp_def":75,"speed":85,"height":"14","weight":"620","image":"http://pokeapi.co/media/img/34.png"}, 36 | {"name":"Clefairy","national_id":35,"hp":70,"attack":45,"sp_atk":60,"sp_def":65,"speed":35,"height":"6","weight":"75","image":"http://pokeapi.co/media/img/35.png"}, 37 | {"name":"Clefable","national_id":36,"hp":95,"attack":70,"sp_atk":95,"sp_def":90,"speed":60,"height":"13","weight":"400","image":"http://pokeapi.co/media/img/36.png"}, 38 | {"name":"Vulpix","national_id":37,"hp":38,"attack":41,"sp_atk":50,"sp_def":65,"speed":65,"height":"6","weight":"99","image":"http://pokeapi.co/media/img/37.png"}, 39 | {"name":"Ninetales","national_id":38,"hp":73,"attack":76,"sp_atk":81,"sp_def":100,"speed":100,"height":"11","weight":"199","image":"http://pokeapi.co/media/img/38.png"}, 40 | {"name":"Jigglypuff","national_id":39,"hp":115,"attack":45,"sp_atk":45,"sp_def":25,"speed":20,"height":"5","weight":"55","image":"http://pokeapi.co/media/img/39.png"}, 41 | {"name":"Wigglytuff","national_id":40,"hp":140,"attack":70,"sp_atk":85,"sp_def":50,"speed":45,"height":"10","weight":"120","image":"http://pokeapi.co/media/img/40.png"}, 42 | {"name":"Zubat","national_id":41,"hp":40,"attack":45,"sp_atk":30,"sp_def":40,"speed":55,"height":"8","weight":"75","image":"http://pokeapi.co/media/img/41.png"}, 43 | {"name":"Golbat","national_id":42,"hp":75,"attack":80,"sp_atk":65,"sp_def":75,"speed":90,"height":"16","weight":"550","image":"http://pokeapi.co/media/img/42.png"}, 44 | {"name":"Oddish","national_id":43,"hp":45,"attack":50,"sp_atk":75,"sp_def":65,"speed":30,"height":"5","weight":"54","image":"http://pokeapi.co/media/img/43.png"}, 45 | {"name":"Gloom","national_id":44,"hp":60,"attack":65,"sp_atk":85,"sp_def":75,"speed":40,"height":"8","weight":"86","image":"http://pokeapi.co/media/img/44.png"}, 46 | {"name":"Vileplume","national_id":45,"hp":75,"attack":80,"sp_atk":110,"sp_def":90,"speed":50,"height":"12","weight":"186","image":"http://pokeapi.co/media/img/45.png"}, 47 | {"name":"Paras","national_id":46,"hp":35,"attack":70,"sp_atk":45,"sp_def":55,"speed":25,"height":"3","weight":"54","image":"http://pokeapi.co/media/img/46.png"}, 48 | {"name":"Parasect","national_id":47,"hp":60,"attack":95,"sp_atk":60,"sp_def":80,"speed":30,"height":"10","weight":"295","image":"http://pokeapi.co/media/img/47.png"}, 49 | {"name":"Venonat","national_id":48,"hp":60,"attack":55,"sp_atk":40,"sp_def":55,"speed":45,"height":"10","weight":"300","image":"http://pokeapi.co/media/img/48.png"}, 50 | {"name":"Venomoth","national_id":49,"hp":70,"attack":65,"sp_atk":90,"sp_def":75,"speed":90,"height":"15","weight":"125","image":"http://pokeapi.co/media/img/49.png"}, 51 | {"name":"Diglett","national_id":50,"hp":10,"attack":55,"sp_atk":35,"sp_def":45,"speed":95,"height":"2","weight":"8","image":"http://pokeapi.co/media/img/50.png"}, 52 | {"name":"Dugtrio","national_id":51,"hp":35,"attack":80,"sp_atk":50,"sp_def":70,"speed":120,"height":"7","weight":"333","image":"http://pokeapi.co/media/img/51.png"}, 53 | {"name":"Meowth","national_id":52,"hp":40,"attack":45,"sp_atk":40,"sp_def":40,"speed":90,"height":"4","weight":"42","image":"http://pokeapi.co/media/img/52.png"}, 54 | {"name":"Persian","national_id":53,"hp":65,"attack":70,"sp_atk":65,"sp_def":65,"speed":115,"height":"10","weight":"320","image":"http://pokeapi.co/media/img/53.png"}, 55 | {"name":"Psyduck","national_id":54,"hp":50,"attack":52,"sp_atk":65,"sp_def":50,"speed":55,"height":"8","weight":"196","image":"http://pokeapi.co/media/img/54.png"}, 56 | {"name":"Golduck","national_id":55,"hp":80,"attack":82,"sp_atk":95,"sp_def":80,"speed":85,"height":"17","weight":"766","image":"http://pokeapi.co/media/img/55.png"}, 57 | {"name":"Mankey","national_id":56,"hp":40,"attack":80,"sp_atk":35,"sp_def":45,"speed":70,"height":"5","weight":"280","image":"http://pokeapi.co/media/img/56.png"}, 58 | {"name":"Primeape","national_id":57,"hp":65,"attack":105,"sp_atk":60,"sp_def":70,"speed":95,"height":"10","weight":"320","image":"http://pokeapi.co/media/img/57.png"}, 59 | {"name":"Growlithe","national_id":58,"hp":55,"attack":70,"sp_atk":70,"sp_def":50,"speed":60,"height":"7","weight":"190","image":"http://pokeapi.co/media/img/58.png"}, 60 | {"name":"Arcanine","national_id":59,"hp":90,"attack":110,"sp_atk":100,"sp_def":80,"speed":95,"height":"19","weight":"1550","image":"http://pokeapi.co/media/img/59.png"}, 61 | {"name":"Poliwag","national_id":60,"hp":40,"attack":50,"sp_atk":40,"sp_def":40,"speed":90,"height":"6","weight":"124","image":"http://pokeapi.co/media/img/60.png"}, 62 | {"name":"Poliwhirl","national_id":61,"hp":65,"attack":65,"sp_atk":50,"sp_def":50,"speed":90,"height":"10","weight":"200","image":"http://pokeapi.co/media/img/61.png"}, 63 | {"name":"Poliwrath","national_id":62,"hp":90,"attack":95,"sp_atk":70,"sp_def":90,"speed":70,"height":"13","weight":"540","image":"http://pokeapi.co/media/img/62.png"}, 64 | {"name":"Abra","national_id":63,"hp":25,"attack":20,"sp_atk":105,"sp_def":55,"speed":90,"height":"9","weight":"195","image":"http://pokeapi.co/media/img/63.png"}, 65 | {"name":"Kadabra","national_id":64,"hp":40,"attack":35,"sp_atk":120,"sp_def":70,"speed":105,"height":"13","weight":"565","image":"http://pokeapi.co/media/img/64.png"}, 66 | {"name":"Alakazam","national_id":65,"hp":55,"attack":50,"sp_atk":135,"sp_def":95,"speed":120,"height":"15","weight":"480","image":"http://pokeapi.co/media/img/65.png"}, 67 | {"name":"Machop","national_id":66,"hp":70,"attack":80,"sp_atk":35,"sp_def":35,"speed":35,"height":"8","weight":"195","image":"http://pokeapi.co/media/img/66.png"}, 68 | {"name":"Machoke","national_id":67,"hp":80,"attack":100,"sp_atk":50,"sp_def":60,"speed":45,"height":"15","weight":"705","image":"http://pokeapi.co/media/img/67.png"}, 69 | {"name":"Machamp","national_id":68,"hp":90,"attack":130,"sp_atk":65,"sp_def":85,"speed":55,"height":"16","weight":"1300","image":"http://pokeapi.co/media/img/68.png"}, 70 | {"name":"Bellsprout","national_id":69,"hp":50,"attack":75,"sp_atk":70,"sp_def":30,"speed":40,"height":"7","weight":"40","image":"http://pokeapi.co/media/img/69.png"}, 71 | {"name":"Weepinbell","national_id":70,"hp":65,"attack":90,"sp_atk":85,"sp_def":45,"speed":55,"height":"10","weight":"64","image":"http://pokeapi.co/media/img/70.png"}, 72 | {"name":"Victreebel","national_id":71,"hp":80,"attack":105,"sp_atk":100,"sp_def":70,"speed":70,"height":"17","weight":"155","image":"http://pokeapi.co/media/img/71.png"}, 73 | {"name":"Tentacool","national_id":72,"hp":40,"attack":40,"sp_atk":50,"sp_def":100,"speed":70,"height":"9","weight":"455","image":"http://pokeapi.co/media/img/72.png"}, 74 | {"name":"Tentacruel","national_id":73,"hp":80,"attack":70,"sp_atk":80,"sp_def":120,"speed":100,"height":"16","weight":"550","image":"http://pokeapi.co/media/img/73.png"}, 75 | {"name":"Geodude","national_id":74,"hp":40,"attack":80,"sp_atk":30,"sp_def":30,"speed":20,"height":"4","weight":"200","image":"http://pokeapi.co/media/img/74.png"}, 76 | {"name":"Graveler","national_id":75,"hp":55,"attack":95,"sp_atk":45,"sp_def":45,"speed":35,"height":"10","weight":"1050","image":"http://pokeapi.co/media/img/75.png"}, 77 | {"name":"Golem","national_id":76,"hp":80,"attack":120,"sp_atk":55,"sp_def":65,"speed":45,"height":"14","weight":"3000","image":"http://pokeapi.co/media/img/76.png"}, 78 | {"name":"Ponyta","national_id":77,"hp":50,"attack":85,"sp_atk":65,"sp_def":65,"speed":90,"height":"10","weight":"300","image":"http://pokeapi.co/media/img/77.png"}, 79 | {"name":"Rapidash","national_id":78,"hp":65,"attack":100,"sp_atk":80,"sp_def":80,"speed":105,"height":"17","weight":"950","image":"http://pokeapi.co/media/img/78.png"}, 80 | {"name":"Slowpoke","national_id":79,"hp":90,"attack":65,"sp_atk":40,"sp_def":40,"speed":15,"height":"12","weight":"360","image":"http://pokeapi.co/media/img/79.png"}, 81 | {"name":"Slowbro","national_id":80,"hp":95,"attack":75,"sp_atk":100,"sp_def":80,"speed":30,"height":"16","weight":"785","image":"http://pokeapi.co/media/img/80.png"}, 82 | {"name":"Magnemite","national_id":81,"hp":25,"attack":35,"sp_atk":95,"sp_def":55,"speed":45,"height":"3","weight":"60","image":"http://pokeapi.co/media/img/81.png"}, 83 | {"name":"Magneton","national_id":82,"hp":50,"attack":60,"sp_atk":120,"sp_def":70,"speed":70,"height":"10","weight":"600","image":"http://pokeapi.co/media/img/82.png"}, 84 | {"name":"Farfetchd","national_id":83,"hp":52,"attack":65,"sp_atk":58,"sp_def":62,"speed":60,"height":"8","weight":"150","image":"http://pokeapi.co/media/img/83.png"}, 85 | {"name":"Doduo","national_id":84,"hp":35,"attack":85,"sp_atk":35,"sp_def":35,"speed":75,"height":"14","weight":"392","image":"http://pokeapi.co/media/img/84.png"}, 86 | {"name":"Dodrio","national_id":85,"hp":60,"attack":110,"sp_atk":60,"sp_def":60,"speed":100,"height":"18","weight":"852","image":"http://pokeapi.co/media/img/85.png"}, 87 | {"name":"Seel","national_id":86,"hp":65,"attack":45,"sp_atk":45,"sp_def":70,"speed":45,"height":"11","weight":"900","image":"http://pokeapi.co/media/img/86.png"}, 88 | {"name":"Dewgong","national_id":87,"hp":90,"attack":70,"sp_atk":70,"sp_def":95,"speed":70,"height":"17","weight":"1200","image":"http://pokeapi.co/media/img/87.png"}, 89 | {"name":"Grimer","national_id":88,"hp":80,"attack":80,"sp_atk":40,"sp_def":50,"speed":25,"height":"9","weight":"300","image":"http://pokeapi.co/media/img/88.png"}, 90 | {"name":"Muk","national_id":89,"hp":105,"attack":105,"sp_atk":65,"sp_def":100,"speed":50,"height":"12","weight":"300","image":"http://pokeapi.co/media/img/89.png"}, 91 | {"name":"Shellder","national_id":90,"hp":30,"attack":65,"sp_atk":45,"sp_def":25,"speed":40,"height":"3","weight":"40","image":"http://pokeapi.co/media/img/90.png"}, 92 | {"name":"Cloyster","national_id":91,"hp":50,"attack":95,"sp_atk":85,"sp_def":45,"speed":70,"height":"15","weight":"1325","image":"http://pokeapi.co/media/img/91.png"}, 93 | {"name":"Gastly","national_id":92,"hp":30,"attack":35,"sp_atk":100,"sp_def":35,"speed":80,"height":"13","weight":"1","image":"http://pokeapi.co/media/img/92.png"}, 94 | {"name":"Haunter","national_id":93,"hp":45,"attack":50,"sp_atk":115,"sp_def":55,"speed":95,"height":"16","weight":"1","image":"http://pokeapi.co/media/img/93.png"}, 95 | {"name":"Gengar","national_id":94,"hp":60,"attack":65,"sp_atk":130,"sp_def":75,"speed":110,"height":"15","weight":"405","image":"http://pokeapi.co/media/img/94.png"}, 96 | {"name":"Onix","national_id":95,"hp":35,"attack":45,"sp_atk":30,"sp_def":45,"speed":70,"height":"88","weight":"2100","image":"http://pokeapi.co/media/img/95.png"}, 97 | {"name":"Drowzee","national_id":96,"hp":60,"attack":48,"sp_atk":43,"sp_def":90,"speed":42,"height":"10","weight":"324","image":"http://pokeapi.co/media/img/96.png"}, 98 | {"name":"Hypno","national_id":97,"hp":85,"attack":73,"sp_atk":73,"sp_def":115,"speed":67,"height":"16","weight":"756","image":"http://pokeapi.co/media/img/97.png"}, 99 | {"name":"Krabby","national_id":98,"hp":30,"attack":105,"sp_atk":25,"sp_def":25,"speed":50,"height":"4","weight":"65","image":"http://pokeapi.co/media/img/98.png"}, 100 | {"name":"Kingler","national_id":99,"hp":55,"attack":130,"sp_atk":50,"sp_def":50,"speed":75,"height":"13","weight":"600","image":"http://pokeapi.co/media/img/99.png"}, 101 | {"name":"Voltorb","national_id":100,"hp":40,"attack":30,"sp_atk":55,"sp_def":55,"speed":100,"height":"5","weight":"104","image":"http://pokeapi.co/media/img/100.png"}, 102 | {"name":"Electrode","national_id":101,"hp":60,"attack":50,"sp_atk":80,"sp_def":80,"speed":140,"height":"12","weight":"666","image":"http://pokeapi.co/media/img/101.png"}, 103 | {"name":"Exeggcute","national_id":102,"hp":60,"attack":40,"sp_atk":60,"sp_def":45,"speed":40,"height":"4","weight":"25","image":"http://pokeapi.co/media/img/102.png"}, 104 | {"name":"Exeggutor","national_id":103,"hp":95,"attack":95,"sp_atk":125,"sp_def":65,"speed":55,"height":"20","weight":"1200","image":"http://pokeapi.co/media/img/103.png"}, 105 | {"name":"Cubone","national_id":104,"hp":50,"attack":50,"sp_atk":40,"sp_def":50,"speed":35,"height":"4","weight":"65","image":"http://pokeapi.co/media/img/104.png"}, 106 | {"name":"Marowak","national_id":105,"hp":60,"attack":80,"sp_atk":50,"sp_def":80,"speed":45,"height":"10","weight":"450","image":"http://pokeapi.co/media/img/105.png"}, 107 | {"name":"Hitmonlee","national_id":106,"hp":50,"attack":120,"sp_atk":35,"sp_def":110,"speed":87,"height":"15","weight":"498","image":"http://pokeapi.co/media/img/106.png"}, 108 | {"name":"Hitmonchan","national_id":107,"hp":50,"attack":105,"sp_atk":35,"sp_def":110,"speed":76,"height":"14","weight":"502","image":"http://pokeapi.co/media/img/107.png"}, 109 | {"name":"Lickitung","national_id":108,"hp":90,"attack":55,"sp_atk":60,"sp_def":75,"speed":30,"height":"12","weight":"655","image":"http://pokeapi.co/media/img/108.png"}, 110 | {"name":"Koffing","national_id":109,"hp":40,"attack":65,"sp_atk":60,"sp_def":45,"speed":35,"height":"6","weight":"10","image":"http://pokeapi.co/media/img/109.png"}, 111 | {"name":"Weezing","national_id":110,"hp":65,"attack":90,"sp_atk":85,"sp_def":70,"speed":60,"height":"12","weight":"95","image":"http://pokeapi.co/media/img/110.png"}, 112 | {"name":"Rhyhorn","national_id":111,"hp":80,"attack":85,"sp_atk":30,"sp_def":30,"speed":25,"height":"10","weight":"1150","image":"http://pokeapi.co/media/img/111.png"}, 113 | {"name":"Rhydon","national_id":112,"hp":105,"attack":130,"sp_atk":45,"sp_def":45,"speed":40,"height":"19","weight":"1200","image":"http://pokeapi.co/media/img/112.png"}, 114 | {"name":"Chansey","national_id":113,"hp":250,"attack":5,"sp_atk":35,"sp_def":105,"speed":50,"height":"11","weight":"346","image":"http://pokeapi.co/media/img/113.png"}, 115 | {"name":"Tangela","national_id":114,"hp":65,"attack":55,"sp_atk":100,"sp_def":40,"speed":60,"height":"10","weight":"350","image":"http://pokeapi.co/media/img/114.png"}, 116 | {"name":"Kangaskhan","national_id":115,"hp":105,"attack":95,"sp_atk":40,"sp_def":80,"speed":90,"height":"22","weight":"800","image":"http://pokeapi.co/media/img/115.png"}, 117 | {"name":"Horsea","national_id":116,"hp":30,"attack":40,"sp_atk":70,"sp_def":25,"speed":60,"height":"4","weight":"80","image":"http://pokeapi.co/media/img/116.png"}, 118 | {"name":"Seadra","national_id":117,"hp":55,"attack":65,"sp_atk":95,"sp_def":45,"speed":85,"height":"12","weight":"250","image":"http://pokeapi.co/media/img/117.png"}, 119 | {"name":"Goldeen","national_id":118,"hp":45,"attack":67,"sp_atk":35,"sp_def":50,"speed":63,"height":"6","weight":"150","image":"http://pokeapi.co/media/img/118.png"}, 120 | {"name":"Seaking","national_id":119,"hp":80,"attack":92,"sp_atk":65,"sp_def":80,"speed":68,"height":"13","weight":"390","image":"http://pokeapi.co/media/img/119.png"}, 121 | {"name":"Staryu","national_id":120,"hp":30,"attack":45,"sp_atk":70,"sp_def":55,"speed":85,"height":"8","weight":"345","image":"http://pokeapi.co/media/img/120.png"}, 122 | {"name":"Starmie","national_id":121,"hp":60,"attack":75,"sp_atk":100,"sp_def":85,"speed":115,"height":"11","weight":"800","image":"http://pokeapi.co/media/img/121.png"}, 123 | {"name":"Mr-mime","national_id":122,"hp":40,"attack":45,"sp_atk":100,"sp_def":120,"speed":90,"height":"13","weight":"545","image":"http://pokeapi.co/media/img/122.png"}, 124 | {"name":"Scyther","national_id":123,"hp":70,"attack":110,"sp_atk":55,"sp_def":80,"speed":105,"height":"15","weight":"560","image":"http://pokeapi.co/media/img/123.png"}, 125 | {"name":"Jynx","national_id":124,"hp":65,"attack":50,"sp_atk":115,"sp_def":95,"speed":95,"height":"14","weight":"406","image":"http://pokeapi.co/media/img/124.png"}, 126 | {"name":"Electabuzz","national_id":125,"hp":65,"attack":83,"sp_atk":95,"sp_def":85,"speed":105,"height":"11","weight":"300","image":"http://pokeapi.co/media/img/125.png"}, 127 | {"name":"Magmar","national_id":126,"hp":65,"attack":95,"sp_atk":100,"sp_def":85,"speed":93,"height":"13","weight":"445","image":"http://pokeapi.co/media/img/126.png"}, 128 | {"name":"Pinsir","national_id":127,"hp":65,"attack":125,"sp_atk":55,"sp_def":70,"speed":85,"height":"15","weight":"550","image":"http://pokeapi.co/media/img/127.png"}, 129 | {"name":"Tauros","national_id":128,"hp":75,"attack":100,"sp_atk":40,"sp_def":70,"speed":110,"height":"14","weight":"884","image":"http://pokeapi.co/media/img/128.png"}, 130 | {"name":"Magikarp","national_id":129,"hp":20,"attack":10,"sp_atk":15,"sp_def":20,"speed":80,"height":"9","weight":"100","image":"http://pokeapi.co/media/img/129.png"}, 131 | {"name":"Gyarados","national_id":130,"hp":95,"attack":125,"sp_atk":60,"sp_def":100,"speed":81,"height":"65","weight":"2350","image":"http://pokeapi.co/media/img/130.png"}, 132 | {"name":"Lapras","national_id":131,"hp":130,"attack":85,"sp_atk":85,"sp_def":95,"speed":60,"height":"25","weight":"2200","image":"http://pokeapi.co/media/img/131.png"}, 133 | {"name":"Ditto","national_id":132,"hp":48,"attack":48,"sp_atk":48,"sp_def":48,"speed":48,"height":"3","weight":"40","image":"http://pokeapi.co/media/img/132.png"}, 134 | {"name":"Eevee","national_id":133,"hp":55,"attack":55,"sp_atk":45,"sp_def":65,"speed":55,"height":"3","weight":"65","image":"http://pokeapi.co/media/img/133.png"}, 135 | {"name":"Vaporeon","national_id":134,"hp":130,"attack":65,"sp_atk":110,"sp_def":95,"speed":65,"height":"10","weight":"290","image":"http://pokeapi.co/media/img/134.png"}, 136 | {"name":"Jolteon","national_id":135,"hp":65,"attack":65,"sp_atk":110,"sp_def":95,"speed":130,"height":"8","weight":"245","image":"http://pokeapi.co/media/img/135.png"}, 137 | {"name":"Flareon","national_id":136,"hp":65,"attack":130,"sp_atk":95,"sp_def":110,"speed":65,"height":"9","weight":"250","image":"http://pokeapi.co/media/img/136.png"}, 138 | {"name":"Porygon","national_id":137,"hp":65,"attack":60,"sp_atk":85,"sp_def":75,"speed":40,"height":"8","weight":"365","image":"http://pokeapi.co/media/img/137.png"}, 139 | {"name":"Omanyte","national_id":138,"hp":35,"attack":40,"sp_atk":90,"sp_def":55,"speed":35,"height":"4","weight":"75","image":"http://pokeapi.co/media/img/138.png"}, 140 | {"name":"Omastar","national_id":139,"hp":70,"attack":60,"sp_atk":115,"sp_def":70,"speed":55,"height":"10","weight":"350","image":"http://pokeapi.co/media/img/139.png"}, 141 | {"name":"Kabuto","national_id":140,"hp":30,"attack":80,"sp_atk":55,"sp_def":45,"speed":55,"height":"5","weight":"115","image":"http://pokeapi.co/media/img/140.png"}, 142 | {"name":"Kabutops","national_id":141,"hp":60,"attack":115,"sp_atk":65,"sp_def":70,"speed":80,"height":"13","weight":"405","image":"http://pokeapi.co/media/img/141.png"}, 143 | {"name":"Aerodactyl","national_id":142,"hp":80,"attack":105,"sp_atk":60,"sp_def":75,"speed":130,"height":"18","weight":"590","image":"http://pokeapi.co/media/img/142.png"}, 144 | {"name":"Snorlax","national_id":143,"hp":160,"attack":110,"sp_atk":65,"sp_def":110,"speed":30,"height":"21","weight":"4600","image":"http://pokeapi.co/media/img/143.png"}, 145 | {"name":"Articuno","national_id":144,"hp":90,"attack":85,"sp_atk":95,"sp_def":125,"speed":85,"height":"17","weight":"554","image":"http://pokeapi.co/media/img/144.png"}, 146 | {"name":"Zapdos","national_id":145,"hp":90,"attack":90,"sp_atk":125,"sp_def":90,"speed":100,"height":"16","weight":"526","image":"http://pokeapi.co/media/img/145.png"}, 147 | {"name":"Moltres","national_id":146,"hp":90,"attack":100,"sp_atk":125,"sp_def":85,"speed":90,"height":"20","weight":"600","image":"http://pokeapi.co/media/img/146.png"}, 148 | {"name":"Dratini","national_id":147,"hp":41,"attack":64,"sp_atk":50,"sp_def":50,"speed":50,"height":"18","weight":"33","image":"http://pokeapi.co/media/img/147.png"}, 149 | {"name":"Dragonair","national_id":148,"hp":61,"attack":84,"sp_atk":70,"sp_def":70,"speed":70,"height":"40","weight":"165","image":"http://pokeapi.co/media/img/148.png"}, 150 | {"name":"Dragonite","national_id":149,"hp":91,"attack":134,"sp_atk":100,"sp_def":100,"speed":80,"height":"22","weight":"2100","image":"http://pokeapi.co/media/img/149.png"}, 151 | {"name":"Mewtwo","national_id":150,"hp":106,"attack":110,"sp_atk":154,"sp_def":90,"speed":130,"height":"20","weight":"1220","image":"http://pokeapi.co/media/img/150.png"}, 152 | {"name":"Mew","national_id":151,"hp":100,"attack":100,"sp_atk":100,"sp_def":100,"speed":100,"height":"4","weight":"40","image":"http://pokeapi.co/media/img/151.png"} 153 | ]; 154 | -------------------------------------------------------------------------------- /example/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onefinestay/react-choice/5855ae446334d22a92669d1758d7d42a30a445b4/example/img/logo.png -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var fs = require('fs'); 5 | var _ = require('lodash'); 6 | 7 | var Header = require('./components/header.jsx'); 8 | var Footer = require('./components/footer.jsx'); 9 | var GithubRibbon = require('./components/github-ribbon.jsx'); 10 | var CodeSnippet = require('./components/code-snippet.jsx'); 11 | var Install = require('./components/install.jsx'); 12 | var Features = require('./components/features.jsx'); 13 | var CustomRender = require('./components/custom-render.jsx'); 14 | 15 | var Choice = require('../src'); 16 | 17 | var singleExample = fs.readFileSync(__dirname + '/code-snippets/single.jsx', 'utf8'); 18 | var multipleExample = fs.readFileSync(__dirname + '/code-snippets/multiple.jsx', 'utf8'); 19 | 20 | var COUNTRIES = require('./data/countries.js'); 21 | 22 | var Index = React.createClass({ 23 | getDefaultProps: function() { 24 | return {}; 25 | }, 26 | 27 | render: function() { 28 | var options = _.map(COUNTRIES, function(option) { 29 | return ( 30 | 31 | {option.label} 32 | 33 | ); 34 | }); 35 | 36 | return ( 37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 |
45 |

Single Choice

46 | 47 | {options} 48 | 49 | 50 | 51 | {singleExample} 52 | 53 |
54 |
55 | 56 |
57 |
58 |

Multiple Choice

59 | 60 | {options} 61 | 62 | 63 | 64 | {multipleExample} 65 | 66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 |
74 |

Tutorials

75 | 76 |
77 |
78 | 79 |
81 | ); 82 | } 83 | }); 84 | 85 | module.exports = Index; 86 | -------------------------------------------------------------------------------- /example/js/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var Index = React.createFactory(require('../index.jsx')); 5 | 6 | window.React = React; 7 | 8 | React.render( 9 | Index(), 10 | document.getElementById('app') 11 | ); 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.assign = require('object.assign'); 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var gulp = require('gulp'); 8 | var autoprefixer = require('gulp-autoprefixer'); 9 | var extReplace = require('gulp-ext-replace'); 10 | var watch = require('gulp-watch'); 11 | var babel = require('gulp-babel'); 12 | var connect = require('gulp-connect'); 13 | var sass = require('gulp-sass'); 14 | var deploy = require('gulp-gh-pages'); 15 | var React = require('react'); 16 | var webpack = require('webpack'); 17 | var gulpWebpack = require('gulp-webpack'); 18 | 19 | var PRODUCTION = (process.env.NODE_ENV === 'production'); 20 | 21 | var gulpPlugins = [ 22 | // Fix for moment including all locales 23 | // Ref: http://stackoverflow.com/a/25426019 24 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 25 | ]; 26 | 27 | if (PRODUCTION) { 28 | gulpPlugins.push(new webpack.DefinePlugin({ 29 | "process.env": { 30 | NODE_ENV: JSON.stringify("production") 31 | } 32 | })); 33 | gulpPlugins.push(new webpack.optimize.DedupePlugin()); 34 | gulpPlugins.push(new webpack.optimize.UglifyJsPlugin({ 35 | compress: true, 36 | mangle: true, 37 | sourceMap: true 38 | })); 39 | } 40 | 41 | var webpackConfig = { 42 | cache: true, 43 | debug: !PRODUCTION, 44 | devtool: PRODUCTION ? 'source-map' : 'eval-source-map', 45 | context: __dirname, 46 | output: { 47 | path: path.resolve('./example/build/'), 48 | filename: 'index.js' 49 | }, 50 | module: { 51 | loaders: [ 52 | { 53 | test: /\.jsx|.js$/, 54 | exclude: /node_modules\//, 55 | loaders: [ 56 | 'babel-loader?stage=1' 57 | ] 58 | }, 59 | ], 60 | postLoaders: [ 61 | { 62 | loader: "transform/cacheable?brfs" 63 | } 64 | ] 65 | }, 66 | resolve: { 67 | extensions: ['', '.js', '.jsx'] 68 | }, 69 | plugins: gulpPlugins 70 | }; 71 | 72 | gulp.task('build-dist-js', function() { 73 | // build javascript files 74 | return gulp.src('src/**/*.{js,jsx}') 75 | .pipe(babel({ 76 | stage: 1 77 | })) 78 | .pipe(extReplace('.js')) 79 | .pipe(gulp.dest('dist')); 80 | }); 81 | 82 | gulp.task('build-example-js', function() { 83 | var compiler = gulpWebpack(webpackConfig, webpack); 84 | 85 | return gulp.src('./example/js/index.js') 86 | .pipe(compiler) 87 | .pipe(gulp.dest('./example/build')); 88 | }); 89 | 90 | gulp.task('watch-example-js', function() { 91 | var compiler = gulpWebpack(Object.assign({}, {watch: true}, webpackConfig), webpack); 92 | return gulp.src('./example/js/index.js') 93 | .pipe(compiler) 94 | .pipe(gulp.dest('./example/build')); 95 | }); 96 | 97 | gulp.task('build-example', function() { 98 | // setup babel hook 99 | require("babel/register")({ 100 | stage: 1 101 | }); 102 | 103 | var Index = React.createFactory(require('./example/base.jsx')); 104 | var markup = '' + React.renderToString(Index()); 105 | 106 | // write file 107 | fs.writeFileSync('./example/index.html', markup); 108 | }); 109 | 110 | gulp.task('build-example-scss', function() { 111 | gulp.src('./example/css/**/*.scss') 112 | .pipe(sass()) 113 | .pipe(autoprefixer()) 114 | .pipe(gulp.dest('./example/css')); 115 | }); 116 | 117 | gulp.task('watch-example-scss', ['build-example-scss'], function() { 118 | watch('./example/**/*.scss', function(files, cb) { 119 | gulp.start('build-example-scss', cb); 120 | }); 121 | }); 122 | 123 | gulp.task('example-server', function() { 124 | connect.server({ 125 | root: 'example', 126 | port: '9989' 127 | }); 128 | }); 129 | 130 | gulp.task('build', ['build-dist-js', 'build-example', 'build-example-js', 'build-example-scss']); 131 | gulp.task('develop', ['build-example', 'watch-example-js', 'watch-example-scss', 'example-server']); 132 | 133 | gulp.task('deploy-example', ['build'], function() { 134 | return gulp.src('./example/**/*') 135 | .pipe(deploy()); 136 | }); 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-choice", 3 | "version": "0.4.3", 4 | "description": "A React based customisable select box", 5 | "main": "dist/index.js", 6 | "author": "Jonathan Kim ", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/onefinestay/react-choice" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/onefinestay/react-choice/issues" 13 | }, 14 | "license": "Apache 2.0", 15 | "peerDependencies": { 16 | "react": ">=0.12.0" 17 | }, 18 | "dependencies": { 19 | "lodash": "^2.4.1", 20 | "sifter": "^0.4.1" 21 | }, 22 | "devDependencies": { 23 | "babel": "^5.2.16", 24 | "babel-core": "^5.2.6", 25 | "babel-loader": "^5.0.0", 26 | "brfs": "^1.2.0", 27 | "gulp": "^3.8.9", 28 | "gulp-autoprefixer": "^2.1.0", 29 | "gulp-babel": "^5.1.0", 30 | "gulp-connect": "^2.0.6", 31 | "gulp-ext-replace": "^0.1.0", 32 | "gulp-gh-pages": "^0.4.0", 33 | "gulp-sass": "^1.2.2", 34 | "gulp-util": "^3.0.1", 35 | "gulp-watch": "^2.0.0", 36 | "gulp-webpack": "^1.4.0", 37 | "object.assign": "^1.1.1", 38 | "transform-loader": "^0.2.1", 39 | "webpack": "^1.5.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/humanize-string.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(string, upperCase) { 4 | if (typeof string == 'string') { 5 | var firstCharacter = string.charAt(0); 6 | 7 | if (typeof upperCase === 'undefined' || upperCase === true) { 8 | firstCharacter = firstCharacter.toUpperCase(); 9 | } 10 | 11 | var display = firstCharacter + string.slice(1); 12 | return display.replace(/_|-/g, ' '); 13 | } else { 14 | return string; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/icon.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | var Icon = React.createClass({ 7 | propTypes: { 8 | focused: React.PropTypes.bool.isRequired 9 | }, 10 | 11 | render: function() { 12 | var arrowClasses = cx({ 13 | 'react-choice-icon__arrow': true, 14 | 'react-choice-icon__arrow--up': this.props.focused, 15 | 'react-choice-icon__arrow--down': !this.props.focused 16 | }); 17 | 18 | return ( 19 |
20 | ); 21 | } 22 | }); 23 | 24 | module.exports = Icon; 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Select': require('./single'), 3 | 'SelectMultiple': require('./multiple'), 4 | 'Option': require('./option'), 5 | 'OptionMixin': require('./option-mixin'), 6 | 'TextHighlight': require('./text-highlight') 7 | }; 8 | -------------------------------------------------------------------------------- /src/multiple.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var _ = require('lodash'); 5 | var cx = React.addons.classSet; 6 | var cloneWithProps = React.addons.cloneWithProps; 7 | 8 | var Options = require('./options'); 9 | var OptionWrapper = require('./option-wrapper'); 10 | 11 | var SearchMixin = require('./search-mixin'); 12 | 13 | var ValueWrapper = React.createClass({ 14 | propTypes: { 15 | onClick: React.PropTypes.func.isRequired, 16 | onDeleteClick: React.PropTypes.func.isRequired 17 | }, 18 | 19 | onDeleteClick: function(event) { 20 | event.stopPropagation(); 21 | this.props.onDeleteClick(event); 22 | }, 23 | 24 | render: function() { 25 | var classes = cx({ 26 | 'react-choice-value': true, 27 | 'react-choice-value--is-selected': this.props.selected 28 | }); 29 | 30 | return ( 31 |
32 |
{this.props.children}
33 | x 34 |
35 | ); 36 | } 37 | }); 38 | 39 | var MultipleChoice = React.createClass({ 40 | mixins: [SearchMixin], 41 | 42 | propTypes: { 43 | name: React.PropTypes.string, // name of input 44 | placeholder: React.PropTypes.string, // input placeholder 45 | values: React.PropTypes.array, // initial values 46 | 47 | children: React.PropTypes.array.isRequired, 48 | 49 | valueField: React.PropTypes.string, // value field name 50 | labelField: React.PropTypes.string, // label field name 51 | 52 | searchField: React.PropTypes.array, // array of search fields 53 | 54 | onSelect: React.PropTypes.func, // function called when option is selected 55 | onDelete: React.PropTypes.func, // function called when option is deleted 56 | allowDuplicates: React.PropTypes.bool // if true, the same values can be added multiple times 57 | }, 58 | 59 | getDefaultProps: function() { 60 | return { 61 | values: [], 62 | valueField: 'value', 63 | labelField: 'children', 64 | searchField: ['children'], 65 | allowDuplicates: false 66 | }; 67 | }, 68 | 69 | getInitialState: function() { 70 | var props = this.props.searchField; 71 | props.push(this.props.valueField); 72 | props.push(this.props.searchField); 73 | props = _.uniq(props); 74 | 75 | var options = _.map(this.props.children, function(child) { 76 | // TODO Validation ? 77 | return _.pick(child.props, props); 78 | }, this); 79 | 80 | return { 81 | focus: false, 82 | searchResults: this._sort(options), 83 | values: this.props.values, 84 | initialOptions: options, 85 | highlighted: null, 86 | selected: null, 87 | selectedIndex: -1, 88 | searchTokens: [] 89 | }; 90 | }, 91 | 92 | _handleContainerInput: function(event) { 93 | var keys = { 94 | 37: this._moveLeft, 95 | 39: this._moveRight, 96 | 8: this._removeSelectedContainer 97 | }; 98 | 99 | if (typeof keys[event.keyCode] === 'function') { 100 | keys[event.keyCode](event); 101 | } 102 | }, 103 | 104 | _handleContainerBlur: function() { 105 | if (this.state.selectedIndex) { 106 | this.setState({ 107 | selectedIndex: -1 108 | }); 109 | } 110 | }, 111 | 112 | _selectOption: function(option) { 113 | if (option) { 114 | var values = this.state.values.slice(0); // copy 115 | var options = this._getAvailableOptions(values); 116 | 117 | // determine which item to highlight 118 | var valueField = this.props.valueField; 119 | var optionIndex = _.findIndex(options, function(o) { 120 | return option[valueField] === o[valueField]; 121 | }); 122 | 123 | values.push(option); 124 | 125 | options = this._getAvailableOptions(values); 126 | var state = this._resetSearch(options); 127 | state.values = values; 128 | 129 | var nextOption = options[optionIndex]; 130 | if (_.isUndefined(nextOption)) { 131 | // at the end of the list so select previous one 132 | nextOption = options[optionIndex - 1]; 133 | if (_.isUndefined(nextOption)) { 134 | // bail out 135 | nextOption = _.first(options); 136 | } 137 | } 138 | 139 | state.highlighted = nextOption; 140 | 141 | this.setState(state); 142 | 143 | if (typeof this.props.onSelect === 'function') { 144 | this.props.onSelect(option, values); 145 | } 146 | } 147 | }, 148 | 149 | _getAvailableOptions: function(values) { 150 | var options = this.state.initialOptions; 151 | var valueField = this.props.valueField; 152 | 153 | if (this.props.allowDuplicates === false && values) { 154 | options = _.filter(options, function(option) { 155 | var found = _.find(values, function(value) { 156 | return value[valueField] === option[valueField]; 157 | }); 158 | 159 | return typeof found === 'undefined'; 160 | }); 161 | } 162 | 163 | return this._sort(options); 164 | }, 165 | 166 | _moveLeft: function(event) { 167 | var input = this.refs.input.getDOMNode(); 168 | 169 | if (!this.state.values.length) { 170 | return false; 171 | } 172 | 173 | if ( 174 | event.target === input && 175 | event.target.selectionStart === 0 176 | ) { 177 | event.preventDefault(); 178 | 179 | // select stage 180 | this.setState({ 181 | selectedIndex: this.state.values.length - 1 182 | }); 183 | 184 | // focus on container 185 | this.refs.container.getDOMNode().focus(); 186 | } else if (this.state.selectedIndex !== -1) { 187 | var nextIndex = this.state.selectedIndex - 1; 188 | if (nextIndex > -1) { 189 | this.setState({ 190 | selectedIndex: nextIndex 191 | }); 192 | } 193 | } 194 | }, 195 | 196 | _moveRight: function() { 197 | var input = this.refs.input.getDOMNode(); 198 | 199 | if (!this.state.values.length) { 200 | return false; 201 | } 202 | 203 | if (this.state.selectedIndex !== -1) { 204 | var nextIndex = this.state.selectedIndex + 1; 205 | if (nextIndex < this.state.values.length) { 206 | this.setState({ 207 | selectedIndex: nextIndex 208 | }); 209 | } else { 210 | // focus input box 211 | input.focus(); 212 | this.setState({ 213 | selectedIndex: -1 214 | }); 215 | } 216 | } 217 | }, 218 | 219 | _removeValue: function(index) { 220 | var values = this.state.values.slice(0); // copy 221 | var removedOption = values.splice(index, 1); 222 | 223 | var options = this._getAvailableOptions(values); 224 | 225 | var state = this._resetSearch(options); 226 | state.values = values; 227 | 228 | this.setState(state); 229 | 230 | if (typeof this.props.onDelete === 'function') { 231 | this.props.onDelete(removedOption, values); 232 | } 233 | }, 234 | 235 | // removes last element 236 | _remove: function(event) { 237 | if (!this.state.value) { 238 | event.preventDefault(); 239 | 240 | // remove last stage 241 | if (this.state.values.length) { 242 | this._removeValue(this.state.values.length - 1); 243 | } 244 | } 245 | }, 246 | 247 | // called from within, removes selected element 248 | _removeSelectedContainer: function(event) { 249 | if (this.state.selectedIndex !== -1) { 250 | event.preventDefault(); 251 | 252 | // move selection to the element before the removed one (gmail behavior) 253 | this.setState({ 254 | selectedIndex: this.state.selectedIndex - 1 255 | }); 256 | 257 | this._removeValue(this.state.selectedIndex); 258 | } 259 | }, 260 | 261 | _removeDeletedContainer: function(index) { 262 | this._removeValue(index); 263 | }, 264 | 265 | _selectValue: function(index, event) { 266 | if (event) { 267 | event.preventDefault(); 268 | event.stopPropagation(); 269 | } 270 | 271 | this.setState({ 272 | selectedIndex: index 273 | }); 274 | 275 | this.refs.container.getDOMNode().focus(); 276 | }, 277 | 278 | _handleBlur: function(event) { 279 | if (this._optionsMouseDown === true) { 280 | this._optionsMouseDown = false; 281 | this.refs.input.getDOMNode().focus(); 282 | event.preventDefault(); 283 | event.stopPropagation(); 284 | } else { 285 | event.preventDefault(); 286 | this.setState({ 287 | focus: false 288 | }); 289 | } 290 | }, 291 | 292 | _handleOptionsMouseDown: function() { 293 | this._optionsMouseDown = true; 294 | }, 295 | 296 | componentWillReceiveProps: function(nextProps) { 297 | if (_.isEqual(nextProps.values, this.props.values)) { 298 | var options = this._getAvailableOptions(nextProps.values); 299 | 300 | var state = this._resetSearch(options); 301 | state.values = nextProps.values; 302 | state.selected = null; 303 | 304 | this.setState(state); 305 | } 306 | }, 307 | 308 | componentDidUpdate: function() { 309 | this._updateScrollPosition(); 310 | }, 311 | 312 | render: function() { 313 | var values = _.map(this.state.values, function(v, i) { 314 | var key = v[this.props.valueField]; 315 | 316 | var selected = i === this.state.selectedIndex; 317 | 318 | var label = v[this.props.labelField]; 319 | 320 | return ( 321 | 325 |
{label}
326 |
327 | ); 328 | }, this); 329 | 330 | var options = _.map(this.state.searchResults, function(option) { 331 | var valueField = this.props.valueField; 332 | var v = option[valueField]; 333 | 334 | var child = _.find(this.props.children, function(c) { 335 | return c.props[valueField] === v; 336 | }); 337 | 338 | var highlighted = this.state.highlighted && 339 | v === this.state.highlighted[valueField]; 340 | 341 | child = cloneWithProps(child, { tokens: this.state.searchTokens }); 342 | 343 | return ( 344 | 350 | {child} 351 | 352 | ); 353 | }, this); 354 | 355 | var value = this.state.value; 356 | 357 | var wrapperClasses = cx({ 358 | 'react-choice-wrapper': true, 359 | 'react-choice-multiple': true, 360 | 'react-choice-multiple--in-focus': this.state.focus, 361 | 'react-choice-multiple--not-in-focus': !this.state.focus 362 | }); 363 | 364 | return ( 365 |
366 |
369 | {values} 370 | 385 |
386 | 387 | {this.state.focus ? 388 | 389 | {options} 390 | : null} 391 |
392 | ); 393 | } 394 | }); 395 | 396 | module.exports = MultipleChoice; 397 | -------------------------------------------------------------------------------- /src/option-mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | 5 | var OptionMixin = { 6 | propTypes: { 7 | tokens: React.PropTypes.array.isRequired, 8 | children: React.PropTypes.string.isRequired 9 | } 10 | }; 11 | 12 | module.exports = OptionMixin; 13 | -------------------------------------------------------------------------------- /src/option-wrapper.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | function isArray(test) { 7 | return Object.prototype.toString.call(test) === '[object Array]'; 8 | } 9 | 10 | var OptionWrapper = React.createClass({ 11 | render: function() { 12 | var classes = cx({ 13 | 'react-choice-option': true, 14 | 'react-choice-option--selected': !!this.props.selected 15 | }); 16 | 17 | return ( 18 |
  • 22 | {this.props.children} 23 |
  • 24 | ); 25 | } 26 | }); 27 | 28 | module.exports = OptionWrapper; 29 | -------------------------------------------------------------------------------- /src/option.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var cx = React.addons.classSet; 5 | 6 | var OptionMixin = require('./option-mixin'); 7 | var TextHighlight = require('./text-highlight'); 8 | 9 | // 10 | // Select option 11 | // 12 | var SelectOption = React.createClass({ 13 | mixins: [OptionMixin], 14 | 15 | render: function() { 16 | return ( 17 |
    18 | 19 | {this.props.children} 20 | 21 |
    22 | ); 23 | } 24 | }); 25 | 26 | module.exports = SelectOption; 27 | -------------------------------------------------------------------------------- /src/options.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | 5 | var Options = React.createClass({ 6 | propTypes: { 7 | children: React.PropTypes.array.isRequired, 8 | onMouseDown: React.PropTypes.func.isRequired 9 | }, 10 | 11 | _handleMouseDown: function(event) { 12 | this.props.onMouseDown(event); 13 | }, 14 | 15 | render: function() { 16 | return ( 17 |
    20 | 23 |
    24 | ); 25 | } 26 | }); 27 | 28 | module.exports = Options; 29 | -------------------------------------------------------------------------------- /src/search-mixin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('lodash'); 4 | var Sifter = require('sifter'); 5 | 6 | var SearchMixin = { 7 | // 8 | // Public methods 9 | // 10 | focus: function(openOptions) { 11 | this.refs.input.getDOMNode().focus(); 12 | }, 13 | 14 | _sort: function(list) { 15 | if (typeof this.props.sorter === 'function') { 16 | return this.props.sorter(list); 17 | } 18 | return _.sortBy(list, this.props.labelField); 19 | }, 20 | 21 | _handleClick: function(event) { 22 | this.refs.input.getDOMNode().focus(); 23 | }, 24 | 25 | _handleInput: function(event) { 26 | var keys = { 27 | 13: this._enter, 28 | 37: this._moveLeft, 29 | 38: this._moveUp, 30 | 39: this._moveRight, 31 | 40: this._moveDown, 32 | 8: this._remove 33 | }; 34 | 35 | if (typeof keys[event.keyCode] == 'function') { 36 | keys[event.keyCode](event); 37 | } 38 | }, 39 | 40 | _handleChange: function(event) { 41 | event.preventDefault(); 42 | 43 | var query = event.target.value; 44 | 45 | var options = this._getAvailableOptions(); 46 | 47 | var searcher = new Sifter(options); 48 | 49 | var result = searcher.search(query, { 50 | fields: this.props.searchField 51 | }); 52 | 53 | var searchResults = _.map(result.items, function(res) { 54 | return options[res.id]; 55 | }); 56 | 57 | var highlighted = _.first(searchResults); 58 | 59 | this.setState({ 60 | value: query, 61 | searchResults: searchResults, 62 | searchTokens: result.tokens, 63 | highlighted: highlighted, 64 | selected: null, 65 | }); 66 | }, 67 | 68 | _handleFocus: function(event) { 69 | event.preventDefault(); 70 | 71 | var highlighted; 72 | if (this.state.selected) { 73 | highlighted = _.find(this.state.searchResults, function(option) { 74 | return option[this.props.valueField] == this.state.selected[this.props.valueField]; 75 | }, this); 76 | } else { 77 | highlighted = _.first(this.state.searchResults); 78 | } 79 | 80 | this.setState({ 81 | focus: true, 82 | highlighted: highlighted 83 | }); 84 | }, 85 | 86 | _handleOptionHover: function(option, event) { 87 | event.preventDefault(); 88 | this.setState({ 89 | highlighted: option 90 | }); 91 | }, 92 | 93 | _handleOptionClick: function(option, event) { 94 | event.preventDefault(); 95 | event.stopPropagation(); 96 | this._selectOption(option); 97 | }, 98 | 99 | _moveUp: function(event) { 100 | var options = this.state.searchResults; 101 | if (options.length > 0) { 102 | event.preventDefault(); 103 | var index = _.indexOf(options, this.state.highlighted); 104 | if (!_.isUndefined(options[index - 1])) { 105 | this.setState({ 106 | highlighted: options[index - 1] 107 | }); 108 | } 109 | } 110 | }, 111 | 112 | _moveDown: function(event) { 113 | var options = this.state.searchResults; 114 | if (options.length > 0) { 115 | event.preventDefault(); 116 | var index = _.indexOf(options, this.state.highlighted); 117 | if (!_.isUndefined(options[index + 1])) { 118 | this.setState({ 119 | highlighted: options[index + 1] 120 | }); 121 | } 122 | } 123 | }, 124 | 125 | _enter: function(event) { 126 | event.preventDefault(); 127 | this._selectOption(this.state.highlighted); 128 | }, 129 | 130 | _updateScrollPosition: function() { 131 | var highlighted = this.refs.highlighted; 132 | if (highlighted) { 133 | // find if highlighted option is not visible 134 | var el = highlighted.getDOMNode(); 135 | var parent = this.refs.options.getDOMNode(); 136 | var offsetTop = el.offsetTop + el.clientHeight - parent.scrollTop; 137 | 138 | // scroll down 139 | if (offsetTop > parent.clientHeight) { 140 | var diff = el.offsetTop + el.clientHeight - parent.clientHeight; 141 | parent.scrollTop = diff; 142 | } else if (offsetTop - el.clientHeight < 0) { // scroll up 143 | parent.scrollTop = el.offsetTop; 144 | } 145 | } 146 | }, 147 | 148 | _resetSearch: function(options) { 149 | return { 150 | value: '', 151 | searchResults: options, 152 | searchTokens: [], 153 | highlighted: _.first(options) 154 | }; 155 | }, 156 | }; 157 | 158 | module.exports = SearchMixin; 159 | -------------------------------------------------------------------------------- /src/single.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var _ = require('lodash'); 5 | var cx = React.addons.classSet; 6 | var cloneWithProps = React.addons.cloneWithProps; 7 | 8 | var Icon = require('./icon'); 9 | var Options = require('./options'); 10 | var OptionWrapper = require('./option-wrapper'); 11 | 12 | var SearchMixin = require('./search-mixin'); 13 | 14 | // 15 | // Auto complete select box 16 | // 17 | var SingleChoice = React.createClass({ 18 | mixins: [SearchMixin], 19 | 20 | propTypes: { 21 | name: React.PropTypes.string, // name of input 22 | placeholder: React.PropTypes.string, // input placeholder 23 | value: React.PropTypes.string, // initial value for input field 24 | children: React.PropTypes.array.isRequired, 25 | 26 | valueField: React.PropTypes.string, // value field name 27 | labelField: React.PropTypes.string, // label field name 28 | 29 | searchField: React.PropTypes.array, // array of search fields 30 | 31 | icon: React.PropTypes.func, // icon render 32 | 33 | onSelect: React.PropTypes.func // function called when option is selected 34 | }, 35 | 36 | getDefaultProps: function() { 37 | return { 38 | valueField: 'value', 39 | labelField: 'children', 40 | searchField: ['children'] 41 | }; 42 | }, 43 | 44 | _getAvailableOptions: function() { 45 | var options = this.state.initialOptions; 46 | 47 | return this._sort(options); 48 | }, 49 | 50 | getInitialState: function() { 51 | var selected = null; 52 | 53 | var props = this.props.searchField; 54 | props.push(this.props.valueField); 55 | props.push(this.props.searchField); 56 | props = _.uniq(props); 57 | 58 | var options = _.map(this.props.children, function(child) { 59 | // TODO Validation ? 60 | return _.pick(child.props, props); 61 | }, this); 62 | 63 | if (this.props.value) { 64 | // find selected value 65 | selected = _.find(options, function(option) { 66 | return option[this.props.valueField] === this.props.value; 67 | }, this); 68 | } 69 | 70 | return { 71 | value: selected ? selected[this.props.labelField] : this.props.value, 72 | focus: false, 73 | searchResults: this._sort(options), 74 | initialOptions: options, 75 | highlighted: null, 76 | selected: selected, 77 | searchTokens: [] 78 | }; 79 | }, 80 | 81 | // 82 | // Public methods 83 | // 84 | getValue: function() { 85 | return this.state.selected ? 86 | this.state.selected[this.props.valueField] : null; 87 | }, 88 | 89 | // 90 | // Events 91 | // 92 | _handleArrowClick: function(event) { 93 | if (this.state.focus) { 94 | this._handleBlur(event); 95 | this.refs.input.getDOMNode().blur(); 96 | } else { 97 | this._handleFocus(event); 98 | this.refs.input.getDOMNode().focus(); 99 | } 100 | }, 101 | 102 | _remove: function(event) { 103 | if (this.state.selected) { 104 | event.preventDefault(); 105 | 106 | var state = this._resetSearch(this.state.initialOptions); 107 | state.selected = null; 108 | 109 | this.setState(state); 110 | } 111 | }, 112 | 113 | _selectOption: function(option) { 114 | this._optionsMouseDown = false; 115 | this.refs.input.getDOMNode().blur(); 116 | this.setState({ 117 | focus: false 118 | }); 119 | 120 | if (option) { 121 | var options = this._getAvailableOptions(); 122 | var state = this._resetSearch(options); 123 | state.selected = option; 124 | 125 | this.setState(state); 126 | 127 | if (typeof this.props.onSelect === 'function') { 128 | this.props.onSelect(option); 129 | } 130 | } 131 | }, 132 | 133 | _handleBlur: function(event) { 134 | event.preventDefault(); 135 | if (this._optionsMouseDown === true) { 136 | this._optionsMouseDown = false; 137 | this.refs.input.getDOMNode().focus(); 138 | event.stopPropagation(); 139 | } else { 140 | this.setState({ 141 | focus: false 142 | }); 143 | } 144 | }, 145 | 146 | _handleOptionsMouseDown: function() { 147 | this._optionsMouseDown = true; 148 | }, 149 | 150 | componentWillReceiveProps: function(nextProps) { 151 | if (nextProps.value !== this.props.value) { 152 | var options = this._getAvailableOptions(); 153 | 154 | var selected = _.find(options, function(option) { 155 | return option[this.props.valueField] === nextProps.value; 156 | }, this); 157 | 158 | var state = this._resetSearch(options); 159 | state.value = selected ? selected[this.props.labelField] : nextProps.value; 160 | state.selected = selected; 161 | 162 | this.setState(state); 163 | } 164 | }, 165 | 166 | componentDidUpdate: function(prevProps, prevState) { 167 | if (prevState.focus === false && this.state.focus === true) { 168 | this._updateScrollPosition(); 169 | } 170 | 171 | // select selected text in input box 172 | if (this.state.selected && this.state.focus) { 173 | setTimeout(function() { 174 | if (this.isMounted()) { 175 | this.refs.input.getDOMNode().select(); 176 | } 177 | }.bind(this), 50); 178 | } 179 | }, 180 | 181 | render: function() { 182 | var options = _.map(this.state.searchResults, function(option) { 183 | var valueField = this.props.valueField; 184 | var v = option[valueField]; 185 | 186 | var child = _.find(this.props.children, function(c) { 187 | return c.props[valueField] === v; 188 | }); 189 | 190 | var highlighted = this.state.highlighted && 191 | v === this.state.highlighted[valueField]; 192 | 193 | child = cloneWithProps(child, { tokens: this.state.searchTokens }); 194 | 195 | return ( 196 | 202 | {child} 203 | 204 | ); 205 | }, this); 206 | 207 | var value = this.state.selected ? 208 | this.state.selected[this.props.valueField] : null; 209 | var label = this.state.selected ? 210 | this.state.selected[this.props.labelField] : this.state.value; 211 | 212 | var wrapperClasses = cx({ 213 | 'react-choice-wrapper': true, 214 | 'react-choice-single': true, 215 | 'react-choice-single--in-focus': this.state.focus, 216 | 'react-choice-single--not-in-focus': !this.state.focus 217 | }); 218 | 219 | var IconRenderer = this.props.icon || Icon; 220 | 221 | return ( 222 |
    223 | 224 | 225 |
    226 | 241 |
    242 | 243 |
    244 | 245 |
    246 | 247 | {this.state.focus ? 248 | 249 | {options} 250 | : null} 251 |
    252 | ); 253 | } 254 | }); 255 | 256 | module.exports = SingleChoice; 257 | -------------------------------------------------------------------------------- /src/text-highlight.jsx: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var React = require('react/addons'); 4 | var _ = require('lodash'); 5 | 6 | var humanizeString = require('./humanize-string'); 7 | 8 | var TextHighlight = React.createClass({ 9 | propTypes: { 10 | tokens: React.PropTypes.array.isRequired, // array of search tokens 11 | children: React.PropTypes.string.isRequired // text to highlight 12 | }, 13 | 14 | shouldComponentUpdate: function(nextProps) { 15 | if (!_.isEqual(nextProps, this.props)) { 16 | return true; 17 | } 18 | return false; 19 | }, 20 | 21 | splitText: function(splits, regex) { 22 | var _splits = []; 23 | _.each(splits, function(split) { 24 | if (split.match === false) { 25 | var match = split.text.match(regex); 26 | if (match) { 27 | var s = split.text.split(regex); 28 | 29 | _.each(s, function(_s, index) { 30 | _splits.push({ 31 | text: _s, 32 | match: false 33 | }); 34 | 35 | if (index !== s.length - 1) { 36 | var matchCharacter = match[0]; 37 | 38 | var i = _splits.length - 1; 39 | if ((_.isEmpty(_s) && i === 0) || _s.slice(-1) === ' ') { 40 | matchCharacter = humanizeString(matchCharacter); 41 | } else { 42 | matchCharacter = matchCharacter.toLowerCase(); 43 | } 44 | 45 | _splits.push({ 46 | text: matchCharacter, 47 | match: true 48 | }); 49 | } 50 | }); 51 | } else { 52 | _splits.push(split); 53 | } 54 | } else { 55 | _splits.push(split); 56 | } 57 | }); 58 | return _splits; 59 | }, 60 | 61 | render: function() { 62 | var label = this.props.children; 63 | var tokens = this.props.tokens; 64 | 65 | var splits = [{ 66 | text: label, 67 | match: false 68 | }]; 69 | 70 | _.each(tokens, function(token) { 71 | splits = this.splitText(splits, token.regex); 72 | }, this); 73 | 74 | var output = _.map(splits, function(split, i) { 75 | var key = [split.text, split.match, i].join('.'); 76 | if (split.match) { 77 | return {split.text}; 78 | } else { 79 | return {split.text}; 80 | } 81 | }); 82 | 83 | return ( 84 | 85 | {output} 86 | 87 | ); 88 | } 89 | }); 90 | 91 | module.exports = TextHighlight; 92 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cache: true, 3 | context: __dirname, 4 | entry: './example/js/index.js', 5 | output: { 6 | path: './example/build/', 7 | filename: 'index.js' 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.jsx?$/, 13 | exclude: /node_modules/, 14 | loaders: [ 15 | 'jsx?harmony&sourceMap=true' 16 | ] 17 | } 18 | ], 19 | postLoaders: [ 20 | { 21 | loader: "transform/cacheable?brfs" 22 | } 23 | ] 24 | }, 25 | resolve: { 26 | extensions: ['', '.js', '.jsx'] 27 | } 28 | }; 29 | --------------------------------------------------------------------------------