├── .eslintignore ├── .gitignore ├── .npmignore ├── README.md ├── build ├── docs.config.js └── lib.config.js ├── docs ├── .htaccess ├── App.vue ├── app-style.scss ├── assets │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── OxygenMono-Regular.ttf │ │ ├── README.txt │ │ ├── Raleway-Regular.ttf │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-MediumItalic.ttf │ │ ├── Roboto-Regular.ttf │ │ └── material-icons.woff2 │ └── img │ │ ├── android-logo.png │ │ ├── apple-logo.png │ │ └── github.png ├── colors.scss ├── components │ ├── auto-docs-item.vue │ ├── breaking-changes.vue │ ├── compare.vue │ ├── el-code.vue │ ├── el-navbar.vue │ ├── generic-api-page.vue │ ├── icons │ │ ├── chrome.vue │ │ ├── edge.vue │ │ ├── firefox.vue │ │ ├── github.vue │ │ ├── ie.vue │ │ ├── opera.vue │ │ └── safari.vue │ └── slot-docs-item.vue ├── docs-formatter.js ├── docs-loader.js ├── fonts.css ├── index.html ├── main.js ├── pages │ ├── api │ │ ├── create-tags-helper.vue │ │ └── index.js │ ├── changelog.vue │ ├── examples │ │ ├── autocomplete │ │ │ ├── example1.demo.html │ │ │ ├── example1.vue │ │ │ ├── example2.demo.html │ │ │ ├── example2.vue │ │ │ └── index.vue │ │ ├── hooks │ │ │ ├── example1.demo.html │ │ │ ├── example1.demo.js │ │ │ ├── example1.vue │ │ │ ├── example2.demo.html │ │ │ ├── example2.demo.js │ │ │ ├── example2.vue │ │ │ └── index.vue │ │ ├── index.js │ │ ├── nuxt.vue │ │ ├── styling │ │ │ ├── example1.demo.html │ │ │ ├── example1.demo.js │ │ │ └── index.vue │ │ ├── templates │ │ │ ├── example1.demo.html │ │ │ ├── example1.demo.js │ │ │ ├── example1.vue │ │ │ ├── example2.demo.css │ │ │ ├── example2.demo.html │ │ │ ├── example2.demo.js │ │ │ ├── example2.vue │ │ │ ├── example3.demo.html │ │ │ ├── example3.vue │ │ │ ├── example4.demo.html │ │ │ ├── example4.demo.js │ │ │ ├── example4.vue │ │ │ └── index.vue │ │ └── validation │ │ │ ├── example1.demo.html │ │ │ ├── example1.demo.js │ │ │ └── index.vue │ ├── getting-started.demo.html │ ├── getting-started.vue │ ├── migration.vue │ ├── project-features.vue │ └── to-develop.vue ├── router.js ├── slots-data.js └── vue-tags-input-dark.scss ├── e2e ├── .eslintrc ├── add-save-on-key.test.js ├── autocomplete.test.js ├── check-navigation.test.js ├── edit-tag.test.js ├── getting-started.test.js ├── hooks.test.js ├── suite │ ├── add-save-on-key.vue │ ├── autocomplete.vue │ ├── edit-tag.vue │ ├── hooks.vue │ ├── index.js │ └── validation.vue └── validation.test.js ├── package-lock.json ├── package.json └── vue-tags-input ├── assets └── fonts │ ├── icomoon.eot │ ├── icomoon.ttf │ └── icomoon.woff ├── create-tags.js ├── publish.js ├── tag-input.vue ├── vue-tags-input.js ├── vue-tags-input.props.js ├── vue-tags-input.scss └── vue-tags-input.vue /.eslintignore: -------------------------------------------------------------------------------- 1 | *.demo.* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | docs-dist/ 5 | npm-debug.log 6 | yarn-error.log 7 | tmp 8 | screenshots 9 | 10 | # Editor directories and files 11 | .idea 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # we need something here to publish everything 2 | .git 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-tags-input 2 | 3 | A tags input component for VueJS with autocompletion, custom validation, templating and much more 4 | 5 | [Demo & Docs](http://www.vue-tags-input.com) 6 | 7 | ## Features 8 | 9 | * No dependencies 10 | * Custom validation rules 11 | * Hooks: Before adding, Before deleting ... 12 | * Edit tags after creation 13 | * Fast setup 14 | * Works with Vuex 15 | * Small size: 34kb minified (css included) | gzipped 9kb 16 | * Autocompletion 17 | * Many customization options 18 | * Own templates 19 | * Delete tags on backspace 20 | * Add tags on paste 21 | * Examples & Docs 22 | 23 | ## Install 24 | 25 | NPM 26 | ``` 27 | npm install @johmun/vue-tags-input 28 | ``` 29 | 30 | CDN 31 | ``` 32 | 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```html 38 | 47 | ``` 48 | 49 | ```javascript 50 | 65 | ``` 66 | 67 | ## License 68 | 69 | [MIT](https://opensource.org/licenses/MIT) 70 | 71 | Copyright (c) 2019 Johannes Munari 72 | -------------------------------------------------------------------------------- /build/docs.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CleanPlugin = require('clean-webpack-plugin'); 6 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 7 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 8 | const ip = require('ip'); 9 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 10 | const mode = process.env.NODE_ENV === 'development' ? 'development' : 'production'; 11 | 12 | const resolve = src => path.resolve(__dirname, src); 13 | const port = 3000; 14 | 15 | module.exports = { 16 | mode, 17 | entry: ['@babel/polyfill', resolve('../docs/main.js')], 18 | output: { 19 | path: resolve('../docs-dist'), 20 | filename: '[name].[hash].js', 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.demo\./, 26 | use: 'raw-loader', 27 | }, 28 | { 29 | test: /\.vue$/, 30 | loader: 'vue-loader', 31 | }, 32 | { 33 | test: /\.js$/, 34 | exclude: /(node_modules|\.demo\.)/, 35 | use: { 36 | loader: 'babel-loader', 37 | options: { 38 | presets: ['@babel/preset-env'], 39 | plugins: ['@babel/plugin-proposal-object-rest-spread'], 40 | }, 41 | }, 42 | }, 43 | { 44 | test: /\.(scss|css)$/, 45 | exclude: /\.demo\./, 46 | use: [ 47 | 'vue-style-loader', 48 | { 49 | loader: 'css-loader', 50 | options: { 51 | importLoaders: 1, 52 | sourceMap: true, 53 | }, 54 | }, 55 | { 56 | loader: 'postcss-loader', 57 | options: { 58 | plugins: () => [ 59 | require('autoprefixer')(), 60 | ], 61 | sourceMap: true, 62 | }, 63 | }, 64 | 'sass-loader', 65 | ], 66 | }, 67 | { 68 | test: /\.(woff|woff2|eot|ttf|mp3|mp4|webm)$/, 69 | loader: 'file-loader?name=[path][name]-[hash].[ext]', 70 | }, 71 | { 72 | test: /\.(gif|png|jpe?g|svg|webp)$/i, 73 | use: [ 74 | 'file-loader', 75 | { 76 | loader: 'image-webpack-loader', 77 | options: mode === 'production' ? { 78 | mozjpeg: { 79 | progressive: true, 80 | quality: 75, 81 | }, 82 | optipng: { 83 | enabled: false, 84 | }, 85 | pngquant: { 86 | quality: '75-90', 87 | speed: 4, 88 | }, 89 | } : { disable: true }, 90 | }, 91 | ], 92 | }, 93 | ], 94 | }, 95 | plugins: [ 96 | new VueLoaderPlugin(), 97 | new CopyWebpackPlugin([{ from: resolve('../docs/.htaccess') }]), 98 | new CleanPlugin(['../docs-dist'], { allowExternal: true }), 99 | new HtmlWebpackPlugin({ 100 | template: resolve('../docs/index.html'), 101 | }), 102 | new webpack.DefinePlugin({ 103 | 'process.env': { 104 | NODE_ENV: JSON.stringify(mode), 105 | }, 106 | }), 107 | ], 108 | resolve: { 109 | extensions: ['.js', '.vue'], 110 | alias: { 111 | '@components': resolve('../docs/components'), 112 | '@johmun/vue-tags-input': resolve('../vue-tags-input/vue-tags-input.vue'), 113 | '@tag-input': resolve('../vue-tags-input/tag-input.vue'), 114 | 'colors': resolve('../docs/colors.scss'), 115 | 'vue$': 'vue/dist/vue.esm.js', 116 | }, 117 | }, 118 | devServer: { 119 | historyApiFallback: true, 120 | noInfo: true, 121 | port, 122 | public: 'localhost:' + port, 123 | host: '0.0.0.0', 124 | after() { 125 | console.log('\nServing on: ', 'localhost:' + port); 126 | console.log('IP: ', `${ip.address()}:${port}`); 127 | }, 128 | }, 129 | performance: { 130 | hints: false, 131 | }, 132 | devtool: '#eval-source-map', 133 | optimization: { 134 | splitChunks: { 135 | cacheGroups: { 136 | commons: { 137 | test: /[\\/]node_modules[\\/]/, 138 | name: 'vendors', 139 | chunks: 'all', 140 | }, 141 | }, 142 | }, 143 | }, 144 | }; 145 | 146 | if (mode === 'production') { 147 | module.exports.devtool = '#source-map'; 148 | module.exports.plugins = (module.exports.plugins || []).concat([ 149 | new webpack.LoaderOptionsPlugin({ minimize: true }), 150 | ]); 151 | } 152 | 153 | if (process.env.ANALYZE === 'true') { 154 | module.exports.plugins = (module.exports.plugins || []).concat([ 155 | new BundleAnalyzerPlugin(), 156 | ]); 157 | } 158 | -------------------------------------------------------------------------------- /build/lib.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const CleanPlugin = require('clean-webpack-plugin'); 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 5 | 6 | const resolve = src => path.resolve(__dirname, src); 7 | 8 | module.exports = { 9 | mode: 'production', 10 | entry: [resolve('../vue-tags-input/publish.js')], 11 | output: { 12 | path: resolve('../dist'), 13 | publicPath: '/dist/', 14 | filename: 'vue-tags-input.js', 15 | library: 'vueTagsInput', 16 | libraryTarget: 'umd', 17 | }, 18 | externals:{ 19 | vue: 'vue', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.vue$/, 25 | loader: 'vue-loader', 26 | }, 27 | { 28 | test: /\.js$/, 29 | exclude: /(node_modules)/, 30 | use: { 31 | loader: 'babel-loader', 32 | options: { 33 | plugins: ['@babel/plugin-proposal-object-rest-spread'], 34 | presets: ['@babel/preset-env'] 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.(scss|css)$/, 40 | use: [ 41 | 'vue-style-loader', 42 | { 43 | loader: 'css-loader', 44 | options: { 45 | importLoaders: 1, 46 | sourceMap: true, 47 | }, 48 | }, 49 | { 50 | loader: 'postcss-loader', 51 | options: { 52 | plugins: () => [ 53 | require('autoprefixer')(), 54 | ], 55 | sourceMap: true, 56 | }, 57 | }, 58 | 'sass-loader', 59 | ], 60 | }, 61 | { 62 | test: /\.(ttf|eot|woff|woff2|otf)$/, 63 | loader: 'url-loader', 64 | options: { 65 | limit: 100000, 66 | }, 67 | }, 68 | ], 69 | }, 70 | plugins: [ 71 | new VueLoaderPlugin(), 72 | new CleanPlugin(['../dist'], { allowExternal: true }), 73 | ], 74 | resolve: { 75 | extensions: ['.js', '.vue'], 76 | alias: { 77 | 'vue$': 'vue/dist/vue.esm.js', 78 | }, 79 | }, 80 | devtool: '#source-map', 81 | optimization: { 82 | minimize: true, 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /docs/.htaccess: -------------------------------------------------------------------------------- 1 | FileETag MTime Size 2 | 3 | ExpiresActive On 4 | ExpiresByType text/css "access plus 1 weeks" 5 | ExpiresByType application/javascript "access plus 1 weeks" 6 | ExpiresByType application/x-javascript "access plus 1 weeks" 7 | ExpiresByType image/gif "access plus 1 months" 8 | ExpiresByType image/jpeg "access plus 1 months" 9 | ExpiresByType image/png "access plus 1 months" 10 | ExpiresByType image/x-icon "access plus 1 months" 11 | 12 | 13 | AddOutputFilterByType DEFLATE text/plain 14 | AddOutputFilterByType DEFLATE text/html 15 | AddOutputFilterByType DEFLATE text/xml 16 | AddOutputFilterByType DEFLATE text/css 17 | AddOutputFilterByType DEFLATE application/xml 18 | AddOutputFilterByType DEFLATE application/xhtml+xml 19 | AddOutputFilterByType DEFLATE application/rss+xml 20 | AddOutputFilterByType DEFLATE application/javascript 21 | AddOutputFilterByType DEFLATE application/x-javascript 22 | 23 | -------------------------------------------------------------------------------- /docs/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 64 | 65 | 220 | 221 | 241 | -------------------------------------------------------------------------------- /docs/app-style.scss: -------------------------------------------------------------------------------- 1 | @import "~colors"; 2 | 3 | *, *:before, *:after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | margin: 0px; 9 | padding: 0px; 10 | height: 100%; 11 | outline: none; 12 | } 13 | 14 | body { 15 | font-family: 'Roboto', sans-serif; 16 | background: $dark; 17 | overflow-y: hidden; 18 | box-sizing: border-box; 19 | font-size: 16px; 20 | line-height: 24px; 21 | color: $lightestGrey; 22 | } 23 | 24 | ul { 25 | margin: 0px; 26 | padding: 0px; 27 | list-style-type: none; 28 | } 29 | 30 | a { 31 | font-weight: bold; 32 | color: inherit; 33 | cursor: pointer; 34 | } 35 | 36 | a:focus { 37 | outline: none; 38 | } 39 | 40 | input:focus { 41 | outline: none; 42 | } 43 | 44 | ::-moz-selection { 45 | color: #283944; 46 | background: yellow; 47 | } 48 | ::selection { 49 | color: #283944; 50 | background: yellow; 51 | } 52 | 53 | h1, h2, h3, h4 { 54 | color: $lightGrey; 55 | font-family: 'Roboto', sans-serif; 56 | font-weight: 500; 57 | } 58 | 59 | h1, h2 { 60 | font-family: 'Raleway', sans-serif; 61 | margin-bottom: 15px; 62 | color: #fff; 63 | } 64 | 65 | h1 { 66 | margin-top: 42px; 67 | line-height: 1.1em; 68 | } 69 | 70 | h2 { 71 | margin-top: 35px; 72 | } 73 | 74 | .code { 75 | background-color: $middle; 76 | color: $lightestGrey; 77 | font-size: 0.9em; 78 | padding: 2px 3px; 79 | font-family: 'Oxygen Mono', monospace; 80 | } 81 | 82 | .content { 83 | max-width: 960px; 84 | } 85 | 86 | @media (max-width: 940px) { 87 | body { 88 | font-size: 14px; 89 | line-height: 21px; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/assets/fonts/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/assets/fonts/OxygenMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/OxygenMono-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Raleway Variable Font 2 | ===================== 3 | 4 | This download contains Raleway as both variable fonts and static fonts. 5 | 6 | Raleway is a variable font with this axis: 7 | wght 8 | 9 | This means all the styles are contained in these files: 10 | Raleway-VariableFont_wght.ttf 11 | Raleway-Italic-VariableFont_wght.ttf 12 | 13 | If your app fully supports variable fonts, you can now pick intermediate styles 14 | that aren’t available as static fonts. Not all apps support variable fonts, and 15 | in those cases you can use the static font files for Raleway: 16 | static/Raleway-Thin.ttf 17 | static/Raleway-ExtraLight.ttf 18 | static/Raleway-Light.ttf 19 | static/Raleway-Regular.ttf 20 | static/Raleway-Medium.ttf 21 | static/Raleway-SemiBold.ttf 22 | static/Raleway-Bold.ttf 23 | static/Raleway-ExtraBold.ttf 24 | static/Raleway-Black.ttf 25 | static/Raleway-ThinItalic.ttf 26 | static/Raleway-ExtraLightItalic.ttf 27 | static/Raleway-LightItalic.ttf 28 | static/Raleway-Italic.ttf 29 | static/Raleway-MediumItalic.ttf 30 | static/Raleway-SemiBoldItalic.ttf 31 | static/Raleway-BoldItalic.ttf 32 | static/Raleway-ExtraBoldItalic.ttf 33 | static/Raleway-BlackItalic.ttf 34 | 35 | Get started 36 | ----------- 37 | 38 | 1. Install the font files you want to use 39 | 40 | 2. Use your app's font picker to view the font family and all the 41 | available styles 42 | 43 | Learn more about variable fonts 44 | ------------------------------- 45 | 46 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts 47 | https://variablefonts.typenetwork.com 48 | https://medium.com/variable-fonts 49 | 50 | In desktop apps 51 | 52 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc 53 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts 54 | 55 | Online 56 | 57 | https://developers.google.com/fonts/docs/getting_started 58 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide 59 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts 60 | 61 | Installing fonts 62 | 63 | MacOS: https://support.apple.com/en-us/HT201749 64 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux 65 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows 66 | 67 | Android Apps 68 | 69 | https://developers.google.com/fonts/docs/android 70 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts 71 | 72 | License 73 | ------- 74 | Please read the full license text (OFL.txt) to understand the permissions, 75 | restrictions and requirements for usage, redistribution, and modification. 76 | 77 | You can use them in your products & projects – print or digital, 78 | commercial or otherwise. 79 | 80 | This isn't legal advice, please consider consulting a lawyer and see the full 81 | license for all details. 82 | -------------------------------------------------------------------------------- /docs/assets/fonts/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Raleway-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-Black.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/fonts/material-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/fonts/material-icons.woff2 -------------------------------------------------------------------------------- /docs/assets/img/android-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/img/android-logo.png -------------------------------------------------------------------------------- /docs/assets/img/apple-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/img/apple-logo.png -------------------------------------------------------------------------------- /docs/assets/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/docs/assets/img/github.png -------------------------------------------------------------------------------- /docs/colors.scss: -------------------------------------------------------------------------------- 1 | $primary: #ebde6e; 2 | $accent: #b54588; 3 | $error: #e54d42; 4 | $success: #68cd86; 5 | $warn: #ffb648; 6 | $lightestGrey: #b7c4c9; 7 | $lightGrey: #a4b1b6; 8 | $grey: #8b9396; 9 | $middle: #324652; 10 | $dark: #283944; 11 | $darker: #1e2a31; 12 | -------------------------------------------------------------------------------- /docs/components/auto-docs-item.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 141 | 142 | 208 | -------------------------------------------------------------------------------- /docs/components/breaking-changes.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 26 | 27 | 66 | -------------------------------------------------------------------------------- /docs/components/compare.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | 36 | 84 | -------------------------------------------------------------------------------- /docs/components/el-code.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 40 | 67 | -------------------------------------------------------------------------------- /docs/components/el-navbar.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 150 | 151 | 308 | -------------------------------------------------------------------------------- /docs/components/generic-api-page.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /docs/components/icons/edge.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /docs/components/icons/github.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 20 | -------------------------------------------------------------------------------- /docs/components/icons/opera.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /docs/components/icons/safari.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /docs/components/slot-docs-item.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 53 | 54 | 103 | -------------------------------------------------------------------------------- /docs/docs-formatter.js: -------------------------------------------------------------------------------- 1 | import flatten from 'lodash/flatten'; 2 | import uniq from 'lodash/uniq'; 3 | import mapKeys from 'lodash/mapKeys'; 4 | import mapValues from 'lodash/mapValues'; 5 | import slots from './slots-data'; 6 | 7 | export const mergeDocs = (docA, docB) => { 8 | return docB.map(entry => { 9 | const replace = docA.find(aEntry => aEntry.name === entry.name); 10 | return replace ? replace : entry; 11 | }); 12 | }; 13 | 14 | export const format = unformatted => { 15 | const types = uniq(flatten( 16 | unformatted.map(i => { 17 | const property = i.tags.find(t => t.title === 'property'); 18 | if (!property) { 19 | console.log('Missing property at:', i); 20 | throw `Expected @property to be set for documentation purposes. 21 | Possible values are: [props|events|helpers] e.g. @property {props}. 22 | If your comment should not show up in the documentation, avoid JSDoc syntax`; 23 | } 24 | return property.type.name; 25 | })) 26 | ); 27 | 28 | const keyed = mapKeys(types, t => t); 29 | const formatted = mapValues(keyed, k => { 30 | return unformatted.filter(i => i.tags.find(t => t.title === 'property').type.name === k); 31 | }); 32 | 33 | return { ...formatted, slots }; 34 | }; 35 | -------------------------------------------------------------------------------- /docs/docs-loader.js: -------------------------------------------------------------------------------- 1 | const documentation = require('documentation'); 2 | const path = require('path'); 3 | 4 | module.exports = function() { 5 | const callback = this.async(); 6 | const filepath = path.relative(process.cwd(), this.resourcePath); 7 | 8 | documentation.build(['./' + filepath], { 9 | extension: ['js', 'vue'], 10 | }).then(documentation.formats.json) 11 | .then(res => callback(null, 'module.exports = ' + res + ';')); 12 | }; 13 | -------------------------------------------------------------------------------- /docs/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url('./assets/fonts/material-icons.woff2') format('woff2'); 6 | } 7 | 8 | .material-icons { 9 | font-family: 'Material Icons'; 10 | font-weight: normal; 11 | font-style: normal; 12 | font-size: 24px; 13 | line-height: 1; 14 | letter-spacing: normal; 15 | text-transform: none; 16 | display: inline-block; 17 | white-space: nowrap; 18 | word-wrap: normal; 19 | direction: ltr; 20 | -webkit-font-feature-settings: 'liga'; 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | 24 | /* roboto-300 - latin */ 25 | @font-face { 26 | font-family: 'Roboto'; 27 | font-style: normal; 28 | font-weight: 300; 29 | src: url('./assets/fonts/Roboto-Light.ttf') format('truetype'), 30 | 31 | } 32 | /* roboto-300italic - latin */ 33 | @font-face { 34 | font-family: 'Roboto'; 35 | font-style: italic; 36 | font-weight: 300; 37 | src: url('./assets/fonts/Roboto-Italic.ttf') format('truetype'), 38 | } 39 | /* roboto-regular - latin */ 40 | @font-face { 41 | font-family: 'Roboto'; 42 | font-style: normal; 43 | font-weight: 400; 44 | src: url('./assets/fonts/Roboto-Regular.ttf') format('truetype'), 45 | } 46 | /* roboto-italic - latin */ 47 | @font-face { 48 | font-family: 'Roboto'; 49 | font-style: italic; 50 | font-weight: 400; 51 | src: url('./assets/fonts/Roboto-Italic.ttf') format('truetype'), 52 | } 53 | 54 | @font-face { 55 | font-family: 'Roboto'; 56 | font-style: normal; 57 | font-weight: 500; 58 | src: url('./assets/fonts/Roboto-Medium.ttf') format('truetype'), 59 | } 60 | 61 | @font-face { 62 | font-family: 'Roboto'; 63 | font-style: italic; 64 | font-weight: 500; 65 | src: url('./assets/fonts/Roboto-MediumItalic.ttf') format('truetype'), 66 | } 67 | 68 | /* roboto-700 - latin */ 69 | @font-face { 70 | font-family: 'Roboto'; 71 | font-style: normal; 72 | font-weight: 700; 73 | src: url('./assets/fonts/Roboto-Bold.ttf') format('truetype'), 74 | } 75 | 76 | /* roboto-700italic - latin */ 77 | @font-face { 78 | font-family: 'Roboto'; 79 | font-style: italic; 80 | font-weight: 700; 81 | src: url('./assets/fonts/Roboto-BoldItalic.ttf') format('truetype'), 82 | } 83 | 84 | /* raleway */ 85 | @font-face { 86 | font-family: 'Raleway'; 87 | font-style: normal; 88 | font-weight: 400; 89 | src: url('./assets/fonts/Raleway-Regular.ttf') format('truetype'), 90 | } 91 | 92 | /* oxygen */ 93 | @font-face { 94 | font-family: 'Oxygen Mono'; 95 | font-style: normal; 96 | font-weight: 500; 97 | src: url('./assets/fonts/OxygenMono-Regular.ttf') format('truetype'), 98 | } 99 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | vue-tags-input | A tags input component for VueJs with autocompletion, custom validation, templating and much more 16 | 17 | 18 | 19 | 24 | 25 | 26 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/main.js: -------------------------------------------------------------------------------- 1 | import 'normalize-css'; 2 | import './fonts.css'; 3 | import Vue from 'vue'; 4 | import App from './App.vue'; 5 | import router from './router'; 6 | import { format, mergeDocs } from './docs-formatter'; 7 | 8 | // require the docs file which contains all the information 9 | const docs = require('!!./docs-loader!../vue-tags-input/vue-tags-input.js'); 10 | 11 | // we require the props file extra and merge it later, 12 | // then the webpack "browser reload on file change" works with this file, too 13 | const props = require('!!./docs-loader!../vue-tags-input/vue-tags-input.props.js'); 14 | 15 | const merged = mergeDocs(props, docs); 16 | window.docs = format(merged); 17 | 18 | new Vue({ 19 | el: '#app', 20 | render: h => h(App), 21 | router, 22 | }); 23 | -------------------------------------------------------------------------------- /docs/pages/api/create-tags-helper.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 52 | 53 | 55 | -------------------------------------------------------------------------------- /docs/pages/api/index.js: -------------------------------------------------------------------------------- 1 | import CreateTagsHelper from './create-tags-helper'; 2 | import GenericApiPage from '@components/generic-api-page'; 3 | 4 | const apiRoutes = ['events', 'props', 'slots'].map(type => { 5 | return { 6 | path: `/api/${type}`, 7 | name: `api.${type}`, 8 | component: GenericApiPage, 9 | meta: { type }, 10 | }; 11 | }); 12 | 13 | export default [ 14 | ...apiRoutes, 15 | { 16 | path: '/api/create-tags-helper', 17 | name: 'api.create-tags-helper', 18 | component: CreateTagsHelper, 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /docs/pages/changelog.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 100 | 101 | 111 | -------------------------------------------------------------------------------- /docs/pages/examples/autocomplete/example1.demo.html: -------------------------------------------------------------------------------- 1 | 11 | 12 | 45 | -------------------------------------------------------------------------------- /docs/pages/examples/autocomplete/example1.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /docs/pages/examples/autocomplete/example2.demo.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 54 | -------------------------------------------------------------------------------- /docs/pages/examples/autocomplete/example2.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 73 | 74 | 89 | -------------------------------------------------------------------------------- /docs/pages/examples/autocomplete/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/example1.demo.html: -------------------------------------------------------------------------------- 1 | 7 |
8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/example1.demo.js: -------------------------------------------------------------------------------- 1 | /* Other stuff before like import tagsinput ... */ 2 | data() { 3 | return { 4 | tag: '', 5 | tags: [], 6 | handlers: [], 7 | }; 8 | }, 9 | methods: { 10 | cancel() { 11 | // for some reason we need nextTick here 12 | this.$nextTick(() => this.handlers = []); 13 | this.tag = ''; 14 | }, 15 | add() { 16 | this.handlers.forEach(h => h()); 17 | this.$nextTick(() => this.handlers = []); 18 | }, 19 | }, 20 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/example1.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 67 | 68 | 78 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/example2.demo.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/example2.demo.js: -------------------------------------------------------------------------------- 1 | /* Other stuff before like template, data, import tagsinput ... */ 2 | 3 | methods: { 4 | checkTag(obj) { 5 | if (obj.tag.text.includes('e')) alert('Letter "e" is forbidden'); 6 | else obj.addTag(); 7 | }, 8 | }, 9 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/example2.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 45 | -------------------------------------------------------------------------------- /docs/pages/examples/hooks/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | -------------------------------------------------------------------------------- /docs/pages/examples/index.js: -------------------------------------------------------------------------------- 1 | import Styling from './styling/'; 2 | import Autocomplete from './autocomplete/'; 3 | import Validation from './validation/'; 4 | import Hooks from './hooks/'; 5 | import Templates from './templates/'; 6 | import Nuxt from './nuxt'; 7 | 8 | export default [{ 9 | path: '/examples/styling', 10 | name: 'examples.styling', 11 | component: Styling, 12 | }, { 13 | path: '/examples/autocomplete', 14 | name: 'examples.autocomplete', 15 | component: Autocomplete, 16 | }, { 17 | path: '/examples/validation', 18 | name: 'examples.validation', 19 | component: Validation, 20 | }, { 21 | path: '/examples/hooks', 22 | name: 'examples.hooks', 23 | component: Hooks, 24 | }, { 25 | path: '/examples/templates', 26 | name: 'examples.templates', 27 | component: Templates, 28 | }, { 29 | path: '/examples/nuxt', 30 | name: 'examples.nuxt', 31 | component: Nuxt, 32 | }]; 33 | -------------------------------------------------------------------------------- /docs/pages/examples/nuxt.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 63 | -------------------------------------------------------------------------------- /docs/pages/examples/styling/example1.demo.html: -------------------------------------------------------------------------------- 1 | 102 | -------------------------------------------------------------------------------- /docs/pages/examples/styling/example1.demo.js: -------------------------------------------------------------------------------- 1 | /* Other stuff before, like template, import tagsinput ... */ 2 | 3 | data() { 4 | return { 5 | tag: 'Also valid8ed', 6 | tags: [{ 7 | text: 'custom class', 8 | classes: 'custom-class', 9 | }, { 10 | text: 'valid tag', 11 | }, { 12 | text: 'toShort', 13 | }, { 14 | text: '8 is invalid', 15 | }, { 16 | text: 'duplicate', 17 | }, { 18 | text: 'duplicate', 19 | }, { 20 | text: 'Inline styled tag', 21 | style: 'color: #56c1da; background-color: transparent; border: 1px solid #56c1da', 22 | }], 23 | autocompleteItems: [{ 24 | text: 'invalid', 25 | }, { 26 | text: 'Invalid cause of "1"', 27 | }, { 28 | text: 'valid item', 29 | }], 30 | validation: [{ 31 | classes: 'min-length', 32 | rule: '^.{8,}$', 33 | }, { 34 | classes: 'no-numbers', 35 | rule: '^([^0-9]*)$', 36 | }], 37 | }; 38 | }, 39 | 40 | /* Computed properties, methods and more ... */ 41 | -------------------------------------------------------------------------------- /docs/pages/examples/styling/index.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 132 | 133 | 236 | 237 | 245 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example1.demo.html: -------------------------------------------------------------------------------- 1 | 9 |
15 | 19 | {{ props.item.text }} 20 | {{ props.item.text }} 21 |
22 |
28 | 32 | {{ props.tag.text }} 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example1.demo.js: -------------------------------------------------------------------------------- 1 | /* Other stuff before, like template, import tagsinput ... */ 2 | 3 | data() { 4 | return { 5 | tag: '', 6 | tags: [], 7 | icons: [{ 8 | text: 'done', 9 | iconColor: '#086A87', 10 | }, { 11 | text: 'fingerprint', 12 | iconColor: '#8A0886', 13 | }, { 14 | text: 'label', 15 | iconColor: '#B43104', 16 | }, { 17 | text: 'pregnant_woman', 18 | }, { 19 | text: 'touch_app', 20 | iconColor: '#AC58FA', 21 | }, { 22 | text: 'group_work', 23 | }, { 24 | text: 'pets', 25 | iconColor: '#8A4B08', 26 | }], 27 | }; 28 | }, 29 | computed: { 30 | items() { 31 | return this.icons.filter(i => { 32 | return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1; 33 | }); 34 | }, 35 | }, 36 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example1.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 99 | 100 | 139 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example2.demo.css: -------------------------------------------------------------------------------- 1 | .tags-input .inputs { 2 | display: flex; 3 | } 4 | 5 | .tags-input .inputs i { 6 | font-size: 20px; 7 | cursor: pointer; 8 | } 9 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example2.demo.html: -------------------------------------------------------------------------------- 1 | 10 |
14 | 18 | {{ props.tag.text }} 19 | 20 |
24 | 30 | 34 | check 35 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example2.demo.js: -------------------------------------------------------------------------------- 1 | data() { 2 | return { 3 | animals: [ 4 | 'Lion', 'Turtle', 'Rabbit', 'Frog', 'Squirrel', 'Owl', 'Bee', 5 | ], 6 | tag: '', 7 | tags: [], 8 | }; 9 | }, 10 | computed: { 11 | items() { 12 | return this.animals.filter(a => { 13 | return a.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1; 14 | }).map(a => ({ text: a })); 15 | }, 16 | }, 17 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example2.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 71 | 72 | 82 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example3.demo.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example3.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example4.demo.html: -------------------------------------------------------------------------------- 1 | 7 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example4.demo.js: -------------------------------------------------------------------------------- 1 | data() { 2 | return { 3 | tag: '', 4 | tags: [], 5 | bikeMakers: [{ 6 | text: 'Honda', 7 | }, { 8 | text: 'Yamaha', 9 | }, { 10 | text: 'Suzuki', 11 | }, { 12 | text: 'Triumph', 13 | }, { 14 | text: 'Kawasaki', 15 | }, { 16 | text: 'Triumph', 17 | }, { 18 | text: 'husqvarna', 19 | }], 20 | }; 21 | }, 22 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/example4.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 60 | -------------------------------------------------------------------------------- /docs/pages/examples/templates/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 79 | 80 | 150 | -------------------------------------------------------------------------------- /docs/pages/examples/validation/example1.demo.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /docs/pages/examples/validation/example1.demo.js: -------------------------------------------------------------------------------- 1 | /* Other stuff like import tagsinput ... */ 2 | 3 | data() { 4 | return { 5 | tag: '', 6 | tags: [], 7 | autocompleteItems: [{ 8 | text: 'Invalid because of "8"', 9 | }, { 10 | text: 'toShort', 11 | }, { 12 | text: 'I am valid', 13 | }, { 14 | text: 'Cannot be added', 15 | }, { 16 | text: 'Invalid cause of "{"', 17 | }], 18 | validation: [{ 19 | classes: 'min-length', 20 | rule: tag => tag.text.length < 8, 21 | }, { 22 | classes: 'no-numbers', 23 | rule: /^([^0-9]*)$/, 24 | }, { 25 | classes: 'avoid-item', 26 | rule: /^(?!Cannot).*$/, 27 | disableAdd: true, 28 | }, { 29 | classes: 'no-braces', 30 | rule: ({ text }) => text.indexOf('{') !== -1 || text.indexOf('}') !== -1, 31 | }], 32 | }; 33 | }, 34 | 35 | /* Computed properties, methods and more ... */ 36 | -------------------------------------------------------------------------------- /docs/pages/examples/validation/index.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 145 | 146 | 151 | -------------------------------------------------------------------------------- /docs/pages/getting-started.demo.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /docs/pages/getting-started.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 63 | 64 | 74 | -------------------------------------------------------------------------------- /docs/pages/migration.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 80 | 81 | 83 | -------------------------------------------------------------------------------- /docs/pages/project-features.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 120 | 121 | 172 | 173 | 227 | -------------------------------------------------------------------------------- /docs/pages/to-develop.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 101 | 102 | 131 | -------------------------------------------------------------------------------- /docs/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import ToDevelop from './pages/to-develop'; 4 | import ProjectFeatures from './pages/project-features'; 5 | import GettingStarted from './pages/getting-started'; 6 | import api from './pages/api'; 7 | import examples from './pages/examples'; 8 | import e2eSuite from '../e2e/suite/'; 9 | import Changelog from './pages/changelog'; 10 | import Migration from './pages/migration'; 11 | 12 | Vue.use(Router); 13 | 14 | const routes = [{ 15 | path: '/', 16 | name: 'features', 17 | component: ProjectFeatures, 18 | }, { 19 | path: '/start', 20 | name: 'gettingStarted', 21 | component: GettingStarted, 22 | }, 23 | { 24 | path: '/migration', 25 | name: 'migration', 26 | component: Migration, 27 | }, 28 | { 29 | path: '/changelog', 30 | name: 'changelog', 31 | component: Changelog, 32 | }, 33 | ...api, 34 | ...examples, 35 | ...e2eSuite, 36 | { 37 | path: '/develop', 38 | name: 'develop', 39 | component: ToDevelop, 40 | }]; 41 | 42 | const router = new Router({ routes }); 43 | 44 | export default router; 45 | -------------------------------------------------------------------------------- /docs/slots-data.js: -------------------------------------------------------------------------------- 1 | const deletionMark = { 2 | name: 'deletionMark', 3 | description: `If the user wants to delete the tag and presses backspace, 4 | the property is true for 1 second, because the tag is marked to delete. `, 5 | type: 'Boolean', 6 | }; 7 | 8 | const tag = { 9 | name: 'tag', 10 | type: 'Object', 11 | }; 12 | 13 | const index = { 14 | name: 'index', 15 | description: 'The tags index', 16 | type: 'Number', 17 | }; 18 | 19 | const edit = { 20 | name: 'edit', 21 | description: 'It is true, if the tag is in edit mode', 22 | type: 'Boolean', 23 | }; 24 | 25 | const performDelete = { 26 | name: 'performDelete', 27 | description: `Call this function and pass an index as parameter 28 | to start the deletion process for a tag`, 29 | type: 'Function', 30 | expectedParams: 'index|Number', 31 | }; 32 | 33 | const performOpenEdit = { 34 | name: 'performOpenEdit', 35 | description: `Call this function and pass an index as parameter 36 | to open the edit mode for a tag`, 37 | type: 'Function', 38 | expectedParams: 'index|Number', 39 | }; 40 | 41 | const performCancelEdit = { 42 | name: 'performCancelEdit', 43 | description: `Call this function and pass an index as parameter 44 | to cancel the edit mode for a tag`, 45 | type: 'Function', 46 | expectedParams: 'index|Number', 47 | }; 48 | 49 | const performSaveEdit = { 50 | name: 'performSaveEdit', 51 | description: `Call this function and pass an index as parameter 52 | to save a modified tag`, 53 | type: 'Function', 54 | expectedParams: 'index|Number', 55 | }; 56 | 57 | export default [ 58 | { 59 | slot: 'tag-left', 60 | description: 'The slot is positioned on the left of the text value', 61 | props: [ 62 | tag, 63 | index, 64 | edit, 65 | deletionMark, 66 | performDelete, 67 | performOpenEdit, 68 | performCancelEdit, 69 | performSaveEdit, 70 | ], 71 | }, 72 | { 73 | slot: 'tag-right', 74 | description: 'The slot is positioned between the text value and the actions', 75 | props: [ 76 | tag, 77 | index, 78 | edit, 79 | deletionMark, 80 | performDelete, 81 | performOpenEdit, 82 | performCancelEdit, 83 | performSaveEdit, 84 | ], 85 | }, 86 | { 87 | slot: 'tag-actions', 88 | description: `The slot is positioned on the right side. 89 | At default, it holds the 'check', 'undo' and 'close' icons`, 90 | props: [ 91 | tag, 92 | index, 93 | edit, 94 | deletionMark, 95 | performDelete, 96 | performOpenEdit, 97 | performCancelEdit, 98 | performSaveEdit, 99 | ], 100 | }, 101 | { 102 | slot: 'tag-center', 103 | description: 'At default, it holds the tags text value and an input to edit the text', 104 | props: [ 105 | tag, 106 | index, 107 | edit, 108 | { 109 | name: 'maxlength', 110 | type: 'Number', 111 | description: 'The maximum amount of characters the input is allowed to hold', 112 | }, 113 | deletionMark, 114 | performDelete, 115 | performOpenEdit, 116 | performCancelEdit, 117 | performSaveEdit, 118 | { 119 | name: 'validateTag', 120 | description: `Call this function if the input of a tag changes 121 | to validate the new value e.g. the function could be binded to @input`, 122 | type: 'Function', 123 | expectedParams: 'index|Number, inputEvent', 124 | example: '@input="props.validateTag(props.index, $event)', 125 | }, 126 | ], 127 | }, 128 | { 129 | slot: 'autocomplete-item', 130 | description: 'Slot to create a autocomplete item in the autocomplete layer', 131 | props: [ 132 | { 133 | name: 'item', 134 | description: 'A autocomplete item, which has the same properties like a tag object', 135 | type: 'Object', 136 | }, 137 | { 138 | name: 'index', 139 | description: 'The items index', 140 | type: 'Number', 141 | }, 142 | { 143 | name: 'selected', 144 | description: 'It is true, if the autocomplete item is selected', 145 | type: 'Boolean', 146 | }, 147 | { 148 | name: 'performAdd', 149 | description: `Call this function and pass an autocomplete item as parameter 150 | to add it to the tags array`, 151 | type: 'Function', 152 | expectedParams: 'item|Object', 153 | }, 154 | ], 155 | }, 156 | { 157 | slot: 'autocomplete-header', 158 | description: 'The slot is at the top of the autocomplete layer.', 159 | }, 160 | { 161 | slot: 'autocomplete-footer', 162 | description: 'The slot is at the bottom of the autocomplete layer.', 163 | }, 164 | { 165 | slot: 'between-elements', 166 | description: `The slot is positioned between the tags and the autocomplete layer. 167 | Maybe someone needs it.`, 168 | }, 169 | ]; 170 | -------------------------------------------------------------------------------- /docs/vue-tags-input-dark.scss: -------------------------------------------------------------------------------- 1 | @import 'colors'; 2 | 3 | #app .vue-tags-input:not(.light) { 4 | background: transparent; 5 | 6 | input { 7 | background: transparent; 8 | color: $lightestGrey; 9 | } 10 | 11 | .ti-tag input { 12 | color: #fff; 13 | } 14 | 15 | .ti-input { 16 | border: 1px solid $grey; 17 | } 18 | 19 | ::-webkit-input-placeholder { 20 | color: $lightGrey; 21 | } 22 | 23 | ::-moz-placeholder { 24 | color: $lightGrey; 25 | } 26 | 27 | :-ms-input-placeholder { 28 | color: $lightGrey; 29 | } 30 | 31 | :-moz-placeholder { 32 | color: $lightGrey; 33 | } 34 | 35 | .ti-autocomplete { 36 | background: $dark; 37 | border: 1px solid $grey; 38 | border-top: none; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /e2e/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "globals": { 6 | "test": true, 7 | "fixture": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/add-save-on-key.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | fixture `Check if custom registered trigger keys work` 4 | .page('http://localhost:3000/#/e2e-suite/add-save-on-key'); 5 | 6 | test('test the props addOnKey and saveOnKey', async t => { 7 | 8 | // add a tag with the keycode 188 (,) 9 | await t 10 | .typeText(Selector('.add-save-on .ti-new-tag-input'), 'test') 11 | .pressKey(',') 12 | .expect(Selector('.add-save-on .ti-tags li').count).eql(2) 13 | .expect(Selector('.add-save-on .ti-tags li:nth-child(1) span').textContent).eql('test'); 14 | 15 | // add a tag from autocomplete with keycode 32 (space) 16 | await t 17 | .typeText(Selector('.add-save-on .ti-new-tag-input'), 'fr') 18 | .pressKey('down') 19 | .pressKey('down') 20 | .pressKey('space') 21 | .expect(Selector('.add-save-on .ti-tags li').count).eql(3) 22 | .expect(Selector('.add-save-on .ti-tags li:nth-child(2) span').textContent).eql('china'); 23 | 24 | // edit tag 1 and try to submit with keycode 13 (enter) 25 | await t 26 | .click(Selector('.add-save-on .ti-tags li:nth-child(1)')) 27 | .pressKey('ctrl+a delete') 28 | .typeText(Selector('.add-save-on .ti-tags li:nth-child(1) .ti-tag-input'), 'test2') 29 | .pressKey('enter') 30 | .expect(Selector('.add-save-on .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(true); 31 | 32 | // now submit with keycode 188 (,) 33 | await t 34 | .pressKey(',') 35 | .expect(Selector('.add-save-on .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(false) 36 | .expect(Selector('.add-save-on .ti-tags li:nth-child(1) span').textContent).eql('test2'); 37 | }); 38 | -------------------------------------------------------------------------------- /e2e/autocomplete.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | // checking the default options on getting-started page for a simple tagsinput 4 | 5 | fixture `Check autocomplete functionality` 6 | .page('http://localhost:3000/#/e2e-suite/autocomplete'); 7 | 8 | test('test autocomplete', async t => { 9 | 10 | 11 | /*** check the autocomplete show and hide behaviour ***/ 12 | 13 | /* check autocomplete visibility status with empty input and focus input-0 */ 14 | await t 15 | .click(Selector('.e2e-suite .input-0 .ti-input')) 16 | .expect(Selector('.e2e-suite .input-0 .ti-autocomplete').exists).eql(false) 17 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete').exists).eql(false) 18 | .expect(Selector('.e2e-suite .input-2 .ti-autocomplete').exists).eql(true); 19 | 20 | 21 | /* check autocomplete visibility status with empty input and focus input-1 */ 22 | await t 23 | .click(Selector('.e2e-suite .input-1 .ti-input')) 24 | .expect(Selector('.e2e-suite .input-0 .ti-autocomplete').exists).eql(false) 25 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete').exists).eql(true) 26 | .expect(Selector('.e2e-suite .input-2 .ti-autocomplete').exists).eql(true); 27 | 28 | 29 | /* check autocomplete visibility status with input and focus input-0 */ 30 | /* input-1 blur and should hide */ 31 | await t 32 | .typeText(Selector('.e2e-suite .input-0'), 'tag') 33 | .expect(Selector('.e2e-suite .input-0 .ti-autocomplete').exists).eql(true) 34 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete').exists).eql(false) 35 | .expect(Selector('.e2e-suite .input-2 .ti-autocomplete').exists).eql(true); 36 | 37 | /* clear input from input-0 and check autocomplete visibility status */ 38 | await t 39 | .click(Selector('.e2e-suite .input-0 input')) 40 | .pressKey('ctrl+a delete') 41 | .expect(Selector('.e2e-suite .input-0 .ti-autocomplete').exists).eql(false); 42 | 43 | /* check if input-1 is shown when clicking around on it */ 44 | await t 45 | .click(Selector('.e2e-suite .input-1 .ti-input')) 46 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete').exists).eql(true) 47 | .click(Selector('.e2e-suite .input-1 .ti-input')) 48 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete').exists).eql(true); 49 | 50 | /* check if items are filtered on input-1 and -2 when adding a tag */ 51 | await t 52 | .click(Selector('.e2e-suite .input-1 .ti-autocomplete li:nth-child(1)')) 53 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete').exists).eql(true) 54 | .expect(Selector('.e2e-suite .input-1 .ti-autocomplete li').count).eql(2) 55 | .expect(Selector('.e2e-suite .input-2 .ti-autocomplete li').count).eql(4); 56 | 57 | /* on input-0 it should be impossbile to add item-1 again - input-2 allows it */ 58 | await t 59 | .typeText(Selector('.e2e-suite .input-0'), 'item-1') 60 | .pressKey('enter') 61 | .expect(Selector('.e2e-suite .input-0 .ti-tags li').count).eql(2) 62 | .expect(Selector('.e2e-suite .input-0 .ti-new-tag-input').value).eql('item-1'); 63 | 64 | await t 65 | .click(Selector('.e2e-suite .input-2')) 66 | .pressKey('enter') 67 | .expect(Selector('.e2e-suite .input-0 .ti-tags li').count).eql(3) 68 | .expect(Selector('.e2e-suite .input-2 .ti-tags li').count).eql(3) 69 | .expect(Selector('.e2e-suite .input-0 .ti-new-tag-input').value).eql(''); 70 | 71 | 72 | /*** check if its possible to navigate with arrow up and down ***/ 73 | 74 | /* do arrow down on input-1 and check which items has the class selected-item */ 75 | await t 76 | .click(Selector('.e2e-suite .input-2 input')) 77 | .expect( 78 | Selector('.e2e-suite .input-2 .ti-autocomplete li:nth-child(1)').hasClass('ti-selected-item') 79 | ).eql(false) 80 | .pressKey('down') 81 | .expect( 82 | Selector('.e2e-suite .input-2 .ti-autocomplete li:nth-child(1)').hasClass('ti-selected-item') 83 | ).eql(true); 84 | 85 | /* do arrow down three times and check selected item on input-2 */ 86 | await t 87 | .pressKey('down').pressKey('down').pressKey('down') 88 | .expect( 89 | Selector('.e2e-suite .input-2 .ti-autocomplete li:nth-child(1)').hasClass('ti-selected-item') 90 | ).eql(false) 91 | .expect( 92 | Selector('.e2e-suite .input-2 .ti-autocomplete li:nth-child(4)').hasClass('ti-selected-item') 93 | ).eql(true); 94 | 95 | /* do arrow down again and check selected item on input-2 (we should be back on top again) */ 96 | await t 97 | .pressKey('down') 98 | .expect( 99 | Selector('.e2e-suite .input-2 .ti-autocomplete li:nth-child(1)').hasClass('ti-selected-item') 100 | ).eql(true); 101 | 102 | /* do arrow up on input-2 -> we should be back on bottom */ 103 | await t 104 | .pressKey('up') 105 | .expect( 106 | Selector('.e2e-suite .input-2 .ti-autocomplete li:nth-child(4)').hasClass('ti-selected-item') 107 | ).eql(true); 108 | 109 | 110 | /* focus input-1 -> item with index 0 should be selected*/ 111 | await t 112 | .click(Selector('.e2e-suite .input-1 input')) 113 | .expect( 114 | Selector('.e2e-suite .input-1 .ti-autocomplete li:nth-child(1)').hasClass('ti-selected-item') 115 | ).eql(true); 116 | 117 | 118 | /*** check if its possible to add tags with enter from 119 | autocomplete and input (add-only-from-autocomplete) ***/ 120 | 121 | /* enter item- on input-2 and blur -> no tag should be added */ 122 | await t 123 | .expect(Selector('.e2e-suite .input-1 .ti-tags li').count).eql(3) 124 | .typeText(Selector('.e2e-suite .input-1'), 'item-') 125 | .click(Selector('.e2e-suite .input-1 input')) 126 | .expect(Selector('.e2e-suite .input-1 .ti-tags li').count).eql(3); 127 | 128 | /* adding tags via select and enter from autocomplete should be allowed on input-2 */ 129 | await t 130 | .pressKey('down') 131 | .pressKey('enter') 132 | .expect(Selector('.e2e-suite .input-1 .ti-tags li').count).eql(4) 133 | .expect( 134 | Selector('.e2e-suite .input-1 .ti-tags li:nth-child(3) span').textContent 135 | ).eql('item-3'); 136 | }); 137 | -------------------------------------------------------------------------------- /e2e/check-navigation.test.js: -------------------------------------------------------------------------------- 1 | import { Selector, ClientFunction } from 'testcafe'; 2 | 3 | const getLocation = ClientFunction(() => document.location.href); 4 | 5 | fixture `Check Docs Navigation` 6 | .page('http://localhost:3000'); 7 | 8 | test('Move to page "Getting Started"', async t => { 9 | await t 10 | .resizeWindow(1280, 1024) 11 | .click(Selector('.el-navbar .nav-items li .label span').withText('Getting Started')); 12 | 13 | await t 14 | .expect(Selector('main .main-content h1').innerText).eql('Getting Started') 15 | .expect(getLocation()).contains('start'); 16 | }); 17 | -------------------------------------------------------------------------------- /e2e/edit-tag.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | fixture `Test editing tags` 4 | .page('http://localhost:3000/#/e2e-suite/edit-tag'); 5 | 6 | test('test to edit a tag', async t => { 7 | 8 | // we sould see a tag on startup 9 | await t.expect(Selector('.edit-tag .ti-tags li').count).eql(2); 10 | 11 | // the text of the tag should be 'a tag' 12 | await t.expect(Selector('.edit-tag .ti-tags li span').textContent).eql('a tag'); 13 | 14 | const changeTag = async (text) => { 15 | await t 16 | .click(Selector('.edit-tag .ti-tags li:nth-child(1)')) 17 | .pressKey('ctrl+a delete') 18 | .typeText(Selector('.edit-tag .ti-tags li:nth-child(1)'), text); 19 | }; 20 | 21 | // let's see if it discards on blur 22 | await changeTag('test'); 23 | await t 24 | .click(Selector('.edit-tag')) 25 | .expect(Selector('.edit-tag .ti-tags li span').textContent).eql('a tag'); 26 | 27 | // let's see if it discards when clicking discard icon 28 | await changeTag('test'); 29 | await t 30 | .expect(Selector('.edit-tag .ti-tags li:nth-child(1) .ti-icon-close').visible).eql(false) 31 | .expect(Selector('.edit-tag .ti-tags li:nth-child(1) .ti-icon-undo').visible).eql(true) 32 | .click(Selector('.edit-tag .ti-tags li:nth-child(1) .ti-icon-undo')) 33 | .expect(Selector('.edit-tag .ti-tags li span').textContent).eql('a tag'); 34 | 35 | // let's see if the tag is changed when enter is pressed 36 | await changeTag('test'); 37 | await t 38 | .pressKey('enter') 39 | .expect(Selector('.edit-tag .ti-tags li:nth-child(1) .ti-icon-close').visible).eql(true) 40 | .expect(Selector('.edit-tag .ti-tags li:nth-child(1) .ti-icon-undo').visible).eql(false) 41 | .expect(Selector('.edit-tag .ti-tags li span').textContent).eql('test'); 42 | }); 43 | -------------------------------------------------------------------------------- /e2e/getting-started.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | // checking the default options on getting-started page for a simple tagsinput 4 | 5 | fixture `Getting Started` 6 | .page('http://localhost:3000/#/start'); 7 | 8 | test('test basic functions', async t => { 9 | await t 10 | .typeText(Selector('.getting-started .vue-tags-input .ti-new-tag-input'), 'e2eTag') 11 | .pressKey('enter') 12 | .expect(Selector('.getting-started .vue-tags-input .ti-tags li').count).eql(2) 13 | .expect(Selector('.getting-started .vue-tags-input .ti-tag:nth-child(1) span') 14 | .textContent).eql('e2eTag'); 15 | 16 | // add second tag 17 | await t 18 | .typeText(Selector('.getting-started .vue-tags-input .ti-new-tag-input'), 'e2eTag 2') 19 | .pressKey('enter') 20 | .expect(Selector('.getting-started .vue-tags-input .ti-tags li').count).eql(3) 21 | .expect(Selector('.getting-started .vue-tags-input .ti-tag:nth-child(2) span') 22 | .innerText).eql('e2eTag 2'); 23 | 24 | // delete first tag 25 | await t 26 | .click(Selector('.getting-started .ti-tag:nth-child(1) .ti-actions .ti-icon-close')) 27 | .expect(Selector('.getting-started .vue-tags-input .ti-tags li').count).eql(2) 28 | .expect(Selector('.getting-started .vue-tags-input .ti-tag:nth-child(1) span') 29 | .innerText).eql('e2eTag 2'); 30 | 31 | // add tag on blur 32 | await t 33 | .typeText(Selector('.getting-started .vue-tags-input .ti-new-tag-input'), 'e2eTag 3') 34 | .click(Selector('.getting-started h1')) 35 | .expect(Selector('.getting-started .vue-tags-input .ti-tags li').count).eql(3) 36 | .expect(Selector('.getting-started .vue-tags-input .ti-tag:nth-child(2) span') 37 | .innerText).eql('e2eTag 3'); 38 | 39 | // delete last tag on backsspace 40 | await t 41 | .click(Selector('.getting-started .vue-tags-input .ti-new-tag-input')) 42 | .pressKey('backspace') 43 | .expect(Selector('.getting-started .vue-tags-input .ti-tag:nth-child(2)') 44 | .hasClass('ti-deletion-mark')).eql(true) 45 | .wait(2000) 46 | .expect(Selector('.getting-started .vue-tags-input .ti-tag:nth-child(2)') 47 | .hasClass('deletion-mark')).eql(false) 48 | .pressKey('backspace') 49 | .pressKey('backspace') 50 | .expect(Selector('.getting-started .vue-tags-input .ti-tags li').count).eql(2); 51 | 52 | // add a tag on paste 53 | 54 | /* testcafe doesnt fire the paste event when setting the option paste: true 55 | https://github.com/DevExpress/testcafe/issues/2075 56 | */ 57 | 58 | // await t 59 | // .typeText( 60 | // Selector('.getting-started .vue-tags-input .new-tag-input'), 'fromPaste', { paste: true } 61 | // ) 62 | // .expect(Selector('.getting-started .vue-tags-input .tags li').count).eql(3) 63 | // .expect(Selector('.getting-started .vue-tags-input .tag:nth-child(2) span') 64 | // .textContent).eql('fromPaste'); 65 | }); 66 | -------------------------------------------------------------------------------- /e2e/hooks.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | fixture `Check if vue-tags-input hooks work` 4 | .page('http://localhost:3000/#/e2e-suite/hooks'); 5 | 6 | test('test all hooks', async t => { 7 | 8 | 9 | /*** check before-adding-tag hook ***/ 10 | 11 | // enter tag -> actions should be visible 12 | const enterTag = async () => { 13 | await t 14 | .typeText(Selector('.hooks .ti-new-tag-input'), 'tag') 15 | .pressKey('enter') 16 | .expect(Selector('.hooks > .actions').exists).eql(true) 17 | .expect(Selector('.hooks .ti-tags').count).eql(1) 18 | .expect(Selector('.hooks .ti-new-tag-input').value).eql('tag'); 19 | }; 20 | 21 | // cancel adding -> input shouldnt be a tag 22 | await enterTag(); 23 | await t 24 | .click(Selector('.actions .cancel')) 25 | .expect(Selector('.hooks > .actions').exists).eql(false) 26 | .expect(Selector('.hooks .ti-tags').count).eql(1) 27 | .expect(Selector('.hooks .ti-new-tag-input').value).eql(''); 28 | 29 | // perform adding -> input should become a tags 30 | await enterTag(); 31 | await t 32 | .click(Selector('.actions .perform')) 33 | .expect(Selector('.hooks > .actions').exists).eql(false) 34 | .expect(Selector('.hooks .ti-tags li').count).eql(2) 35 | .expect(Selector('.hooks .ti-new-tag-input').value).eql(''); 36 | 37 | 38 | /*** check before-editing-tag hook ***/ 39 | 40 | // edit tag -> actions should be visible 41 | const editTag = async () => { 42 | await t 43 | .click(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-center')) 44 | .expect(Selector('.hooks > .actions').exists).eql(true) 45 | .expect(Selector('.hooks .ti-tags:nth-child(1) .ti-tag-input').exists).eql(false); 46 | }; 47 | 48 | // cancel edit 49 | await editTag(); 50 | await t 51 | .click(Selector('.actions .cancel')) 52 | .expect(Selector('.hooks > .actions').exists).eql(false) 53 | .expect(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(false); 54 | 55 | // perform edit 56 | await editTag(); 57 | await t 58 | .click(Selector('.actions .perform')) 59 | .expect(Selector('.hooks > .actions').exists).eql(false) 60 | .expect(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(true); 61 | 62 | 63 | /*** check before-saving-tag hook ***/ 64 | 65 | // do some changes and save 66 | const saveChanges = async () => { 67 | await t 68 | .pressKey('ctrl+a delete') 69 | .typeText(Selector('.hooks .ti-tags li:nth-child(1)'), 'changed') 70 | .pressKey('enter') 71 | .expect(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(true) 72 | .expect(Selector('.hooks > .actions').exists).eql(true); 73 | }; 74 | 75 | // cancel save 76 | await saveChanges(); 77 | await t 78 | .click(Selector('.actions .cancel')) 79 | .expect(Selector('.hooks .ti-tags li:nth-child(1) span').textContent).eql('tag') 80 | .expect(Selector('.hooks > .actions').exists).eql(false) 81 | .expect(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(false); 82 | 83 | // perform save 84 | await t 85 | .click(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-center')) 86 | .click(Selector('.actions .perform')); 87 | 88 | await saveChanges(); 89 | await t 90 | .click(Selector('.actions .perform')) 91 | .expect(Selector('.hooks .ti-tags li:nth-child(1) span').textContent).eql('changed') 92 | .expect(Selector('.hooks > .actions').exists).eql(false) 93 | .expect(Selector('.hooks .ti-tags li:nth-child(1) .ti-tag-input').exists).eql(false); 94 | 95 | 96 | /*** check before-deleting-tag hook ***/ 97 | const deleteTag = async () => { 98 | await t 99 | .click(Selector('.hooks .ti-tags li:nth-child(1) .ti-icon-close')) 100 | .expect(Selector('.hooks .ti-tags li').count).eql(2); 101 | }; 102 | 103 | // cancel delete 104 | await deleteTag(); 105 | await t 106 | .click(Selector('.actions .cancel')) 107 | .expect(Selector('.hooks .ti-tags li').count).eql(2) 108 | .expect(Selector('.hooks > .actions').exists).eql(false); 109 | 110 | // perform delete 111 | await deleteTag(); 112 | await t 113 | .click(Selector('.actions .perform')) 114 | .expect(Selector('.hooks .ti-tags li').count).eql(1) 115 | .expect(Selector('.hooks > .actions').exists).eql(false); 116 | }); 117 | -------------------------------------------------------------------------------- /e2e/suite/add-save-on-key.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | -------------------------------------------------------------------------------- /e2e/suite/autocomplete.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 57 | 58 | 68 | -------------------------------------------------------------------------------- /e2e/suite/edit-tag.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /e2e/suite/hooks.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 51 | -------------------------------------------------------------------------------- /e2e/suite/index.js: -------------------------------------------------------------------------------- 1 | import Validation from './validation'; 2 | import Autocomplete from './autocomplete'; 3 | import Hooks from './hooks'; 4 | import EditTag from './edit-tag'; 5 | import AddSaveOnKey from './add-save-on-key'; 6 | 7 | export default [{ 8 | path: '/e2e-suite/validation', 9 | component: Validation, 10 | }, { 11 | path: '/e2e-suite/autocomplete', 12 | component: Autocomplete, 13 | }, { 14 | path: '/e2e-suite/hooks', 15 | component: Hooks, 16 | }, { 17 | path: '/e2e-suite/edit-tag', 18 | component: EditTag, 19 | }, { 20 | path: '/e2e-suite/add-save-on-key', 21 | component: AddSaveOnKey, 22 | }]; 23 | -------------------------------------------------------------------------------- /e2e/suite/validation.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 66 | -------------------------------------------------------------------------------- /e2e/validation.test.js: -------------------------------------------------------------------------------- 1 | import { Selector } from 'testcafe'; 2 | 3 | fixture `Check validation` 4 | .page('http://localhost:3000/#/e2e-suite/validation'); 5 | 6 | 7 | /* because of https://github.com/DevExpress/testcafe/issues/1770 we dont make a difference between 8 | * fixture and test explanation 9 | */ 10 | test('Test validation', async t => { 11 | 12 | 13 | /*** check autocomplete items ***/ 14 | 15 | await t 16 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'i') 17 | /* does the autocomplete show the correct amount of items */ 18 | .expect(Selector('.tags-input-1 .ti-autocomplete li').count).eql(3); 19 | 20 | await t 21 | /* does the first item has the class no-numbers */ 22 | .expect( 23 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(1)').hasClass('no-numbers') 24 | ).eql(true) 25 | /* does the first item has the class invalid */ 26 | .expect( 27 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(1)').hasClass('ti-invalid') 28 | ).eql(true) 29 | /* does the first item has not the class valid */ 30 | .expect( 31 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(1)').hasClass('ti-valid') 32 | ).eql(false); 33 | 34 | await t 35 | /* does the second item has the class valid */ 36 | .expect( 37 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(2)').hasClass('ti-valid') 38 | ).eql(true); 39 | 40 | await t 41 | /* does the third item has the class no-braces */ 42 | .expect( 43 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(3)').hasClass('no-braces') 44 | ).eql(true) 45 | /* does the third item has the class invalid */ 46 | .expect( 47 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(3)').hasClass('ti-invalid') 48 | ).eql(true); 49 | 50 | /* has item with text "Cannot be added" all expected classes */ 51 | await t 52 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 53 | .pressKey('ctrl+a delete') 54 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'Cannot') 55 | .expect(Selector('.tags-input-1 .ti-autocomplete li').count).eql(1) 56 | .expect( 57 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(1)').hasClass('avoid-item') 58 | ).eql(true) 59 | .expect( 60 | Selector('.tags-input-1 .ti-autocomplete li:nth-child(1)').hasClass('ti-invalid') 61 | ).eql(true); 62 | 63 | /*Is it imbossible to add the item with text "Cannot be added" via click (disableAdd = true)*/ 64 | await t 65 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 66 | .expect(Selector('.tags-input-1 .ti-tags li').count).eql(1); 67 | 68 | 69 | /*** check new-tags-input ***/ 70 | 71 | /*Is it impossible to add the item with text "Cannot be added" via enter (disableAdd = true)*/ 72 | await t 73 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), ' be added') 74 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('avoid-item')).eql(true) 75 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('ti-invalid')).eql(true) 76 | .pressKey('enter') 77 | .expect(Selector('.tags-input-1 .ti-tags li').count).eql(1); 78 | 79 | /*Is it imbossible to add the item with text "Cannot be added" via paste (disableAdd = true)*/ 80 | await t 81 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 82 | .pressKey('ctrl+a delete') 83 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'Cannot be added', { paste: true }) 84 | .expect(Selector('.tags-input-1 .ti-tags li').count).eql(1); 85 | 86 | /*Is it imbossible to add the item with text "Cannot be added" via blur (disableAdd = true)*/ 87 | await t 88 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 89 | .click(Selector('.validation')) 90 | .expect(Selector('.tags-input-1 .ti-tags li').count).eql(1); 91 | 92 | /* does a short text input has the class min-length */ 93 | await t 94 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 95 | .pressKey('ctrl+a delete') 96 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'short') 97 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('ti-invalid')).eql(true) 98 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('min-length')).eql(true) 99 | .pressKey('enter') 100 | .expect(Selector('.tags-input-1 .ti-tags li').count).eql(2); 101 | 102 | /* does a duplicate input has the class duplicate */ 103 | await t 104 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 105 | .pressKey('ctrl+a delete') 106 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'short') 107 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('ti-invalid')).eql(true) 108 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('ti-duplicate')).eql(true); 109 | 110 | /* does the input gets the classes min-length, no-braces, no-numbers in combination */ 111 | await t 112 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), '8{') 113 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('min-length')).eql(true) 114 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('no-braces')).eql(true) 115 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('no-numbers')).eql(true) 116 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), '6') 117 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('min-length')).eql(false); 118 | 119 | /* is it possible to make a valid input */ 120 | await t 121 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 122 | .pressKey('ctrl+a delete') 123 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'this is valid tag') 124 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('ti-valid')).eql(true) 125 | .expect(Selector('.tags-input-1 .ti-new-tag-input').hasClass('ti-invalid')).eql(false); 126 | 127 | 128 | /*** check tags ***/ 129 | 130 | /* does duplicate tags have the class duplicate */ 131 | await t 132 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 133 | .pressKey('enter') 134 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(2)').hasClass('ti-valid')).eql(true) 135 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'this is valid tag') 136 | .pressKey('enter') 137 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(2)').hasClass('ti-duplicate')).eql(true) 138 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(2)').hasClass('ti-invalid')).eql(true) 139 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(3)').hasClass('ti-duplicate')).eql(true) 140 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(3)').hasClass('ti-invalid')).eql(true); 141 | 142 | /* does duplicate tags lose the class duplicate when chaning one */ 143 | await t 144 | .click(Selector('.tags-input-1 .ti-tags li:nth-child(2)')) 145 | .click(Selector('.tags-input-1 .ti-tags li:nth-child(2) input')) 146 | .pressKey('backspace') 147 | .pressKey('enter') 148 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(2)').hasClass('ti-duplicate')).eql(false) 149 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(2)').hasClass('ti-invalid')).eql(false) 150 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(3)').hasClass('ti-duplicate')).eql(false) 151 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(3)').hasClass('ti-invalid')).eql(false); 152 | 153 | /* does the first tag has the class min-length */ 154 | await t 155 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(1)').hasClass('min-length')).eql(true); 156 | 157 | /* does the first tag lose the class min-length when chaning */ 158 | await t 159 | .click(Selector('.tags-input-1 .ti-tags li:nth-child(1)')) 160 | .typeText(Selector('.tags-input-1 .ti-tags li:nth-child(1) input'), ' and now longer') 161 | .pressKey('enter') 162 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(1)').hasClass('ti-valid')).eql(true) 163 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(1)').hasClass('min-length')).eql(false) 164 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(1)').hasClass('ti-invalid')).eql(false); 165 | 166 | /* does a added tag with text "valid tag but }" has the class no-braces */ 167 | await t 168 | .click(Selector('.tags-input-1 .ti-new-tag-input')) 169 | .typeText(Selector('.tags-input-1 .ti-new-tag-input'), 'valid tag but }') 170 | .pressKey('enter') 171 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(4)').hasClass('no-braces')).eql(true) 172 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(4)').hasClass('min-length')).eql(false) 173 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(4)').hasClass('no-numbers')).eql(false) 174 | .expect(Selector('.tags-input-1 .ti-tags li:nth-child(4)').hasClass('ti-invalid')).eql(true); 175 | }); 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@johmun/vue-tags-input", 3 | "version": "2.1.0", 4 | "author": "Johannes Munari ", 5 | "license": "MIT", 6 | "description": "A tags input component for VueJS with autocompletion, custom validation, templating and much more", 7 | "homepage": "http://www.vue-tags-input.com", 8 | "private": false, 9 | "keywords": [ 10 | "javascript", 11 | "vue", 12 | "tags", 13 | "vue-tags-input", 14 | "vue-component", 15 | "autocomplete" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/JohMun/vue-tags-input.git" 20 | }, 21 | "main": "dist/vue-tags-input.js", 22 | "scripts": { 23 | "dev": "npm run docs", 24 | "docs": "cross-env NODE_ENV=development webpack-dev-server --hot --config ./build/docs.config.js", 25 | "build-docs": "cross-env NODE_ENV=production webpack --config ./build/docs.config.js --hide-modules", 26 | "analyze": "cross-env ANALYZE=true npm run build-docs", 27 | "build-lib": "webpack --config ./build/lib.config.js --hide-modules", 28 | "precommit": "lint-staged", 29 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore . --ignore-pattern *.demo.*", 30 | "lint-fix": "eslint --fix --ignore-path .gitignore --ext .js,.vue . --ignore-pattern *.demo.*", 31 | "e2e": "testcafe all e2e/*test* --app \"npm run dev\" --app-init-delay 10000 -S -s screenshots" 32 | }, 33 | "lint-staged": { 34 | "*.js": "eslint", 35 | "*.vue": "eslint" 36 | }, 37 | "peerDependencies": { 38 | "vue": "2.x" 39 | }, 40 | "dependencies": { 41 | "vue": "2.x" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.1.2", 45 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 46 | "@babel/polyfill": "^7.0.0", 47 | "@babel/preset-env": "^7.1.0", 48 | "@hydrant/eslint-config": "^2.0.1", 49 | "autoprefixer": "^9.4.3", 50 | "axios": "^0.18.0", 51 | "babel-eslint": "^10.0.1", 52 | "babel-loader": "^8.0.4", 53 | "clean-webpack-plugin": "^1.0.0", 54 | "copy-webpack-plugin": "^4.5.1", 55 | "cross-env": "^5.1.4", 56 | "css-loader": "^2.0.2", 57 | "documentation": "^9.1.1", 58 | "eslint": "^5.10.0", 59 | "eslint-plugin-vue": "^5.0.0-beta.3", 60 | "fast-deep-equal": "^2.0.1", 61 | "file-loader": "^3.0.1", 62 | "html-webpack-plugin": "^3.2.0", 63 | "http-server": "^0.11.1", 64 | "husky": "^1.1.2", 65 | "image-webpack-loader": "^4.6.0", 66 | "ip": "^1.1.5", 67 | "lint-staged": "^8.1.0", 68 | "normalize-css": "^2.3.1", 69 | "postcss-loader": "^3.0.0", 70 | "raw-loader": "^1.0.0", 71 | "sass": "^1.55.0", 72 | "sass-loader": "^8.0.2", 73 | "testcafe": "^0.23.3", 74 | "testcafe-vue-selectors": "^3.0.0", 75 | "url-loader": "^1.1.2", 76 | "vue-loader": "^15.4.2", 77 | "vue-router": "^3.0.1", 78 | "vue-style-loader": "^4.1.2", 79 | "vue-template-compiler": "^2.6.10", 80 | "webpack": "^4.28.1", 81 | "webpack-bundle-analyzer": "^3.0.3", 82 | "webpack-cli": "^3.1.2", 83 | "webpack-dev-server": "^3.1.10" 84 | }, 85 | "eslintConfig": { 86 | "parserOptions": { 87 | "parser": "babel-eslint", 88 | "ecmaVersion": 2017, 89 | "sourceType": "module" 90 | }, 91 | "extends": "@hydrant/eslint-config/vue", 92 | "rules": { 93 | "vue/max-attributes-per-line": "off", 94 | "vue/no-use-v-if-with-v-for": "off", 95 | "vue/no-unused-components": "off", 96 | "vue/component-name-in-template-casing": "off", 97 | "vue/singleline-html-element-content-newline": "off", 98 | "vue/no-v-html": "off", 99 | "vue/require-default-prop": "off", 100 | "vue/use-v-on-exact": "off", 101 | "vue/multiline-html-element-content-newline": "off" 102 | } 103 | }, 104 | "browserslist": [ 105 | "ie >= 11", 106 | "last 2 Firefox versions", 107 | "last 5 Chrome versions", 108 | "last 2 iOS versions" 109 | ], 110 | "postcss": { 111 | "plugins": { 112 | "autoprefixer": {} 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /vue-tags-input/assets/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/vue-tags-input/assets/fonts/icomoon.eot -------------------------------------------------------------------------------- /vue-tags-input/assets/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/vue-tags-input/assets/fonts/icomoon.ttf -------------------------------------------------------------------------------- /vue-tags-input/assets/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohMun/vue-tags-input/294026aeed583154df3aeaf9f8007136404c132e/vue-tags-input/assets/fonts/icomoon.woff -------------------------------------------------------------------------------- /vue-tags-input/create-tags.js: -------------------------------------------------------------------------------- 1 | // helper functions 2 | 3 | const validateUserRules = (tag, validation) => { 4 | return validation.filter(val => { 5 | const { text } = tag; 6 | // if the rule is a string, we convert it to RegExp 7 | if (typeof val.rule === 'string') return !new RegExp(val.rule).test(text); 8 | 9 | if (val.rule instanceof RegExp) return !val.rule.test(text); 10 | 11 | // if we deal with a function, invoke it 12 | const isFunction = {}.toString.call(val.rule) === '[object Function]'; 13 | if (isFunction) return val.rule(tag); 14 | 15 | }).map(val => val.classes); 16 | }; 17 | 18 | const clone = node => JSON.parse(JSON.stringify(node)); 19 | 20 | const findIndex = (arr, callback) => { 21 | let index = 0; 22 | while (index < arr.length) { 23 | if (callback(arr[index], index, arr)) return index; 24 | index++; 25 | } 26 | return -1; 27 | }; 28 | 29 | const createClasses = (tag, tags, validation = [], customDuplicateFn) => { 30 | if (tag.text === undefined) tag = { text: tag }; 31 | 32 | // create css classes from the user validation array 33 | const classes = validateUserRules(tag, validation); 34 | 35 | // if we find the tag, it's an exsting one which is edited. 36 | // in this case we must splice it out 37 | const index = findIndex(tags, t => t === tag); 38 | const tagsDiff = clone(tags); 39 | const inputTag = index !== -1 ? tagsDiff.splice(index, 1)[0] : clone(tag); 40 | 41 | // check whether the tag is a duplicate or not 42 | const duplicate = customDuplicateFn ? customDuplicateFn(tagsDiff, inputTag) : 43 | tagsDiff.map(t => t.text).indexOf(inputTag.text) !== -1; 44 | 45 | // if it's a duplicate, push the class duplicate to the array 46 | if (duplicate) classes.push('ti-duplicate'); 47 | 48 | // if we find no classes, the tag is valid → push the class valid 49 | classes.length === 0 ? classes.push('ti-valid') : classes.push('ti-invalid'); 50 | return classes; 51 | }; 52 | 53 | /** 54 | * @description Create one tag out of a String or validate an existing one 55 | * @property {helpers} 56 | * @param {Object|String} tag A tag which should be validated | A String to create a tag 57 | * @param {Array} tagsarray The tags array 58 | * @param {Array} [validation=[]] The validation Array is optional (pass it if you use one) 59 | * @returns {Object} The created (validated) tag 60 | */ 61 | const createTag = (tag, ...rest) => { 62 | // if text is undefined, a string is passed. let's make a tag out of it 63 | if (tag.text === undefined) tag = { text: tag }; 64 | 65 | // we better make a clone to not getting reference trouble 66 | const t = clone(tag); 67 | 68 | // create the validation classes 69 | t.tiClasses = createClasses(tag, ...rest); 70 | return t; 71 | }; 72 | 73 | /** 74 | * @description Create multiple tags out of Strings or validate existing tags 75 | * @property {helpers} 76 | * @param {Array} tagsarray An Array containing tags or strings. See example below. 77 | * @param {Array} [validation=[]] The validation Array is optional (pass it if you use one) 78 | * @returns {Array} An array containing (validated) tags 79 | * @example /* Example to call the function */ 80 | const validatedTags = createTags(['tag1Text', 'tag2Text'], [{ type: 'length', rule: /[0-9]/ }]) 81 | */ 82 | const createTags = (tags, ...rest) => tags.map(t => createTag(t, tags, ...rest)); 83 | 84 | export { createClasses, createTag, createTags, clone }; 85 | -------------------------------------------------------------------------------- /vue-tags-input/publish.js: -------------------------------------------------------------------------------- 1 | import VueTagsInput from '../vue-tags-input/vue-tags-input.vue'; 2 | import { createClasses, createTag, createTags } from '../vue-tags-input/create-tags'; 3 | import TagInput from '../vue-tags-input/tag-input.vue'; 4 | 5 | // add autoinstall support if the component is attached to the windows object e.g. if added by CDN 6 | VueTagsInput.install = Vue => Vue.component(VueTagsInput.name, VueTagsInput); 7 | if (typeof window !== 'undefined' && window.Vue) window.Vue.use(VueTagsInput); 8 | 9 | export { 10 | VueTagsInput, 11 | createClasses, 12 | createTag, 13 | createTags, 14 | TagInput, 15 | }; 16 | 17 | export default VueTagsInput; 18 | -------------------------------------------------------------------------------- /vue-tags-input/tag-input.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 29 | 30 | 56 | -------------------------------------------------------------------------------- /vue-tags-input/vue-tags-input.props.js: -------------------------------------------------------------------------------- 1 | // The file contains all props and validators which are provided by the component 2 | 3 | const propValidatorTag = value => { 4 | return !value.some(t => { 5 | const invalidText = !t.text; 6 | if (invalidText) console.warn('Missing property "text"', t); 7 | 8 | let invalidClasses = false; 9 | if (t.classes) invalidClasses = typeof t.classes !== 'string'; 10 | if (invalidClasses) console.warn('Property "classes" must be type of string', t); 11 | 12 | return invalidText || invalidClasses; 13 | }); 14 | }; 15 | 16 | const propValidatorStringNumeric = value => { 17 | return !value.some(v => { 18 | if (typeof v === 'number') { 19 | const numeric = isFinite(v) && Math.floor(v) === v; 20 | if (!numeric) console.warn('Only numerics are allowed for this prop. Found:', v); 21 | return !numeric; 22 | } else if (typeof v === 'string') { 23 | /* 24 | * Regex: || Not totally fool-proof yet, still matches "0a" and such 25 | * - allow non-word characters (aka symbols e.g. ;, :, ' etc) 26 | * - allow alpha characters 27 | * - deny numbers 28 | */ 29 | const string = /\W|[a-z]|!\d/i.test(v); 30 | if (!string) console.warn('Only alpha strings are allowed for this prop. Found:', v); 31 | return !string; 32 | } else { 33 | console.warn('Only numeric and string values are allowed. Found:', v); 34 | return false; 35 | } 36 | }); 37 | }; 38 | 39 | export default { 40 | /** 41 | * @description Property to bind a model to the input. 42 | If the user changes the input value, the model updates, too. 43 | If the user presses enter with an valid input, 44 | a new tag is created with the value of this model. 45 | After creating the new tag, the model is cleared. 46 | * @property {props} 47 | * @required 48 | * @type {String} 49 | * @model 50 | * @default '' 51 | */ 52 | value: { 53 | type: String, 54 | default: '', 55 | required: true, 56 | }, 57 | /** 58 | * @description Pass an array containing objects like in the example below. 59 | The properties 'style' and 'class' are optional. Of course it is possible to add custom 60 | properties to a tag object. vue-tags-input won't change the key and value. 61 | * @property {props} 62 | * @type {Array} 63 | * @sync 64 | * @default [] 65 | * @example 66 | { 67 |  text: 'My tag value', /* The visible text on display */ 68 |  style: 'background-color: #ccc', /* Adding inline styles is possible */ 69 |  classes: 'custom-class another', /* The value will be added as css classes */ 70 | } 71 | */ 72 | tags: { 73 | type: Array, 74 | default: () => [], 75 | validator: propValidatorTag, 76 | }, 77 | /** 78 | * @description Expects an array containing objects inside. The objects 79 | can have the same properties as a tag object. 80 | * @property {props} 81 | * @type {Array} 82 | * @default [] 83 | */ 84 | autocompleteItems: { 85 | type: Array, 86 | default: () => [], 87 | validator: propValidatorTag, 88 | }, 89 | /** 90 | * @description Defines whether a tag is editable after creation or not. 91 | * @property {props} 92 | * @type {Boolean} 93 | * @default false 94 | */ 95 | allowEditTags: { 96 | type: Boolean, 97 | default: false, 98 | }, 99 | /** 100 | * @description Defines if duplicate autocomplete items are filtered out from the view or not. 101 | * @property {props} 102 | * @type {Boolean} 103 | * @default true 104 | */ 105 | autocompleteFilterDuplicates: { 106 | default: true, 107 | type: Boolean, 108 | }, 109 | /** 110 | * @description If it's true, the user can add tags only via the autocomplete layer. 111 | * @property {props} 112 | * @type {Boolean} 113 | * @default false 114 | */ 115 | addOnlyFromAutocomplete: { 116 | type: Boolean, 117 | default: false, 118 | }, 119 | /** 120 | * @description The minimum character length which is required 121 | until the autocomplete layer is shown. If set to 0, 122 | then it'll be shown on focus. 123 | * @property {props} 124 | * @type {Number} 125 | * @default 1 126 | */ 127 | autocompleteMinLength: { 128 | type: Number, 129 | default: 1, 130 | }, 131 | /** 132 | * @description If it's true, the autocomplete layer is always shown, regardless if 133 | an input or an autocomplete items exists. 134 | * @property {props} 135 | * @type {Boolean} 136 | * @default false 137 | */ 138 | autocompleteAlwaysOpen: { 139 | type: Boolean, 140 | default: false, 141 | }, 142 | /** 143 | * @description Property to disable vue-tags-input. 144 | * @property {props} 145 | * @type {Boolean} 146 | * @default false 147 | */ 148 | disabled: { 149 | type: Boolean, 150 | default: false, 151 | }, 152 | /** 153 | * @description The placeholder text which is shown in the input, when it's empty. 154 | * @property {props} 155 | * @type {String} 156 | * @default Add Tag 157 | */ 158 | placeholder: { 159 | type: String, 160 | default: 'Add Tag', 161 | }, 162 | /** 163 | * @description Custom trigger key codes can be registrated. If the user presses one of these, 164 | a tag will be generated out of the input value. Can be either a numeric keyCode or the key 165 | as a string. 166 | * @property {props} 167 | * @type {Array} 168 | * @default [13] 169 | * @example add-on-key="[13, ':', ';']" 170 | */ 171 | addOnKey: { 172 | type: Array, 173 | default: () => [13], 174 | validator: propValidatorStringNumeric, 175 | }, 176 | /** 177 | * @description Custom trigger key codes can be registrated. If the user edits a tag 178 | and presses one of these, the edited tag will be saved. 179 | Can be either a numeric keyCode or the key as a string. 180 | * @property {props} 181 | * @type {Array} 182 | * @default [13] 183 | * @example save-on-key="[13, ':', ';']" 184 | */ 185 | saveOnKey: { 186 | type: Array, 187 | default: () => [13], 188 | validator: propValidatorStringNumeric, 189 | }, 190 | /** 191 | * @description The maximum amount the tags array is allowed to hold. 192 | * @property {props} 193 | * @type {Number} 194 | */ 195 | maxTags: { 196 | type: Number, 197 | }, 198 | /** 199 | * @description The maximum amount of characters the input is allowed to hold. 200 | * @property {props} 201 | * @type {Number} 202 | */ 203 | maxlength: { 204 | type: Number, 205 | }, 206 | /** 207 | * @description Pass an array containing objects like in the example below. 208 | The property 'classes' will be added as css classes, if the property 'rule' matches the text 209 | of a tag, an autocomplete item or the input. The property 'rule' can be type of 210 | RegExp or function. If the property 'disableAdd' is 'true', the item can't be added 211 | to the tags array, if the appropriated rule matches. 212 | * @property {props} 213 | * @type {Array} 214 | * @default [] 215 | * @example 216 | { 217 |  classes: 'class', /* css class */ 218 |  rule: /^([^0-9]*)$/, /* RegExp */ 219 | }, { 220 |  classes: 'no-braces', /* css class */ 221 |  rule(text) { /* function with text as param */ 222 |   return text.indexOf('{') !== -1 || text.indexOf('}') !== -1; 223 |  }, 224 |  disableAdd: true, /* if the rule matches, the item cannot be added */, 225 | }, 226 | */ 227 | validation: { 228 | type: Array, 229 | default: () => [], 230 | validator(value) { 231 | return !value.some(v => { 232 | const missingRule = !v.rule; 233 | if (missingRule) console.warn('Property "rule" is missing', v); 234 | 235 | const validRule = v.rule && ( 236 | typeof v.rule === 'string' || 237 | v.rule instanceof RegExp || 238 | {}.toString.call(v.rule) === '[object Function]' 239 | ); 240 | 241 | if (!validRule) { 242 | console.warn( 243 | 'A rule must be type of string, RegExp or function. Found:', 244 | JSON.stringify(v.rule) 245 | ); 246 | } 247 | 248 | const missingClasses = !v.classes; 249 | if (missingClasses) console.warn('Property "classes" is missing', v); 250 | 251 | const invalidType = v.type && typeof v.type !== 'string'; 252 | if (invalidType) console.warn('Property "type" must be type of string. Found:', v); 253 | 254 | return !validRule || missingRule || missingClasses || invalidType; 255 | }); 256 | }, 257 | }, 258 | /** 259 | * @description Defines the characters which splits a text into different pieces, 260 | to generate tags out of this pieces. 261 | * @property {props} 262 | * @type {Array} 263 | * @default [';'] 264 | * @example 265 | separators: [';', ','] 266 | input: some; user input, has random; commas, an,d semicolons 267 | will split into: some - user input - has random - commas - an - d semicolons 268 | */ 269 | separators: { 270 | type: Array, 271 | default: () => [';'], 272 | validator(value) { 273 | return !value.some(s => { 274 | const invalidType = typeof s !== 'string'; 275 | if (invalidType) console.warn('Separators must be type of string. Found:', s); 276 | return invalidType; 277 | }); 278 | }, 279 | }, 280 | /** 281 | * @description If it's true, the user can't add or save a tag, 282 | if another exists, with the same text value. 283 | * @property {props} 284 | * @type {Boolean} 285 | * @default true 286 | */ 287 | avoidAddingDuplicates: { 288 | type: Boolean, 289 | default: true, 290 | }, 291 | /** 292 | * @description If the input holds a value and loses the focus, 293 | a tag will be generated out of this value, if possible. 294 | * @property {props} 295 | * @type {Boolean} 296 | * @default true 297 | */ 298 | addOnBlur: { 299 | type: Boolean, 300 | default: true, 301 | }, 302 | /** 303 | * @description Custom function to detect duplicates. If the function returns 'true', 304 | the tag will be marked as duplicate. 305 | * @property {props} 306 | * @type {Function} 307 | * @param {Array} tagsarray The Array of tags minus the one which is edited/created. 308 | * @param {Object} tag The tag which is edited or should be added to the tags array. 309 | * @example 310 | // The duplicate function to recreate the default behaviour, would look like this: 311 | isDuplicate(tags, tag) { 312 |  return tags.map(t => t.text).indexOf(tag.text) !== -1; 313 | } 314 | */ 315 | isDuplicate: { 316 | type: Function, 317 | default: null, 318 | }, 319 | /** 320 | * @description If it's true, the user can paste into the input element and 321 | vue-tags-input will create tags out of the incoming text. 322 | * @property {props} 323 | * @type {Boolean} 324 | * @default true 325 | */ 326 | addFromPaste: { 327 | type: Boolean, 328 | default: true, 329 | }, 330 | /** 331 | * @description Defines if it's possible to delete tags by pressing backspace. 332 | If so and the user wants to delete a tag, 333 | the tag gets the css class 'deletion-mark' for 1 second. 334 | If the user presses backspace again in that time period, 335 | the tag is removed from the tags array and the view. 336 | * @property {props} 337 | * @type {Boolean} 338 | * @default true 339 | */ 340 | deleteOnBackspace: { 341 | default: true, 342 | type: Boolean, 343 | }, 344 | }; 345 | -------------------------------------------------------------------------------- /vue-tags-input/vue-tags-input.scss: -------------------------------------------------------------------------------- 1 | $primary: #5C6BC0; 2 | $error: #e54d42; 3 | $success: #68cd86; 4 | $warn: #ffb648; 5 | 6 | @font-face { 7 | font-family: 'icomoon'; 8 | src: url('./assets/fonts/icomoon.eot?7grlse'); 9 | src: url('./assets/fonts/icomoon.eot?7grlse#iefix') format('embedded-opentype'), 10 | url('./assets/fonts/icomoon.ttf?7grlse') format('truetype'), 11 | url('./assets/fonts/icomoon.woff?7grlse') format('woff'); 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | 16 | [class^="ti-icon-"], [class*=" ti-icon-"] { 17 | font-family: 'icomoon' !important; 18 | speak: none; 19 | font-style: normal; 20 | font-weight: normal; 21 | font-variant: normal; 22 | text-transform: none; 23 | line-height: 1; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | .ti-icon-check:before { 29 | content: "\e902"; 30 | } 31 | 32 | .ti-icon-close:before { 33 | content: "\e901"; 34 | } 35 | 36 | .ti-icon-undo:before { 37 | content: "\e900"; 38 | } 39 | 40 | ul { 41 | margin: 0px; 42 | padding: 0px; 43 | list-style-type: none; 44 | } 45 | 46 | *, *:before, *:after { 47 | box-sizing: border-box; 48 | } 49 | 50 | input:focus { 51 | outline: none; 52 | } 53 | 54 | input[disabled] { 55 | background-color: transparent; 56 | } 57 | 58 | .vue-tags-input { 59 | max-width: 450px; 60 | position: relative; 61 | background-color: #fff; 62 | } 63 | 64 | div.vue-tags-input.disabled { 65 | opacity: 0.5; 66 | 67 | * { 68 | cursor: default; 69 | } 70 | } 71 | 72 | .ti-input { 73 | border: 1px solid #ccc; 74 | display: flex; 75 | padding: 4px; 76 | flex-wrap: wrap; 77 | } 78 | 79 | .ti-tags { 80 | display: flex; 81 | flex-wrap: wrap; 82 | width: 100%; 83 | line-height: 1em; 84 | } 85 | 86 | .ti-tag { 87 | background-color: $primary; 88 | color: #fff; 89 | border-radius: 2px; 90 | display: flex; 91 | padding: 3px 5px; 92 | margin: 2px; 93 | font-size: .85em; 94 | 95 | &:focus { 96 | outline: none; 97 | } 98 | 99 | .ti-content { 100 | display: flex; 101 | align-items: center; 102 | } 103 | 104 | .ti-tag-center { 105 | position: relative; 106 | } 107 | 108 | span { 109 | line-height: .85em; 110 | } 111 | 112 | span.ti-hidden { 113 | padding-left: 14px; 114 | visibility: hidden; 115 | height: 0px; 116 | white-space: pre; 117 | } 118 | 119 | .ti-actions { 120 | margin-left: 2px; 121 | display: flex; 122 | align-items: center; 123 | font-size: 1.15em; 124 | 125 | i { 126 | cursor: pointer; 127 | } 128 | } 129 | 130 | &:last-child { 131 | margin-right: 4px; 132 | } 133 | 134 | &.ti-invalid, &.ti-tag.ti-deletion-mark { 135 | background-color: $error; 136 | } 137 | } 138 | 139 | .ti-new-tag-input-wrapper { 140 | display: flex; 141 | flex: 1 0 auto; 142 | padding: 3px 5px; 143 | margin: 2px; 144 | font-size: .85em; 145 | 146 | input { 147 | flex: 1 0 auto; 148 | min-width: 100px; 149 | border: none; 150 | padding: 0px; 151 | margin: 0px; 152 | } 153 | } 154 | 155 | .ti-new-tag-input { 156 | line-height: initial; 157 | } 158 | 159 | .ti-autocomplete { 160 | border: 1px solid #ccc; 161 | border-top: none; 162 | position: absolute; 163 | width: 100%; 164 | background-color: #fff; 165 | z-index: 20; 166 | } 167 | 168 | .ti-item > div { 169 | cursor: pointer; 170 | padding: 3px 6px; 171 | width: 100%; 172 | } 173 | 174 | .ti-selected-item { 175 | background-color: $primary; 176 | color: #fff; 177 | } 178 | -------------------------------------------------------------------------------- /vue-tags-input/vue-tags-input.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 189 | 190 | 191 | 192 | 193 | --------------------------------------------------------------------------------