diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..3915e8b --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "react", + "es2015" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ddb127e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# This file is for unifying the coding style for different editors and IDEs. + +# No .editorconfig files above the root directory +root = true + +# All files unicode +[*] +charset = utf-8 + +# Main styles for our files +[*] +indent_style = tab +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_size = 4 \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b2398bf --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +/node_modules/ +/dist/ +/components/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..b200980 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,87 @@ +{ + "parser": "babel-eslint", + "rules": { + "eqeqeq": [2, "allow-null"], + "no-extra-semi": 2, + "semi": ["error", "always"], + "no-cond-assign": 2, + "accessor-pairs": 2, + "no-with": 2, + "no-sparse-arrays": 2, + "vars-on-top": 2, + "no-void": 2, + "no-undef": 2, + "no-caller": 2, + "no-new": 2, + "yoda": 2, + "no-whitespace-before-property": 2, + "no-inner-declarations": 2, + "no-ex-assign": 2, + "no-else-return": 2, + "no-dupe-keys": 2, + "constructor-super": 2, + "no-extra-boolean-cast": 2, + "no-obj-calls": 2, + "no-empty": 2, + "semi-spacing": ["error", { + "before": false, "after": true + }], + "keyword-spacing": ["error", {"overrides": { + "if": {"after": true}, + "else": {"before": true, "after": true}, + "for": {"after": false}, + "while": {"after": false}, + "function": {"after": true} + }}], + "space-before-function-paren": ["error", { + "anonymous": "never", "named": "never" + }], + "comma-dangle": [2, "never"], + "comma-style": [2, "last"], + "comma-spacing": ["error", { + "before": false, "after": true + }], + "quotes": [2, "single", "avoid-escape"], + "no-trailing-spaces": 2, + "no-lonely-if": 0, + "eol-last": 2, + "no-nested-ternary": 2, + "space-in-parens": [2, "never"], + "brace-style": [2, "1tbs", { + "allowSingleLine": true + }], + "object-curly-spacing": [2, "never", { + "objectsInObjects": true, + "arraysInObjects": true + }], + "computed-property-spacing": ["error", "never"], + "block-spacing": ["error", "always"], + "one-var": [2, { + "let": "always", + "const": "never" + }], + "no-spaced-func": 2, + "spaced-comment": 0, + "quote-props": [2, "as-needed", { + "keywords": true + }], + "linebreak-style": ["error", "unix"], + "guard-for-in": 2, + "prefer-const": 2, + "no-var": 2, + "prefer-arrow-callback": 2, + "no-const-assign": 2, + "arrow-parens": [2, "as-needed"], + "arrow-spacing": [2, { + "before": true, + "after": true + }] + }, + "globals": { + "console": true, + "alert": true, + "document": true, + "window": true, + "WeakMap": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb700a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/.idea/ +*.DS_Store +npm-debug.log \ No newline at end of file diff --git a/.scsslint.yml b/.scsslint.yml new file mode 100644 index 0000000..0ac3e60 --- /dev/null +++ b/.scsslint.yml @@ -0,0 +1,166 @@ +linters: + + BangFormat: + enabled: true + space_before_bang: true + space_after_bang: false + + BorderZero: + enabled: true + + ColorKeyword: + enabled: true + + Comment: + enabled: false + + DebugStatement: + enabled: true + + DeclarationOrder: + enabled: true + + DuplicateProperty: + enabled: false + + ElsePlacement: + enabled: true + style: same_line + + EmptyLineBetweenBlocks: + enabled: true + ignore_single_line_blocks: false + + EmptyRule: + enabled: true + + FinalNewline: + enabled: false + present: false + + HexLength: + enabled: true + style: short + + HexNotation: + enabled: true + style: uppercase + + HexValidation: + enabled: true + + IdSelector: + enabled: false + + ImportPath: + enabled: true + leading_underscore: false + filename_extension: false + + ImportantRule: + enabled: false + + Indentation: + enabled: true + character: tab + width: 1 + + LeadingZero: + enabled: true + style: include_zero + + MergeableSelector: + enabled: false + force_nesting: false + + NameFormat: + enabled: true + convention: hyphenated_lowercase + allow_leading_underscore: true + + NestingDepth: + enabled: true + max_depth: 5 + + PlaceholderInExtend: + enabled: false + + PropertySortOrder: + enabled: false + ignore_unspecified: false + + PropertySpelling: + enabled: true + extra_properties: [] + + QualifyingElement: + enabled: true + allow_element_with_attribute: true + allow_element_with_class: false + allow_element_with_id: false + + SelectorDepth: + enabled: true + max_depth: 5 + + SelectorFormat: + enabled: true + convention: hyphenated_lowercase + + Shorthand: + enabled: true + + SingleLinePerProperty: + enabled: true + allow_single_line_rule_sets: false + + SingleLinePerSelector: + enabled: true + + SpaceAfterComma: + enabled: true + + SpaceAfterPropertyColon: + enabled: true + style: one_space + + SpaceAfterPropertyName: + enabled: true + + SpaceBeforeBrace: + enabled: true + style: space + allow_single_line_padding: true + + SpaceBetweenParens: + enabled: true + spaces: 0 + + StringQuotes: + enabled: true + style: double_quotes + + TrailingSemicolon: + enabled: true + + TrailingZero: + enabled: true + + UnnecessaryMantissa: + enabled: true + + UnnecessaryParentReference: + enabled: true + + UrlFormat: + enabled: false + + UrlQuotes: + enabled: true + + VendorPrefix: + enabled: false + + ZeroUnit: + enabled: true + severity: warning diff --git a/README.md b/README.md index cd6e6ac..75e3df3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # react-tags -Simple tagging component for React projects +> Simple tagging component for React projects. \ No newline at end of file diff --git a/dist-components/Tag.js b/dist-components/Tag.js new file mode 100644 index 0000000..8a35563 --- /dev/null +++ b/dist-components/Tag.js @@ -0,0 +1,46 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var Tag = function Tag(props) { + var onRemoveClick = function onRemoveClick(e) { + e.preventDefault(); + + props.removeTag(); + }; + + var removeIcon = function removeIcon() { + if (props.readOnly) return null; + + return _react2.default.createElement( + 'a', + { onClick: onRemoveClick, href: '#' }, + props.removeTagIcon + ); + }; + + return _react2.default.createElement( + 'span', + null, + props.name, + removeIcon() + ); +}; + +exports.default = Tag; + + +Tag.propTypes = { + name: _react2.default.PropTypes.string.isRequired, + removeTag: _react2.default.PropTypes.func.isRequired, + 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 new file mode 100644 index 0000000..8ac7cb6 --- /dev/null +++ b/dist-components/Tags.js @@ -0,0 +1,195 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _reactAddonsUpdate = require('react-addons-update'); + +var _reactAddonsUpdate2 = _interopRequireDefault(_reactAddonsUpdate); + +var _tag = require('./tag'); + +var _tag2 = _interopRequireDefault(_tag); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +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); + + function Tags(props) { + _classCallCheck(this, Tags); + + var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Tags).call(this, props)); + + _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: + if (this.state.tags.length === 0) return; + + if (this.empty) { + this.removeTag(this.state.tags.length - 1); + } + + if (this.input.value.length === 0) { + this.empty = true; + } + break; + + case 13: + this.addTag(); + break; + } + } + }, { + key: 'addTag', + value: function addTag() { + var _this2 = this; + + var value = this.input.value.trim(); + + this.setState({ + tags: (0, _reactAddonsUpdate2.default)(this.state.tags, { $push: [value] }) + }, function () { + if (typeof _this2.props.change !== 'undefined') { + _this2.props.change(_this2.state.tags); + } + + if (typeof _this2.props.added !== 'undefined') { + _this2.props.added(value); + } + + _this2.empty = true; + + _this2.input.value = ''; + }); + } + }, { + key: 'removeTag', + value: function removeTag(index) { + var _this3 = this; + + var value = this.state.tags[index]; + + this.setState({ + tags: (0, _reactAddonsUpdate2.default)(this.state.tags, { $splice: [[index, 1]] }) + }, function () { + if (typeof _this3.props.change !== 'undefined') { + _this3.props.change(_this3.state.tags); + } + + if (typeof _this3.props.removed !== 'undefined') { + _this3.props.removed(value); + } + }); + } + }, { + key: 'render', + value: function render() { + var _this4 = this; + + var tagItems = this.state.tags.map(function (tag, v) { + return _react2.default.createElement(_tag2.default, { + key: v, + readOnly: _this4.props.readOnly, + name: tag, + removeTagIcon: _this4.props.removeTagIcon, + removeTag: _this4.removeTag.bind(_this4, v) }); + }); + + var tagInput = !this.props.readOnly ? _react2.default.createElement('input', { + type: 'text', + role: 'textbox', + placeholder: this.props.placeholder, + onKeyUp: this.onInputKey.bind(this), + ref: function ref(el) { + return _this4.input = el; + } }) : null; + + var classNames = function classNames() { + var classes = 'tags-container'; + + return _this4.props.readOnly ? classes + ' readonly' : classes; + }; + + return _react2.default.createElement( + 'div', + { className: 'react-tags', id: this.props.id }, + _react2.default.createElement( + 'div', + { 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.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, + 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, + allowDupes: true, + readOnly: false, + removeTagWithDeleteKey: true, + removeTagIcon: String.fromCharCode(215) +}; \ No newline at end of file diff --git a/dist/react-tags.css b/dist/react-tags.css new file mode 100644 index 0000000..c09cc23 --- /dev/null +++ b/dist/react-tags.css @@ -0,0 +1,47 @@ +.react-tags input, .react-tags .tags-container > span { + 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 { + 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, 0.5); } + .react-tags input::-moz-placeholder { + color: rgba(98, 97, 102, 0.5); } + .react-tags input:-ms-input-placeholder { + color: rgba(98, 97, 102, 0.5); } + .react-tags input:focus { + outline: none; } + +.react-tags .tags-container { + display: inline; } + .react-tags .tags-container > span { + background: #D9D8DD; + margin: 5px 5px 0 0; + transition: background 0.3s ease; + cursor: default; } + .react-tags .tags-container > span:hover { + background: #c9c8cf; } + .react-tags .tags-container a { + font-size: 12px; + color: #000; + transition: color 0.3s ease; + display: inline-block; + margin-left: 7px; + text-decoration: none; } + .react-tags .tags-container a:hover { + color: #666666; } + .react-tags .tags-container i { + font-style: normal; } + .react-tags .tags-container.readonly { + pointer-events: none; } diff --git a/dist/react-tags.min.css b/dist/react-tags.min.css new file mode 100644 index 0000000..1a46826 --- /dev/null +++ b/dist/react-tags.min.css @@ -0,0 +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{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}.react-tags .tags-container{display:inline}.react-tags .tags-container>span{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} \ No newline at end of file diff --git a/dist/react-tags.min.js b/dist/react-tags.min.js new file mode 100644 index 0000000..dbef1ca --- /dev/null +++ b/dist/react-tags.min.js @@ -0,0 +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 p(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 u=function(){function e(e,t){for(var n=0;n { + tasks.forEach(name => { + require('./tasks/' + name)(gulp, $); + }); +}; diff --git a/gulp/tasks/babel.js b/gulp/tasks/babel.js new file mode 100644 index 0000000..0cf7e26 --- /dev/null +++ b/gulp/tasks/babel.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('babel', () => { + return gulp.src('./src/js/Tags.js', {read: false}) + .pipe($.shell('babel src/js --out-dir dist-components')); + }); +}; diff --git a/gulp/tasks/clean-css.js b/gulp/tasks/clean-css.js new file mode 100644 index 0000000..c887a4c --- /dev/null +++ b/gulp/tasks/clean-css.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('cleanCSS', () => { + return gulp.src('./dist/react-tags.css') + .pipe($.cleanCss({ + debug: true + }, details => { + $.util.log($.util.colors.red.bold(`${details.name} -> original file size: ${details.stats.originalSize}`)); + $.util.log($.util.colors.green.bold(`${details.name} -> minified file size: ${details.stats.minifiedSize}`)); + })) + .pipe($.rename({extname: '.min.css'})) + .pipe($.filelog('minify CSS')) + .pipe(gulp.dest('./dist/')); + }); +}; diff --git a/gulp/tasks/eslint.js b/gulp/tasks/eslint.js new file mode 100644 index 0000000..e54e1dd --- /dev/null +++ b/gulp/tasks/eslint.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('eslint', () => { + return gulp.src('./src/**/*.js', {read: false}) + .pipe($.shell('eslint --fix --ignore-path .eslintignore ./src/js/!*')) + .pipe($.filelog('Fixing JS')); + }); +}; diff --git a/gulp/tasks/sass.js b/gulp/tasks/sass.js new file mode 100644 index 0000000..a5dd6d1 --- /dev/null +++ b/gulp/tasks/sass.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('sass', ['scss-lint'], () => { + return gulp.src('./src/scss/**/*.scss') + .pipe($.sass().on('error', $.sass.logError)) + .pipe(gulp.dest('./dist')) + .pipe($.filelog('SASS')); + }); +}; diff --git a/gulp/tasks/scss-lint.js b/gulp/tasks/scss-lint.js new file mode 100644 index 0000000..dd97d45 --- /dev/null +++ b/gulp/tasks/scss-lint.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('scss-lint', function() { + return gulp.src('./src/scss/**/*.scss') + .pipe($.scssLint({ + config: './.scsslint.yml', + customReport: $.scssLintStylish + })) + .pipe($.scssLint.failReporter('E')) + .pipe($.filelog('SCSS lint')); + }); +}; diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js new file mode 100644 index 0000000..3894dc9 --- /dev/null +++ b/gulp/tasks/watch.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('watch', () => { + gulp.watch(['./src/js/**/*.js'], ['eslint']); + gulp.watch(['./src/scss/**/*.scss'], ['scss-lint']); + }); +}; diff --git a/gulp/tasks/webpack.js b/gulp/tasks/webpack.js new file mode 100644 index 0000000..515d2e5 --- /dev/null +++ b/gulp/tasks/webpack.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = (gulp, $) => { + gulp.task('webpack', () => { + return gulp.src('./src/js/Tags.js', {read: false}) + .pipe($.shell('webpack --display-chunks --display-modules --progress --colors')); + }); +}; diff --git a/gulpfile.babel.js b/gulpfile.babel.js new file mode 100644 index 0000000..ace0de0 --- /dev/null +++ b/gulpfile.babel.js @@ -0,0 +1,29 @@ +'use strict'; + +import gulp from 'gulp'; +import gulpLoadPlugins from 'gulp-load-plugins'; + +const $ = gulpLoadPlugins({pattern: ['gulp-*', 'run-sequence', 'minimist'], scope: ['devDependencies']}); +const argv = $.minimist(process.argv.slice(2)); + +require('./gulp/bridge.js')(gulp, [ + 'eslint', + 'webpack', + 'babel', + 'scss-lint', + 'sass', + 'clean-css', + 'watch' +], $); + +gulp.task('default', cb => { + $.runSequence('sass', 'cleanCSS', 'eslint', 'webpack', 'babel', () => { + $.util.log($.util.colors.green.bold('FINISHED BUILD')); + + if(argv.w){ + $.runSequence('watch'); + } + + cb(); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..5ef67ec --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "react-tags", + "version": "1.0.0", + "description": "Simple tagging component", + "main": "dist-components/Tags.js", + "repository": { + "type": "git", + "url": "https://github.com/JFusco/react-tags" + }, + "peerDependencies": { + "react": "^15.2.1", + "react-addons-update": "^15.2.1" + }, + "devDependencies": { + "babel-cli": "^6.10.1", + "babel-core": "^6.10.4", + "babel-loader": "^6.2.4", + "babel-preset-es2015": "^6.9.0", + "babel-preset-react": "^6.11.1", + "babel-register": "^6.9.0", + "babel-eslint": "~6.0.4", + "gulp-sass": "^2.3.2", + "gulp-shell": "^0.5.2", + "http-server": "^0.9.0", + "react": "^15.2.1", + "react-addons-update": "^15.2.1", + "webpack": "^1.13.1", + "gulp": "^3.9.1", + "gulp-rename": "^1.2.2", + "gulp-eslint": "~2.0.0", + "gulp-load-plugins": "~1.2.4", + "gulp-clean-css": "^2.0.11", + "gulp-scss-lint": "^0.3.9", + "gulp-scss-lint-stylish": "^1.0.1", + "gulp-util": "~3.0.7", + "gulp-watch": "~4.3.6", + "run-sequence": "^1.2.1", + "gulp-filelog": "~0.4.1", + "minimist": "~1.2.0" + } +} diff --git a/src/js/Tag.js b/src/js/Tag.js new file mode 100644 index 0000000..8b82af6 --- /dev/null +++ b/src/js/Tag.js @@ -0,0 +1,43 @@ +'use strict'; + +import React from 'react'; + +const Tag = props => { + const onRemoveClick = e => { + e.preventDefault(); + + props.removeTag(); + }; + + const removeIcon = () => { + if (props.readOnly) return null; + + return ( + + {props.removeTagIcon} + + ); + }; + + return ( + + {/* Tag name */} + {props.name} + + {/* Tag remove icon */} + {removeIcon()} + + ); +}; + +export default Tag; + +Tag.propTypes = { + name: React.PropTypes.string.isRequired, + removeTag: React.PropTypes.func.isRequired, + readOnly: React.PropTypes.bool, + removeTagIcon: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.element + ]) +}; diff --git a/src/js/Tags.js b/src/js/Tags.js new file mode 100644 index 0000000..42a0b28 --- /dev/null +++ b/src/js/Tags.js @@ -0,0 +1,151 @@ +'use strict'; + +import React, {Component} from 'react'; +import update from 'react-addons-update'; +import Tag from './tag'; + +const map = new WeakMap(); + +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: + if (this.state.tags.length === 0) return; + + if (this.empty){ + this.removeTag(this.state.tags.length - 1); + } + + if (this.input.value.length === 0){ + this.empty = true; + } + break; + + case 13: + this.addTag(); + break; + } + } + + addTag(){ + const value = this.input.value.trim(); + + this.setState({ + tags: update(this.state.tags, {$push: [value] }) + }, () => { + if (typeof this.props.change !== 'undefined'){ + this.props.change(this.state.tags); + } + + if (typeof this.props.added !== 'undefined'){ + this.props.added(value); + } + + this.empty = true; + + this.input.value = ''; + }); + } + + removeTag(index){ + const value = this.state.tags[index]; + + this.setState({ + tags: update(this.state.tags, {$splice: [[index, 1]] }) + }, () => { + if (typeof this.props.change !== 'undefined'){ + this.props.change(this.state.tags); + } + + if (typeof this.props.removed !== 'undefined'){ + this.props.removed(value); + } + }); + } + + 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 classNames = () => { + const classes = 'tags-container'; + + return this.props.readOnly ? `${classes} readonly` : classes; + }; + + return ( +
+
+ {tagItems} +
+ + {tagInput} +
+ ); + } +} + +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) +}; diff --git a/src/scss/_colorpalette.scss b/src/scss/_colorpalette.scss new file mode 100644 index 0000000..5c1385c --- /dev/null +++ b/src/scss/_colorpalette.scss @@ -0,0 +1,8 @@ +//** +// React tags colors +//** + +$white: #FFF; +$light-gray: #D9D8DD; +$dark-gray: #626166; +$black: #000; diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss new file mode 100644 index 0000000..a7f77ec --- /dev/null +++ b/src/scss/_variables.scss @@ -0,0 +1,24 @@ +//** +// React tags variables +//-- Variables may be overridden +//** + +//-- Global UI +$tag-base-height: 26px !default; +$tag-base-font-size: 12px !default; +$tag-base-border-radius: 12px !default; +$tag-base-font-color: $dark-gray !default; +$tag-base-margin: 5px !default; +$tag-base-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !default; + +//-- Tags +$tag-background-color: $light-gray !default; +$tag-background-hover-color: darken($light-gray, 6%) !default; +$tag-remove-color: $black !default; +$tag-remove-font-size: $tag-base-font-size !default; +$tag-remove-hover-color: lighten($tag-remove-color, 40%) !default; + +//-- Input +$tag-input-bg-color: $white !default; +$tag-input-border: 1px solid $light-gray !default; +$tag-input-placeholder-color: rgba($dark-gray, 0.5) !default; diff --git a/src/scss/react-tags.scss b/src/scss/react-tags.scss new file mode 100644 index 0000000..f04cba7 --- /dev/null +++ b/src/scss/react-tags.scss @@ -0,0 +1,86 @@ +//** +// React tags component +//** + +@import +"colorpalette", +"variables"; + +%tags-ui-base { + font-size: $tag-base-font-size; + height: $tag-base-height; + line-height: $tag-base-height; + color: $tag-base-font-color; + border-radius: $tag-base-border-radius; + font-family: $tag-base-font-family; + display: inline-block; + padding: 0 9px; +} + +.react-tags { + input { + @extend %tags-ui-base; + + $height: $tag-base-height - 2; + + background: $tag-input-bg-color; + border: $tag-input-border; + height: $height; + line-height: $height; + margin-top: $tag-base-margin; + + &::-webkit-input-placeholder { + color: $tag-input-placeholder-color; + } + + &::-moz-placeholder { + color: $tag-input-placeholder-color; + } + + &:-ms-input-placeholder { + color: $tag-input-placeholder-color; + } + + &:focus { + outline: none; + } + } + + .tags-container { + display: inline; + + > span { + @extend %tags-ui-base; + + background: $tag-background-color; + margin: $tag-base-margin $tag-base-margin 0 0; + transition: background 0.3s ease; + cursor: default; + + &:hover { + background: $tag-background-hover-color; + } + } + + a { + font-size: $tag-remove-font-size; + color: $tag-remove-color; + transition: color 0.3s ease; + display: inline-block; + margin-left: 7px; + text-decoration: none; + + &:hover { + color: $tag-remove-hover-color; + } + } + + i { + font-style: normal; + } + + &.readonly { + pointer-events: none; + } + } +} diff --git a/webpack.config.babel.js b/webpack.config.babel.js new file mode 100644 index 0000000..a29ee3e --- /dev/null +++ b/webpack.config.babel.js @@ -0,0 +1,48 @@ +'use strict'; + +import webpack from 'webpack'; +import path from 'path'; + +const paths = { + output: path.join(__dirname, '/dist'), + src: './src/js' +}; + +export default { + entry: `${paths.src}/Tags.js`, + output: { + path: paths.output, + filename: 'react-tags.min.js', + libraryTarget: 'umd', + library: 'Tags' + }, + module: { + loaders: [ + { + loader: 'babel', + exclude: '/node_modules/', + test: /\.js?$/ + } + ] + }, + externals: { + 'react': 'React', + 'react-addons-update': 'update' + }, + plugins: [ + new webpack.optimize.UglifyJsPlugin({ + mangle: true, + compress: { + warnings: false + }, + output: { + comments: false + } + }), + new webpack.NoErrorsPlugin(), + new webpack.optimize.DedupePlugin() + ], + resolve: { + extensions: ['', '.js'] + } +};