├── .browserslistrc ├── publish.sh ├── babel.config.js ├── demo.gif ├── .npmignore ├── vue.config.js ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── components │ ├── index.js │ └── InputTag.vue ├── assets │ └── logo.png ├── main.js ├── styles │ ├── github-light.css │ ├── stylesheet.css │ └── normalize.css └── App.vue ├── .github └── FUNDING.yml ├── tests └── unit │ ├── .eslintrc.js │ └── InputTag.spec.js ├── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── .gitignore ├── .eslintrc.js ├── jest.config.js ├── LICENSE.md ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | npm run build-bundle && npm publish --access public 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiastucci/vue-input-tag/HEAD/demo.gif -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | coverage 5 | .log 6 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | extract: false 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiastucci/vue-input-tag/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import InputTag from "./InputTag.vue"; 2 | export default InputTag; 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [matiastucci] 4 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiastucci/vue-input-tag/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | }, 5 | rules: { 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Version of `Vue` I'm using?: _____ 2 | 3 | Version of `vue-input-tag` I'm using?: _____ 4 | 5 | Small [JSFiddle](https://jsfiddle.net) showing the issue: _____ 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import "ferrocarriloeste"; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | new Vue({ 8 | render: h => h(App) 9 | }).$mount("#app"); 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## Development Setup 4 | 5 | ``` bash 6 | # install dependencies 7 | npm install 8 | 9 | # run unit tests 10 | npm run unit 11 | 12 | # serve docs with hot reload at localhost:8080 13 | npm run dev 14 | 15 | # build for production with minification 16 | npm run build 17 | ``` 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /coverage 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "babel-eslint" 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vue Input Tag 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.jsx?$": "babel-jest" 8 | }, 9 | moduleNameMapper: { 10 | "^@/(.*)$": "/src/$1" 11 | }, 12 | snapshotSerializers: ["jest-serializer-vue"], 13 | testMatch: [ 14 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 15 | ], 16 | testURL: "http://localhost/", 17 | collectCoverage: true, 18 | collectCoverageFrom: ["**/components/*.{vue}", "!**/node_modules/**"] 19 | }; 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matias Tucci 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-input-tag", 3 | "version": "2.0.7", 4 | "description": "Vue.js input tag editor component", 5 | "keywords": [ 6 | "vueinputtag", 7 | "input", 8 | "tag", 9 | "inputtag" 10 | ], 11 | "author": "Matias Tucci ", 12 | "repository": "https://github.com/matiastucci/vue-input-tag", 13 | "homepage": "https://matiastucci.github.io/vue-input-tag", 14 | "license": "MIT", 15 | "main": "./dist/vueInputTag.common.js", 16 | "unpkg": "./dist/vueInputTag.umd.min.js", 17 | "files": [ 18 | "dist/*", 19 | "src/*", 20 | "public/*", 21 | "*.json", 22 | "*.js" 23 | ], 24 | "scripts": { 25 | "serve": "vue-cli-service serve", 26 | "build": "vue-cli-service build", 27 | "build-bundle": "vue-cli-service build --target lib --name vueInputTag ./src/components/index.js", 28 | "lint": "vue-cli-service lint", 29 | "test:unit": "vue-cli-service test:unit", 30 | "predeploy": "npm run build", 31 | "deploy": "gh-pages -d dist", 32 | "prepublishOnly": "npm run build-bundle" 33 | }, 34 | "dependencies": { 35 | "vue": "^2.5.17" 36 | }, 37 | "devDependencies": { 38 | "@vue/cli-plugin-babel": "^4.2.3", 39 | "@vue/cli-plugin-eslint": "^3.0.1", 40 | "@vue/cli-plugin-unit-jest": "^4.2.3", 41 | "@vue/cli-service": "^4.2.3", 42 | "@vue/eslint-config-prettier": "^4.0.1", 43 | "@vue/test-utils": "^1.0.0-beta.20", 44 | "babel-core": "7.0.0-bridge.0", 45 | "coveralls": "^3.0.3", 46 | "ferrocarriloeste": "^1.0.1", 47 | "gh-pages": "^2.2.0", 48 | "vue-template-compiler": "^2.5.17" 49 | }, 50 | "engines": { 51 | "node": ">= 6.0.0", 52 | "npm": ">= 4.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-input-tag 2 | > A Vue.js 2.0 input tag component inspired in [react-tagsinput](https://github.com/olahol/react-tagsinput) 3 | 4 | [![Codeship Status for matiastucci/vue-input-tag](https://app.codeship.com/projects/0f715410-172a-0137-7928-063230c44fa3/status?branch=master)](https://app.codeship.com/projects/328055) 5 | [![Coverage Status](https://coveralls.io/repos/github/matiastucci/vue-input-tag/badge.svg?branch=master)](https://coveralls.io/github/matiastucci/vue-input-tag?branch=master) 6 | [![Version](https://img.shields.io/npm/v/vue-input-tag.svg)](https://www.npmjs.com/package/vue-input-tag) 7 | [![License](https://img.shields.io/npm/l/vue-input-tag.svg)](https://www.npmjs.com/package/vue-input-tag) 8 | [![Monthly Downloads](https://img.shields.io/npm/dm/vue-input-tag.svg)](https://www.npmjs.com/package/vue-input-tag) 9 | 10 |

11 | Logo 12 |

13 | 14 | ## Installation 15 | 16 | #### NPM / Yarn 17 | 18 | ```bash 19 | npm install vue-input-tag --save 20 | ``` 21 | 22 | ```bash 23 | yarn add vue-input-tag 24 | ``` 25 | 26 | Then you need to import and register it: 27 | 28 | ```js 29 | import InputTag from 'vue-input-tag' 30 | ``` 31 | 32 | ```js 33 | Vue.component('input-tag', InputTag) 34 | ``` 35 | 36 | #### CDN 37 | 38 | ```html 39 | 40 | 41 | ``` 42 | 43 | Then you need to register it: 44 | 45 | `Vue.component('input-tag', vueInputTag.default)` 46 | 47 | ## Usage 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | ## Props 54 | | Name | Type | Default | Description | 55 | | ---:| --- | ---| --- | 56 | | value | Array | [] | Tags to be render in the input | 57 | | placeholder | String | "" | Placeholder to be shown when no tags | 58 | | read-only | Boolean | false | Set input to readonly | 59 | | add-tag-on-blur | Boolean | false | Add tag on input blur | 60 | | limit | String or Number | -1 (none) | Set a limit for the amount of tags | 61 | | validate | String or Function (allows async) or Object | "" | Apply certain validator for user input. Choose from `email`, `url`, `text`, `digits` or `isodate`. Or pass a `function` or a `RegExp` object for custom validation | 62 | | add-tag-on-keys | Array | [ 13 (return), 188 (comma), 9 (tab) ] | Keys that are going to add the new tag 63 | | allow-duplicates | Boolean | false | Allow duplicate tags 64 | | before-adding | Function | null | Function (allows async) to normalize tag before adding. E.g `(tag) => tag.toUpperCase()` 65 | 66 | ## Events 67 | | Name | Arguments | Description | 68 | | ---: | --- | --- | 69 | | input | Array with tags | Emitted when a tag is added (after successful validation) and when a tag is removed | 70 | | update:tags | Array with tags | Same as input event | 71 | -------------------------------------------------------------------------------- /src/styles/github-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 GitHub, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | .pl-c { 27 | color: #969896; 28 | } 29 | 30 | .pl-c1, 31 | .pl-s .pl-v { 32 | color: #0086b3; 33 | } 34 | 35 | .pl-e, 36 | .pl-en { 37 | color: #795da3; 38 | } 39 | 40 | .pl-smi, 41 | .pl-s .pl-s1 { 42 | color: #333; 43 | } 44 | 45 | .pl-ent { 46 | color: #63a35c; 47 | } 48 | 49 | .pl-k { 50 | color: #a71d5d; 51 | } 52 | 53 | .pl-s, 54 | .pl-pds, 55 | .pl-s .pl-pse .pl-s1 { 56 | color: #183691; 57 | } 58 | 59 | .pl-sr { 60 | color: #183691; 61 | .pl-cce, 62 | .pl-sre, 63 | .pl-sra { 64 | color: #183691; 65 | } 66 | } 67 | 68 | .pl-v { 69 | color: #ed6a43; 70 | } 71 | 72 | .pl-id { 73 | color: #b52a1d; 74 | } 75 | 76 | .pl-ii { 77 | color: #f8f8f8; 78 | background-color: #b52a1d; 79 | } 80 | 81 | .pl-sr .pl-cce { 82 | font-weight: bold; 83 | color: #63a35c; 84 | } 85 | 86 | .pl-ml { 87 | color: #693a17; 88 | } 89 | 90 | .pl-mh { 91 | font-weight: bold; 92 | color: #1d3e81; 93 | .pl-en { 94 | font-weight: bold; 95 | color: #1d3e81; 96 | } 97 | } 98 | 99 | .pl-ms { 100 | font-weight: bold; 101 | color: #1d3e81; 102 | } 103 | 104 | .pl-mq { 105 | color: #008080; 106 | } 107 | 108 | .pl-mi { 109 | font-style: italic; 110 | color: #333; 111 | } 112 | 113 | .pl-mb { 114 | font-weight: bold; 115 | color: #333; 116 | } 117 | 118 | .pl-md { 119 | color: #bd2c00; 120 | background-color: #ffecec; 121 | } 122 | 123 | .pl-mi1 { 124 | color: #55a532; 125 | background-color: #eaffea; 126 | } 127 | 128 | .pl-mdr { 129 | font-weight: bold; 130 | color: #795da3; 131 | } 132 | 133 | .pl-mo { 134 | color: #1d3e81; 135 | } 136 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mati@tucci.me. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 138 | 139 | 145 | -------------------------------------------------------------------------------- /src/components/InputTag.vue: -------------------------------------------------------------------------------- 1 | 181 | 182 | 211 | 212 | 271 | -------------------------------------------------------------------------------- /src/styles/stylesheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font-family: "Roboto", sans-serif; 5 | font-size: 16px; 6 | line-height: 1.5; 7 | color: #606c71; 8 | } 9 | 10 | a { 11 | color: #fff; 12 | } 13 | a:hover { 14 | opacity: 0.8; 15 | } 16 | 17 | .site-footer a { 18 | color: #046c43; 19 | } 20 | 21 | .btn { 22 | display: inline-block; 23 | margin-bottom: 1rem; 24 | color: rgba(255, 255, 255, 0.7); 25 | background-color: rgba(255, 255, 255, 0.08); 26 | border-color: rgba(255, 255, 255, 0.2); 27 | border-style: solid; 28 | border-width: 1px; 29 | border-radius: 0.3rem; 30 | transition: color 0.2s, background-color 0.2s, border-color 0.2s; 31 | text-decoration: none; 32 | } 33 | .btn + .btn { 34 | margin-left: 1rem; 35 | } 36 | .btn:hover { 37 | color: rgba(255, 255, 255, 0.8); 38 | text-decoration: none; 39 | background-color: rgba(255, 255, 255, 0.2); 40 | border-color: rgba(255, 255, 255, 0.3); 41 | } 42 | 43 | @media screen and (min-width: 64em) { 44 | .btn { 45 | padding: 0.75rem 1rem; 46 | } 47 | } 48 | @media screen and (min-width: 42em) and (max-width: 64em) { 49 | .btn { 50 | padding: 0.6rem 0.9rem; 51 | font-size: 0.9rem; 52 | } 53 | } 54 | @media screen and (max-width: 42em) { 55 | .btn { 56 | display: block; 57 | width: 100%; 58 | padding: 0.75rem; 59 | font-size: 0.9rem; 60 | } 61 | .btn + .btn { 62 | margin-top: 1rem; 63 | margin-left: 0; 64 | } 65 | } 66 | .page-header { 67 | color: #fff; 68 | text-align: center; 69 | background-color: #046c43; 70 | } 71 | 72 | @media screen and (min-width: 64em) { 73 | .page-header { 74 | padding: 5rem 6rem; 75 | } 76 | } 77 | @media screen and (min-width: 42em) and (max-width: 64em) { 78 | .page-header { 79 | padding: 3rem 4rem; 80 | } 81 | } 82 | @media screen and (max-width: 42em) { 83 | .page-header { 84 | padding: 2rem 1rem; 85 | } 86 | } 87 | .project-name { 88 | margin-top: 0; 89 | margin-bottom: 0.1rem; 90 | } 91 | 92 | @media screen and (min-width: 64em) { 93 | .project-name { 94 | font-size: 3.25rem; 95 | } 96 | } 97 | @media screen and (min-width: 42em) and (max-width: 64em) { 98 | .project-name { 99 | font-size: 2.25rem; 100 | } 101 | } 102 | @media screen and (max-width: 42em) { 103 | .project-name { 104 | font-size: 1.75rem; 105 | } 106 | } 107 | .project-tagline { 108 | margin-bottom: 2rem; 109 | font-weight: normal; 110 | opacity: 0.7; 111 | } 112 | 113 | @media screen and (min-width: 64em) { 114 | .project-tagline { 115 | font-size: 1.25rem; 116 | } 117 | } 118 | @media screen and (min-width: 42em) and (max-width: 64em) { 119 | .project-tagline { 120 | font-size: 1.15rem; 121 | } 122 | } 123 | @media screen and (max-width: 42em) { 124 | .project-tagline { 125 | font-size: 1rem; 126 | } 127 | } 128 | .main-content :first-child { 129 | margin-top: 0; 130 | } 131 | .main-content img { 132 | max-width: 100%; 133 | } 134 | .main-content h1, 135 | .main-content h2, 136 | .main-content h3, 137 | .main-content h4, 138 | .main-content h5, 139 | .main-content h6 { 140 | margin-top: 2rem; 141 | margin-bottom: 1rem; 142 | font-weight: normal; 143 | color: #159957; 144 | } 145 | .main-content p { 146 | margin-bottom: 1em; 147 | } 148 | .main-content code { 149 | padding: 2px 4px; 150 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 151 | font-size: 0.9rem; 152 | color: #383e41; 153 | background-color: #f3f6fa; 154 | border-radius: 0.3rem; 155 | } 156 | .main-content pre { 157 | padding: 0.8rem; 158 | margin-top: 0; 159 | margin-bottom: 1rem; 160 | font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; 161 | color: #567482; 162 | word-wrap: normal; 163 | background-color: #f3f6fa; 164 | border: solid 1px #dce6f0; 165 | border-radius: 0.3rem; 166 | } 167 | .main-content pre > code { 168 | padding: 0; 169 | margin: 0; 170 | font-size: 0.9rem; 171 | color: #567482; 172 | word-break: normal; 173 | white-space: pre; 174 | background: transparent; 175 | border: 0; 176 | } 177 | .main-content .highlight { 178 | margin-bottom: 1rem; 179 | } 180 | .main-content .highlight pre { 181 | margin-bottom: 0; 182 | word-break: normal; 183 | padding: 0.8rem; 184 | overflow: auto; 185 | font-size: 0.9rem; 186 | line-height: 1.45; 187 | border-radius: 0.3rem; 188 | } 189 | .main-content pre { 190 | padding: 0.8rem; 191 | overflow: auto; 192 | font-size: 0.9rem; 193 | line-height: 1.45; 194 | border-radius: 0.3rem; 195 | } 196 | .main-content pre code, 197 | .main-content pre tt { 198 | display: inline; 199 | max-width: initial; 200 | padding: 0; 201 | margin: 0; 202 | overflow: initial; 203 | line-height: inherit; 204 | word-wrap: normal; 205 | background-color: transparent; 206 | border: 0; 207 | } 208 | .main-content pre code:before, 209 | .main-content pre code:after { 210 | content: normal; 211 | } 212 | .main-content pre tt:before, 213 | .main-content pre tt:after { 214 | content: normal; 215 | } 216 | .main-content ul, 217 | .main-content ol { 218 | margin-top: 0; 219 | } 220 | .main-content blockquote { 221 | padding: 0 1rem; 222 | margin-left: 0; 223 | color: #819198; 224 | border-left: 0.3rem solid #dce6f0; 225 | } 226 | .main-content blockquote > :first-child { 227 | margin-top: 0; 228 | } 229 | .main-content blockquote > :last-child { 230 | margin-bottom: 0; 231 | } 232 | .main-content table { 233 | display: block; 234 | width: 100%; 235 | overflow: auto; 236 | word-break: normal; 237 | word-break: keep-all; 238 | } 239 | .main-content table th { 240 | font-weight: bold; 241 | padding: 0.5rem 1rem; 242 | border: 1px solid #e9ebec; 243 | } 244 | .main-content table td { 245 | padding: 0.5rem 1rem; 246 | border: 1px solid #e9ebec; 247 | } 248 | .main-content dl { 249 | padding: 0; 250 | } 251 | .main-content dl dt { 252 | padding: 0; 253 | margin-top: 1rem; 254 | font-size: 1rem; 255 | font-weight: bold; 256 | } 257 | .main-content dl dd { 258 | padding: 0; 259 | margin-bottom: 1rem; 260 | } 261 | .main-content hr { 262 | height: 2px; 263 | padding: 0; 264 | margin: 1rem 0; 265 | background-color: #eff0f1; 266 | border: 0; 267 | } 268 | 269 | @media screen and (min-width: 64em) { 270 | .main-content { 271 | max-width: 64rem; 272 | padding: 2rem 6rem; 273 | margin: 0 auto; 274 | font-size: 1.1rem; 275 | } 276 | } 277 | @media screen and (min-width: 42em) and (max-width: 64em) { 278 | .main-content { 279 | padding: 2rem 4rem; 280 | font-size: 1.1rem; 281 | } 282 | } 283 | @media screen and (max-width: 42em) { 284 | .main-content { 285 | padding: 2rem 1rem; 286 | font-size: 1rem; 287 | } 288 | } 289 | .site-footer { 290 | padding-top: 2rem; 291 | margin-top: 2rem; 292 | border-top: solid 1px #eff0f1; 293 | } 294 | 295 | .site-footer-owner { 296 | display: block; 297 | font-weight: bold; 298 | } 299 | 300 | .site-footer-credits { 301 | color: #819198; 302 | } 303 | 304 | @media screen and (min-width: 64em) { 305 | .site-footer { 306 | font-size: 1rem; 307 | } 308 | } 309 | @media screen and (min-width: 42em) and (max-width: 64em) { 310 | .site-footer { 311 | font-size: 1rem; 312 | } 313 | } 314 | @media screen and (max-width: 42em) { 315 | .site-footer { 316 | font-size: 0.9rem; 317 | } 318 | } 319 | 320 | .form-group p.label { 321 | display: inline-block; 322 | margin-right: 2rem; 323 | width: 100px; 324 | } 325 | 326 | .form-group input { 327 | padding: 5px; 328 | } 329 | 330 | .playground .vue-input-tag { 331 | margin-bottom: 20px; 332 | } 333 | -------------------------------------------------------------------------------- /src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; 11 | /* 1 */ 12 | -ms-text-size-adjust: 100%; 13 | /* 2 */ 14 | -webkit-text-size-adjust: 100%; 15 | /* 2 */ 16 | } 17 | 18 | /** 19 | * Remove default margin. 20 | */ 21 | 22 | body { 23 | margin: 0; 24 | } 25 | 26 | /* HTML5 display definitions 27 | ========================================================================== */ 28 | 29 | /** 30 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 31 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 32 | * and Firefox. 33 | * Correct `block` display not defined for `main` in IE 11. 34 | */ 35 | 36 | article, 37 | aside, 38 | details, 39 | figcaption, 40 | figure, 41 | footer, 42 | header, 43 | hgroup, 44 | main, 45 | menu, 46 | nav, 47 | section, 48 | summary { 49 | display: block; 50 | } 51 | 52 | /** 53 | * 1. Correct `inline-block` display not defined in IE 8/9. 54 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 55 | */ 56 | 57 | audio, 58 | canvas, 59 | progress, 60 | video { 61 | display: inline-block; 62 | /* 1 */ 63 | vertical-align: baseline; 64 | /* 2 */ 65 | } 66 | 67 | /** 68 | * Prevent modern browsers from displaying `audio` without controls. 69 | * Remove excess height in iOS 5 devices. 70 | */ 71 | 72 | audio:not([controls]) { 73 | display: none; 74 | height: 0; 75 | } 76 | 77 | /** 78 | * Address `[hidden]` styling not present in IE 8/9/10. 79 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 80 | */ 81 | 82 | [hidden], 83 | template { 84 | display: none; 85 | } 86 | 87 | /* Links 88 | ========================================================================== */ 89 | 90 | /** 91 | * Remove the gray background color from active links in IE 10. 92 | */ 93 | 94 | a { 95 | background-color: transparent; 96 | &:active, 97 | &:hover { 98 | outline: 0; 99 | } 100 | } 101 | 102 | /** 103 | * Improve readability when focused and also mouse hovered in all browsers. 104 | */ 105 | 106 | /* Text-level semantics 107 | ========================================================================== */ 108 | 109 | /** 110 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: 1px dotted; 115 | } 116 | 117 | /** 118 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 119 | */ 120 | 121 | b, 122 | strong { 123 | font-weight: bold; 124 | } 125 | 126 | /** 127 | * Address styling not present in Safari and Chrome. 128 | */ 129 | 130 | dfn { 131 | font-style: italic; 132 | } 133 | 134 | /** 135 | * Address variable `h1` font-size and margin within `section` and `article` 136 | * contexts in Firefox 4+, Safari, and Chrome. 137 | */ 138 | 139 | h1 { 140 | font-size: 2em; 141 | margin: 0.67em 0; 142 | } 143 | 144 | /** 145 | * Address styling not present in IE 8/9. 146 | */ 147 | 148 | mark { 149 | background: #ff0; 150 | color: #000; 151 | } 152 | 153 | /** 154 | * Address inconsistent and variable font size in all browsers. 155 | */ 156 | 157 | small { 158 | font-size: 80%; 159 | } 160 | 161 | /** 162 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 163 | */ 164 | 165 | sub { 166 | font-size: 75%; 167 | line-height: 0; 168 | position: relative; 169 | vertical-align: baseline; 170 | } 171 | 172 | sup { 173 | font-size: 75%; 174 | line-height: 0; 175 | position: relative; 176 | vertical-align: baseline; 177 | top: -0.5em; 178 | } 179 | 180 | sub { 181 | bottom: -0.25em; 182 | } 183 | 184 | /* Embedded content 185 | ========================================================================== */ 186 | 187 | /** 188 | * Remove border when inside `a` element in IE 8/9/10. 189 | */ 190 | 191 | img { 192 | border: 0; 193 | } 194 | 195 | /** 196 | * Correct overflow not hidden in IE 9/10/11. 197 | */ 198 | 199 | svg:not(:root) { 200 | overflow: hidden; 201 | } 202 | 203 | /* Grouping content 204 | ========================================================================== */ 205 | 206 | /** 207 | * Address margin not present in IE 8/9 and Safari. 208 | */ 209 | 210 | figure { 211 | margin: 1em 40px; 212 | } 213 | 214 | /** 215 | * Address differences between Firefox and other browsers. 216 | */ 217 | 218 | hr { 219 | box-sizing: content-box; 220 | height: 0; 221 | } 222 | 223 | /** 224 | * Contain overflow in all browsers. 225 | */ 226 | 227 | pre { 228 | overflow: auto; 229 | } 230 | 231 | /** 232 | * Address odd `em`-unit font size rendering in all browsers. 233 | */ 234 | 235 | code, 236 | kbd, 237 | pre, 238 | samp { 239 | font-family: monospace, monospace; 240 | font-size: 1em; 241 | } 242 | 243 | /* Forms 244 | ========================================================================== */ 245 | 246 | /** 247 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 248 | * styling of `select`, unless a `border` property is set. 249 | */ 250 | 251 | /** 252 | * 1. Correct color not being inherited. 253 | * Known issue: affects color of disabled elements. 254 | * 2. Correct font properties not being inherited. 255 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 256 | */ 257 | 258 | button, 259 | input, 260 | optgroup, 261 | select, 262 | textarea { 263 | color: inherit; 264 | /* 1 */ 265 | font: inherit; 266 | /* 2 */ 267 | margin: 0; 268 | /* 3 */ 269 | } 270 | 271 | /** 272 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 273 | */ 274 | 275 | button { 276 | overflow: visible; 277 | text-transform: none; 278 | } 279 | 280 | /** 281 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 282 | * All other form control elements do not inherit `text-transform` values. 283 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 284 | * Correct `select` style inheritance in Firefox. 285 | */ 286 | 287 | select { 288 | text-transform: none; 289 | } 290 | 291 | /** 292 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 293 | * and `video` controls. 294 | * 2. Correct inability to style clickable `input` types in iOS. 295 | * 3. Improve usability and consistency of cursor style between image-type 296 | * `input` and others. 297 | */ 298 | 299 | button, 300 | html input[type="button"] { 301 | -webkit-appearance: button; 302 | /* 2 */ 303 | cursor: pointer; 304 | /* 3 */ 305 | } 306 | 307 | input { 308 | &[type="reset"], 309 | &[type="submit"] { 310 | -webkit-appearance: button; 311 | /* 2 */ 312 | cursor: pointer; 313 | /* 3 */ 314 | } 315 | } 316 | 317 | /** 318 | * Re-set default cursor for disabled elements. 319 | */ 320 | 321 | button[disabled], 322 | html input[disabled] { 323 | cursor: default; 324 | } 325 | 326 | /** 327 | * Remove inner padding and border in Firefox 4+. 328 | */ 329 | 330 | button::-moz-focus-inner { 331 | border: 0; 332 | padding: 0; 333 | } 334 | 335 | input { 336 | &::-moz-focus-inner { 337 | border: 0; 338 | padding: 0; 339 | } 340 | line-height: normal; 341 | &[type="checkbox"], 342 | &[type="radio"] { 343 | box-sizing: border-box; 344 | /* 1 */ 345 | padding: 0; 346 | /* 2 */ 347 | } 348 | &[type="number"] { 349 | &::-webkit-inner-spin-button, 350 | &::-webkit-outer-spin-button { 351 | height: auto; 352 | } 353 | } 354 | &[type="search"] { 355 | -webkit-appearance: textfield; 356 | /* 1 */ 357 | /* 2 */ 358 | box-sizing: content-box; 359 | &::-webkit-search-cancel-button, 360 | &::-webkit-search-decoration { 361 | -webkit-appearance: none; 362 | } 363 | } 364 | } 365 | 366 | /** 367 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 368 | * the UA stylesheet. 369 | */ 370 | 371 | /** 372 | * It's recommended that you don't attempt to style these elements. 373 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 374 | * 375 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 376 | * 2. Remove excess padding in IE 8/9/10. 377 | */ 378 | 379 | /** 380 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 381 | * `font-size` values of the `input`, it causes the cursor style of the 382 | * decrement button to change from `default` to `text`. 383 | */ 384 | 385 | /** 386 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 387 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 388 | * (include `-moz` to future-proof). 389 | */ 390 | 391 | /** 392 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 393 | * Safari (but not Chrome) clips the cancel button when the search input has 394 | * padding (and `textfield` appearance). 395 | */ 396 | 397 | /** 398 | * Define consistent border, margin, and padding. 399 | */ 400 | 401 | fieldset { 402 | border: 1px solid #c0c0c0; 403 | margin: 0 2px; 404 | padding: 0.35em 0.625em 0.75em; 405 | } 406 | 407 | /** 408 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 409 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 410 | */ 411 | 412 | legend { 413 | border: 0; 414 | /* 1 */ 415 | padding: 0; 416 | /* 2 */ 417 | } 418 | 419 | /** 420 | * Remove default vertical scrollbar in IE 8/9/10/11. 421 | */ 422 | 423 | textarea { 424 | overflow: auto; 425 | } 426 | 427 | /** 428 | * Don't inherit the `font-weight` (applied by a rule above). 429 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 430 | */ 431 | 432 | optgroup { 433 | font-weight: bold; 434 | } 435 | 436 | /* Tables 437 | ========================================================================== */ 438 | 439 | /** 440 | * Remove most spacing between table cells. 441 | */ 442 | 443 | table { 444 | border-collapse: collapse; 445 | border-spacing: 0; 446 | } 447 | 448 | td, 449 | th { 450 | padding: 0; 451 | } 452 | -------------------------------------------------------------------------------- /tests/unit/InputTag.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import InputTag from "@/components/InputTag.vue"; 3 | 4 | async function addTag(wrapper, newTag) { 5 | // TODO: use wrapper.trigger('keydown', { which: 65 }) 6 | // so we have an event on the vm.addNew method 7 | wrapper.setData({ newTag }); 8 | await wrapper.vm.addNew(); 9 | } 10 | 11 | describe("InputTag.vue", () => { 12 | let wrapper; 13 | 14 | beforeEach(() => { 15 | wrapper = shallowMount(InputTag); 16 | }); 17 | 18 | it("should have a new tag input without placeholder", () => { 19 | const input = wrapper.find("input.new-tag"); 20 | expect(input.attributes().placeholder).toEqual(""); 21 | }); 22 | 23 | describe("addNew()", () => { 24 | beforeEach(async () => { 25 | await addTag(wrapper, "tag 1"); 26 | await addTag(wrapper, "tag 1"); 27 | await addTag(wrapper, "tag 2"); 28 | }); 29 | 30 | it("should have 2 tags", () => { 31 | expect(wrapper.vm.innerTags.length).toEqual(2); 32 | }); 33 | 34 | it("should have a 'tag 1'", () => { 35 | expect(wrapper.vm.innerTags[0]).toEqual("tag 1"); 36 | }); 37 | 38 | it("should have a 'tag 2'", () => { 39 | expect(wrapper.vm.innerTags[1]).toEqual("tag 2"); 40 | }); 41 | 42 | it("should reset the new tag", () => { 43 | expect(wrapper.vm.newTag).toEqual(""); 44 | }); 45 | 46 | it("should emmit a tag change event", () => { 47 | expect(wrapper.emitted()["update:tags"]).toBeTruthy(); 48 | }); 49 | 50 | it("should emmit an input event", () => { 51 | expect(wrapper.emitted()["input"]).toBeTruthy(); 52 | }); 53 | 54 | it("should have 2 remove tag buttons", () => { 55 | expect(wrapper.findAll("a.remove").length).toEqual(2); 56 | }); 57 | }); 58 | 59 | describe("remove(index)", () => { 60 | beforeEach(async () => { 61 | await addTag(wrapper, "tag 1"); 62 | await addTag(wrapper, "tag 2"); 63 | await addTag(wrapper, "tag 3"); 64 | 65 | wrapper.vm.remove(1); 66 | }); 67 | 68 | it("should have 2 tags", () => { 69 | expect(wrapper.vm.innerTags.length).toEqual(2); 70 | }); 71 | 72 | it("should have a 'tag 1'", () => { 73 | expect(wrapper.vm.innerTags[0]).toEqual("tag 1"); 74 | }); 75 | 76 | it("should have a 'tag 3'", () => { 77 | expect(wrapper.vm.innerTags[1]).toEqual("tag 3"); 78 | }); 79 | }); 80 | 81 | describe("removeLastTag()", () => { 82 | beforeEach(async () => { 83 | await addTag(wrapper, "tag 1"); 84 | await addTag(wrapper, "tag 2"); 85 | await addTag(wrapper, "tag 3"); 86 | 87 | wrapper.vm.removeLastTag(); 88 | console.log(wrapper.vm.innerTags); 89 | }); 90 | 91 | it("should have 2 tags", () => { 92 | expect(wrapper.vm.innerTags.length).toEqual(2); 93 | }); 94 | 95 | it("should have a 'tag 1'", () => { 96 | expect(wrapper.vm.innerTags[0]).toEqual("tag 1"); 97 | }); 98 | 99 | it("should have a 'tag 2'", () => { 100 | expect(wrapper.vm.innerTags[1]).toEqual("tag 2"); 101 | }); 102 | }); 103 | 104 | describe("Props", () => { 105 | describe("allow-duplicates='false'", () => { 106 | beforeEach(() => { 107 | wrapper = shallowMount(InputTag, { 108 | propsData: { allowDuplicates: true } 109 | }); 110 | addTag(wrapper, "tag 1"); 111 | addTag(wrapper, "tag 1"); 112 | addTag(wrapper, "tag 1"); 113 | }); 114 | 115 | it("should have 3 tags", () => { 116 | expect(wrapper.vm.innerTags.length).toEqual(3); 117 | }); 118 | 119 | it("should have a 'tag 1'", () => { 120 | expect(wrapper.vm.innerTags[2]).toEqual("tag 1"); 121 | }); 122 | }); 123 | 124 | describe("read-only='true'", () => { 125 | beforeEach(() => { 126 | wrapper = shallowMount(InputTag, { 127 | propsData: { readOnly: true } 128 | }); 129 | 130 | addTag(wrapper, "tag 1"); 131 | }); 132 | 133 | it("should have a read-only CSS class", () => { 134 | expect(wrapper.findAll(".read-only").length).toEqual(1); 135 | }); 136 | 137 | it("shouldn't have a remove tag button", () => { 138 | expect(wrapper.findAll("a.remove").length).toEqual(0); 139 | }); 140 | 141 | it("shouldn't have a new input tag", () => { 142 | expect(wrapper.findAll("input.new-tag").length).toEqual(0); 143 | }); 144 | }); 145 | 146 | describe("value='[1, 2, 3]'", () => { 147 | beforeEach(() => { 148 | wrapper = shallowMount(InputTag, { 149 | propsData: { value: [1, 2, 3] } 150 | }); 151 | }); 152 | 153 | it("should have 3 tags", () => { 154 | expect(wrapper.vm.innerTags.length).toEqual(3); 155 | }); 156 | }); 157 | 158 | describe.skip("dynamic value", () => { 159 | beforeEach(() => { 160 | wrapper = shallowMount(InputTag, { 161 | propsData: { value: [1, 2, 3] } 162 | }); 163 | }); 164 | 165 | it("should watch value property changes", () => { 166 | wrapper.setProps({ value: [1, 2, 3, 4] }); 167 | expect(wrapper.vm.innerTags.length).toEqual(4); 168 | }); 169 | }); 170 | 171 | describe("validate='text'", () => { 172 | beforeEach(() => { 173 | wrapper = shallowMount(InputTag, { 174 | propsData: { validate: "text" } 175 | }); 176 | 177 | addTag(wrapper, "123"); 178 | addTag(wrapper, "mati@tucci.me"); 179 | addTag(wrapper, "https://tucci.me"); 180 | addTag(wrapper, "2002-04-03"); 181 | addTag(wrapper, "foo"); 182 | }); 183 | 184 | it("should have 1 tag", () => { 185 | expect(wrapper.vm.innerTags.length).toEqual(1); 186 | }); 187 | 188 | it("should have a tag 'foo'", () => { 189 | expect(wrapper.vm.innerTags[0]).toEqual("foo"); 190 | }); 191 | }); 192 | 193 | describe("validate='digits'", () => { 194 | beforeEach(() => { 195 | wrapper = shallowMount(InputTag, { 196 | propsData: { validate: "digits" } 197 | }); 198 | 199 | addTag(wrapper, "mati@tucci.me"); 200 | addTag(wrapper, "https://tucci.me"); 201 | addTag(wrapper, "123"); 202 | addTag(wrapper, "2002-04-03"); 203 | addTag(wrapper, "foo"); 204 | }); 205 | 206 | it("should have 1 tag", () => { 207 | expect(wrapper.vm.innerTags.length).toEqual(2); 208 | }); 209 | 210 | it("should have a tag '123'", () => { 211 | expect(wrapper.vm.innerTags[0]).toEqual("123"); 212 | }); 213 | }); 214 | 215 | describe("validate='email'", () => { 216 | beforeEach(() => { 217 | wrapper = shallowMount(InputTag, { 218 | propsData: { validate: "email" } 219 | }); 220 | 221 | addTag(wrapper, "https://tucci.me"); 222 | addTag(wrapper, "2002-04-03"); 223 | addTag(wrapper, "foo"); 224 | addTag(wrapper, "123"); 225 | addTag(wrapper, "mati@tucci.me"); 226 | }); 227 | 228 | it("should have 1 tag", () => { 229 | expect(wrapper.vm.innerTags.length).toEqual(1); 230 | }); 231 | 232 | it("should have a tag 'mati@tucci.me'", () => { 233 | expect(wrapper.vm.innerTags[0]).toEqual("mati@tucci.me"); 234 | }); 235 | }); 236 | 237 | describe("validate='url'", () => { 238 | beforeEach(() => { 239 | wrapper = shallowMount(InputTag, { 240 | propsData: { validate: "url" } 241 | }); 242 | 243 | addTag(wrapper, "2002-04-03"); 244 | addTag(wrapper, "foo"); 245 | addTag(wrapper, "123"); 246 | addTag(wrapper, "mati@tucci.me"); 247 | addTag(wrapper, "https://tucci.me"); 248 | }); 249 | 250 | it("should have 1 tag", () => { 251 | expect(wrapper.vm.innerTags.length).toEqual(1); 252 | }); 253 | 254 | it("should have a tag 'https://tucci.me'", () => { 255 | expect(wrapper.vm.innerTags[0]).toEqual("https://tucci.me"); 256 | }); 257 | }); 258 | 259 | describe("validate='isodate'", () => { 260 | beforeEach(() => { 261 | wrapper = shallowMount(InputTag, { 262 | propsData: { validate: "isodate" } 263 | }); 264 | 265 | addTag(wrapper, "foo"); 266 | addTag(wrapper, "123"); 267 | addTag(wrapper, "mati@tucci.me"); 268 | addTag(wrapper, "https://tucci.me"); 269 | addTag(wrapper, "2002-04-03"); 270 | }); 271 | 272 | it("should have 1 tag", () => { 273 | expect(wrapper.vm.innerTags.length).toEqual(1); 274 | }); 275 | 276 | it("should have a tag '2002-04-03'", () => { 277 | expect(wrapper.vm.innerTags[0]).toEqual("2002-04-03"); 278 | }); 279 | }); 280 | 281 | describe("before-adding", () => { 282 | beforeEach(() => { 283 | wrapper = shallowMount(InputTag, { 284 | propsData: { beforeAdding: tag => tag.toUpperCase() } 285 | }); 286 | 287 | addTag(wrapper, "new tag"); 288 | }); 289 | 290 | it("should have an uppercase tag", () => { 291 | expect(wrapper.vm.innerTags[0]).toEqual("NEW TAG"); 292 | }); 293 | }); 294 | }); 295 | 296 | describe.skip("CSS classes", () => { 297 | it("should add activity class when input is focused", () => { 298 | const vueInputTagWrapper = wrapper.find(".vue-input-tag-wrapper"); 299 | 300 | const input = wrapper.find("input.new-tag"); 301 | input.trigger("focus"); 302 | 303 | expect(vueInputTagWrapper.classes()).toContain( 304 | "vue-input-tag-wrapper--active" 305 | ); 306 | }); 307 | 308 | it("should remove activity class when input is blurred", () => { 309 | const vueInputTagWrapper = wrapper.find(".vue-input-tag-wrapper"); 310 | 311 | const input = wrapper.find("input.new-tag"); 312 | input.trigger("focus"); 313 | input.trigger("blur"); 314 | 315 | expect( 316 | vueInputTagWrapper.classes()["vue-input-tag-wrapper--active"] 317 | ).toBeUndefined(); 318 | }); 319 | }); 320 | 321 | describe("slots", () => { 322 | beforeEach(() => { 323 | wrapper = shallowMount(InputTag, { 324 | slots: { 325 | "remove-icon": '' 326 | } 327 | }); 328 | 329 | addTag(wrapper, "foo"); 330 | }); 331 | 332 | it("should render 'remove icon' slot as remove icon for a tag", () => { 333 | expect(wrapper.find("a.remove").html()).toBe( 334 | '' 335 | ); 336 | }); 337 | }); 338 | }); 339 | --------------------------------------------------------------------------------