From 27b9a954e75e69859132d4fd197fa1735cf8aa93 Mon Sep 17 00:00:00 2001 From: jfusco Date: Mon, 25 Jul 2016 15:22:35 -0400 Subject: [PATCH] Adding functionality for delimiters option - finishing unit tests for tags and tag components - refactored tests for tags component - installled class properties plugin so we could use static members inside a React Component class - fixing unit test task runner - changing tag container to an unordered list element --- .babelrc | 3 + README.md | 11 ++ __tests__/Tag-test.js | 159 ++++++++++++++++++ __tests__/Tags-test.js | 354 ++++++++++++++++++++++++++++----------- dist-components/Tag.js | 19 +-- dist-components/Tags.js | 73 ++++---- dist/react-tags.css | 11 +- dist/react-tags.min.css | 2 +- dist/react-tags.min.js | 2 +- gulp/tasks/test.js | 5 - gulp/tasks/watch.js | 2 +- package.json | 3 +- src/js/Tag.js | 23 +-- src/js/Tags.js | 129 +++++++------- src/scss/react-tags.scss | 5 +- 15 files changed, 555 insertions(+), 246 deletions(-) create mode 100644 __tests__/Tag-test.js diff --git a/.babelrc b/.babelrc index 3915e8b..039e180 100644 --- a/.babelrc +++ b/.babelrc @@ -2,5 +2,8 @@ "presets": [ "react", "es2015" + ], + "plugins": [ + "transform-class-properties" ] } diff --git a/README.md b/README.md index 23bd808..4dc7c7c 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,10 @@ render(, document.getElementById('application')); #### Options * **[`initialTags`](#initialTags)** * **[`placeholder`](#placeholder)** +* **[`delimiters`](#delimiters)** * **[`change`](#change)** * **[`added`](#added)** +* **[`removed`](#removed)** * **[`readOnly`](#readOnly)** * **[`removeTagWithDeleteKey`](#removeTagWithDeleteKey)** * **[`removeTagIcon`](#removeTagIcon)** @@ -81,6 +83,15 @@ A `string` used as placeholder text in the tags input field ``` + +##### delimiters ~ optional ~ default `[13, 9, 32]` +An `array` of keyCodes used to tell the tags component which delimiter to use to add a tag + +[Here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode) is more info and a list of keyCodes +```js + +``` + ##### change ~ optional A `function` fired anytime there is a change - returns the new `array` of tags diff --git a/__tests__/Tag-test.js b/__tests__/Tag-test.js new file mode 100644 index 0000000..629bdd7 --- /dev/null +++ b/__tests__/Tag-test.js @@ -0,0 +1,159 @@ +'use strict'; + +jest.disableAutomock(); + +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import { createRenderer, Simulate, renderIntoDocument } from 'react-addons-test-utils'; +import Tag from '../src/js/Tag'; + +const TAG_NAME = 'foo'; + +describe('Tag', () => { + let renderer, + tag; + + beforeEach(() => { + renderer = createRenderer(); + + renderer.render( + + ); + + tag = renderer.getRenderOutput(); + }); + + afterEach(() => { + renderer = null; + tag = null; + }); + + it('should render', () => { + expect(tag).not.toBeUndefined(); + expect(tag.type).toBe('li'); + }); + + it('should render with a name', () => { + const props = tag.props.children; + + expect(props[0]).toBe(TAG_NAME); + }); +}); + + +describe('Tag - "readOnly"', () => { + let renderer; + + beforeEach(() => { + renderer = createRenderer(); + }); + + afterEach(() => { + renderer = null; + }); + + describe('when readOnly is false', () => { + it('should render the removeTagIcon', () => { + renderer.render( + + ); + + const tag = renderer.getRenderOutput(); + const removeIcon = tag.props.children[1]; + + expect(removeIcon).not.toBeNull(); + expect(removeIcon.type).toBe('a'); + }); + }); + + describe('when readOnly is true', () => { + it('should not render the removeTagIcon', () => { + renderer.render( + + ); + + const tag = renderer.getRenderOutput(); + + expect(tag.props.children[1]).toBeNull(); + }); + }); +}); + +describe('Tag - "removeTagIcon"', () => { + let renderer; + + beforeEach(() => { + renderer = createRenderer(); + }); + + afterEach(() => { + renderer = null; + }); + + describe('when a custom element is passed in', () => { + it('should render the element', () => { + const customRemoveIcon = ( + + ); + + renderer.render( + + ); + + const tag = renderer.getRenderOutput(); + const removeIcon = tag.props.children[1].props.children; + + expect(tag.props.children[1].type).toBe('a'); + expect(removeIcon.type).toBe('i'); + expect(removeIcon.props.className).toBe('icon-remove'); + }); + }); + + describe('when a custom string is passed in', () => { + it('should render the string', () => { + const renderer = createRenderer(); + + const customRemoveString = 'remove'; + + renderer.render( + + ); + + const tag = renderer.getRenderOutput(); + const removeIcon = tag.props.children[1].props.children; + + expect(tag.props.children[1].type).toBe('a'); + expect(removeIcon).toBe('remove'); + }); + }); +}); + +describe('Tag - "removeTag"', () => { + it('should be called when clicking the remove icon', () => { + const onRemoveClick = jest.genMockFunction(); + + const tag = renderIntoDocument( +
+ +
+ ); + + const statelessTag = findDOMNode(tag).children[0]; + const removeIcon = statelessTag.getElementsByTagName('a')[0]; + + Simulate.click(removeIcon); + + expect(onRemoveClick).toBeCalled(); + }); +}); diff --git a/__tests__/Tags-test.js b/__tests__/Tags-test.js index d9ebba9..b6f1017 100644 --- a/__tests__/Tags-test.js +++ b/__tests__/Tags-test.js @@ -3,173 +3,333 @@ jest.disableAutomock(); import React from 'react'; -import ReactDOM from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; +import { findDOMNode } from 'react-dom'; +import { createRenderer, Simulate, renderIntoDocument } from 'react-addons-test-utils'; import Tags from '../src/js/Tags'; +const TEST_TAGS = [ + 'foo', + 'bar' +]; + describe('Tags', () => { - it('should render', () => { - const tags = TestUtils.renderIntoDocument( - + let renderer, + tags; + + beforeEach(() => { + renderer = createRenderer(); + + renderer.render( + ); - expect(TestUtils.isCompositeComponent(tags)).toBeTruthy(); + tags = renderer.getRenderOutput(); }); - it('should build tags from an array passed as prop', () => { - const tags = TestUtils.renderIntoDocument( - - ); + it('should render', () => { + expect(tags).not.toBeUndefined(); + expect(tags.type).toBe('div'); + }); - const renderedDOM = ReactDOM.findDOMNode(tags); - const tagContainer = renderedDOM.querySelector('.tags-container'); + it('should check ID of tags component', () => { + expect(tags.type).toEqual('div'); + expect(tags.props.id).toEqual('test-tags'); + }); - expect(tagContainer.children.length).toBe(2); + it('should render custom placeholder if provided', () => { + expect(tags.props.children[1].type).toBe('input'); + expect(tags.props.children[1].props.placeholder).toBe('Custom placeholder text'); + }); +}); + + +describe('Tags - "initialTags"', () => { + it('should render tags (shallow render)', () => { + const renderer = createRenderer(); + + const renderList = () => { + renderer.render( + + ); + + const list = renderer.getRenderOutput(); + return list.props.children.filter(component => component.type == 'ul'); + }; + + const items = renderList(); + + expect(items[0].props.children.length).toBe(TEST_TAGS.length); }); - it('should be read only', () => { - const tags = TestUtils.renderIntoDocument( + it('should render tags (DOM render)', () => { + const tags = renderIntoDocument( + initialTags={TEST_TAGS} /> ); - const renderedDOM = ReactDOM.findDOMNode(tags); + const renderedDOM = findDOMNode(tags); const tagContainer = renderedDOM.querySelector('.tags-container'); - expect(renderedDOM.getElementsByTagName('input').length).toBe(0); - expect(renderedDOM.getElementsByTagName('div')[0].classList.contains('readonly')).toBeTruthy(); - expect(tagContainer.children[0].getElementsByTagName('a').length).toBe(0); - expect(tagContainer.children[0].textContent).toContain('hello'); + expect(tagContainer.children.length).toBe(TEST_TAGS.length); }); +}); + +describe('Tags - "readOnly"', () => { + let renderer; - it('should render custom remove tag icon element', () => { - const removeContent = ; + beforeEach(() => { + renderer = createRenderer(); - const tags = TestUtils.renderIntoDocument( + renderer.render( + initialTags={TEST_TAGS} + readOnly={true} /> ); + }); - const renderedDOM = ReactDOM.findDOMNode(tags); - const tagContainer = renderedDOM.querySelector('.tags-container'); + it('should only render the tags container, not the input', () => { + const list = renderer.getRenderOutput(); + const items = list.props.children; - expect(tagContainer.children[0].getElementsByTagName('i').length).toBe(1); - expect(tagContainer.children[0].querySelector('.icon-remove')).not.toBeNull(); + expect(items.length).toBe(2); + expect(items[0].type).toBe('ul'); + expect(items[1]).toBeNull(); }); - it('should render custom remove tag icon string', () => { - const removeContent = 'close'; + it('should add the className "readonly" to tags container', () => { + const list = renderer.getRenderOutput(); + const items = list.props.children; + + expect(items[0].props.className).toContain('readonly'); + }); +}); + + +describe('Tags - "delimiters"', () => { + let tags, + input, + tagContainer; - const tags = TestUtils.renderIntoDocument( + beforeEach(() => { + tags = renderIntoDocument( + initialTags={TEST_TAGS} + delimiters={[13, 9, 32, 188]} /> ); - const renderedDOM = ReactDOM.findDOMNode(tags); - const tagContainer = renderedDOM.querySelector('.tags-container'); + const renderedDOM = findDOMNode(tags); + tagContainer = renderedDOM.querySelector('.tags-container'); + input = renderedDOM.getElementsByTagName('input')[0]; + + input.value = TEST_TAGS[0]; - expect(tagContainer.children[0].getElementsByTagName('a')[0].textContent).toContain('close'); + Simulate.change(input); }); + afterEach(() => { + tags = null; + input = null; + tagContainer = null; + }); - it('should remove a single tag', () => { - const tags = TestUtils.renderIntoDocument( - - ); + describe('when pressing "enter"', () => { + it('should add a tag', () => { + Simulate.keyDown(input, {key: 'Enter', keyCode: 13, which: 13}); - const renderedDOM = ReactDOM.findDOMNode(tags); - const tagContainer = renderedDOM.querySelector('.tags-container'); + expect(tagContainer.children.length).toBe(3); + }); + }); - TestUtils.Simulate.click( - tagContainer.children[0].getElementsByTagName('a')[0] - ); + describe('when pressing "tab"', () => { + it('should add a tag', () => { + Simulate.keyDown(input, {key: 'Tab', keyCode: 9, which: 9}); - expect(tagContainer.children.length).toBe(1); + expect(tagContainer.children.length).toBe(3); + }); }); - it('should add a single tag', () => { - const tags = TestUtils.renderIntoDocument( - - ); + describe('when pressing "spacebar"', () => { + it('should add a tag', () => { + Simulate.keyDown(input, {key: 'Spacebar', keyCode: 32, which: 32}); - const renderedDOM = ReactDOM.findDOMNode(tags); - const tagContainer = renderedDOM.querySelector('.tags-container'); - const input = renderedDOM.getElementsByTagName('input')[0]; + expect(tagContainer.children.length).toBe(3); + }); + }); - input.value = 'foo'; - TestUtils.Simulate.change(input); - TestUtils.Simulate.keyUp(input, {key: 'Enter', keyCode: 13, which: 13}); + describe('when pressing ","', () => { + it('should add a tag', () => { + Simulate.keyDown(input, {key: 'Comma', keyCode: 188, which: 188}); - expect(tagContainer.children.length).toBe(3); + expect(tagContainer.children.length).toBe(3); + }); }); +}); - it('should call event added and return the tag that was just added', () => { - const onTagAdded = jest.genMockFunction(); +describe('Tags - events', () => { + let tags, + input, + tagContainer; - const tags = TestUtils.renderIntoDocument( + const onTagAdded = jest.genMockFunction(); + const onTagRemoved = jest.genMockFunction(); + const onTagsChanged = jest.genMockFunction(); + + beforeEach(() => { + tags = renderIntoDocument( + initialTags={TEST_TAGS} + added={onTagAdded} + removed={onTagRemoved} + change={onTagsChanged} /> ); - const renderedDOM = ReactDOM.findDOMNode(tags); - const input = renderedDOM.getElementsByTagName('input')[0]; + const renderedDOM = findDOMNode(tags); + tagContainer = renderedDOM.querySelector('.tags-container'); + input = renderedDOM.getElementsByTagName('input')[0]; + }); + + afterEach(() => { + tags = null; + input = null; + tagContainer = null; + }); + + describe('when adding a tag', () => { + beforeEach(() => { + input.value = TEST_TAGS[0]; + + Simulate.change(input); + Simulate.keyDown(input, {key: 'Enter', keyCode: 13, which: 13}); + }); - input.value = 'foo'; - TestUtils.Simulate.change(input); - TestUtils.Simulate.keyUp(input, {key: 'Enter', keyCode: 13, which: 13}); + it('should call the "added" event and return the new tag', () => { + expect(onTagAdded).toBeCalledWith(TEST_TAGS[0]); + }); - expect(onTagAdded).toBeCalledWith('foo'); + it('should call the "changed" event and return the new tags list as an array', () => { + const newArray = TEST_TAGS.concat('foo'); + + expect(onTagsChanged).toBeCalledWith(newArray); + }); + }); + + describe('when removing a tag', () => { + beforeEach(() => { + input.value = ''; + + Simulate.change(input); + Simulate.keyDown(input, {key: 'Delete', keyCode: 8, which: 8}); + }); + + it('should call the "removed" event and return the tag that was removed', () => { + expect(onTagRemoved).toBeCalledWith(TEST_TAGS[1]); + }); + + it('should call the "changed" event and return the new tags list as an array', () => { + expect(onTagsChanged).toBeCalledWith([TEST_TAGS[0]]); + }); }); +}); - it('should call event removed and return the tag that was just removed', () => { - const onTagRemoved = jest.genMockFunction(); +describe('Tags - removing', () => { + let tags, + input, + tagContainer; - const tags = TestUtils.renderIntoDocument( + beforeEach(() => { + tags = renderIntoDocument( + initialTags={TEST_TAGS} /> ); - const renderedDOM = ReactDOM.findDOMNode(tags); - const tagContainer = renderedDOM.querySelector('.tags-container'); + const renderedDOM = findDOMNode(tags); + tagContainer = renderedDOM.querySelector('.tags-container'); + input = renderedDOM.getElementsByTagName('input')[0]; + }); - TestUtils.Simulate.click( - tagContainer.children[0].getElementsByTagName('a')[0] - ); + afterEach(() => { + tags = null; + input = null; + tagContainer = null; + }); + + describe('when the remove icon is clicked on a tag', () => { + it('should remove a single tag', () => { + const removeIcon = tagContainer.children[0].getElementsByTagName('a')[0]; + + Simulate.click(removeIcon); + + expect(tagContainer.children[0].textContent).toContain(TEST_TAGS[1]); + expect(tagContainer.children.length).toBe(1); + }); + }); - expect(onTagRemoved).toBeCalledWith('hello'); + describe('when the input field is empty and backspace is pressed', () => { + it('should remove a single tag', () => { + input.value = ''; + + Simulate.change(input); + Simulate.keyDown(input, {key: 'Delete', keyCode: 8, which: 8}); + + expect(tagContainer.children.length).toBe(1); + expect(tagContainer.children[0].textContent).toContain(TEST_TAGS[0]); + }); }); - it('should call event change and return the new tags list as an array', () => { - const onTagsChange = jest.genMockFunction(); + describe('when the input field is not empty and backspace is pressed', () => { + it('should not remove a tag', () => { + input.value = 'a'; + + Simulate.change(input); + Simulate.keyDown(input, {key: 'Delete', keyCode: 8, which: 8}); + + expect(tagContainer.children.length).toBe(2); + expect(tagContainer.children[1].textContent).toContain(TEST_TAGS[1]); + }); + }); +}); - const tags = TestUtils.renderIntoDocument( +describe('Tags - "allowDupes"', () => { + it('should allow duplicate tags to be created', () => { + const tags = renderIntoDocument( + initialTags={TEST_TAGS} /> ); - const renderedDOM = ReactDOM.findDOMNode(tags); + const renderedDOM = findDOMNode(tags); const input = renderedDOM.getElementsByTagName('input')[0]; + const tagContainer = renderedDOM.querySelector('.tags-container'); + + input.value = TEST_TAGS[0]; - input.value = 'foo'; - TestUtils.Simulate.change(input); - TestUtils.Simulate.keyUp(input, {key: 'Enter', keyCode: 13, which: 13}); + Simulate.change(input); + Simulate.keyDown(input, {key: 'Enter', keyCode: 13, which: 13}); - expect(onTagsChange).toBeCalledWith(['hello', 'world', 'foo']); + expect(tagContainer.children.length).toBe(3); + expect(tagContainer.children[2].textContent).toContain(TEST_TAGS[0]); + expect(tagContainer.children[2].textContent).toBe(tagContainer.children[0].textContent); }); - it('should add a placeholder to the input element', () => { - const tags = TestUtils.renderIntoDocument( - + it('should not allow duplicate tags to be created', () => { + const tags = renderIntoDocument( + ); - var input = TestUtils.findRenderedDOMComponentWithTag(tags, 'input'); + const renderedDOM = findDOMNode(tags); + const input = renderedDOM.getElementsByTagName('input')[0]; + const tagContainer = renderedDOM.querySelector('.tags-container'); + + input.value = TEST_TAGS[0]; - expect(input.getAttribute('placeholder')).toEqual('add a tag'); + Simulate.change(input); + Simulate.keyDown(input, {key: 'Enter', keyCode: 13, which: 13}); + + expect(tagContainer.children.length).toBe(2); }); }); diff --git a/dist-components/Tag.js b/dist-components/Tag.js index ae65ee6..b89e3da 100644 --- a/dist-components/Tag.js +++ b/dist-components/Tag.js @@ -17,21 +17,17 @@ var Tag = function Tag(props) { props.removeTag(); }; - var removeIcon = function removeIcon() { - if (props.readOnly) return null; - - return _react2.default.createElement( - 'a', - { onClick: onRemoveClick, href: '#' }, - props.removeTagIcon - ); - }; + var removeIcon = !props.readOnly ? _react2.default.createElement( + 'a', + { onClick: onRemoveClick, href: '#' }, + props.removeTagIcon || String.fromCharCode(215) + ) : null; return _react2.default.createElement( - 'span', + 'li', null, props.name, - removeIcon() + removeIcon ); }; @@ -41,6 +37,7 @@ exports.default = Tag; Tag.propTypes = { name: _react2.default.PropTypes.string.isRequired, removeTag: _react2.default.PropTypes.func, + selectedTag: _react2.default.PropTypes.bool, readOnly: _react2.default.PropTypes.bool, removeTagIcon: _react2.default.PropTypes.oneOfType([_react2.default.PropTypes.string, _react2.default.PropTypes.element]) }; \ No newline at end of file diff --git a/dist-components/Tags.js b/dist-components/Tags.js index 151d20b..f20224e 100644 --- a/dist-components/Tags.js +++ b/dist-components/Tags.js @@ -26,8 +26,6 @@ function _possibleConstructorReturn(self, call) { if (!self) { throw new Referen function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } -var map = new WeakMap(); - var Tags = function (_Component) { _inherits(Tags, _Component); @@ -39,35 +37,33 @@ var Tags = function (_Component) { _this.state = { tags: _this.props.initialTags }; - - map.set(_this, { - empty: true - }); return _this; } _createClass(Tags, [{ key: 'onInputKey', value: function onInputKey(e) { - if (this.input.value.length !== 0 && this.empty) { - this.empty = false; - } - switch (e.keyCode) { - case 8: + case Tags.KEYS.backspace: if (this.state.tags.length === 0) return; - if (this.empty) { + if (this.input.value === '') { this.removeTag(this.state.tags.length - 1); } - if (this.input.value.length === 0) { - this.empty = true; - } break; - case 13: - this.addTag(); + default: + if (this.input.value === '') return; + + if (this.props.delimiters.indexOf(e.keyCode) !== -1) { + if (Tags.KEYS.enter !== e.keyCode) { + e.preventDefault(); + } + + this.addTag(); + } + break; } } @@ -93,8 +89,6 @@ var Tags = function (_Component) { _this2.props.added(value); } - _this2.empty = true; - _this2.input.value = ''; }); } @@ -125,8 +119,8 @@ var Tags = function (_Component) { var tagItems = this.state.tags.map(function (tag, v) { return _react2.default.createElement(_Tag2.default, { key: v, - readOnly: _this4.props.readOnly, name: tag, + readOnly: _this4.props.readOnly, removeTagIcon: _this4.props.removeTagIcon, removeTag: _this4.removeTag.bind(_this4, v) }); }); @@ -135,7 +129,7 @@ var Tags = function (_Component) { type: 'text', role: 'textbox', placeholder: this.props.placeholder, - onKeyUp: this.onInputKey.bind(this), + onKeyDown: this.onInputKey.bind(this), ref: function ref(el) { return _this4.input = el; } }) : null; @@ -146,50 +140,43 @@ var Tags = function (_Component) { 'div', { className: 'react-tags', id: this.props.id }, _react2.default.createElement( - 'div', + 'ul', { className: classNames }, tagItems ), tagInput ); } - }, { - key: 'empty', - set: function set(empty) { - map.set(this, { - empty: empty - }); - }, - get: function get() { - return map.get(this).empty; - } }]); return Tags; }(_react.Component); -exports.default = Tags; - - +Tags.KEYS = { + enter: 13, + tab: 9, + spacebar: 32, + backspace: 8, + left: 37, + right: 39 +}; Tags.propTypes = { initialTags: _react2.default.PropTypes.arrayOf(_react2.default.PropTypes.string), change: _react2.default.PropTypes.func, added: _react2.default.PropTypes.func, removed: _react2.default.PropTypes.func, placeholder: _react2.default.PropTypes.string, + delimiters: _react2.default.PropTypes.arrayOf(_react2.default.PropTypes.number), id: _react2.default.PropTypes.string, readOnly: _react2.default.PropTypes.bool, allowDupes: _react2.default.PropTypes.bool, - removeTagWithDeleteKey: _react2.default.PropTypes.bool, removeTagIcon: _react2.default.PropTypes.oneOfType([_react2.default.PropTypes.string, _react2.default.PropTypes.element]) }; - Tags.defaultProps = { initialTags: [], - placeholder: null, - id: null, + placeholder: 'Add a tag', + delimiters: [Tags.KEYS.enter, Tags.KEYS.tab, Tags.KEYS.spacebar], allowDupes: true, - readOnly: false, - removeTagWithDeleteKey: true, - removeTagIcon: String.fromCharCode(215) -}; \ No newline at end of file + readOnly: false +}; +exports.default = Tags; \ No newline at end of file diff --git a/dist/react-tags.css b/dist/react-tags.css index 47d823c..077a7da 100644 --- a/dist/react-tags.css +++ b/dist/react-tags.css @@ -1,4 +1,4 @@ -.react-tags input, .react-tags .tags-container > span { +.react-tags input, .react-tags .tags-container > li { font-size: 12px; height: 26px; line-height: 26px; @@ -26,8 +26,11 @@ border-color: #b1afb9; } .react-tags .tags-container { - display: inline; } - .react-tags .tags-container > span { + display: inline; + margin: 0; + padding: 0; + list-style: none; } + .react-tags .tags-container > li { -webkit-animation: slide-left 0.8s cubic-bezier(0.19, 1, 0.22, 1); -moz-animation: slide-left 0.8s cubic-bezier(0.19, 1, 0.22, 1); -o-animation: slide-left 0.8s cubic-bezier(0.19, 1, 0.22, 1); @@ -36,7 +39,7 @@ margin: 5px 5px 0 0; transition: background 0.3s ease; cursor: default; } - .react-tags .tags-container > span:hover { + .react-tags .tags-container > li:hover { background: #c9c8cf; } .react-tags .tags-container a { font-size: 12px; diff --git a/dist/react-tags.min.css b/dist/react-tags.min.css index 399cfb2..074a50b 100644 --- a/dist/react-tags.min.css +++ b/dist/react-tags.min.css @@ -1 +1 @@ -.react-tags .tags-container>span,.react-tags input{font-size:12px;height:26px;line-height:26px;color:#626166;border-radius:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;display:inline-block;padding:0 9px}.react-tags input{transition:border-color .3s ease;background:#FFF;border:1px solid #D9D8DD;height:24px;line-height:24px;margin-top:5px}.react-tags input::-webkit-input-placeholder{color:rgba(98,97,102,.5)}.react-tags input::-moz-placeholder{color:rgba(98,97,102,.5)}.react-tags input:-ms-input-placeholder{color:rgba(98,97,102,.5)}.react-tags input:focus{outline:0;border-color:#b1afb9}.react-tags .tags-container{display:inline}.react-tags .tags-container>span{-webkit-animation:slide-left .8s cubic-bezier(.19,1,.22,1);-moz-animation:slide-left .8s cubic-bezier(.19,1,.22,1);-o-animation:slide-left .8s cubic-bezier(.19,1,.22,1);animation:slide-left .8s cubic-bezier(.19,1,.22,1);background:#D9D8DD;margin:5px 5px 0 0;transition:background .3s ease;cursor:default}.react-tags .tags-container>span:hover{background:#c9c8cf}.react-tags .tags-container a{font-size:12px;color:#000;transition:color .3s ease;display:inline-block;margin-left:7px;text-decoration:none}.react-tags .tags-container a:hover{color:#666}.react-tags .tags-container i{font-style:normal}.react-tags .tags-container.readonly{pointer-events:none}@-webkit-keyframes slide-left{0%{-webkit-transform:translate3d(8,0,0)}100%{-webkit-transform:translate3d(0,0,0)}}@-moz-keyframes slide-left{0%{-moz-transform:translate3d(8px,0,0)}100%{-moz-transform:translate3d(0,0,0)}}@-o-keyframes slide-left{0%{-o-transform:translate3d(8px,0,0)}100%{opacity:1;-o-transform:translate3d(0,0,0)}}@keyframes slide-left{0%{transform:translate3d(8px,0,0)}100%{transform:translate3d(0,0,0)}} \ No newline at end of file +.react-tags .tags-container>li,.react-tags input{font-size:12px;height:26px;line-height:26px;color:#626166;border-radius:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;display:inline-block;padding:0 9px}.react-tags input{transition:border-color .3s ease;background:#FFF;border:1px solid #D9D8DD;height:24px;line-height:24px;margin-top:5px}.react-tags input::-webkit-input-placeholder{color:rgba(98,97,102,.5)}.react-tags input::-moz-placeholder{color:rgba(98,97,102,.5)}.react-tags input:-ms-input-placeholder{color:rgba(98,97,102,.5)}.react-tags input:focus{outline:0;border-color:#b1afb9}.react-tags .tags-container{display:inline;margin:0;padding:0;list-style:none}.react-tags .tags-container>li{-webkit-animation:slide-left .8s cubic-bezier(.19,1,.22,1);-moz-animation:slide-left .8s cubic-bezier(.19,1,.22,1);-o-animation:slide-left .8s cubic-bezier(.19,1,.22,1);animation:slide-left .8s cubic-bezier(.19,1,.22,1);background:#D9D8DD;margin:5px 5px 0 0;transition:background .3s ease;cursor:default}.react-tags .tags-container>li:hover{background:#c9c8cf}.react-tags .tags-container a{font-size:12px;color:#000;transition:color .3s ease;display:inline-block;margin-left:7px;text-decoration:none}.react-tags .tags-container a:hover{color:#666}.react-tags .tags-container i{font-style:normal}.react-tags .tags-container.readonly{pointer-events:none}@-webkit-keyframes slide-left{0%{-webkit-transform:translate3d(8,0,0)}100%{-webkit-transform:translate3d(0,0,0)}}@-moz-keyframes slide-left{0%{-moz-transform:translate3d(8px,0,0)}100%{-moz-transform:translate3d(0,0,0)}}@-o-keyframes slide-left{0%{-o-transform:translate3d(8px,0,0)}100%{opacity:1;-o-transform:translate3d(0,0,0)}}@keyframes slide-left{0%{transform:translate3d(8px,0,0)}100%{transform:translate3d(0,0,0)}} \ No newline at end of file diff --git a/dist/react-tags.min.js b/dist/react-tags.min.js index 787014d..f8bbeb5 100644 --- a/dist/react-tags.min.js +++ b/dist/react-tags.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React"),require("update")):"function"==typeof define&&define.amd?define(["React","update"],t):"object"==typeof exports?exports.Tags=t(require("React"),require("update")):e.Tags=t(e.React,e.update)}(this,function(e,t){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var p=function(){function e(e,t){for(var n=0;n=0||this.setState({tags:(0,f["default"])(this.state.tags,{$push:[t]})},function(){"undefined"!=typeof e.props.change&&e.props.change(e.state.tags),"undefined"!=typeof e.props.added&&e.props.added(t),e.empty=!0,e.input.value=""})}},{key:"removeTag",value:function(e){var t=this,n=this.state.tags[e];this.setState({tags:(0,f["default"])(this.state.tags,{$splice:[[e,1]]})},function(){"undefined"!=typeof t.props.change&&t.props.change(t.state.tags),"undefined"!=typeof t.props.removed&&t.props.removed(n)})}},{key:"render",value:function(){var e=this,t=this.state.tags.map(function(t,n){return i["default"].createElement(c["default"],{key:n,readOnly:e.props.readOnly,name:t,removeTagIcon:e.props.removeTagIcon,removeTag:e.removeTag.bind(e,n)})}),n=this.props.readOnly?null:i["default"].createElement("input",{type:"text",role:"textbox",placeholder:this.props.placeholder,onKeyUp:this.onInputKey.bind(this),ref:function(t){return e.input=t}}),r=this.props.readOnly?"tags-container readonly":"tags-container";return i["default"].createElement("div",{className:"react-tags",id:this.props.id},i["default"].createElement("div",{className:r},t),n)}},{key:"empty",set:function(e){y.set(this,{empty:e})},get:function(){return y.get(this).empty}}]),t}(u.Component);t["default"]=h,h.propTypes={initialTags:i["default"].PropTypes.arrayOf(i["default"].PropTypes.string),change:i["default"].PropTypes.func,added:i["default"].PropTypes.func,removed:i["default"].PropTypes.func,placeholder:i["default"].PropTypes.string,id:i["default"].PropTypes.string,readOnly:i["default"].PropTypes.bool,allowDupes:i["default"].PropTypes.bool,removeTagWithDeleteKey:i["default"].PropTypes.bool,removeTagIcon:i["default"].PropTypes.oneOfType([i["default"].PropTypes.string,i["default"].PropTypes.element])},h.defaultProps={initialTags:[],placeholder:null,id:null,allowDupes:!0,readOnly:!1,removeTagWithDeleteKey:!0,removeTagIcon:String.fromCharCode(215)}},function(t,n){t.exports=e},function(e,n){e.exports=t},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var o=n(1),a=r(o),s=function(e){var t=function(t){t.preventDefault(),e.removeTag()},n=function(){return e.readOnly?null:a["default"].createElement("a",{onClick:t,href:"#"},e.removeTagIcon)};return a["default"].createElement("span",null,e.name,n())};t["default"]=s,s.propTypes={name:a["default"].PropTypes.string.isRequired,removeTag:a["default"].PropTypes.func,readOnly:a["default"].PropTypes.bool,removeTagIcon:a["default"].PropTypes.oneOfType([a["default"].PropTypes.string,a["default"].PropTypes.element])}}])}); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("React"),require("update")):"function"==typeof define&&define.amd?define(["React","update"],t):"object"==typeof exports?exports.Tags=t(require("React"),require("update")):e.Tags=t(e.React,e.update)}(this,function(e,t){return function(e){function t(a){if(r[a])return r[a].exports;var n=r[a]={exports:{},id:a,loaded:!1};return e[a].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var r={};return t.m=e,t.c=r,t.p="",t(0)}([function(e,t,r){"use strict";function a(e){return e&&e.__esModule?e:{"default":e}}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var p=function(){function e(e,t){for(var r=0;r=0||this.setState({tags:(0,f["default"])(this.state.tags,{$push:[t]})},function(){"undefined"!=typeof e.props.change&&e.props.change(e.state.tags),"undefined"!=typeof e.props.added&&e.props.added(t),e.input.value=""})}},{key:"removeTag",value:function(e){var t=this,r=this.state.tags[e];this.setState({tags:(0,f["default"])(this.state.tags,{$splice:[[e,1]]})},function(){"undefined"!=typeof t.props.change&&t.props.change(t.state.tags),"undefined"!=typeof t.props.removed&&t.props.removed(r)})}},{key:"render",value:function(){var e=this,t=this.state.tags.map(function(t,r){return i["default"].createElement(c["default"],{key:r,name:t,readOnly:e.props.readOnly,removeTagIcon:e.props.removeTagIcon,removeTag:e.removeTag.bind(e,r)})}),r=this.props.readOnly?null:i["default"].createElement("input",{type:"text",role:"textbox",placeholder:this.props.placeholder,onKeyDown:this.onInputKey.bind(this),ref:function(t){return e.input=t}}),a=this.props.readOnly?"tags-container readonly":"tags-container";return i["default"].createElement("div",{className:"react-tags",id:this.props.id},i["default"].createElement("ul",{className:a},t),r)}}]),t}(u.Component);y.KEYS={enter:13,tab:9,spacebar:32,backspace:8,left:37,right:39},y.propTypes={initialTags:i["default"].PropTypes.arrayOf(i["default"].PropTypes.string),change:i["default"].PropTypes.func,added:i["default"].PropTypes.func,removed:i["default"].PropTypes.func,placeholder:i["default"].PropTypes.string,delimiters:i["default"].PropTypes.arrayOf(i["default"].PropTypes.number),id:i["default"].PropTypes.string,readOnly:i["default"].PropTypes.bool,allowDupes:i["default"].PropTypes.bool,removeTagIcon:i["default"].PropTypes.oneOfType([i["default"].PropTypes.string,i["default"].PropTypes.element])},y.defaultProps={initialTags:[],placeholder:"Add a tag",delimiters:[y.KEYS.enter,y.KEYS.tab,y.KEYS.spacebar],allowDupes:!0,readOnly:!1},t["default"]=y},function(t,r){t.exports=e},function(e,r){e.exports=t},function(e,t,r){"use strict";function a(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0});var n=r(1),o=a(n),s=function(e){var t=function(t){t.preventDefault(),e.removeTag()},r=e.readOnly?null:o["default"].createElement("a",{onClick:t,href:"#"},e.removeTagIcon||String.fromCharCode(215));return o["default"].createElement("li",null,e.name,r)};t["default"]=s,s.propTypes={name:o["default"].PropTypes.string.isRequired,removeTag:o["default"].PropTypes.func,selectedTag:o["default"].PropTypes.bool,readOnly:o["default"].PropTypes.bool,removeTagIcon:o["default"].PropTypes.oneOfType([o["default"].PropTypes.string,o["default"].PropTypes.element])}}])}); \ No newline at end of file diff --git a/gulp/tasks/test.js b/gulp/tasks/test.js index b79b6f6..3a6da61 100644 --- a/gulp/tasks/test.js +++ b/gulp/tasks/test.js @@ -5,9 +5,4 @@ module.exports = (gulp, $) => { return gulp.src('./src/js/Tags.js', {read: false}) .pipe($.shell('npm test')); }); - - gulp.task('test-dev', () => { - return gulp.src('./src/js/Tags.js', {read: false}) - .pipe($.shell('jest -o')); - }); }; diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js index f20f962..f077893 100644 --- a/gulp/tasks/watch.js +++ b/gulp/tasks/watch.js @@ -2,7 +2,7 @@ module.exports = (gulp, $) => { gulp.task('watch', () => { - gulp.watch(['./src/js/**/*.js'], ['eslint', 'test-dev']); + gulp.watch(['./src/js/**/*.js'], ['eslint', 'test']); gulp.watch(['./src/scss/**/*.scss'], ['scss-lint']); }); }; diff --git a/package.json b/package.json index 26b280d..7f54038 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-tagging-input", - "version": "1.1.6", + "version": "1.2.6", "description": "Simple tagging component", "main": "dist-components/Tags.js", "license": "MIT", @@ -30,6 +30,7 @@ "babel-loader": "^6.2.4", "babel-preset-es2015": "^6.9.0", "babel-preset-react": "^6.11.1", + "babel-plugin-transform-class-properties": "^6.10.2", "babel-register": "^6.9.0", "babel-eslint": "^6.1.2", "babel-jest": "^13.2.2", diff --git a/src/js/Tag.js b/src/js/Tag.js index cb93b32..56ddfc3 100644 --- a/src/js/Tag.js +++ b/src/js/Tag.js @@ -9,23 +9,17 @@ const Tag = props => { props.removeTag(); }; - const removeIcon = () => { - if (props.readOnly) return null; - - return ( - - {props.removeTagIcon} - - ); - }; + const removeIcon = !props.readOnly ? ( + + {props.removeTagIcon|| String.fromCharCode(215)} + + ) : null; return ( - - {/* Tag name */} +
  • {props.name} - {/* Tag remove icon */} - {removeIcon()} - + {removeIcon} +
  • ); }; @@ -34,6 +28,7 @@ export default Tag; Tag.propTypes = { name: React.PropTypes.string.isRequired, removeTag: React.PropTypes.func, + selectedTag: React.PropTypes.bool, readOnly: React.PropTypes.bool, removeTagIcon: React.PropTypes.oneOfType([ React.PropTypes.string, diff --git a/src/js/Tags.js b/src/js/Tags.js index 9e65ac1..993cd58 100644 --- a/src/js/Tags.js +++ b/src/js/Tags.js @@ -4,41 +4,70 @@ import React, {Component} from 'react'; import update from 'react-addons-update'; import Tag from './Tag'; -const map = new WeakMap(); +class Tags extends Component{ + static KEYS = { + enter: 13, + tab: 9, + spacebar: 32, + backspace: 8, + left: 37, + right: 39 + }; + + static propTypes = { + initialTags: React.PropTypes.arrayOf(React.PropTypes.string), + change: React.PropTypes.func, + added: React.PropTypes.func, + removed: React.PropTypes.func, + placeholder: React.PropTypes.string, + delimiters: React.PropTypes.arrayOf(React.PropTypes.number), + id: React.PropTypes.string, + readOnly: React.PropTypes.bool, + allowDupes: React.PropTypes.bool, + removeTagIcon: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]) + }; + + static defaultProps = { + initialTags: [], + placeholder: 'Add a tag', + delimiters: [Tags.KEYS.enter, Tags.KEYS.tab, Tags.KEYS.spacebar], + allowDupes: true, + readOnly: false + }; + + state = { + tags: this.props.initialTags + }; -export default class Tags extends Component{ constructor(props){ super(props); - - this.state = { - tags: this.props.initialTags - }; - - map.set(this, { - empty: true - }); } onInputKey(e){ - if (this.input.value.length !== 0 && this.empty){ - this.empty = false; - } - switch (e.keyCode){ - case 8: + case Tags.KEYS.backspace: if (this.state.tags.length === 0) return; - if (this.empty){ + if (this.input.value === ''){ this.removeTag(this.state.tags.length - 1); } - if (this.input.value.length === 0){ - this.empty = true; - } break; - case 13: - this.addTag(); + default: + if (this.input.value === '') return; + + if (this.props.delimiters.indexOf(e.keyCode) !== -1){ + if (Tags.KEYS.enter !== e.keyCode){ + e.preventDefault(); + } + + this.addTag(); + } + break; } } @@ -61,8 +90,6 @@ export default class Tags extends Component{ this.props.added(value); } - this.empty = true; - this.input.value = ''; }); } @@ -83,40 +110,32 @@ export default class Tags extends Component{ }); } - set empty(empty){ - map.set(this, { - empty - }); - } - - get empty(){ - return map.get(this).empty; - } - render(){ const tagItems = this.state.tags.map((tag, v) => { return ; }); - const tagInput = !this.props.readOnly ? this.input = el} /> : null; + const tagInput = !this.props.readOnly ? ( + this.input = el} /> + ) : null; const classNames = this.props.readOnly ? 'tags-container readonly' : 'tags-container'; return (
    -
    +
      {tagItems} -
    + {tagInput}
    @@ -124,28 +143,4 @@ export default class Tags extends Component{ } } -Tags.propTypes = { - initialTags: React.PropTypes.arrayOf(React.PropTypes.string), - change: React.PropTypes.func, - added: React.PropTypes.func, - removed: React.PropTypes.func, - placeholder: React.PropTypes.string, - id: React.PropTypes.string, - readOnly: React.PropTypes.bool, - allowDupes: React.PropTypes.bool, - removeTagWithDeleteKey: React.PropTypes.bool, - removeTagIcon: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.element - ]) -}; - -Tags.defaultProps = { - initialTags: [], - placeholder: null, - id: null, - allowDupes: true, - readOnly: false, - removeTagWithDeleteKey: true, - removeTagIcon: String.fromCharCode(215) -}; +export default Tags; diff --git a/src/scss/react-tags.scss b/src/scss/react-tags.scss index 92b182b..f10abc0 100644 --- a/src/scss/react-tags.scss +++ b/src/scss/react-tags.scss @@ -50,8 +50,11 @@ .tags-container { display: inline; + margin: 0; + padding: 0; + list-style: none; - > span { + > li { @extend %tags-ui-base; -webkit-animation: $tag-intro-animation;