├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── categorized-tag-input.css ├── categorized-tag-input.js ├── categorized-tag-input.js.map ├── index.html ├── index.js ├── package.json ├── server.js ├── src ├── CategorizedTagInput.jsx ├── Category.jsx ├── Input.jsx ├── Panel.jsx ├── Tag.jsx ├── index.js └── keyboard.js ├── test ├── CategorizedTagInput_spec.js ├── Category_spec.js ├── Input_spec.js ├── Panel_spec.js ├── Tag_spec.js └── jsdomReact.js ├── webpack.config.js └── webpack.test.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | test/manual/bundle.js 29 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "eqeqeq": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Miguel Molina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-categorized-tag-input [![Build Status](https://travis-ci.org/erizocosmico/react-categorized-tag-input.svg)](https://travis-ci.org/erizocosmico/react-categorized-tag-input) 2 | React.js component for making tag autocompletion inputs with categorized results with no dependencies and 10KB minified. 3 | 4 | **Note:** v1.x versions only work with react 0.14.0 or higher. For compatibility with previous versions use the v0.x versions. 5 | 6 | ## Install 7 | 8 | ``` 9 | npm install react-categorized-tag-input 10 | ``` 11 | 12 | ## Include 13 | 14 | With webpack, browserify, etc (recommended way): 15 | ```javascript 16 | import TagInput from 'react-categorized-tag-input'; 17 | ``` 18 | or (if you are not yet using ES2015, which you should) 19 | ```javascript 20 | var TagInput = require('react-categorized-tag-input'); 21 | ``` 22 | 23 | With other tools: 24 | Just include the `categorized-tag-input.js` file in your HTML and your good to go. It is already minified. 25 | 26 | If you want to use the default style you have to include the `categorized-tag-input.css` file. It is plain CSS, no LESS, no SASS, no dependencies. 27 | 28 | As a personal suggestion, I recommend webpack. You would just need to `require('node_modules/react-categorized-tag-input/categorized-tag-input.css');`. 29 | 30 | ## Usage 31 | 32 | To use this component we will assume the imported variable name is `TagInput`. 33 | The props are very straightforward. 34 | 35 | |Name | Type | Description | default| 36 | |-----|------|-------------|--------| 37 | |addNew|boolean|If true, allows the user to create new tags that are not set in the dataset|true| 38 | |categories|Array of objects|Dataset with categories and items|Required| 39 | |transformTag|function|A function that will receive the tag object (which has at least keys `title` and `category`) and must return a string. This string will be displayed to the user. Useful if you need to apply a transformation to the tags.|(tag) => tag.title| 40 | |value|Array of tags. Tags are objects with (at least) keys `title` and `category`, where `category` is the id of a category in the array passed in for the `categories` prop|Array with the initial tags|[]| 41 | |onBlur|function|Callback for when the input loses focus|noop| 42 | |onChange|function|Callback for when the input changes. It does not get an event as parameter, it gets the array of tags after the change.|noop| 43 | |placeholder|string|A placeholder will be given in the input box.|Add a tag| 44 | |getTagStyle|function| A function from the tag text (string) to an object with any or all of the following keys: `base`, `content` and `delete`. The values are React style objects. This example renders 1-letter long tags in red: `text => text.length === 1 ? {base: {color: "red"}} : {}` | () => ({}) | 45 | |getCreateNewText|function| A function that returns the text to display when the user types an unrecognized tag, given a title and text.| (title, text) => `Create new ${title} "${text}"` | 46 | |getTagStyle|function| A function from the tag (object with at least the keys `title` and `category`) to an object with any or all of the following keys: `base`, `content` and `delete`. The values are React style objects. This example renders 1-letter long tags in red: `text => text.length === 1 ? {base: {color: "red"}} : {}` | () => ({}) | 47 | 48 | #### The tag object 49 | Tag objects look like this: 50 | ``` 51 | { 52 | title: 'String to used to identify the tag', 53 | category: 'id of the category for the tag' 54 | 55 | } 56 | ``` 57 | 58 | #### The category object 59 | The category object for the dataset looks like this: 60 | ``` 61 | { 62 | id: 'string or number identifying the category', 63 | type: 'word to describe the category. Will be used on the create new tag button. E.g: "Create new animal foo"', 64 | title: 'Title displayed on the category row', 65 | items: ['Array', 'With', 'Tags'], 66 | single: optional boolean. If is true the row will be treated as one-valued row. It does not have the option of adding new items to the category 67 | } 68 | ``` 69 | 70 | #### Create the object 71 | 72 | ```jsx 73 | 77 | ``` 78 | 79 | #### Get the value 80 | 81 | You can either use the `onChange` callback or use the `value()` method of the component. It will return the existing tags as an array of strings. 82 | 83 | ### How to use the rendered component 84 | 85 | When you click on the input you will be able to write on it. Right away, a panel with the categories with matches will be shown. You can navigate through categories and options using the arrow keys to change the selected tag. Backspace when there is nothing written erases the last tag. Enter and `,` add the currently selected tag to the input. 86 | -------------------------------------------------------------------------------- /categorized-tag-input.css: -------------------------------------------------------------------------------- 1 | .cti__root { 2 | font-family: sans-serif; 3 | font-weight: normal; 4 | font-size: 14px; 5 | position: relative; 6 | width: 330px; 7 | } 8 | 9 | .cti__root * { 10 | box-sizing: border-box; 11 | outline: none; 12 | } 13 | 14 | .cti__input { 15 | border: 1px solid rgb(220, 220, 220); 16 | padding: .1em 24px .1em .1em; 17 | min-height: 38px; 18 | position: relative; 19 | background: white; 20 | } 21 | 22 | .cti__input__arrow { 23 | position: absolute; 24 | display: block; 25 | width: 0; 26 | height: 0; 27 | border-left: 4px solid transparent; 28 | border-right: 4px solid transparent; 29 | border-top: 4px solid rgb(140, 140, 140); 30 | right: 10px; 31 | top: 50%; 32 | margin-top: -2px; 33 | } 34 | 35 | .cti__input__tags, .cti__input__input { 36 | display: inline-block; 37 | vertical-align: middle; 38 | } 39 | 40 | .cti__input__input { 41 | max-width: 100%; 42 | padding: .4em 0 !important; 43 | margin: .1em 0 .1em .4em !important; 44 | font-size: 1em !important; 45 | border: none; 46 | } 47 | 48 | .cti__tag { 49 | background-color: white; 50 | border: 1px solid rgb(220, 220, 220); 51 | border-radius: 3px; 52 | padding: .3em .7em; 53 | color: rgb(146, 146, 146); 54 | display: inline-block; 55 | margin: .1em 0 .1em .4em; 56 | cursor: pointer; 57 | word-break: break-all; 58 | max-width: 100%; 59 | } 60 | 61 | .cti__tag__content--match { 62 | color: black; 63 | font-weight: bold; 64 | } 65 | 66 | .cti__tag__delete, .cti__tag__content, .cti__tag__content > span { 67 | display: inline-block; 68 | vertical-align: middle; 69 | } 70 | 71 | .cti__tag__delete { 72 | background-color: transparent; 73 | padding: 0 0 0 .3em; 74 | margin: -4px 0 0 0; 75 | color: rgb(146, 146, 146); 76 | font-size: 1.5em; 77 | border: none; 78 | } 79 | 80 | .cti__tag__content > span { 81 | margin-top: -3px; 82 | } 83 | 84 | .cti__panel { 85 | position: absolute; 86 | background: white; 87 | border: 1px solid rgb(220, 220, 220); 88 | border-top: none; 89 | width: 100%; 90 | z-index: 9997; 91 | } 92 | 93 | .cti__category__add-item { 94 | background: transparent; 95 | border: none; 96 | color: rgb(28, 99, 166); 97 | font-size: 1em; 98 | } 99 | 100 | .cti__category__add-item.cti-selected { 101 | margin-left: .4em; 102 | } 103 | 104 | .cti__category__or { 105 | margin-left: .4em; 106 | } 107 | 108 | .cti-selected { 109 | background-color: rgb(229, 241, 255); 110 | border: 1px solid rgb(207, 219, 234); 111 | border-radius: 3px; 112 | } 113 | 114 | .cti__category { 115 | padding: .4em; 116 | } 117 | 118 | .cti__category__title { 119 | padding: .3em 0; 120 | margin: 0; 121 | color: rgb(200, 200, 200); 122 | font-weight: normal; 123 | } 124 | -------------------------------------------------------------------------------- /categorized-tag-input.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("react")):"function"==typeof define&&define.amd?define(["react"],t):"object"==typeof exports?exports.CategorizedTagInput=t(require("react")):e.CategorizedTagInput=t(e.React)}(this,function(e){return function(e){function t(n){if(s[n])return s[n].exports;var r=s[n]={exports:{},id:n,loaded:!1};return e[n].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var s={};return t.m=e,t.c=s,t.p="",t(0)}([function(e,t,s){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var r=s(3),a=n(r);t["default"]=a["default"],e.exports=t["default"]},function(t,s){t.exports=e},function(e,t,s){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var r=s(1),a=n(r),i=a["default"].PropTypes,o=a["default"].createClass({displayName:"Tag",propTypes:{selected:i.bool,input:i.string.isRequired,text:i.string.isRequired,addable:i.bool,deletable:i.bool,onAdd:i.func,onDelete:i.func,style:i.object},getDefaultProps:function(){return{text:""}},tagContent:function(){var e=[],t=this.props.text.trim().toLowerCase().indexOf(this.props.input.trim().toLowerCase()),s=t+this.props.input.length;return t>0&&e.push(a["default"].createElement("span",{key:1,className:"cti__tag__content--regular"},this.props.text.substring(0,t))),e.push(a["default"].createElement("span",{key:2,className:"cti__tag__content--match"},this.props.text.substring(t,s))),s0}function i(e){return"object"==typeof e&&e.id&&e.title&&e.items&&Array.isArray(e.items)&&e.items.every(a)&&(e.type||e.single)}Object.defineProperty(t,"__esModule",{value:!0}),t.isCategoryItemValid=a,t.isCategoryValid=i;var o=s(1),l=r(o),u=s(5),c=r(u),p=s(6),d=r(p),f=s(7),h=n(f),g=l["default"].PropTypes,m=l["default"].createClass({displayName:"CategorizedTagInput",propTypes:{addNew:g.bool,categories:g.arrayOf(g.object).isRequired,transformTag:g.func,value:g.arrayOf(g.object),onBlur:g.func,onChange:g.func,placeholder:g.string,getTagStyle:g.func,getCreateNewText:g.func},getInitialState:function(){return{value:"",selection:{item:0,category:0},panelOpened:!1,categories:[],addNew:void 0===this.props.addNew?!0:this.props.addNew}},getDefaultProps:function(){return{onChange:function(e){}}},componentWillMount:function(){if(!this.props.categories.every(i))throw new Error("invalid categories source provided for react-categorized-tag-input")},componentWillUnmount:function(){this.timeout&&clearTimeout(this.timeout)},filterCategories:function(e){var t=this,s=this.props.categories.map(function(s){return s=Object.assign({},s,{items:s.items.filter(t.filterItems(e))}),0!==s.items.length||t.state.addNew&&!s.single?s:null}).filter(function(e){return null!==e}),n=this.state.selection;this.state.selection.category>=s.length?n={category:0,item:0}:n.item>=s[n.category].items.length&&(n.item=0),this.setState({categories:s,selection:n})},filterItems:function(e){return function(t){return 1===e.length?t.toLowerCase().trim()===e:t.toLowerCase().indexOf(e.trim().toLowerCase())>=0}},openPanel:function(){this.setState({panelOpened:!0})},closePanel:function(){var e=this;this.timeout&&clearTimeout(this.timeout),this.timeout=setTimeout(function(){e.timeout=void 0,e.setState({panelOpened:!1})},150)},onValueChange:function(e){var t=e.target.value;this.setState({value:t,panelOpened:t.trim().length>0||!isNaN(Number(t.trim()))}),this.filterCategories(t)},onTagDeleted:function(e){var t=this.props.value.slice();t.splice(e,1),this.props.onChange(t)},onAdd:function(e){var t=this.props.value.concat([e]);this.setState({value:"",panelOpened:!0}),this.refs.input.focusInput(),this.props.onChange(t)},addSelectedTag:function(){if(this.state.panelOpened&&this.state.value.length>0){var e=this.state.categories[this.state.selection.category],t=e.items[this.state.selection.item];this.onAdd({category:e.id,title:t||this.state.value})}},handleBackspace:function(e){0===this.state.value.trim().length&&(e.preventDefault(),this.onTagDeleted(this.props.value.length-1))},handleArrowLeft:function(){var e=this.state.selection.item-1;this.setState({selection:{category:this.state.selection.category,item:e>=0?e:0}})},handleArrowUp:function(){var e=this.state.selection.category-1;this.setState({selection:{category:e>=0?e:0,item:0}})},handleArrowRight:function(){var e=this.state.selection.item+1,t=this.state.categories[this.state.selection.category];this.setState({selection:{category:this.state.selection.category,item:e<=t.items.length?e:t.items.length}})},handleArrowDown:function(){var e=this.state.selection.category+1,t=this.state.categories;this.setState({selection:{category:e0?l["default"].createElement(d["default"],{categories:this.state.categories,selection:this.state.selection,onAdd:this.onAdd,input:this.state.value,getCreateNewText:this.props.getCreateNewText,addNew:void 0===this.props.addNew?!0:this.props.addNew}):"")}});t["default"]=m},function(e,t,s){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var r=s(1),a=n(r),i=s(2),o=n(i),l=a["default"].PropTypes,u=function(e,t){return"Create new "+e+' "'+t+'"'},c=a["default"].createClass({displayName:"Category",propTypes:{items:l.array.isRequired,category:l.oneOfType([l.string,l.number]).isRequired,title:l.string.isRequired,selected:l.bool.isRequired,selectedItem:l.number.isRequired,input:l.string.isRequired,addNew:l.bool,type:l.string,onAdd:l.func.isRequired,single:l.bool,getTagStyle:l.func,getCreateNewText:l.func},onAdd:function(e){var t=this;return function(){t.props.onAdd({category:t.props.category,title:e})}},onCreateNew:function(e){e.preventDefault(),this.onAdd(this.props.input)()},getTagStyle:function(e){return this.props.getTagStyle?this.props.getTagStyle(e):{}},itemToTag:function(e,t){return a["default"].createElement(o["default"],{selected:this.isSelected(t),input:this.props.input,text:e,addable:!0,deletable:!1,onAdd:this.onAdd(e),key:e+"_"+t,style:this.getTagStyle(e)})},fullMatchInItems:function(){for(var e=0,t=this.props.items.length;t>e;e++)if(this.props.items[e]===this.props.input)return!0;return!1},getItems:function(){return{items:this.props.items.map(this.itemToTag),fullMatch:this.fullMatchInItems()}},isSelected:function(e){return this.props.selected&&(e===this.props.selectedItem||this.props.single)},getAddBtn:function(e,t){var s=this.props.type||this.props.title,n=this.props.input,r=this.props.getCreateNewText||u;return!this.props.addNew||e||this.props.single?null:[this.props.items.length>0?a["default"].createElement("span",{key:"cat_or",className:"cti__category__or"},"or"):null,a["default"].createElement("button",{key:"add_btn",className:"cti__category__add-item"+(t?" cti-selected":""),onClick:this.onCreateNew},r(s,n))]},render:function(){var e=this.getItems(),t=e.items,s=e.fullMatch,n=this.getAddBtn(s,(0===t.length||this.props.selectedItem>=t.length)&&this.props.selected);return a["default"].createElement("div",{className:"cti__category"},a["default"].createElement("h5",{className:"cti__category__title"},this.props.title),a["default"].createElement("div",{className:"cti__category__tags"},t,n))}});t["default"]=c,e.exports=t["default"]},function(e,t,s){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var r=s(1),a=n(r),i=s(2),o=n(i),l=a["default"].PropTypes,u=a["default"].createClass({displayName:"Input",propTypes:{openPanel:l.func.isRequired,closePanel:l.func.isRequired,onValueChange:l.func.isRequired,onTagDeleted:l.func.isRequired,onKeyDown:l.func.isRequired,value:l.string.isRequired,tags:l.arrayOf(l.object).isRequired,placeholder:l.string,onBlur:l.func,getTagStyle:l.func,transformTag:l.func},focusInput:function(){this.refs.input.focus()},getDefaultProps:function(){return{getTagStyle:function(e){return{}},transformTag:function(e){return e.title}}},getTags:function(){var e=this;return this.props.tags.map(function(t,s){return a["default"].createElement(o["default"],{selected:!1,input:"",text:e.props.transformTag(t),addable:!1,deletable:!0,key:t.title+"_"+s,onDelete:function(){return e.props.onTagDeleted(s)},style:e.props.getTagStyle(t)})})},onBlur:function(e){this.props.closePanel(),"function"==typeof this.props.onBlur&&this.props.onBlur(e)},render:function(){var e=this.props.placeholder||"",t=0===this.props.value.length?e.length:this.props.value.length;return a["default"].createElement("div",{className:"cti__input",onClick:this.focusInput},this.getTags(),a["default"].createElement("input",{type:"text",ref:"input",value:this.props.value,size:t+2,onFocus:this.props.openPanel,onBlur:this.onBlur,onChange:this.props.onValueChange,onKeyDown:this.props.onKeyDown,placeholder:e,"aria-label":e,className:"cti__input__input"}),a["default"].createElement("div",{className:"cti__input__arrow"}))}});t["default"]=u,e.exports=t["default"]},function(e,t,s){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var r=s(1),a=n(r),i=s(4),o=n(i),l=a["default"].PropTypes,u=a["default"].createClass({displayName:"Panel",propTypes:{categories:l.arrayOf(l.object).isRequired,selection:l.object.isRequired,onAdd:l.func.isRequired,input:l.string.isRequired,addNew:l.bool,getTagStyle:l.func,getCreateNewText:l.func},getCategories:function(){var e=this;return this.props.categories.map(function(t,s){return a["default"].createElement(o["default"],{key:t.id,items:t.items,category:t.id,title:t.title,selected:e.props.selection.category===s,selectedItem:e.props.selection.item,input:e.props.input,addNew:e.props.addNew,type:t.type,onAdd:e.props.onAdd,single:t.single,getTagStyle:e.props.getTagStyle,getCreateNewText:e.props.getCreateNewText})})},render:function(){return a["default"].createElement("div",{className:"cti__panel"},this.getCategories())}});t["default"]=u,e.exports=t["default"]},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var s=9;t.TAB=s;var n=13;t.ENTER=n;var r=8;t.BACKSPACE=r;var a=37;t.LEFT=a;var i=38;t.UP=i;var o=39;t.RIGHT=o;var l=40;t.DOWN=l;var u=188;t.COMMA=u}])}); 2 | //# sourceMappingURL=categorized-tag-input.js.map -------------------------------------------------------------------------------- /categorized-tag-input.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/universalModuleDefinition","webpack:///categorized-tag-input.js","webpack:///webpack/bootstrap affe410ff901bb1c965a","webpack:///./src/index.js","webpack:///external {\"root\":\"React\",\"commonjs2\":\"react\",\"commonjs\":\"react\",\"amd\":\"react\"}","webpack:///./src/Tag.jsx","webpack:///./src/CategorizedTagInput.jsx","webpack:///./src/Category.jsx","webpack:///./src/Input.jsx","webpack:///./src/Panel.jsx","webpack:///./src/keyboard.js"],"names":["root","factory","exports","module","require","define","amd","this","__WEBPACK_EXTERNAL_MODULE_1__","modules","__webpack_require__","moduleId","installedModules","id","loaded","call","m","c","p","_interopRequireDefault","obj","__esModule","default","Object","defineProperty","value","_CategorizedTagInputJsx","_CategorizedTagInputJsx2","_react","_react2","PropTypes","Tag","createClass","displayName","propTypes","selected","bool","input","string","isRequired","text","addable","deletable","onAdd","func","onDelete","style","object","getDefaultProps","tagContent","content","startIndex","props","trim","toLowerCase","indexOf","endIndex","length","push","createElement","key","className","substring","onClick","e","preventDefault","stopPropagation","getDeleteBtn","deleteStyle","dangerouslySetInnerHTML","__html","render","deleteBtn","cls","base","_interopRequireWildcard","newObj","prototype","hasOwnProperty","isCategoryItemValid","i","isCategoryValid","title","items","Array","isArray","every","type","single","_InputJsx","_InputJsx2","_PanelJsx","_PanelJsx2","_keyboard","CategorizedTagInput","addNew","categories","arrayOf","transformTag","onBlur","onChange","placeholder","getTagStyle","getCreateNewText","getInitialState","selection","item","category","panelOpened","undefined","newTags","componentWillMount","Error","componentWillUnmount","timeout","clearTimeout","filterCategories","_this","map","assign","filter","filterItems","state","setState","openPanel","closePanel","_this2","setTimeout","onValueChange","target","isNaN","Number","onTagDeleted","slice","splice","newTag","concat","refs","focusInput","addSelectedTag","handleBackspace","handleArrowLeft","result","handleArrowUp","handleArrowRight","cat","handleArrowDown","cats","onKeyDown","keyCode","TAB","ENTER","COMMA","BACKSPACE","LEFT","UP","RIGHT","DOWN","tags","ref","_TagJsx","_TagJsx2","Category","array","oneOfType","number","selectedItem","onCreateNew","itemToTag","isSelected","fullMatchInItems","len","getItems","fullMatch","getAddBtn","getText","_getItems","addBtn","Input","focus","tag","getTags","size","onFocus","aria-label","_CategoryJsx","_CategoryJsx2","Panel","getCategories"],"mappings":"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","file":"categorized-tag-input.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"react\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"react\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"CategorizedTagInput\"] = factory(require(\"react\"));\n\telse\n\t\troot[\"CategorizedTagInput\"] = factory(root[\"React\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE_1__) {\nreturn \n\n\n/** WEBPACK FOOTER **\n ** webpack/universalModuleDefinition\n **/","(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"react\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"react\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"CategorizedTagInput\"] = factory(require(\"react\"));\n\telse\n\t\troot[\"CategorizedTagInput\"] = factory(root[\"React\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE_1__) {\nreturn /******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId])\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\texports: {},\n/******/ \t\t\tid: moduleId,\n/******/ \t\t\tloaded: false\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.loaded = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"\";\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(0);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _CategorizedTagInputJsx = __webpack_require__(3);\n\t\n\tvar _CategorizedTagInputJsx2 = _interopRequireDefault(_CategorizedTagInputJsx);\n\n\texports['default'] = _CategorizedTagInputJsx2['default'];\n\tmodule.exports = exports['default'];\n\n/***/ },\n/* 1 */\n/***/ function(module, exports) {\n\n\tmodule.exports = __WEBPACK_EXTERNAL_MODULE_1__;\n\n/***/ },\n/* 2 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _react = __webpack_require__(1);\n\t\n\tvar _react2 = _interopRequireDefault(_react);\n\t\n\tvar PropTypes = _react2['default'].PropTypes;\n\t\n\tvar Tag = _react2['default'].createClass({\n\t displayName: 'Tag',\n\t\n\t propTypes: {\n\t selected: PropTypes.bool,\n\t input: PropTypes.string.isRequired,\n\t text: PropTypes.string.isRequired,\n\t addable: PropTypes.bool,\n\t deletable: PropTypes.bool,\n\t onAdd: PropTypes.func,\n\t onDelete: PropTypes.func,\n\t style: PropTypes.object\n\t },\n\t\n\t // helps tests pass\n\t getDefaultProps: function getDefaultProps() {\n\t return {\n\t text: ''\n\t };\n\t },\n\t\n\t tagContent: function tagContent() {\n\t var content = [];\n\t var startIndex = this.props.text.trim().toLowerCase().indexOf(this.props.input.trim().toLowerCase());\n\t var endIndex = startIndex + this.props.input.length;\n\t\n\t if (startIndex > 0) {\n\t content.push(_react2['default'].createElement(\n\t 'span',\n\t { key: 1, className: 'cti__tag__content--regular' },\n\t this.props.text.substring(0, startIndex)\n\t ));\n\t }\n\t\n\t content.push(_react2['default'].createElement(\n\t 'span',\n\t { key: 2, className: 'cti__tag__content--match' },\n\t this.props.text.substring(startIndex, endIndex)\n\t ));\n\t\n\t if (endIndex < this.props.text.length) {\n\t content.push(_react2['default'].createElement(\n\t 'span',\n\t { key: 3, className: 'cti__tag__content--regular' },\n\t this.props.text.substring(endIndex)\n\t ));\n\t }\n\t\n\t return content;\n\t },\n\t\n\t onClick: function onClick(e) {\n\t e.preventDefault();\n\t if (this.props.addable) {\n\t this.props.onAdd(e);\n\t }\n\t },\n\t\n\t onDelete: function onDelete(e) {\n\t // Prevents onClick event of the whole tag from being triggered\n\t e.preventDefault();\n\t e.stopPropagation();\n\t this.props.onDelete(e);\n\t },\n\t\n\t getDeleteBtn: function getDeleteBtn() {\n\t var style = this.props.style || {};\n\t var deleteStyle = style['delete'] ? style['delete'] : {};\n\t\n\t return _react2['default'].createElement('span', { className: 'cti__tag__delete', onClick: this.onDelete,\n\t dangerouslySetInnerHTML: { __html: '×' },\n\t style: deleteStyle });\n\t },\n\t\n\t render: function render() {\n\t var deleteBtn = null;\n\t if (this.props.deletable) {\n\t deleteBtn = this.getDeleteBtn();\n\t }\n\t var cls = 'cti__tag' + (this.props.selected ? ' cti-selected' : '');\n\t\n\t var style = this.props.style || {};\n\t\n\t return _react2['default'].createElement(\n\t 'div',\n\t { className: cls, onClick: this.onClick, style: style.base || {} },\n\t _react2['default'].createElement(\n\t 'div',\n\t { className: 'cti__tag__content', style: style.content || {} },\n\t this.tagContent()\n\t ),\n\t deleteBtn\n\t );\n\t }\n\t});\n\t\n\texports['default'] = Tag;\n\tmodule.exports = exports['default'];\n\n/***/ },\n/* 3 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\texports.isCategoryItemValid = isCategoryItemValid;\n\texports.isCategoryValid = isCategoryValid;\n\t\n\tfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj['default'] = obj; return newObj; } }\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _react = __webpack_require__(1);\n\t\n\tvar _react2 = _interopRequireDefault(_react);\n\t\n\tvar _InputJsx = __webpack_require__(5);\n\t\n\tvar _InputJsx2 = _interopRequireDefault(_InputJsx);\n\t\n\tvar _PanelJsx = __webpack_require__(6);\n\t\n\tvar _PanelJsx2 = _interopRequireDefault(_PanelJsx);\n\t\n\tvar _keyboard = __webpack_require__(7);\n\t\n\tvar key = _interopRequireWildcard(_keyboard);\n\t\n\tvar PropTypes = _react2['default'].PropTypes;\n\t\n\tfunction isCategoryItemValid(i) {\n\t return typeof i === 'string' && i.trim().length > 0;\n\t}\n\t\n\tfunction isCategoryValid(c) {\n\t return typeof c === 'object' && c.id && c.title && c.items && Array.isArray(c.items) && c.items.every(isCategoryItemValid) && (c.type || c.single);\n\t}\n\t\n\tvar CategorizedTagInput = _react2['default'].createClass({\n\t displayName: 'CategorizedTagInput',\n\t\n\t propTypes: {\n\t addNew: PropTypes.bool,\n\t categories: PropTypes.arrayOf(PropTypes.object).isRequired,\n\t transformTag: PropTypes.func,\n\t value: PropTypes.arrayOf(PropTypes.object),\n\t onBlur: PropTypes.func,\n\t onChange: PropTypes.func,\n\t placeholder: PropTypes.string,\n\t getTagStyle: PropTypes.func,\n\t getCreateNewText: PropTypes.func\n\t },\n\t\n\t getInitialState: function getInitialState() {\n\t return {\n\t value: '',\n\t selection: {\n\t item: 0,\n\t category: 0\n\t },\n\t panelOpened: false,\n\t categories: [],\n\t addNew: this.props.addNew === undefined ? true : this.props.addNew\n\t };\n\t },\n\t\n\t getDefaultProps: function getDefaultProps() {\n\t return {\n\t onChange: function onChange(newTags) {\n\t // do nothing\n\t }\n\t };\n\t },\n\t\n\t componentWillMount: function componentWillMount() {\n\t if (!this.props.categories.every(isCategoryValid)) {\n\t throw new Error('invalid categories source provided for react-categorized-tag-input');\n\t }\n\t },\n\t\n\t componentWillUnmount: function componentWillUnmount() {\n\t if (this.timeout) {\n\t clearTimeout(this.timeout);\n\t }\n\t },\n\t\n\t filterCategories: function filterCategories(input) {\n\t var _this = this;\n\t\n\t var categories = this.props.categories.map(function (c) {\n\t c = Object.assign({}, c, {\n\t items: c.items.filter(_this.filterItems(input))\n\t });\n\t return c.items.length === 0 && (!_this.state.addNew || c.single) ? null : c;\n\t }).filter(function (c) {\n\t return c !== null;\n\t });\n\t\n\t var selection = this.state.selection;\n\t if (this.state.selection.category >= categories.length) {\n\t selection = {\n\t category: 0,\n\t item: 0\n\t };\n\t } else {\n\t if (selection.item >= categories[selection.category].items.length) {\n\t selection.item = 0;\n\t }\n\t }\n\t\n\t this.setState({\n\t categories: categories,\n\t selection: selection\n\t });\n\t },\n\t\n\t filterItems: function filterItems(input) {\n\t return function (i) {\n\t if (input.length === 1) {\n\t return i.toLowerCase().trim() === input;\n\t }\n\t return i.toLowerCase().indexOf(input.trim().toLowerCase()) >= 0;\n\t };\n\t },\n\t\n\t openPanel: function openPanel() {\n\t this.setState({ panelOpened: true });\n\t },\n\t\n\t closePanel: function closePanel() {\n\t var _this2 = this;\n\t\n\t // Prevent the panel from hiding before the click action takes place\n\t if (this.timeout) {\n\t clearTimeout(this.timeout);\n\t }\n\t this.timeout = setTimeout(function () {\n\t _this2.timeout = undefined;\n\t _this2.setState({ panelOpened: false });\n\t }, 150);\n\t },\n\t\n\t onValueChange: function onValueChange(e) {\n\t var value = e.target.value;\n\t this.setState({ value: value, panelOpened: value.trim().length > 0 || !isNaN(Number(value.trim())) });\n\t this.filterCategories(value);\n\t },\n\t\n\t onTagDeleted: function onTagDeleted(i) {\n\t var newTags = this.props.value.slice();\n\t newTags.splice(i, 1);\n\t this.props.onChange(newTags);\n\t },\n\t\n\t onAdd: function onAdd(newTag) {\n\t var newTags = this.props.value.concat([newTag]);\n\t this.setState({\n\t value: '',\n\t panelOpened: true\n\t });\n\t\n\t this.refs.input.focusInput();\n\t this.props.onChange(newTags);\n\t },\n\t\n\t addSelectedTag: function addSelectedTag() {\n\t if (!(this.state.panelOpened && this.state.value.length > 0)) {\n\t return;\n\t }\n\t\n\t var category = this.state.categories[this.state.selection.category];\n\t var title = category.items[this.state.selection.item];\n\t this.onAdd({\n\t category: category.id,\n\t title: title || this.state.value\n\t });\n\t },\n\t\n\t handleBackspace: function handleBackspace(e) {\n\t if (this.state.value.trim().length === 0) {\n\t e.preventDefault();\n\t this.onTagDeleted(this.props.value.length - 1);\n\t }\n\t },\n\t\n\t handleArrowLeft: function handleArrowLeft() {\n\t var result = this.state.selection.item - 1;\n\t this.setState({ selection: {\n\t category: this.state.selection.category,\n\t item: result >= 0 ? result : 0\n\t } });\n\t },\n\t\n\t handleArrowUp: function handleArrowUp() {\n\t var result = this.state.selection.category - 1;\n\t this.setState({ selection: {\n\t category: result >= 0 ? result : 0,\n\t item: 0\n\t } });\n\t },\n\t\n\t handleArrowRight: function handleArrowRight() {\n\t var result = this.state.selection.item + 1;\n\t var cat = this.state.categories[this.state.selection.category];\n\t this.setState({ selection: {\n\t category: this.state.selection.category,\n\t item: result <= cat.items.length ? result : cat.items.length\n\t } });\n\t },\n\t\n\t handleArrowDown: function handleArrowDown() {\n\t var result = this.state.selection.category + 1;\n\t var cats = this.state.categories;\n\t this.setState({ selection: {\n\t category: result < cats.length ? result : cats.length - 1,\n\t item: 0\n\t } });\n\t },\n\t\n\t onKeyDown: function onKeyDown(e) {\n\t var result = undefined;\n\t switch (e.keyCode) {\n\t case key.TAB:\n\t case key.ENTER:\n\t if (!this.state.value) {\n\t // enable normal tab/enter behavior\n\t // (don't preventDefault)\n\t break;\n\t }\n\t case key.COMMA:\n\t e.preventDefault();\n\t this.addSelectedTag();\n\t break;\n\t case key.BACKSPACE:\n\t this.handleBackspace(e);\n\t break;\n\t case key.LEFT:\n\t this.handleArrowLeft();\n\t break;\n\t case key.UP:\n\t this.handleArrowUp();\n\t break;\n\t case key.RIGHT:\n\t this.handleArrowRight();\n\t break;\n\t case key.DOWN:\n\t this.handleArrowDown();\n\t break;\n\t }\n\t },\n\t\n\t render: function render() {\n\t return _react2['default'].createElement(\n\t 'div',\n\t { className: 'cti__root' },\n\t _react2['default'].createElement(_InputJsx2['default'], { openPanel: this.openPanel, closePanel: this.closePanel,\n\t onValueChange: this.onValueChange, onTagDeleted: this.onTagDeleted,\n\t onKeyDown: this.onKeyDown, placeholder: this.props.placeholder, value: this.state.value,\n\t getTagStyle: this.props.getTagStyle,\n\t tags: this.props.value,\n\t transformTag: this.props.transformTag,\n\t onBlur: this.props.onBlur, ref: 'input' }),\n\t this.state.panelOpened && this.state.value.length > 0 ? _react2['default'].createElement(_PanelJsx2['default'], { categories: this.state.categories,\n\t selection: this.state.selection, onAdd: this.onAdd,\n\t input: this.state.value,\n\t getCreateNewText: this.props.getCreateNewText,\n\t addNew: this.props.addNew === undefined ? true : this.props.addNew }) : ''\n\t );\n\t }\n\t});\n\t\n\texports['default'] = CategorizedTagInput;\n\n/***/ },\n/* 4 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _react = __webpack_require__(1);\n\t\n\tvar _react2 = _interopRequireDefault(_react);\n\t\n\tvar _TagJsx = __webpack_require__(2);\n\t\n\tvar _TagJsx2 = _interopRequireDefault(_TagJsx);\n\t\n\tvar PropTypes = _react2['default'].PropTypes;\n\t\n\tvar getCreateNewText = function getCreateNewText(title, text) {\n\t return 'Create new ' + title + ' \"' + text + '\"';\n\t};\n\t\n\tvar Category = _react2['default'].createClass({\n\t displayName: 'Category',\n\t\n\t propTypes: {\n\t items: PropTypes.array.isRequired,\n\t category: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,\n\t title: PropTypes.string.isRequired,\n\t selected: PropTypes.bool.isRequired,\n\t selectedItem: PropTypes.number.isRequired,\n\t input: PropTypes.string.isRequired,\n\t addNew: PropTypes.bool,\n\t type: PropTypes.string,\n\t onAdd: PropTypes.func.isRequired,\n\t single: PropTypes.bool,\n\t getTagStyle: PropTypes.func,\n\t getCreateNewText: PropTypes.func\n\t },\n\t\n\t onAdd: function onAdd(title) {\n\t var _this = this;\n\t\n\t return function () {\n\t _this.props.onAdd({\n\t category: _this.props.category,\n\t title: title\n\t });\n\t };\n\t },\n\t\n\t onCreateNew: function onCreateNew(e) {\n\t e.preventDefault();\n\t this.onAdd(this.props.input)();\n\t },\n\t\n\t getTagStyle: function getTagStyle(item) {\n\t return this.props.getTagStyle ? this.props.getTagStyle(item) : {};\n\t },\n\t\n\t itemToTag: function itemToTag(item, i) {\n\t return _react2['default'].createElement(_TagJsx2['default'], { selected: this.isSelected(i),\n\t input: this.props.input, text: item, addable: true, deletable: false,\n\t onAdd: this.onAdd(item), key: item + '_' + i, style: this.getTagStyle(item) });\n\t },\n\t\n\t fullMatchInItems: function fullMatchInItems() {\n\t for (var i = 0, len = this.props.items.length; i < len; i++) {\n\t if (this.props.items[i] === this.props.input) {\n\t return true;\n\t }\n\t }\n\t return false;\n\t },\n\t\n\t getItems: function getItems() {\n\t return {\n\t items: this.props.items.map(this.itemToTag),\n\t fullMatch: this.fullMatchInItems()\n\t };\n\t },\n\t\n\t isSelected: function isSelected(i) {\n\t return this.props.selected && (i === this.props.selectedItem || this.props.single);\n\t },\n\t\n\t getAddBtn: function getAddBtn(fullMatch, selected) {\n\t var title = this.props.type || this.props.title;\n\t var text = this.props.input;\n\t var getText = this.props.getCreateNewText || getCreateNewText;\n\t if (this.props.addNew && !fullMatch && !this.props.single) {\n\t return [this.props.items.length > 0 ? _react2['default'].createElement(\n\t 'span',\n\t { key: 'cat_or', className: 'cti__category__or' },\n\t 'or'\n\t ) : null, _react2['default'].createElement(\n\t 'button',\n\t {\n\t key: 'add_btn',\n\t className: 'cti__category__add-item' + (selected ? ' cti-selected' : ''),\n\t onClick: this.onCreateNew\n\t },\n\t getText(title, text)\n\t )];\n\t }\n\t\n\t return null;\n\t },\n\t\n\t render: function render() {\n\t var _getItems = this.getItems();\n\t\n\t var items = _getItems.items;\n\t var fullMatch = _getItems.fullMatch;\n\t\n\t var addBtn = this.getAddBtn(fullMatch, (items.length === 0 || this.props.selectedItem >= items.length) && this.props.selected);\n\t\n\t return _react2['default'].createElement(\n\t 'div',\n\t { className: 'cti__category' },\n\t _react2['default'].createElement(\n\t 'h5',\n\t { className: 'cti__category__title' },\n\t this.props.title\n\t ),\n\t _react2['default'].createElement(\n\t 'div',\n\t { className: 'cti__category__tags' },\n\t items,\n\t addBtn\n\t )\n\t );\n\t }\n\t});\n\t\n\texports['default'] = Category;\n\tmodule.exports = exports['default'];\n\n/***/ },\n/* 5 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _react = __webpack_require__(1);\n\t\n\tvar _react2 = _interopRequireDefault(_react);\n\t\n\tvar _TagJsx = __webpack_require__(2);\n\t\n\tvar _TagJsx2 = _interopRequireDefault(_TagJsx);\n\t\n\tvar PropTypes = _react2['default'].PropTypes;\n\t\n\tvar Input = _react2['default'].createClass({\n\t displayName: 'Input',\n\t\n\t propTypes: {\n\t openPanel: PropTypes.func.isRequired,\n\t closePanel: PropTypes.func.isRequired,\n\t onValueChange: PropTypes.func.isRequired,\n\t onTagDeleted: PropTypes.func.isRequired,\n\t onKeyDown: PropTypes.func.isRequired,\n\t value: PropTypes.string.isRequired,\n\t tags: PropTypes.arrayOf(PropTypes.object).isRequired,\n\t placeholder: PropTypes.string,\n\t onBlur: PropTypes.func,\n\t getTagStyle: PropTypes.func,\n\t transformTag: PropTypes.func\n\t },\n\t\n\t focusInput: function focusInput() {\n\t this.refs.input.focus();\n\t },\n\t\n\t getDefaultProps: function getDefaultProps() {\n\t return {\n\t getTagStyle: function getTagStyle(tag) {\n\t // empty style object by default\n\t return {};\n\t },\n\t transformTag: function transformTag(tag) {\n\t return tag.title;\n\t }\n\t };\n\t },\n\t\n\t getTags: function getTags() {\n\t var _this = this;\n\t\n\t return this.props.tags.map(function (tag, i) {\n\t return _react2['default'].createElement(_TagJsx2['default'], { selected: false, input: '', text: _this.props.transformTag(tag), addable: false,\n\t deletable: true, key: tag.title + '_' + i,\n\t onDelete: function () {\n\t return _this.props.onTagDeleted(i);\n\t },\n\t style: _this.props.getTagStyle(tag) });\n\t });\n\t },\n\t\n\t onBlur: function onBlur(e) {\n\t this.props.closePanel();\n\t if (typeof this.props.onBlur === 'function') {\n\t this.props.onBlur(e);\n\t }\n\t },\n\t\n\t render: function render() {\n\t var placeholder = this.props.placeholder || '';\n\t var size = this.props.value.length === 0 ? placeholder.length : this.props.value.length;\n\t return _react2['default'].createElement(\n\t 'div',\n\t { className: 'cti__input', onClick: this.focusInput },\n\t this.getTags(),\n\t _react2['default'].createElement('input', { type: 'text', ref: 'input', value: this.props.value,\n\t size: size + 2,\n\t onFocus: this.props.openPanel, onBlur: this.onBlur,\n\t onChange: this.props.onValueChange, onKeyDown: this.props.onKeyDown,\n\t placeholder: placeholder, 'aria-label': placeholder,\n\t className: 'cti__input__input' }),\n\t _react2['default'].createElement('div', { className: 'cti__input__arrow' })\n\t );\n\t }\n\t});\n\t\n\texports['default'] = Input;\n\tmodule.exports = exports['default'];\n\n/***/ },\n/* 6 */\n/***/ function(module, exports, __webpack_require__) {\n\n\t'use strict';\n\t\n\tObject.defineProperty(exports, '__esModule', {\n\t value: true\n\t});\n\t\n\tfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\t\n\tvar _react = __webpack_require__(1);\n\t\n\tvar _react2 = _interopRequireDefault(_react);\n\t\n\tvar _CategoryJsx = __webpack_require__(4);\n\t\n\tvar _CategoryJsx2 = _interopRequireDefault(_CategoryJsx);\n\t\n\tvar PropTypes = _react2['default'].PropTypes;\n\t\n\tvar Panel = _react2['default'].createClass({\n\t displayName: 'Panel',\n\t\n\t propTypes: {\n\t categories: PropTypes.arrayOf(PropTypes.object).isRequired,\n\t selection: PropTypes.object.isRequired,\n\t onAdd: PropTypes.func.isRequired,\n\t input: PropTypes.string.isRequired,\n\t addNew: PropTypes.bool,\n\t getTagStyle: PropTypes.func,\n\t getCreateNewText: PropTypes.func\n\t },\n\t\n\t getCategories: function getCategories() {\n\t var _this = this;\n\t\n\t return this.props.categories.map(function (c, i) {\n\t return _react2['default'].createElement(_CategoryJsx2['default'], { key: c.id, items: c.items, category: c.id, title: c.title,\n\t selected: _this.props.selection.category === i,\n\t selectedItem: _this.props.selection.item,\n\t input: _this.props.input, addNew: _this.props.addNew,\n\t type: c.type, onAdd: _this.props.onAdd, single: c.single,\n\t getTagStyle: _this.props.getTagStyle,\n\t getCreateNewText: _this.props.getCreateNewText });\n\t });\n\t },\n\t\n\t render: function render() {\n\t return _react2['default'].createElement(\n\t 'div',\n\t { className: 'cti__panel' },\n\t this.getCategories()\n\t );\n\t }\n\t});\n\t\n\texports['default'] = Panel;\n\tmodule.exports = exports['default'];\n\n/***/ },\n/* 7 */\n/***/ function(module, exports) {\n\n\t\"use strict\";\n\t\n\tObject.defineProperty(exports, \"__esModule\", {\n\t value: true\n\t});\n\tvar TAB = 9;\n\texports.TAB = TAB;\n\tvar ENTER = 13;\n\texports.ENTER = ENTER;\n\tvar BACKSPACE = 8;\n\texports.BACKSPACE = BACKSPACE;\n\tvar LEFT = 37;\n\texports.LEFT = LEFT;\n\tvar UP = 38;\n\texports.UP = UP;\n\tvar RIGHT = 39;\n\texports.RIGHT = RIGHT;\n\tvar DOWN = 40;\n\texports.DOWN = DOWN;\n\tvar COMMA = 188;\n\texports.COMMA = COMMA;\n\n/***/ }\n/******/ ])\n});\n;\n\n\n/** WEBPACK FOOTER **\n ** categorized-tag-input.js\n **/"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap affe410ff901bb1c965a\n **/","import CategorizedTagInput from './CategorizedTagInput.jsx';\nexport default CategorizedTagInput;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/index.js\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_1__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external {\"root\":\"React\",\"commonjs2\":\"react\",\"commonjs\":\"react\",\"amd\":\"react\"}\n ** module id = 1\n ** module chunks = 0\n **/","import React from 'react';\n\nconst { PropTypes } = React;\n\nconst Tag = React.createClass({\n propTypes: {\n selected: PropTypes.bool,\n input: PropTypes.string.isRequired,\n text: PropTypes.string.isRequired,\n addable: PropTypes.bool,\n deletable: PropTypes.bool,\n onAdd: PropTypes.func,\n onDelete: PropTypes.func,\n style: PropTypes.object\n },\n\n // helps tests pass\n getDefaultProps() {\n return {\n text: '' \n };\n },\n\n tagContent() {\n let content = [];\n let startIndex = this.props.text.trim().toLowerCase()\n .indexOf(this.props.input.trim().toLowerCase());\n let endIndex = startIndex + this.props.input.length;\n\n if (startIndex > 0) {\n content.push(\n {this.props.text.substring(0, startIndex)}\n );\n }\n\n content.push(\n {this.props.text.substring(startIndex, endIndex)}\n );\n\n if (endIndex < this.props.text.length) {\n content.push(\n {this.props.text.substring(endIndex)}\n );\n }\n\n return content;\n },\n\n onClick(e) {\n e.preventDefault();\n if (this.props.addable) {\n this.props.onAdd(e);\n }\n },\n\n onDelete(e) {\n // Prevents onClick event of the whole tag from being triggered\n e.preventDefault();\n e.stopPropagation();\n this.props.onDelete(e);\n },\n\n getDeleteBtn() {\n const style = this.props.style || {}\n const deleteStyle = style.delete ? style.delete : {}\n\n return (\n \n );\n },\n\n render() {\n let deleteBtn = null;\n if (this.props.deletable) {\n deleteBtn = this.getDeleteBtn();\n }\n let cls = 'cti__tag' + (this.props.selected ? ' cti-selected' : '');\n\n const style = this.props.style || {}\n\n return (\n
\n
\n {this.tagContent()}\n
\n {deleteBtn}\n
\n );\n }\n});\n\nexport default Tag;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Tag.jsx\n **/","import React from 'react';\n\nimport Input from './Input.jsx';\nimport Panel from './Panel.jsx';\nimport * as key from './keyboard';\n\nconst { PropTypes } = React;\n\nexport function isCategoryItemValid(i) {\n return typeof i === 'string' && i.trim().length > 0;\n}\n\nexport function isCategoryValid(c) {\n return typeof c === 'object'\n && c.id\n && c.title\n && c.items\n && Array.isArray(c.items)\n && c.items.every(isCategoryItemValid)\n && (c.type || c.single);\n}\n\nconst CategorizedTagInput = React.createClass({\n propTypes: {\n addNew: PropTypes.bool,\n categories: PropTypes.arrayOf(PropTypes.object).isRequired,\n transformTag: PropTypes.func,\n value: PropTypes.arrayOf(PropTypes.object),\n onBlur: PropTypes.func,\n onChange: PropTypes.func,\n placeholder: PropTypes.string,\n getTagStyle: PropTypes.func,\n getCreateNewText: PropTypes.func\n },\n\n getInitialState() {\n return {\n value: '',\n selection: {\n item: 0,\n category: 0\n },\n panelOpened: false,\n categories: [],\n addNew: this.props.addNew === undefined ? true : this.props.addNew\n };\n },\n\n getDefaultProps() {\n return {\n onChange(newTags){\n // do nothing\n }\n };\n },\n\n componentWillMount() {\n if (!this.props.categories.every(isCategoryValid)) {\n throw new Error('invalid categories source provided for react-categorized-tag-input');\n }\n },\n\n componentWillUnmount() {\n if (this.timeout) {\n clearTimeout(this.timeout);\n }\n },\n\n filterCategories(input) {\n let categories = this.props.categories.map(c => {\n c = Object.assign({}, c, {\n items: c.items.filter(this.filterItems(input))\n });\n return (c.items.length === 0 && (!this.state.addNew || c.single)) ? null : c;\n }).filter(c => c !== null);\n\n let selection = this.state.selection;\n if (this.state.selection.category >= categories.length) {\n selection = {\n category: 0,\n item: 0\n };\n } else {\n if (selection.item >= categories[selection.category].items.length) {\n selection.item = 0;\n }\n }\n\n this.setState({\n categories,\n selection\n });\n },\n\n filterItems(input) {\n return function (i) {\n if (input.length === 1) {\n return i.toLowerCase().trim() === input;\n }\n return i.toLowerCase().indexOf(input.trim().toLowerCase()) >= 0;\n };\n },\n\n openPanel() {\n this.setState({ panelOpened: true });\n },\n\n closePanel() {\n // Prevent the panel from hiding before the click action takes place\n if (this.timeout) {\n clearTimeout(this.timeout);\n }\n this.timeout = setTimeout(() => {\n this.timeout = undefined;\n this.setState({ panelOpened: false });\n }, 150);\n },\n\n onValueChange(e) {\n let value = e.target.value;\n this.setState({ value, panelOpened: value.trim().length > 0 || !isNaN(Number(value.trim())) });\n this.filterCategories(value);\n },\n\n onTagDeleted(i) {\n const newTags = this.props.value.slice()\n newTags.splice(i, 1)\n this.props.onChange(newTags)\n },\n\n onAdd(newTag) { \n const newTags = this.props.value.concat([newTag]);\n this.setState({\n value: '',\n panelOpened: true\n });\n\n this.refs.input.focusInput();\n this.props.onChange(newTags);\n },\n\n addSelectedTag() {\n if (!(this.state.panelOpened && this.state.value.length > 0)) {\n return;\n }\n\n const category = this.state.categories[this.state.selection.category];\n const title = category.items[this.state.selection.item];\n this.onAdd({\n category: category.id,\n title: title || this.state.value\n });\n },\n\n handleBackspace(e) {\n if (this.state.value.trim().length === 0) {\n e.preventDefault();\n this.onTagDeleted(this.props.value.length - 1);\n }\n },\n\n handleArrowLeft() {\n let result = this.state.selection.item - 1;\n this.setState({selection: {\n category: this.state.selection.category,\n item: result >= 0 ? result : 0\n }});\n },\n\n handleArrowUp() {\n let result = this.state.selection.category - 1;\n this.setState({selection: {\n category: result >= 0 ? result : 0,\n item: 0\n }});\n },\n\n handleArrowRight() {\n let result = this.state.selection.item + 1;\n let cat = this.state.categories[this.state.selection.category];\n this.setState({selection: {\n category: this.state.selection.category,\n item: result <= cat.items.length ? result : cat.items.length\n }});\n },\n\n handleArrowDown() {\n let result = this.state.selection.category + 1;\n let cats = this.state.categories;\n this.setState({selection: {\n category: result < cats.length ? result : cats.length - 1,\n item: 0\n }});\n },\n\n onKeyDown(e) {\n let result;\n switch (e.keyCode) {\n case key.TAB:\n case key.ENTER:\n if (!this.state.value){\n // enable normal tab/enter behavior\n // (don't preventDefault)\n break;\n }\n case key.COMMA:\n e.preventDefault();\n this.addSelectedTag();\n break;\n case key.BACKSPACE:\n this.handleBackspace(e);\n break;\n case key.LEFT:\n this.handleArrowLeft();\n break;\n case key.UP:\n this.handleArrowUp();\n break;\n case key.RIGHT:\n this.handleArrowRight();\n break;\n case key.DOWN:\n this.handleArrowDown();\n break;\n }\n },\n\n render() {\n return (\n
\n \n {this.state.panelOpened && this.state.value.length > 0 ? : ''}\n
\n );\n }\n});\n\nexport default CategorizedTagInput;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/CategorizedTagInput.jsx\n **/","import React from 'react';\n\nimport Tag from './Tag.jsx';\n\nconst { PropTypes } = React;\n\nconst getCreateNewText = (title, text) => `Create new ${title} \"${text}\"`\n\nconst Category = React.createClass({\n propTypes: {\n items: PropTypes.array.isRequired,\n category: PropTypes.oneOfType([\n PropTypes.string,\n PropTypes.number\n ]).isRequired,\n title: PropTypes.string.isRequired,\n selected: PropTypes.bool.isRequired,\n selectedItem: PropTypes.number.isRequired,\n input: PropTypes.string.isRequired,\n addNew: PropTypes.bool,\n type: PropTypes.string,\n onAdd: PropTypes.func.isRequired,\n single: PropTypes.bool,\n getTagStyle: PropTypes.func,\n getCreateNewText: PropTypes.func\n },\n\n onAdd(title) {\n return () => {\n this.props.onAdd({\n category: this.props.category,\n title: title\n });\n };\n },\n\n onCreateNew(e) {\n e.preventDefault();\n this.onAdd(this.props.input)();\n },\n\n getTagStyle(item) {\n return this.props.getTagStyle ? this.props.getTagStyle(item) : {}\n },\n\n itemToTag(item, i) {\n return (\n \n );\n },\n\n fullMatchInItems() {\n for (let i = 0, len = this.props.items.length; i < len; i++) {\n if (this.props.items[i] === this.props.input) {\n return true;\n }\n }\n return false;\n },\n\n getItems() {\n return {\n items: this.props.items.map(this.itemToTag),\n fullMatch: this.fullMatchInItems(),\n };\n },\n\n isSelected(i) {\n return this.props.selected &&\n (i === this.props.selectedItem || this.props.single);\n },\n\n getAddBtn(fullMatch, selected) {\n const title = this.props.type || this.props.title;\n const text = this.props.input;\n const getText = this.props.getCreateNewText || getCreateNewText;\n if (this.props.addNew && !fullMatch && !this.props.single) {\n return [\n this.props.items.length > 0 ?\n or :\n null,\n {getText(title, text)}\n ];\n }\n\n return null;\n },\n\n render() {\n let { items, fullMatch } = this.getItems();\n let addBtn = this.getAddBtn(\n fullMatch,\n (items.length === 0 || this.props.selectedItem >= items.length) &&\n this.props.selected\n );\n\n return (\n
\n
{this.props.title}
\n
\n {items}\n {addBtn}\n
\n
\n );\n }\n});\n\nexport default Category;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Category.jsx\n **/","import React from 'react';\n\nimport Tag from './Tag.jsx';\n\nconst { PropTypes } = React;\n\nconst Input = React.createClass({\n propTypes: {\n openPanel: PropTypes.func.isRequired,\n closePanel: PropTypes.func.isRequired,\n onValueChange: PropTypes.func.isRequired,\n onTagDeleted: PropTypes.func.isRequired,\n onKeyDown: PropTypes.func.isRequired,\n value: PropTypes.string.isRequired,\n tags: PropTypes.arrayOf(PropTypes.object).isRequired,\n placeholder: PropTypes.string,\n onBlur: PropTypes.func,\n getTagStyle: PropTypes.func,\n transformTag: PropTypes.func\n },\n\n focusInput() {\n this.refs.input.focus();\n },\n\n getDefaultProps() {\n return {\n getTagStyle(tag) {\n // empty style object by default\n return {};\n },\n transformTag(tag){\n return tag.title;\n }\n };\n },\n\n getTags() {\n\n return this.props.tags.map((tag, i) => {\n return (\n this.props.onTagDeleted(i)}\n style={this.props.getTagStyle(tag)}/>\n );\n });\n },\n\n onBlur(e) {\n this.props.closePanel();\n if (typeof this.props.onBlur === 'function') {\n this.props.onBlur(e);\n }\n },\n\n render() {\n const placeholder = this.props.placeholder || '';\n let size = this.props.value.length === 0 ?\n placeholder.length :\n this.props.value.length;\n return (\n
\n {this.getTags()}\n \n
\n
\n );\n }\n});\n\nexport default Input;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Input.jsx\n **/","import React from 'react';\n\nimport Category from './Category.jsx';\n\nconst { PropTypes } = React;\n\nconst Panel = React.createClass({\n propTypes: {\n categories: PropTypes.arrayOf(PropTypes.object).isRequired,\n selection: PropTypes.object.isRequired,\n onAdd: PropTypes.func.isRequired,\n input: PropTypes.string.isRequired,\n addNew: PropTypes.bool,\n getTagStyle: PropTypes.func,\n getCreateNewText: PropTypes.func\n },\n\n getCategories() {\n return this.props.categories.map((c, i) => {\n return (\n \n );\n });\n },\n\n render() {\n return (\n
\n {this.getCategories()}\n
\n );\n }\n});\n\nexport default Panel;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Panel.jsx\n **/","export var TAB = 9;\nexport var ENTER = 13;\nexport var BACKSPACE = 8;\nexport var LEFT = 37;\nexport var UP = 38;\nexport var RIGHT = 39;\nexport var DOWN = 40;\nexport var COMMA = 188;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/keyboard.js\n **/"],"sourceRoot":""} -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manual test 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Input from './src/index'; 5 | 6 | const categories = [ 7 | { 8 | id: 'animals', 9 | title: 'Animals', 10 | type: 'animal', 11 | items: ['Dog', 'Cat', 'Bird', 'Dolphin', 'Apes'] 12 | }, 13 | { 14 | id: 'something', 15 | title: 'Something cool', 16 | items: ['Something cool'], 17 | single: true 18 | }, 19 | { 20 | id: 'food', 21 | title: 'food', 22 | type: 'food', 23 | items: ['Apple', 'Banana', 'Grapes', 'Pear'] 24 | }, 25 | { 26 | id: 'professions', 27 | title: 'Professions', 28 | type: 'profession', 29 | items: ['Waiter', 'Writer', 'Hairdresser', 'Policeman'] 30 | } 31 | ]; 32 | 33 | function transformTag(tag) { 34 | const categoryMatches = categories.filter(category => category.id === tag.category); 35 | const categoryTitle = categoryMatches[0].title; 36 | return `${categoryTitle}/${tag.title}`; 37 | } 38 | 39 | 40 | function getTagStyle(tag){ 41 | if (tag.title === "rhino") { 42 | return { 43 | base: { 44 | backgroundColor: "gray", 45 | color: "lightgray" 46 | } 47 | } 48 | return {} 49 | } 50 | } 51 | 52 | function getCreateNewText(title, text){ 53 | return `create new ${title} "${text}"` 54 | } 55 | 56 | const Wrap = React.createClass({ 57 | getInitialState() { 58 | return { 59 | editable: true, 60 | tags: [{ 61 | title: "rhino", 62 | category: 'animals' 63 | }] 64 | }; 65 | }, 66 | 67 | toggleEdit(e) { 68 | e.preventDefault(); 69 | e.stopPropagation(); 70 | this.setState({ editable: !this.state.editable }); 71 | }, 72 | 73 | render() { 74 | return ( 75 |
76 | 77 | {this.state.editable 78 | ? { 85 | console.log('Changed', tags); 86 | this.setState({tags}); 87 | }} 88 | onBlur={() => { 89 | console.log('Blur'); 90 | }} 91 | transformTag={transformTag} 92 | getCreateNewText={getCreateNewText} 93 | /> 94 | : Not editable} 95 |
96 | ); 97 | } 98 | }); 99 | 100 | ReactDOM.render( 101 | React.createElement(Wrap, {}), 102 | document.getElementById('app') 103 | ); 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-categorized-tag-input", 3 | "version": "2.1.2", 4 | "description": "React.js component for making tag autocompletion inputs with categorized results.", 5 | "main": "categorized-tag-input.js", 6 | "scripts": { 7 | "test": "mocha --compilers js:babel/register --recursive", 8 | "build": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.config.js", 9 | "runtest": "node server.js", 10 | "prepublish": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mvader/react-categorized-tag-input.git" 15 | }, 16 | "keywords": [ 17 | "tag", 18 | "autocomplete", 19 | "input", 20 | "categorized", 21 | "react" 22 | ], 23 | "author": "Miguel Molina ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/mvader/react-categorized-tag-input/issues" 27 | }, 28 | "homepage": "https://github.com/mvader/react-categorized-tag-input#readme", 29 | "devDependencies": { 30 | "babel": "^5.8.23", 31 | "babel-core": "^5.8.25", 32 | "babel-loader": "^5.3.2", 33 | "expect": "^1.12.0", 34 | "jsdom": "<4.0.0", 35 | "mocha": "^2.3.3", 36 | "mocha-jsdom": "^1.0.0", 37 | "react": "^15.1.0", 38 | "react-dom": "^15.1.0", 39 | "react-hot-loader": "^1.3.0", 40 | "webpack": "^1.12.2", 41 | "webpack-dev-server": "^1.12.0" 42 | }, 43 | "dependencies": { 44 | "exenv": "^1.2.0", 45 | "react-addons-test-utils": "^15.1.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var WebpackDevServer = require('webpack-dev-server'); 3 | var config = require('./webpack.test.config'); 4 | 5 | new WebpackDevServer(webpack(config), { 6 | publicPath: config.output.publicPath, 7 | hot: true, 8 | historyApiFallback: true 9 | }).listen(5000, 'localhost', function (err) { 10 | if (err) { 11 | console.log(err); 12 | } 13 | console.log('Listening at localhost:5000'); 14 | }); 15 | -------------------------------------------------------------------------------- /src/CategorizedTagInput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Input from './Input.jsx'; 4 | import Panel from './Panel.jsx'; 5 | import * as key from './keyboard'; 6 | 7 | const { PropTypes } = React; 8 | 9 | export function isCategoryItemValid(i) { 10 | return typeof i === 'string' && i.trim().length > 0; 11 | } 12 | 13 | export function isCategoryValid(c) { 14 | return typeof c === 'object' 15 | && c.id 16 | && c.title 17 | && c.items 18 | && Array.isArray(c.items) 19 | && c.items.every(isCategoryItemValid) 20 | && (c.type || c.single); 21 | } 22 | 23 | const CategorizedTagInput = React.createClass({ 24 | propTypes: { 25 | addNew: PropTypes.bool, 26 | categories: PropTypes.arrayOf(PropTypes.object).isRequired, 27 | transformTag: PropTypes.func, 28 | value: PropTypes.arrayOf(PropTypes.object), 29 | onBlur: PropTypes.func, 30 | onChange: PropTypes.func, 31 | placeholder: PropTypes.string, 32 | getTagStyle: PropTypes.func, 33 | getCreateNewText: PropTypes.func 34 | }, 35 | 36 | getInitialState() { 37 | return { 38 | value: '', 39 | selection: { 40 | item: 0, 41 | category: 0 42 | }, 43 | panelOpened: false, 44 | categories: [], 45 | addNew: this.props.addNew === undefined ? true : this.props.addNew 46 | }; 47 | }, 48 | 49 | getDefaultProps() { 50 | return { 51 | onChange(newTags){ 52 | // do nothing 53 | } 54 | }; 55 | }, 56 | 57 | componentWillMount() { 58 | if (!this.props.categories.every(isCategoryValid)) { 59 | throw new Error('invalid categories source provided for react-categorized-tag-input'); 60 | } 61 | }, 62 | 63 | componentWillUnmount() { 64 | if (this.timeout) { 65 | clearTimeout(this.timeout); 66 | } 67 | }, 68 | 69 | filterCategories(input) { 70 | let categories = this.props.categories.map(c => { 71 | c = Object.assign({}, c, { 72 | items: c.items.filter(this.filterItems(input)) 73 | }); 74 | return (c.items.length === 0 && (!this.state.addNew || c.single)) ? null : c; 75 | }).filter(c => c !== null); 76 | 77 | let selection = this.state.selection; 78 | if (this.state.selection.category >= categories.length) { 79 | selection = { 80 | category: 0, 81 | item: 0 82 | }; 83 | } else { 84 | if (selection.item >= categories[selection.category].items.length) { 85 | selection.item = 0; 86 | } 87 | } 88 | 89 | this.setState({ 90 | categories, 91 | selection 92 | }); 93 | }, 94 | 95 | filterItems(input) { 96 | return function (i) { 97 | if (input.length === 1) { 98 | return i.toLowerCase().trim() === input; 99 | } 100 | return i.toLowerCase().indexOf(input.trim().toLowerCase()) >= 0; 101 | }; 102 | }, 103 | 104 | openPanel() { 105 | this.setState({ panelOpened: true }); 106 | }, 107 | 108 | closePanel() { 109 | // Prevent the panel from hiding before the click action takes place 110 | if (this.timeout) { 111 | clearTimeout(this.timeout); 112 | } 113 | this.timeout = setTimeout(() => { 114 | this.timeout = undefined; 115 | this.setState({ panelOpened: false }); 116 | }, 150); 117 | }, 118 | 119 | onValueChange(e) { 120 | let value = e.target.value; 121 | this.setState({ value, panelOpened: value.trim().length > 0 || !isNaN(Number(value.trim())) }); 122 | this.filterCategories(value); 123 | }, 124 | 125 | onTagDeleted(i) { 126 | const newTags = this.props.value.slice() 127 | newTags.splice(i, 1) 128 | this.props.onChange(newTags) 129 | }, 130 | 131 | onAdd(newTag) { 132 | const newTags = this.props.value.concat([newTag]); 133 | this.setState({ 134 | value: '', 135 | panelOpened: true 136 | }); 137 | 138 | this.refs.input.focusInput(); 139 | this.props.onChange(newTags); 140 | }, 141 | 142 | addSelectedTag() { 143 | if (!(this.state.panelOpened && this.state.value.length > 0)) { 144 | return; 145 | } 146 | 147 | const category = this.state.categories[this.state.selection.category]; 148 | const title = category.items[this.state.selection.item]; 149 | this.onAdd({ 150 | category: category.id, 151 | title: title || this.state.value 152 | }); 153 | }, 154 | 155 | handleBackspace(e) { 156 | if (this.state.value.trim().length === 0) { 157 | e.preventDefault(); 158 | this.onTagDeleted(this.props.value.length - 1); 159 | } 160 | }, 161 | 162 | handleArrowLeft() { 163 | let result = this.state.selection.item - 1; 164 | this.setState({selection: { 165 | category: this.state.selection.category, 166 | item: result >= 0 ? result : 0 167 | }}); 168 | }, 169 | 170 | handleArrowUp() { 171 | let result = this.state.selection.category - 1; 172 | this.setState({selection: { 173 | category: result >= 0 ? result : 0, 174 | item: 0 175 | }}); 176 | }, 177 | 178 | handleArrowRight() { 179 | let result = this.state.selection.item + 1; 180 | let cat = this.state.categories[this.state.selection.category]; 181 | this.setState({selection: { 182 | category: this.state.selection.category, 183 | item: result <= cat.items.length ? result : cat.items.length 184 | }}); 185 | }, 186 | 187 | handleArrowDown() { 188 | let result = this.state.selection.category + 1; 189 | let cats = this.state.categories; 190 | this.setState({selection: { 191 | category: result < cats.length ? result : cats.length - 1, 192 | item: 0 193 | }}); 194 | }, 195 | 196 | onKeyDown(e) { 197 | let result; 198 | switch (e.keyCode) { 199 | case key.TAB: 200 | case key.ENTER: 201 | if (!this.state.value){ 202 | // enable normal tab/enter behavior 203 | // (don't preventDefault) 204 | break; 205 | } 206 | case key.COMMA: 207 | e.preventDefault(); 208 | this.addSelectedTag(); 209 | break; 210 | case key.BACKSPACE: 211 | this.handleBackspace(e); 212 | break; 213 | case key.LEFT: 214 | this.handleArrowLeft(); 215 | break; 216 | case key.UP: 217 | this.handleArrowUp(); 218 | break; 219 | case key.RIGHT: 220 | this.handleArrowRight(); 221 | break; 222 | case key.DOWN: 223 | this.handleArrowDown(); 224 | break; 225 | } 226 | }, 227 | 228 | render() { 229 | return ( 230 |
231 | 238 | {this.state.panelOpened && this.state.value.length > 0 ? : ''} 243 |
244 | ); 245 | } 246 | }); 247 | 248 | export default CategorizedTagInput; 249 | -------------------------------------------------------------------------------- /src/Category.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Tag from './Tag.jsx'; 4 | 5 | const { PropTypes } = React; 6 | 7 | const getCreateNewText = (title, text) => `Create new ${title} "${text}"` 8 | 9 | const Category = React.createClass({ 10 | propTypes: { 11 | items: PropTypes.array.isRequired, 12 | category: PropTypes.oneOfType([ 13 | PropTypes.string, 14 | PropTypes.number 15 | ]).isRequired, 16 | title: PropTypes.string.isRequired, 17 | selected: PropTypes.bool.isRequired, 18 | selectedItem: PropTypes.number.isRequired, 19 | input: PropTypes.string.isRequired, 20 | addNew: PropTypes.bool, 21 | type: PropTypes.string, 22 | onAdd: PropTypes.func.isRequired, 23 | single: PropTypes.bool, 24 | getTagStyle: PropTypes.func, 25 | getCreateNewText: PropTypes.func 26 | }, 27 | 28 | onAdd(title) { 29 | return () => { 30 | this.props.onAdd({ 31 | category: this.props.category, 32 | title: title 33 | }); 34 | }; 35 | }, 36 | 37 | onCreateNew(e) { 38 | e.preventDefault(); 39 | this.onAdd(this.props.input)(); 40 | }, 41 | 42 | getTagStyle(item) { 43 | return this.props.getTagStyle ? this.props.getTagStyle(item) : {} 44 | }, 45 | 46 | itemToTag(item, i) { 47 | return ( 48 | 51 | ); 52 | }, 53 | 54 | fullMatchInItems() { 55 | for (let i = 0, len = this.props.items.length; i < len; i++) { 56 | if (this.props.items[i] === this.props.input) { 57 | return true; 58 | } 59 | } 60 | return false; 61 | }, 62 | 63 | getItems() { 64 | return { 65 | items: this.props.items.map(this.itemToTag), 66 | fullMatch: this.fullMatchInItems(), 67 | }; 68 | }, 69 | 70 | isSelected(i) { 71 | return this.props.selected && 72 | (i === this.props.selectedItem || this.props.single); 73 | }, 74 | 75 | getAddBtn(fullMatch, selected) { 76 | const title = this.props.type || this.props.title; 77 | const text = this.props.input; 78 | const getText = this.props.getCreateNewText || getCreateNewText; 79 | if (this.props.addNew && !fullMatch && !this.props.single) { 80 | return [ 81 | this.props.items.length > 0 ? 82 | or : 83 | null, 84 | 89 | ]; 90 | } 91 | 92 | return null; 93 | }, 94 | 95 | render() { 96 | let { items, fullMatch } = this.getItems(); 97 | let addBtn = this.getAddBtn( 98 | fullMatch, 99 | (items.length === 0 || this.props.selectedItem >= items.length) && 100 | this.props.selected 101 | ); 102 | 103 | return ( 104 |
105 |
{this.props.title}
106 |
107 | {items} 108 | {addBtn} 109 |
110 |
111 | ); 112 | } 113 | }); 114 | 115 | export default Category; 116 | -------------------------------------------------------------------------------- /src/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Tag from './Tag.jsx'; 4 | 5 | const { PropTypes } = React; 6 | 7 | const Input = React.createClass({ 8 | propTypes: { 9 | openPanel: PropTypes.func.isRequired, 10 | closePanel: PropTypes.func.isRequired, 11 | onValueChange: PropTypes.func.isRequired, 12 | onTagDeleted: PropTypes.func.isRequired, 13 | onKeyDown: PropTypes.func.isRequired, 14 | value: PropTypes.string.isRequired, 15 | tags: PropTypes.arrayOf(PropTypes.object).isRequired, 16 | placeholder: PropTypes.string, 17 | onBlur: PropTypes.func, 18 | getTagStyle: PropTypes.func, 19 | transformTag: PropTypes.func 20 | }, 21 | 22 | focusInput() { 23 | this.refs.input.focus(); 24 | }, 25 | 26 | getDefaultProps() { 27 | return { 28 | getTagStyle(tag) { 29 | // empty style object by default 30 | return {}; 31 | }, 32 | transformTag(tag){ 33 | return tag.title; 34 | } 35 | }; 36 | }, 37 | 38 | getTags() { 39 | 40 | return this.props.tags.map((tag, i) => { 41 | return ( 42 | this.props.onTagDeleted(i)} 45 | style={this.props.getTagStyle(tag)}/> 46 | ); 47 | }); 48 | }, 49 | 50 | onBlur(e) { 51 | this.props.closePanel(); 52 | if (typeof this.props.onBlur === 'function') { 53 | this.props.onBlur(e); 54 | } 55 | }, 56 | 57 | render() { 58 | const placeholder = this.props.placeholder || ''; 59 | let size = this.props.value.length === 0 ? 60 | placeholder.length : 61 | this.props.value.length; 62 | return ( 63 |
64 | {this.getTags()} 65 | 71 |
72 |
73 | ); 74 | } 75 | }); 76 | 77 | export default Input; 78 | -------------------------------------------------------------------------------- /src/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Category from './Category.jsx'; 4 | 5 | const { PropTypes } = React; 6 | 7 | const Panel = React.createClass({ 8 | propTypes: { 9 | categories: PropTypes.arrayOf(PropTypes.object).isRequired, 10 | selection: PropTypes.object.isRequired, 11 | onAdd: PropTypes.func.isRequired, 12 | input: PropTypes.string.isRequired, 13 | addNew: PropTypes.bool, 14 | getTagStyle: PropTypes.func, 15 | getCreateNewText: PropTypes.func 16 | }, 17 | 18 | getCategories() { 19 | return this.props.categories.map((c, i) => { 20 | return ( 21 | 28 | ); 29 | }); 30 | }, 31 | 32 | render() { 33 | return ( 34 |
35 | {this.getCategories()} 36 |
37 | ); 38 | } 39 | }); 40 | 41 | export default Panel; 42 | -------------------------------------------------------------------------------- /src/Tag.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const { PropTypes } = React; 4 | 5 | const Tag = React.createClass({ 6 | propTypes: { 7 | selected: PropTypes.bool, 8 | input: PropTypes.string.isRequired, 9 | text: PropTypes.string.isRequired, 10 | addable: PropTypes.bool, 11 | deletable: PropTypes.bool, 12 | onAdd: PropTypes.func, 13 | onDelete: PropTypes.func, 14 | style: PropTypes.object 15 | }, 16 | 17 | // helps tests pass 18 | getDefaultProps() { 19 | return { 20 | text: '' 21 | }; 22 | }, 23 | 24 | tagContent() { 25 | let content = []; 26 | let startIndex = this.props.text.trim().toLowerCase() 27 | .indexOf(this.props.input.trim().toLowerCase()); 28 | let endIndex = startIndex + this.props.input.length; 29 | 30 | if (startIndex > 0) { 31 | content.push( 32 | {this.props.text.substring(0, startIndex)} 33 | ); 34 | } 35 | 36 | content.push( 37 | {this.props.text.substring(startIndex, endIndex)} 38 | ); 39 | 40 | if (endIndex < this.props.text.length) { 41 | content.push( 42 | {this.props.text.substring(endIndex)} 43 | ); 44 | } 45 | 46 | return content; 47 | }, 48 | 49 | onClick(e) { 50 | e.preventDefault(); 51 | if (this.props.addable) { 52 | this.props.onAdd(e); 53 | } 54 | }, 55 | 56 | onDelete(e) { 57 | // Prevents onClick event of the whole tag from being triggered 58 | e.preventDefault(); 59 | e.stopPropagation(); 60 | this.props.onDelete(e); 61 | }, 62 | 63 | getDeleteBtn() { 64 | const style = this.props.style || {} 65 | const deleteStyle = style.delete ? style.delete : {} 66 | 67 | return ( 68 | 71 | ); 72 | }, 73 | 74 | render() { 75 | let deleteBtn = null; 76 | if (this.props.deletable) { 77 | deleteBtn = this.getDeleteBtn(); 78 | } 79 | let cls = 'cti__tag' + (this.props.selected ? ' cti-selected' : ''); 80 | 81 | const style = this.props.style || {} 82 | 83 | return ( 84 |
85 |
86 | {this.tagContent()} 87 |
88 | {deleteBtn} 89 |
90 | ); 91 | } 92 | }); 93 | 94 | export default Tag; 95 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import CategorizedTagInput from './CategorizedTagInput.jsx'; 2 | export default CategorizedTagInput; 3 | -------------------------------------------------------------------------------- /src/keyboard.js: -------------------------------------------------------------------------------- 1 | export var TAB = 9; 2 | export var ENTER = 13; 3 | export var BACKSPACE = 8; 4 | export var LEFT = 37; 5 | export var UP = 38; 6 | export var RIGHT = 39; 7 | export var DOWN = 40; 8 | export var COMMA = 188; 9 | -------------------------------------------------------------------------------- /test/CategorizedTagInput_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import jsdomReact from './jsdomReact'; 5 | 6 | import CategorizedTagInput from '../src/CategorizedTagInput.jsx'; 7 | 8 | 9 | describe('CategorizedTagInput', () => { 10 | jsdomReact(); 11 | }); 12 | -------------------------------------------------------------------------------- /test/Category_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import jsdomReact from './jsdomReact'; 5 | 6 | import Category from '../src/Category.jsx'; 7 | 8 | 9 | function category(props) { 10 | return TestUtils.renderIntoDocument(React.createElement(Category, props)); 11 | } 12 | 13 | function props(p = {}) { 14 | return Object.assign({}, { 15 | items: ['foo', 'foobarbaz'], 16 | input: 'fo', 17 | title: 'Things', 18 | selected: true, 19 | selectedItem: 1, 20 | addNew: true, 21 | type: 'thing', 22 | category: 1, 23 | onAdd: () => {} 24 | }, p); 25 | } 26 | 27 | function findAddBtn(c) { 28 | return TestUtils.findRenderedDOMComponentWithClass(c, 'cti__category__add-item'); 29 | } 30 | 31 | function findTags(c) { 32 | return TestUtils.scryRenderedDOMComponentsWithClass(c, 'cti__tag'); 33 | } 34 | 35 | describe('Category', () => { 36 | jsdomReact(); 37 | 38 | describe('when addNew is true', () => { 39 | it('should show the add new button', () => { 40 | let c = category(props()); 41 | let btn = findAddBtn(c); 42 | 43 | expect(btn).toNotBe(undefined); 44 | expect(btn.innerHTML).toBe('Create new thing "fo"'); 45 | }); 46 | 47 | describe('and is a full match', () => { 48 | it('there should be no add new button', () => { 49 | let c = category(props({ input: 'foo' })); 50 | expect(() => { 51 | let btn = findAddBtn(c); 52 | }).toThrow(/.*/); 53 | }); 54 | }); 55 | }); 56 | 57 | describe('when addNew is false', () => { 58 | it('there should be no add new button', () => { 59 | let c = category(props({ addNew: false })); 60 | expect(() => { 61 | let btn = findAddBtn(c); 62 | }).toThrow(/.*/); 63 | }); 64 | }); 65 | 66 | it('should only show the items that match the input', () => { 67 | let c = category(props()); 68 | let tags = findTags(c); 69 | expect(tags.length).toBe(2); 70 | expect(tags[0].textContent).toBe('foo'); 71 | expect(tags[1].textContent).toBe('foobarbaz'); 72 | 73 | c = category(props({ input: 'bar', items: ['bar', 'foobarbaz'] })); 74 | tags = findTags(c); 75 | expect(tags.length).toBe(2); 76 | expect(tags[0].textContent).toBe('bar'); 77 | expect(tags[1].textContent).toBe('foobarbaz'); 78 | 79 | c = category(props({ input: 'ksajdfhskjf', items: [] })); 80 | tags = findTags(c); 81 | expect(tags.length).toBe(0); 82 | }); 83 | 84 | describe('when there are no matching elements', () => { 85 | describe('and addNew is true', () => { 86 | it('should not show any tags, just the new button', () => { 87 | let c = category(props({ input: 'asd', items: [] })); 88 | expect(findTags(c).length).toBe(0); 89 | let btn = findAddBtn(c); 90 | expect(btn).toNotBe(undefined); 91 | expect(btn.innerHTML).toBe('Create new thing "asd"'); 92 | }); 93 | it('should generate a message using getCreateNewText if provided', () => { 94 | const getCreateNewText = (title, text) => `Hacer nuevo ${title} "${text}"` 95 | const c = category(props({ input: 'asd', items: [], getCreateNewText})); 96 | expect(findTags(c).length).toBe(0); 97 | const btn = findAddBtn(c); 98 | expect(btn).toNotBe(undefined); 99 | expect(btn.innerHTML).toBe('Hacer nuevo thing "asd"'); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('when a tag is clicked', () => { 105 | it('should trigger onAdd with category and title', done => { 106 | let c = category(props({ 107 | onAdd(o) { 108 | expect(o.category).toBe(1); 109 | expect(o.title).toBe('foo'); 110 | done(); 111 | } 112 | })); 113 | let tags = findTags(c); 114 | TestUtils.Simulate.click(tags[0]); 115 | }); 116 | }); 117 | 118 | describe('when category is selected', () => { 119 | function isSelected(elem) { 120 | return elem.className.split(' ')[1] === 'cti-selected'; 121 | } 122 | 123 | describe('and an item is selected', () => { 124 | it('should have a selected class', () => { 125 | let c = category(props()); 126 | let tags = findTags(c); 127 | expect(isSelected(tags[0])).toBe(false); 128 | expect(isSelected(tags[1])).toBe(true); 129 | }); 130 | }); 131 | 132 | describe('and the selected item is bigger than the number of elements', () => { 133 | it('should select the create button', () => { 134 | let c = category(props({ selectedItem: 2 })); 135 | let tags = findTags(c); 136 | expect(isSelected(tags[0])).toBe(false); 137 | expect(isSelected(tags[1])).toBe(false); 138 | 139 | expect(isSelected(findAddBtn(c))).toBe(true); 140 | }); 141 | }); 142 | 143 | describe('and there is no matched item', () => { 144 | it('should select the create button', () => { 145 | let c = category(props({ selectedItem: 0, items: [], input: 'asd' })); 146 | expect(isSelected(findAddBtn(c))).toBe(true); 147 | }); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/Input_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import jsdomReact from './jsdomReact'; 5 | 6 | import Input from '../src/Input.jsx'; 7 | 8 | 9 | function createInput(props) { 10 | return TestUtils.renderIntoDocument(React.createElement(Input, props)); 11 | } 12 | 13 | function props(p = {}) { 14 | return Object.assign({}, { 15 | tags: ['foo', 'bar'], 16 | value: 'baz', 17 | onKeyDown: () => {}, 18 | onTagDeleted: () => {}, 19 | onValueChange: () => {}, 20 | openPanel: () => {}, 21 | closePanel: () => {}, 22 | }, p); 23 | } 24 | 25 | function findInput(i) { 26 | return TestUtils.findRenderedDOMComponentWithClass(i, 'cti__input__input'); 27 | } 28 | 29 | function findTags(i) { 30 | return TestUtils.scryRenderedDOMComponentsWithClass(i, 'cti__tag'); 31 | } 32 | 33 | // TODO: can't test autoresize or focus because JSDom does not implement 34 | // layouting or focus/blur 35 | 36 | describe('Input', () => { 37 | jsdomReact(); 38 | 39 | it('should render the given tags and the value', () => { 40 | let i = createInput(props()); 41 | let tags = findTags(i); 42 | expect(tags.length).toBe(2); 43 | 44 | let input = findInput(i); 45 | expect(input.value).toBe(props().value) 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/Panel_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import jsdomReact from './jsdomReact'; 5 | 6 | import Panel from '../src/Panel.jsx'; 7 | 8 | 9 | function panel(props) { 10 | return TestUtils.renderIntoDocument(React.createElement(Panel, props)); 11 | } 12 | 13 | function findCategories(p) { 14 | return TestUtils.scryRenderedDOMComponentsWithClass(p, 'cti__category'); 15 | } 16 | 17 | function props(p = {}) { 18 | return Object.assign({}, { 19 | categories: [ 20 | { 21 | id: 1, 22 | items: ['rabbit'], 23 | title: 'Things', 24 | type: 'thing' 25 | }, 26 | { 27 | id: 2, 28 | items: ['rab'], 29 | title: 'Reversed things', 30 | type: 'reversed thing' 31 | } 32 | ], 33 | selection: { category: 1, item: 1 }, 34 | onAdd: () => {}, 35 | input: 'ra', 36 | addNew: true 37 | }, p); 38 | } 39 | 40 | describe('Panel', () => { 41 | jsdomReact(); 42 | 43 | it('should render categories', () => { 44 | let p = panel(props()); 45 | expect(findCategories(p).length).toBe(2); 46 | }); 47 | 48 | it('should select the corresponding item', () => { 49 | let p = panel(props()); 50 | let selected = TestUtils.scryRenderedDOMComponentsWithClass(p, 'cti-selected'); 51 | expect(selected.length).toBe(1); 52 | expect(selected[0].textContent).toBe('Create new reversed thing "ra"'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/Tag_spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import jsdomReact from './jsdomReact'; 6 | 7 | import Tag from '../src/Tag.jsx'; 8 | 9 | function tag(props) { 10 | return TestUtils.renderIntoDocument(React.createElement(Tag, props)); 11 | } 12 | 13 | function findContentSpans(t) { 14 | return TestUtils.scryRenderedDOMComponentsWithTag(t, 'span'); 15 | } 16 | 17 | function props(props) { 18 | return Object.assign({}, { 19 | input: 'foo', 20 | text: 'fooable', 21 | }, props); 22 | } 23 | 24 | describe('Tag', () => { 25 | jsdomReact(); 26 | 27 | describe('with the input at the start', () => { 28 | it('should have two spans, the first is the match', () => { 29 | let t = tag({ 30 | input: 'foo', 31 | text: 'fooable', 32 | }); 33 | 34 | let spans = findContentSpans(t); 35 | expect(spans.length).toBe(2); 36 | expect(spans[0].className).toBe('cti__tag__content--match'); 37 | expect(spans[0].innerHTML).toBe('foo'); 38 | expect(spans[1].className).toBe('cti__tag__content--regular'); 39 | expect(spans[1].innerHTML).toBe('able'); 40 | }); 41 | }); 42 | 43 | describe('with the input at the middle', () => { 44 | it('should have three spans, the second is the match', () => { 45 | let t = tag({ 46 | input: 'oab', 47 | text: 'fooable', 48 | }); 49 | 50 | let spans = findContentSpans(t); 51 | expect(spans.length).toBe(3); 52 | expect(spans[0].className).toBe('cti__tag__content--regular'); 53 | expect(spans[0].innerHTML).toBe('fo'); 54 | expect(spans[1].className).toBe('cti__tag__content--match'); 55 | expect(spans[1].innerHTML).toBe('oab'); 56 | expect(spans[2].className).toBe('cti__tag__content--regular'); 57 | expect(spans[2].innerHTML).toBe('le'); 58 | }); 59 | }); 60 | 61 | describe('with the input at the end', () => { 62 | it('should have two spans, the last is the match', () => { 63 | let t = tag({ 64 | input: 'able', 65 | text: 'fooable', 66 | }); 67 | 68 | let spans = findContentSpans(t); 69 | expect(spans.length).toBe(2); 70 | expect(spans[0].className).toBe('cti__tag__content--regular'); 71 | expect(spans[0].innerHTML).toBe('foo'); 72 | expect(spans[1].className).toBe('cti__tag__content--match'); 73 | expect(spans[1].innerHTML).toBe('able'); 74 | }); 75 | }); 76 | 77 | describe('if the tag is addable', () => { 78 | it('should trigger onAdd callback', done => { 79 | let added = false; 80 | let t = tag(props({ 81 | addable: true, 82 | onAdd: () => { 83 | added = true; 84 | } 85 | })); 86 | 87 | TestUtils.Simulate.click(ReactDOM.findDOMNode(t)); 88 | 89 | setImmediate(() => { 90 | expect(added).toBe(true); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | describe('if the tag is deletable', () => { 97 | function findDelete(t) { 98 | return TestUtils.findRenderedDOMComponentWithClass(t, 'cti__tag__delete') 99 | ; 100 | } 101 | 102 | it('should trigger onDelete callback', done => { 103 | let deleted = false; 104 | let t = tag(props({ 105 | deletable: true, 106 | onDelete: () => { 107 | deleted = true; 108 | } 109 | })); 110 | 111 | TestUtils.Simulate.click(findDelete(t)); 112 | 113 | setImmediate(() => { 114 | expect(deleted).toBe(true); 115 | done(); 116 | }); 117 | }); 118 | 119 | describe('and addable', () => { 120 | it('should trigger onDelete callback but not onAdd', done => { 121 | let added = false; 122 | let deleted = false; 123 | let t = tag(props({ 124 | addable: true, 125 | deletable: true, 126 | onAdd: () => { 127 | added = true; 128 | }, 129 | onDelete: () => { 130 | deleted = true; 131 | } 132 | })); 133 | 134 | TestUtils.Simulate.click(findDelete(t)); 135 | 136 | setImmediate(() => { 137 | expect(added).toBe(false); 138 | expect(deleted).toBe(true); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | describe('when is selected', () => { 146 | it('should have the class cti-selected', () => { 147 | let t = tag(props({ 148 | selected: true 149 | })); 150 | 151 | expect(ReactDOM.findDOMNode(t).className.split(' ')[1]).toBe('cti-selected'); 152 | }); 153 | }); 154 | 155 | describe('when a getTagStyle func is provided', () => { 156 | it('should apply the correct styles', () => { 157 | const style = { 158 | base: { 159 | color: "red" 160 | }, 161 | content: { 162 | color: "green" 163 | }, 164 | "delete": { 165 | color: "blue" 166 | } 167 | } 168 | 169 | const t = tag(props({ 170 | style 171 | })) 172 | 173 | const domNode = ReactDOM.findDOMNode(t) 174 | expect(domNode.style.color).toBe("red") 175 | 176 | }) 177 | }) 178 | }); 179 | -------------------------------------------------------------------------------- /test/jsdomReact.js: -------------------------------------------------------------------------------- 1 | import ExecutionEnvironment from 'exenv'; 2 | import React from 'react'; 3 | import jsdom from 'mocha-jsdom'; 4 | 5 | export default function jsdomReact() { 6 | jsdom(); 7 | ExecutionEnvironment.canUseDOM = true; 8 | } 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | 6 | output: { 7 | library: 'CategorizedTagInput', 8 | libraryTarget: 'umd', 9 | filename: 'categorized-tag-input.js' 10 | }, 11 | 12 | externals: [ 13 | { 14 | react: { 15 | root: 'React', 16 | commonjs2: 'react', 17 | commonjs: 'react', 18 | amd: 'react' 19 | } 20 | } 21 | ], 22 | 23 | devtool: 'source-map', 24 | 25 | module: { 26 | loaders: [ 27 | { test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel' } 28 | ] 29 | }, 30 | 31 | node: { 32 | Buffer: false 33 | }, 34 | 35 | plugins: [ 36 | new webpack.optimize.OccurenceOrderPlugin(), 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 39 | }), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compress: { 42 | warnings: false 43 | } 44 | }) 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /webpack.test.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: [ 6 | 'webpack-dev-server/client?http://localhost:5000', 7 | 'webpack/hot/dev-server', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, '/'), 12 | filename: 'bundle.js', 13 | publicPath: '/' 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.jsx'] 17 | }, 18 | devtool: 'eval-source-map', 19 | plugins: [ 20 | new webpack.HotModuleReplacementPlugin(), 21 | new webpack.NoErrorsPlugin() 22 | ], 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.jsx?$/, 27 | loaders: ['react-hot', 'babel'], 28 | exclude: /node_modules/ 29 | } 30 | ] 31 | } 32 | }; 33 | --------------------------------------------------------------------------------