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 (
@@ -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;