├── .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 |
39 |
40 | tags = newTags"
44 | />
45 |
46 |
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 |
2 |
28 |
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 |
2 |
3 |
4 |
5 | {{ name }}
6 |
7 |
{{ description }}
8 |
9 | Type
10 | {{ type }}
11 |
12 |
13 | Default
14 | {{ defaultValue }}
15 |
16 |
17 | Required
18 | check
19 |
20 |
21 | Sync available
22 | check
23 |
24 |
60 |
65 |
66 | Hook
67 | check
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
141 |
142 |
208 |
--------------------------------------------------------------------------------
/docs/components/breaking-changes.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
info
4 |
5 |
Breaking changes
6 |
7 |
8 |
9 |
10 | Please check out the
11 | migration page
12 | for further information
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
66 |
--------------------------------------------------------------------------------
/docs/components/compare.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Version 1
5 |
6 |
7 |
8 | Version 2 (Current)
9 |
10 |
11 |
12 |
13 |
14 |
35 |
36 |
84 |
--------------------------------------------------------------------------------
/docs/components/el-code.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
39 |
40 |
67 |
--------------------------------------------------------------------------------
/docs/components/el-navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue Tags Input
7 | A Generic UI Component
8 |
9 |
clear
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
26 | {{ item.icon.type }}
27 |
28 |
32 | expand_less
33 |
34 |
37 | expand_more
38 |
39 |
40 |
{{ item.label }}
41 |
42 |
43 |
49 | {{ child.label }}
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
150 |
151 |
308 |
--------------------------------------------------------------------------------
/docs/components/generic-api-page.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
36 |
37 |
42 |
--------------------------------------------------------------------------------
/docs/components/icons/edge.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
13 |
--------------------------------------------------------------------------------
/docs/components/icons/github.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
20 |
--------------------------------------------------------------------------------
/docs/components/icons/opera.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
13 |
--------------------------------------------------------------------------------
/docs/components/icons/safari.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
13 |
--------------------------------------------------------------------------------
/docs/components/slot-docs-item.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.slot }}
5 |
6 |
7 | {{ item.description }}
8 |
9 |
35 |
36 |
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 |
27 | You need to enable JavaScript to run this app.
28 |
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 |
2 |
3 |
4 |
5 | Create tags helper
6 |
7 |
8 | In some cases it can be useful to build tag objects manually.
9 | For example, if you use the validation feature and pass unvalidated tags
10 | to vue-tags-input. It validates the passed tags
11 | and emits @tags-changed , if the user does a action.
12 |
13 |
14 | But for the time between the mounted hook and the user action, the model holds
15 | unvalidated tags.
16 |
17 |
18 | To solve this problem, you can create validated tags
19 | by yourself and pass them to vue-tags-input.
20 | To achieve that, we can import some helper functions from vue-tags-input.
21 |
22 |
25 |
30 |
31 |
32 |
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 |
2 |
3 |
Changelog
4 |
5 |
6 | {{ node.version }}
7 |
8 |
9 | {{ change }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
100 |
101 |
111 |
--------------------------------------------------------------------------------
/docs/pages/examples/autocomplete/example1.demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | tags = newTags"
8 | />
9 |
10 |
11 |
12 |
45 |
--------------------------------------------------------------------------------
/docs/pages/examples/autocomplete/example1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Autocomplete
4 |
5 | The property
6 | autocomplete-items
7 | expects an array. We pass through the computed property
8 | filteredItems ,
9 | which is a filtered collection based on the string
10 | tag
11 | and the possible items: Spain, France, USA, Germany and China.
12 |
13 | tags = newTags"
18 | />
19 |
20 |
21 |
22 |
23 |
58 |
--------------------------------------------------------------------------------
/docs/pages/examples/autocomplete/example2.demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
54 |
--------------------------------------------------------------------------------
/docs/pages/examples/autocomplete/example2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Add only from autocomplete and http
4 |
5 | In this example music artists can be found with the autocomplete function.
6 | The iTunes API
7 | is requested as data source. As supporting http library,
8 | axios
9 | is used. The property add-only-from-autocomplete
10 | disables adding tags from input directly.
11 | A debounce avoid too much requests when typing.
12 |
13 |
24 |
25 |
26 |
27 |
28 |
73 |
74 |
89 |
--------------------------------------------------------------------------------
/docs/pages/examples/autocomplete/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
22 |
--------------------------------------------------------------------------------
/docs/pages/examples/hooks/example1.demo.html:
--------------------------------------------------------------------------------
1 | tags = newTags"
5 | @before-adding-tag="obj => handlers.push(obj.addTag)"
6 | />
7 |
8 | Cancel
9 | Add
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 |
2 |
3 | Example 1
4 |
5 | In this example, we push the addTag function
6 | in handlers , if the
7 | before-adding-tag hook fires.
8 |
9 |
10 | Its might be suprising that we are using an array here. That's because we can have mulitple
11 | addTag functions. If we are using separators (see prop
12 |
13 | separators
14 | )
15 | vue-tags-input will emit
16 | @before-adding-tag multiple times in a row,
17 | if it has to split the input string into multiple tags.
18 |
19 |
20 | If the handlers array is not null ,
21 | we show two buttons. The "Add" button invokes the saved functions on click
22 | and the tag/s will be added.
23 |
24 | tags = newTags"
28 | @before-adding-tag="obj => handlers.push(obj.addTag)"
29 | />
30 |
31 | Cancel
32 | Add
33 |
34 |
35 |
36 |
37 |
38 |
39 |
67 |
68 |
78 |
--------------------------------------------------------------------------------
/docs/pages/examples/hooks/example2.demo.html:
--------------------------------------------------------------------------------
1 | tags = newTags"
5 | @before-adding-tag="checkTag"
6 | />
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 |
2 |
3 | Example 2
4 |
5 | In the checkTag function, which is called
6 | by the before-adding-tag hook, some logic is executed.
7 | If a to adding tag contains the letter "e", an alert box is shown.
8 |
9 | tags = newTags"
13 | @before-adding-tag="checkTag"
14 | />
15 |
16 |
17 |
18 |
19 |
20 |
45 |
--------------------------------------------------------------------------------
/docs/pages/examples/hooks/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Hooks
5 |
6 | The tags input component provides different hooks.
7 | A callback function can be registered, to control the behaviour of tags input.
8 | For example like that: @before-adding-tag="myCallback" .
9 | Before a tag is added, the function myCallback is invoked
10 | and gets an object as parameter. The object contains the properties
11 | tag , which is the to adding tag and a function named
12 | addTag . If this function is invoked,
13 | tag will be added.
14 | You find more information in the
15 | documentations ,
16 | how the other hooks work.
17 |
18 |
19 |
20 |
21 |
22 |
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 |
2 |
3 |
4 |
Use Taginput with Nuxt
5 |
6 | Create the file vue-tags-input.js with the following content
7 | in the ./plugins folder.
8 |
9 |
10 |
11 | import Vue from 'vue';
12 | import VueTagsInput from '@johmun/vue-tags-input';
13 |
14 | Vue.use(VueTagsInput);
15 |
16 |
17 |
18 |
19 | Add VueTagsInput to the plugins list in your
20 | nuxt.config.js
21 |
22 |
23 |
24 | plugins: [{ src: '~/plugins/vue-tags-input', ssr: false }],
25 | build: {
26 | vendor: ['@johmun/vue-tags-input'],
27 | // more config
28 | }
29 |
30 |
31 |
32 |
33 | Now you can use VueTagsInput like this:
34 |
35 |
36 |
37 |
38 |
39 |
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 |
2 |
3 |
4 |
5 |
6 |
Styling Elements
7 |
8 | All css classes vue-tags-input is using,
9 | are prefixed with ti- . For example:
10 |
11 |
12 | V1: duplicate or
13 | item
14 |
15 |
16 | V2: ti-duplicate or
17 | ti-item
18 |
19 |
20 |
21 |
22 | You can add CSS classes to tags, the input element or
23 | to autocomplete items, in the following ways:
24 |
25 |
26 |
27 | In a validation item, the value of the property
28 | classes
29 | is added as class, if the rule matches.
30 |
31 |
32 | The value of the property classes
33 | in a tag or an autocomplete item, is always appended.
34 |
35 |
36 |
37 | CSS Classes which are added from tags input:
38 |
39 |
40 |
41 | The class "ti-duplicate" is added if an element exists twice.
42 | Duplicates can't be added by default. To achieve that, The property
43 | avoidAddingDuplicates have to be set to
44 | false .
45 |
46 |
47 | The class "ti-deletion-mark" is appended for a short time,
48 | if the user deletes a tag with backspace.
49 |
50 |
51 | The class "ti-valid" is added if a tag came through the validation,
52 | "ti-invalid" if not.
53 |
54 |
55 |
56 | Styles can also be changed with the property
57 | style in an autocomplete item or a tag.
58 | But be aware that those styles have a high priority,
59 | because they will be set inline.
60 |
61 |
62 | tags = newTags"
69 | />
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
132 |
133 |
236 |
237 |
245 |
--------------------------------------------------------------------------------
/docs/pages/examples/templates/example1.demo.html:
--------------------------------------------------------------------------------
1 | tags = newTags"
8 | >
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 |
2 |
3 | Slot: tag-left & autocomplete-item
4 |
5 | Templates are a powerful tool to customize tags input even more.
6 | In this example we use the slots
7 | tag-left and
8 | autocompleteItem to insert
9 | material icons
10 | on the left of each tag and autocomplete item. The property
11 | allow-edit-tags is set to true ,
12 | to enable editing tags after creation.
13 |
14 |
15 | Via slot-scope we access some properties and helper functions.
16 | The autocomplete item slot for example, gets the performAdd
17 | function, which adds a new tag to the collection by passing an index.
18 | You can read the documentations
19 | for further information.
20 |
21 | tags = newTags"
28 | >
29 |
35 |
36 | {{ props.item.text }}
37 | {{ props.item.text }}
38 |
39 |
45 |
46 | {{ props.tag.text }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
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 | tags = newTags"
9 | >
10 |
14 |
18 | {{ props.tag.text }}
19 |
20 |
24 |
28 | {{ animal }}
29 |
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 |
2 |
3 | Slot: tag-center (experimental) with select
4 | tags = newTags"
12 | >
13 |
14 |
18 | {{ props.tag.text }}
19 |
20 |
21 |
25 | {{ animal }}
26 |
27 | check
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
71 |
72 |
82 |
--------------------------------------------------------------------------------
/docs/pages/examples/templates/example3.demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | tags = newTags"
8 | >
9 |
13 |
17 | {{ props.tag.text }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
41 |
--------------------------------------------------------------------------------
/docs/pages/examples/templates/example3.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Slot: tag-center (experimental) with input helper
4 |
5 | This is the default behaviour of every tag recreated with the slot
6 | tag-center .
7 | The component TagInput is a helper to fastly build
8 | the standard text input, which provides the ability to edit tags after creation.
9 | The css class ti-hidden is provided by tags input.
10 | It's a helper class which enlarges a tag while the user is typing.
11 |
12 |
13 |
14 |
15 |
16 |
26 |
--------------------------------------------------------------------------------
/docs/pages/examples/templates/example4.demo.html:
--------------------------------------------------------------------------------
1 | tags = newTags"
6 | >
7 |
8 | Select your favorite bike maker ↓
9 |
10 |
11 |
12 | Or keep going with your worlds...
13 |
14 |
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 |
2 |
3 | Slot: autocomplete-header & autocomplete-footer
4 | tags = newTags"
13 | >
14 |
15 | Select your favorite bike maker ↓
16 |
17 |
18 |
19 | Or keep going with your worlds...
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
60 |
--------------------------------------------------------------------------------
/docs/pages/examples/templates/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Templating
5 |
6 |
7 | All slot names are now in kebab-case.
8 |
9 | V1: tagLeft
10 | V2: tag-left
11 |
12 |
13 |
14 | The function performSaveTag ,
15 | provided via slot-scope, is now called performSaveEdit
16 |
17 |
18 |
19 |
20 |
21 | tag-left
22 |
23 |
24 |
25 |
26 | tag-center
27 | Contains: text & input
28 | (experimental)
29 |
30 |
31 | tag-right
32 |
33 |
34 |
35 |
36 | tag-actions
37 |
38 | Contains:
39 | undo
40 | clear
41 |
42 |
43 |
44 |
45 |
All possible slots in a tag
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
79 |
80 |
150 |
--------------------------------------------------------------------------------
/docs/pages/examples/validation/example1.demo.html:
--------------------------------------------------------------------------------
1 | tags = newTags"
7 | />
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 |
2 |
3 |
4 |
5 | Validation
6 |
7 |
8 |
9 | The key type in a validation item,
10 | has been renamed to classes .
11 |
12 |
13 | The function rule in a validation item,
14 | gets the complete tag as parameter.
15 |
16 |
17 |
18 |
19 | To validate tags, autocomplete items or the user input, a validation array
20 | can be passed to the tags input component. In this example, a tag has to be
21 | at least 8 characters long, can't contain a number or a brace and must not start
22 | with the string "Cannot".
23 |
24 | tags = newTags"
30 | />
31 |
32 | tag
33 |
34 | tags
35 |
36 |
37 |
38 | Each item in the validation array must contain the properties
39 | classes and rule .
40 | classes will be added as css class, if the related
41 | rule matches a tag, the user input or an autocomplete item.
42 | The rule can by type of RegExp or function.
43 | In chapter Styling
44 | we will see how to use these css classes in detail.
45 |
46 |
47 | If the rule is valid, the class "ti-valid", or if not, "ti-invalid" is also added.
48 | If the tag input component finds a duplicate item, the class "ti-duplicate" is appended.
49 | By default the prop avoid-adding-duplicates is true.
50 | So in this example it is impossible to add duplicates.
51 |
52 |
53 | If a validation item holds the property disableAdd: true ,
54 | a tag, which does match the appropriated rule, won't get just some css classes.
55 | In this case, the tag can't be added to the tags array.
56 | Like every tag which starts with "Cannot" in this example.
57 |
58 |
59 |
60 |
61 |
62 |
63 | You should be aware, that if you use the validation feature,
64 | vue-tags-input validates the passed tags and edits/adds the property
65 | tiClasses . To get the validated tags,
66 | vue-tags-input emits @tags-changed .
67 |
68 |
69 | The event is only emitted,
70 | if the user does a action like adding, editing or deleting a tag.
71 |
72 |
73 | If you want updates, if vue-tags-input detects unvalidated tags,
74 | you can use the .sync modifier:
75 | :tags.sync="tags"
76 |
77 |
78 | Another way would be to validate tags by yourself
79 | and pass them to vue-tags-input, see chapter
80 | Create Tags Helper .
81 |
82 |
83 |
84 |
85 |
86 |
87 |
145 |
146 |
151 |
--------------------------------------------------------------------------------
/docs/pages/getting-started.demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | tags = newTags"
7 | />
8 |
9 |
10 |
11 |
26 |
--------------------------------------------------------------------------------
/docs/pages/getting-started.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Getting Started
5 |
Demo
6 |
tags = newTags"
11 | />
12 |
13 | tag
14 |
15 | tags
16 |
17 |
18 | Install
19 | Install vue-tags-input with npm
20 |
21 | npm install @johmun/vue-tags-input
22 |
23 | Usage
24 |
25 | CDN
26 |
27 | vue-tags-input can be included via CDN and it registrates itself as a global component.
28 |
29 |
30 | <script src="{{ cdnUrl }}"></script>
31 |
32 |
33 |
34 |
35 |
36 |
63 |
64 |
74 |
--------------------------------------------------------------------------------
/docs/pages/migration.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
CSS Prefixes
5 |
All CSS classes used by vue-tags-input are prefixed with ti-
6 |
11 |
12 |
Validation item changes
13 |
14 | The property type in a validation item
15 | has been renamed to classes .
16 | And the function rule
17 | gets the complete tag as parameter.
18 |
19 |
24 |
25 |
Slot changes
26 |
27 | All slot names are now in kebab-case. For example in version 1,
28 | most of the slot names where in camelCase like tagLeft
29 | or autocompleteItem . All those names have changed to
30 | the kebab-case pattern:
31 | tag-left or autocomplete-item
32 |
33 |
34 | The function performSaveTag ,
35 | provided via slot-scope, is now called performSaveEdit
36 |
37 |
38 |
Props
39 |
40 | The prop delete-on-backslash
41 | has been renamed to delete-on-backspace
42 |
43 |
44 |
45 |
46 |
80 |
81 |
83 |
--------------------------------------------------------------------------------
/docs/pages/project-features.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Vue Tags Input
4 |
A generic UI component to input tags, with a couple of features
5 |
6 | tags = newTags"
14 | />
15 |
16 |
Browser support
17 |
18 |
19 |
20 | IE 10 <=
21 |
22 |
23 |
24 | iPhone 9 <=
25 |
26 |
27 |
28 | Chrome check
29 |
30 |
31 |
32 | Firefox check
33 |
34 |
35 |
36 | Opera 35 <=
37 |
38 |
39 |
40 | Safari 9.1 <=
41 |
42 |
43 |
44 | Android 4.4 <=
45 |
46 |
47 |
48 | Edge 12 <=
49 |
50 |
51 |
52 | License:
53 | MIT
54 |
55 |
Copyright (c) {{ new Date().getFullYear() }} Johannes Munari
56 |
57 |
58 |
59 |
120 |
121 |
172 |
173 |
227 |
--------------------------------------------------------------------------------
/docs/pages/to-develop.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
All Demo
4 | add tag
5 | delete tag
6 |
20 |
21 | {{ tags }}
22 |
23 |
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 |
2 |
3 | add save
4 | tags = newTags"
12 | />
13 |
14 |
15 |
16 |
41 |
--------------------------------------------------------------------------------
/e2e/suite/autocomplete.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | tags = newTags"
9 | />
10 | tags = newTags"
18 | />
19 | tags = newTags"
28 | />
29 |
30 |
31 |
32 |
57 |
58 |
68 |
--------------------------------------------------------------------------------
/e2e/suite/edit-tag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
27 |
--------------------------------------------------------------------------------
/e2e/suite/hooks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cancel
6 |
7 |
8 | Perform
9 |
10 |
11 |
tags = newTags"
16 | @before-adding-tag="obj => handler = obj.addTag"
17 | @before-editing-tag="obj => handler = obj.editTag"
18 | @before-deleting-tag="obj => handler = obj.deleteTag"
19 | @before-saving-tag="obj => handler = obj.saveTag"
20 | />
21 |
22 |
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 |
2 |
3 | tags = newTags"
12 | />
13 |
14 |
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 |
4 |
15 |
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 |
7 |
188 |
189 |
190 |
191 |
192 |
193 |
--------------------------------------------------------------------------------