├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── ci.yml │ └── dependabot-auto-merge.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .template-lintrc.js ├── .watchmanconfig ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep ├── components │ ├── tag-input.hbs │ └── tag-input.js └── styles │ └── addon.css ├── app ├── .gitkeep ├── components │ └── tag-input.js └── styles │ └── app.scss ├── babel.config.json ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── components │ │ │ └── .gitkeep │ │ │ └── index.hbs │ ├── config │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── destroy-app.js │ ├── module-for-acceptance.js │ ├── resolver.js │ └── start-app.js ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ └── tag-input-test.js ├── test-helper.js └── unit │ └── .gitkeep ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: '@babel/eslint-parser', 6 | plugins: ['ember'], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:ember/recommended', 10 | 'plugin:prettier/recommended' 11 | ], 12 | env: { 13 | browser: true 14 | }, 15 | rules: { 16 | 'ember/no-array-prototype-extensions': 'off', 17 | 'ember/no-classic-components': 'off', 18 | 'ember/no-runloop': 'off', 19 | 'ember/require-tagless-components': 'off', 20 | 'ember/no-component-lifecycle-hooks': 'off' 21 | }, 22 | overrides: [ 23 | // node files 24 | { 25 | files: [ 26 | './.eslintrc.js', 27 | './.prettierrc.js', 28 | './.template-lintrc.js', 29 | './ember-cli-build.js', 30 | './index.js', 31 | './testem.js', 32 | './blueprints/*/index.js', 33 | './config/**/*.js', 34 | './tests/dummy/config/**/*.js' 35 | ], 36 | parserOptions: { 37 | sourceType: 'script' 38 | }, 39 | env: { 40 | browser: false, 41 | node: true 42 | }, 43 | plugins: ['node'], 44 | extends: ['plugin:node/recommended'] 45 | }, 46 | { 47 | // test files 48 | files: ['tests/**/*-test.{js,ts}'], 49 | extends: ['plugin:qunit/recommended'], 50 | rules: { 51 | 'qunit/no-assert-equal': 'off' 52 | } 53 | } 54 | ] 55 | }; 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: {} 9 | 10 | concurrency: 11 | group: ci-${{ github.head_ref || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: "Tests" 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Install Node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20.x 25 | cache: 'yarn' 26 | - name: Install Dependencies 27 | run: yarn install 28 | - name: Lint 29 | run: yarn lint 30 | - name: Run Tests 31 | run: npm run test 32 | 33 | floating: 34 | name: "Floating Dependencies" 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | - uses: actions/setup-node@v3 40 | with: 41 | node-version: 18.x 42 | cache: yarn 43 | - name: Install Dependencies 44 | run: yarn install --no-lockfile 45 | - name: Run Tests 46 | run: yarn test 47 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1.1.1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --merge "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn-error.log 30 | /yarn.lock 31 | .gitkeep 32 | 33 | # ember-try 34 | /.node_modules.ember-try/ 35 | /bower.json.ember-try 36 | /package.json.ember-try 37 | 38 | # ci 39 | /.circleci/ 40 | /.github/ 41 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | trailingComma: 'none' 6 | }; 7 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | rules: { 6 | 'link-href-attributes': false, 7 | 'no-invalid-interactive': false, 8 | 'no-invalid-link-text': false 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd my-addon` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint` 12 | * `npm run lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | * `ember serve` 23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-tag-input 2 | 3 | ember-tag-input is a simple Ember addon that converts a user's typing into tags. New tags are created when the user types a comma, space, or hits the enter key. Tags can be removed using the backspace key or by clicking the x button on each tag. 4 | 5 | ![](http://i.imgur.com/aVRvs7z.png) 6 | 7 | ## Usage 8 | 9 | In the simplest case, just pass a list of tags to render and actions for adding and removing tags. The component will never change the tags list for you, it will instead call actions when changes need to be made. The component will yield each tag in the list, allowing you to render it as you wish. 10 | 11 | ```handlebars 12 | 18 | {{tag}} 19 | 20 | ``` 21 | 22 | ```js 23 | import Controller from '@ember/controller'; 24 | import { tracked } from '@glimmer/tracking'; 25 | import { action } from '@ember/object'; 26 | 27 | export default class ApplicationController extends Controller { 28 | @tracked tags = []; 29 | 30 | @action addTag(tag) { 31 | this.tags.pushObject(tag); 32 | } 33 | 34 | @action removeTagAtIndex(index) { 35 | this.tags.removeAt(index); 36 | } 37 | } 38 | ``` 39 | 40 | The above example works if your tags array is just an simple array of strings. If your tags are more complex objects, you can render them however you want, as demonstrated by the following example: 41 | 42 | ```handlebars 43 | 49 |
50 | {{tag.name}} 51 |
52 |
53 | {{tag.date}} 54 |
55 |
56 | ``` 57 | If you pass tags objects, you can use the `modifiers` property to pass extra classes to individual tags: 58 | ``` 59 | tags = A([ 60 | { name: 'first', modifiers: 'primaryTag' }, 61 | { name: 'second', modifiers: 'secondaryTag' }, 62 | ]); 63 | ``` 64 | ## Options 65 | 66 | ### tags 67 | - An array of tags to render. 68 | - **default: null** 69 | 70 | ### removeConfirmation 71 | - Whether or not it takes two presses of the backspace key to remove a tag. When enabled, the first backspace press will add the class `emberTagInput-tag--remove` to the element that is about to be removed. 72 | - **default: true** 73 | 74 | ### allowCommaInTags 75 | - If tags are allowed to contain comma. 76 | - **default: false** 77 | 78 | ### allowSpacesInTags 79 | - If tags are allowed to contain spaces. 80 | - **default: false** 81 | 82 | ### allowDuplicates 83 | - If duplicates tags are allowed in the list. 84 | - **default: false** 85 | 86 | ### showRemoveButtons 87 | - If 'x' removal links should be displayed at the right side of each tag. 88 | - **default: true** 89 | 90 | ### placeholder 91 | - The placeholder text to display when the user hasn't typed anything. Isn't displayed if readOnly=true. 92 | - **default: ''** 93 | 94 | ### ariaLabel 95 | - The aria-label for the input field. 96 | - **default: ''** 97 | 98 | ### readOnly 99 | - If a read only view of the tags should be displayed. If enabled, existing tags can't be removed and new tags can't be added. 100 | - **default: false** 101 | 102 | ### maxlength 103 | - If maxlength value is passed in, each `` field(tag) will have maximum number of characters allowed. 104 | - **default: ''** 105 | 106 | ## Actions 107 | 108 | ### addTag 109 | - This action will get called when the user is trying to add a new tag. Your implementation should either add the tag to the tags array or return false if the tag wasn't added. 110 | - **parameters: tag** 111 | 112 | ### removeTagAtIndex 113 | - This action will get called when the user is trying to remove a tag. Your implementation should remove the element from the tags array at the specified index. 114 | - **parameters: index** 115 | 116 | ### onKeyUp 117 | - This action will get called after each key press. 118 | - **parameters: currentInputValue** 119 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/addon/.gitkeep -------------------------------------------------------------------------------- /addon/components/tag-input.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.tags as |tag index|~}} 2 |
  • 3 | {{yield tag}} 4 | {{#if this._isRemoveButtonVisible}} 5 | 6 | {{/if}} 7 |
  • 8 | {{~/each~}} 9 | 10 |
  • 11 | 18 |
  • 19 | -------------------------------------------------------------------------------- /addon/components/tag-input.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | const KEY_CODES = { 6 | BACKSPACE: 8, 7 | COMMA: 188, 8 | ENTER: 13, 9 | SPACE: 32 10 | }; 11 | 12 | const TAG_CLASS = 'emberTagInput-tag'; 13 | const REMOVE_CONFIRMATION_CLASS = 'emberTagInput-tag--remove'; 14 | 15 | export default class TagInput extends Component { 16 | classNameBindings = [':emberTagInput', 'readOnly:emberTagInput--readOnly']; 17 | 18 | tagName = 'ul'; 19 | 20 | tags = null; 21 | 22 | removeConfirmation = true; 23 | 24 | allowCommaInTags = false; 25 | 26 | allowDuplicates = false; 27 | 28 | allowSpacesInTags = false; 29 | 30 | @tracked 31 | showRemoveButtons = true; 32 | 33 | @tracked 34 | readOnly = false; 35 | 36 | placeholder = ''; 37 | 38 | ariaLabel = ''; 39 | 40 | get _isRemoveButtonVisible() { 41 | return this.showRemoveButtons && !this.readOnly; 42 | } 43 | 44 | onKeyUp = false; 45 | 46 | addNewTag(tag) { 47 | const tags = this.tags; 48 | const addTag = this.addTag; 49 | const allowDuplicates = this.allowDuplicates; 50 | 51 | if (!allowDuplicates && tags && tags.indexOf(tag) >= 0) { 52 | return false; 53 | } 54 | 55 | return addTag(tag) !== false; 56 | } 57 | 58 | didInsertElement() { 59 | super.didInsertElement(...arguments); 60 | this.initEvents(); 61 | } 62 | 63 | dispatchKeyUp(value) { 64 | if (this.onKeyUp) { 65 | this.onKeyUp(value); 66 | } 67 | } 68 | 69 | _onContainerClick() { 70 | const newTagInput = this.element.querySelector('.js-ember-tag-input-new'); 71 | const isReadOnly = this.readOnly; 72 | 73 | if (!isReadOnly) { 74 | newTagInput.focus(); 75 | } 76 | } 77 | 78 | _onInputKeyDown(e) { 79 | if (!this.readOnly) { 80 | const { allowCommaInTags, allowSpacesInTags, tags } = this; 81 | const backspaceRegex = new RegExp( 82 | String.fromCharCode(KEY_CODES.BACKSPACE), 83 | 'g' 84 | ); 85 | const newTag = e.target.value.trim().replace(backspaceRegex, ''); 86 | 87 | if (e.which === KEY_CODES.BACKSPACE) { 88 | if (newTag.length === 0 && tags.length > 0) { 89 | const removeTagAtIndex = this.removeTagAtIndex; 90 | 91 | if (this.removeConfirmation) { 92 | const tags = this.element.querySelectorAll('.' + TAG_CLASS); 93 | const lastTag = tags[tags.length - 1]; 94 | 95 | if ( 96 | lastTag && 97 | !lastTag.classList.contains(REMOVE_CONFIRMATION_CLASS) 98 | ) { 99 | lastTag.classList.add(REMOVE_CONFIRMATION_CLASS); 100 | return; 101 | } 102 | } 103 | 104 | removeTagAtIndex(tags.length - 1); 105 | } 106 | } else { 107 | if ( 108 | (!allowCommaInTags && e.which === KEY_CODES.COMMA) || 109 | (!allowSpacesInTags && e.which === KEY_CODES.SPACE) || 110 | e.which === KEY_CODES.ENTER 111 | ) { 112 | if (newTag.length > 0) { 113 | if (this.addNewTag(newTag)) { 114 | e.target.value = ''; 115 | } 116 | } 117 | e.preventDefault(); 118 | } 119 | 120 | [].forEach.call( 121 | this.element.querySelectorAll('.' + TAG_CLASS), 122 | function (tagEl) { 123 | tagEl.classList.remove(REMOVE_CONFIRMATION_CLASS); 124 | } 125 | ); 126 | } 127 | } 128 | } 129 | 130 | _onInputBlur(e) { 131 | const newTag = e.target.value.trim(); 132 | 133 | if (newTag.length > 0) { 134 | if (this.addNewTag(newTag)) { 135 | e.target.value = ''; 136 | this.dispatchKeyUp(''); 137 | } 138 | } 139 | } 140 | 141 | _onInputKeyUp(e) { 142 | this.dispatchKeyUp(e.target.value); 143 | } 144 | 145 | initEvents() { 146 | const container = this.element; 147 | const onContainerClick = this._onContainerClick.bind(this); 148 | const onInputKeyDown = this._onInputKeyDown.bind(this); 149 | const onInputBlur = this._onInputBlur.bind(this); 150 | const onInputKeyUp = this._onInputKeyUp.bind(this); 151 | 152 | container.addEventListener('click', onContainerClick); 153 | const newTagInput = this.element.querySelector('.js-ember-tag-input-new'); 154 | 155 | newTagInput.addEventListener('keydown', onInputKeyDown); 156 | newTagInput.addEventListener('blur', onInputBlur); 157 | newTagInput.addEventListener('keyup', onInputKeyUp); 158 | } 159 | 160 | @action 161 | removeTag(index) { 162 | const removeTagAtIndex = this.removeTagAtIndex; 163 | removeTagAtIndex(index); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /addon/styles/addon.css: -------------------------------------------------------------------------------- 1 | .emberTagInput { 2 | margin: 0; 3 | padding: 5px 5px 2px 5px; 4 | border: 1px solid lightgray; 5 | cursor: text; 6 | } 7 | .emberTagInput--readOnly { 8 | cursor: default; 9 | } 10 | 11 | .emberTagInput li { 12 | list-style-type: none; 13 | display: inline-block; 14 | } 15 | 16 | .emberTagInput-tag, 17 | .emberTagInput-input { 18 | font: 12px sans-serif; 19 | padding: 5px 10px; 20 | margin-bottom: 3px; 21 | } 22 | 23 | .emberTagInput-tag { 24 | background: cornflowerblue; 25 | border-radius: 20px; 26 | margin-right: 5px; 27 | color: white; 28 | } 29 | 30 | .emberTagInput-tag--remove { 31 | opacity: 0.75; 32 | } 33 | 34 | .emberTagInput-remove:before { 35 | content: 'x'; 36 | cursor: pointer; 37 | } 38 | 39 | .emberTagInput-input { 40 | border: none; 41 | padding-left: 0; 42 | } 43 | .emberTagInput-input:focus { 44 | outline: none; 45 | } 46 | 47 | .emberTagInput-input.is-disabled { 48 | display: none; 49 | } 50 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/app/.gitkeep -------------------------------------------------------------------------------- /app/components/tag-input.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-tag-input/components/tag-input'; 2 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/app/styles/app.scss -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": false }] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function () { 6 | return { 7 | scenarios: [ 8 | { 9 | name: 'ember-lts-4.12', 10 | npm: { 11 | devDependencies: { 12 | 'ember-source': '4.12' 13 | } 14 | } 15 | }, 16 | { 17 | name: 'ember-release', 18 | npm: { 19 | devDependencies: { 20 | 'ember-source': await getChannelURL('release') 21 | } 22 | } 23 | } 24 | ] 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (env) { 4 | let ENV = { 5 | APP: {} 6 | }; 7 | 8 | if (env === 'test') { 9 | ENV.APP.autoboot = false; 10 | } 11 | 12 | return ENV; 13 | }; 14 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | const { maybeEmbroider } = require('@embroider/test-setup'); 18 | return maybeEmbroider(app, { 19 | skipBabel: [ 20 | { 21 | package: 'qunit' 22 | } 23 | ] 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-tag-input", 3 | "version": "3.1.0", 4 | "description": "Ember tag input", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": "https://github.com/calvinlough/ember-tag-input", 9 | "license": "MIT", 10 | "author": "Calvin Lough", 11 | "directories": { 12 | "doc": "doc", 13 | "test": "tests" 14 | }, 15 | "scripts": { 16 | "build": "ember build --environment=production", 17 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 18 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 19 | "lint:hbs": "ember-template-lint .", 20 | "lint:hbs:fix": "ember-template-lint . --fix", 21 | "lint:js": "eslint . --cache", 22 | "lint:js:fix": "eslint . --fix", 23 | "start": "ember serve", 24 | "test": "ember test", 25 | "test:ember-compatibility": "ember try:each" 26 | }, 27 | "dependencies": { 28 | "ember-cli-babel": "^8.2.0", 29 | "ember-cli-htmlbars": "^6.3.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/eslint-parser": "^7.23.3", 33 | "@ember/optional-features": "^2.0.0", 34 | "@babel/plugin-proposal-decorators": "^7.23.6", 35 | "@ember/test-helpers": "^5.0.0", 36 | "@embroider/test-setup": "^4.0.0", 37 | "@glimmer/component": "^2.0.0", 38 | "@glimmer/tracking": "^1.1.2", 39 | "broccoli-asset-rev": "^3.0.0", 40 | "ember-auto-import": "^2.7.2", 41 | "ember-cli": "~6.2.0", 42 | "ember-cli-dependency-checker": "^3.3.2", 43 | "ember-cli-inject-live-reload": "^2.1.0", 44 | "ember-cli-sri": "^2.1.1", 45 | "ember-cli-terser": "^4.0.2", 46 | "ember-disable-prototype-extensions": "^1.1.3", 47 | "ember-load-initializers": "^3.0.1", 48 | "ember-maybe-import-regenerator": "^1.0.0", 49 | "ember-page-title": "^9.0.1", 50 | "ember-qunit": "^8.0.2", 51 | "ember-resolver": "^13.0.0", 52 | "ember-source": "~6.3.0", 53 | "ember-source-channel-url": "^3.0.0", 54 | "ember-template-lint": "^7.0.0", 55 | "ember-try": "^4.0.0", 56 | "eslint": "^8.56.0", 57 | "eslint-config-prettier": "^10.0.1", 58 | "eslint-plugin-ember": "^12.0.0", 59 | "eslint-plugin-node": "^11.1.0", 60 | "eslint-plugin-prettier": "^5.1.2", 61 | "eslint-plugin-qunit": "^8.0.1", 62 | "loader.js": "^4.7.0", 63 | "npm-run-all": "^4.1.5", 64 | "prettier": "^3.1.1", 65 | "qunit": "^2.20.0", 66 | "qunit-dom": "^3.0.0", 67 | "webpack": "^5.89.0" 68 | }, 69 | "engines": { 70 | "node": "12.* || 14.* || >= 16" 71 | }, 72 | "ember": { 73 | "edition": "octane" 74 | }, 75 | "ember-addon": { 76 | "configPath": "tests/dummy/config" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | framework: 'qunit', 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | tap_quiet_logs: true, 7 | launch_in_ci: ['Chrome'], 8 | launch_in_dev: ['Chrome'], 9 | browser_args: { 10 | Chrome: [ 11 | '--disable-dev-shm-usage', 12 | '--disable-software-rasterizer', 13 | '--disable-web-security', 14 | '--headless', 15 | '--incognito', 16 | '--mute-audio', 17 | '--no-sandbox', 18 | '--remote-debugging-address=0.0.0.0', 19 | '--remote-debugging-port=9222', 20 | '--window-size=1440,900' 21 | ] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { action } from '@ember/object'; 4 | import { A } from '@ember/array'; 5 | 6 | export default class IndexController extends Controller { 7 | @tracked tags = A(['foo', 'bar']); 8 | @tracked readOnly = true; 9 | @tracked currentInputValue; 10 | 11 | @tracked tagsObjects = [{ label: 'foo' }, { label: 'bar' }]; 12 | colors = ['green', 'red', 'purple']; 13 | 14 | @action addTag(tag) { 15 | this.tags.pushObject(tag); 16 | } 17 | 18 | @action removeTagAtIndex(index) { 19 | this.tags.removeAt(index); 20 | } 21 | 22 | @action addTagObject(tag) { 23 | this.tagsObjects.pushObject({ 24 | label: tag, 25 | modifiers: this.colors[Math.floor(Math.random() * 3)] 26 | }); 27 | } 28 | 29 | @action removeTagObjectAtIndex(index) { 30 | this.tagsObjects.removeAt(index); 31 | } 32 | 33 | @action onKeyUp(value) { 34 | this.currentInputValue = value; 35 | } 36 | 37 | @action toggleReadOnly() { 38 | this.readOnly = !this.readOnly; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ember Tag Input 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () { 10 | this.route('index', { path: '' }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | .red { 2 | background-color: rgb(161, 40, 40); 3 | } 4 | .green { 5 | background-color: rgb(40, 161, 40); 6 | } 7 | .purple { 8 | background-color: rgb(137, 74, 153); 9 | } 10 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |

    Default

    2 | 3 | 12 | {{tag}} 13 | 14 | 15 | {{#if this.currentInputValue}} 16 |

    typing: {{this.currentInputValue}}

    17 | {{/if}} 18 | 19 |

    readOnly

    20 | 21 | 31 | {{tag}} 32 | 33 | 34 | 35 | 36 | {{#if this.currentInputValue}} 37 |

    typing: {{this.currentInputValue}}

    38 | {{/if}} 39 | 40 |

    Tags as objects

    41 |

    You can pass tags as strings or objects. If you pass objects, use the property `modifiers` to pass extra classes to tags.

    42 | 43 | 52 | {{tag.label}} 53 | 54 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | module.exports = { 10 | browsers 11 | }; 12 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | 3 | export default function destroyApp(application) { 4 | run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import startApp from '../helpers/start-app'; 3 | import destroyApp from '../helpers/destroy-app'; 4 | import { Promise } from 'rsvp'; 5 | 6 | export default function (name, options = {}) { 7 | module(name, { 8 | beforeEach() { 9 | this.application = startApp(); 10 | 11 | if (options.beforeEach) { 12 | return options.beforeEach.apply(this, arguments); 13 | } 14 | }, 15 | 16 | afterEach() { 17 | let afterEach = 18 | options.afterEach && options.afterEach.apply(this, arguments); 19 | return Promise.resolve(afterEach).then(() => 20 | destroyApp(this.application) 21 | ); 22 | } 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Application from '../../app'; 2 | import config from '../../config/environment'; 3 | import { assign } from '@ember/polyfills'; 4 | import { run } from '@ember/runloop'; 5 | 6 | export default function startApp(attrs) { 7 | let application; 8 | 9 | // use defaults, but you can override 10 | let attributes = assign({}, config.APP, attrs); 11 | 12 | run(() => { 13 | application = Application.create(attributes); 14 | application.setupForTesting(); 15 | application.injectTestHelpers(); 16 | }); 17 | 18 | return application; 19 | } 20 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/components/tag-input-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { 4 | render, 5 | typeIn, 6 | findAll, 7 | blur, 8 | triggerKeyEvent 9 | } from '@ember/test-helpers'; 10 | import { hbs } from 'ember-cli-htmlbars'; 11 | import { A } from '@ember/array'; 12 | 13 | module( 14 | 'tag-input', 15 | 'Integration | Component | Ember Tag Input', 16 | function (hooks) { 17 | setupRenderingTest(hooks); 18 | 19 | const KEY_CODES = { 20 | COMMA: 188, 21 | BACKSPACE: 8 22 | }; 23 | 24 | test('New tags are created when delimiter characters are typed', async function (assert) { 25 | const tags = A(); 26 | 27 | this.addTag = function (tag) { 28 | tags.pushObject(tag); 29 | }; 30 | 31 | this.set('tags', tags); 32 | 33 | await render(hbs` 34 | 39 | {{tag}} 40 | 41 | `); 42 | 43 | await typeIn('.js-ember-tag-input-new', 'first second '); 44 | 45 | assert.dom('.js-ember-tag-input-new').includesText(''); 46 | assert.dom('.emberTagInput-tag').exists({ count: 2 }); 47 | assert.equal( 48 | findAll('.emberTagInput-tag')[0].textContent.trim(), 49 | 'first' 50 | ); 51 | assert.equal( 52 | findAll('.emberTagInput-tag')[1].textContent.trim(), 53 | 'second' 54 | ); 55 | }); 56 | 57 | test('New tags are created when the field is blurred', async function (assert) { 58 | const tags = A(); 59 | 60 | this.addTag = function (tag) { 61 | tags.pushObject(tag); 62 | }; 63 | this.set('tags', tags); 64 | 65 | await render(hbs` 66 | 71 | {{tag}} 72 | 73 | `); 74 | 75 | await typeIn('.js-ember-tag-input-new', 'blurry'); 76 | await blur('.js-ember-tag-input-new'); 77 | 78 | assert.dom('.js-ember-tag-input-new').includesText(''); 79 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 80 | assert.equal( 81 | findAll('.emberTagInput-tag')[0].textContent.trim(), 82 | 'blurry' 83 | ); 84 | }); 85 | 86 | test('Tags can be removed using the backspace key', async function (assert) { 87 | const tags = A(); 88 | 89 | this.addTag = function (tag) { 90 | tags.pushObject(tag); 91 | }; 92 | 93 | this.removeTagAtIndex = function (index) { 94 | tags.removeAt(index); 95 | }; 96 | this.set('tags', tags); 97 | 98 | await render(hbs` 99 | 105 | {{tag}} 106 | 107 | `); 108 | 109 | await typeIn('.js-ember-tag-input-new', 'removeme '); 110 | 111 | assert.dom('.js-ember-tag-input-new').includesText(''); 112 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 113 | 114 | await triggerKeyEvent( 115 | '.js-ember-tag-input-new', 116 | 'keydown', 117 | KEY_CODES.BACKSPACE 118 | ); //First keypress highlights the tag for deletion 119 | 120 | assert.dom('.js-ember-tag-input-new').includesText(''); 121 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 122 | 123 | await triggerKeyEvent( 124 | '.js-ember-tag-input-new', 125 | 'keydown', 126 | KEY_CODES.BACKSPACE 127 | ); //Second keypress deletes the tag 128 | 129 | assert.dom('.emberTagInput-tag').exists({ count: 0 }); 130 | }); 131 | 132 | test('Tags can contain spaces when allowSpacesInTags is set to true', async function (assert) { 133 | const tags = A(); 134 | 135 | this.addTag = function (tag) { 136 | tags.pushObject(tag); 137 | }; 138 | this.set('tags', tags); 139 | 140 | await render(hbs` 141 | 147 | {{tag}} 148 | 149 | `); 150 | 151 | await typeIn('.js-ember-tag-input-new', 'multiple words rock'); 152 | await blur('.js-ember-tag-input-new'); 153 | 154 | assert.dom('.js-ember-tag-input-new').includesText(''); 155 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 156 | assert.equal( 157 | findAll('.emberTagInput-tag')[0].textContent.trim(), 158 | 'multiple words rock' 159 | ); 160 | }); 161 | 162 | test('Tags should not contain comma by default when allowCommaInTags is not provided', async function (assert) { 163 | const tags = A(); 164 | 165 | this.addTag = function (tag) { 166 | tags.pushObject(tag); 167 | }; 168 | this.set('tags', tags); 169 | 170 | await render(hbs` 171 | 177 | {{tag}} 178 | 179 | `); 180 | 181 | await typeIn('.js-ember-tag-input-new', 'Scrabble'); 182 | await triggerKeyEvent( 183 | '.js-ember-tag-input-new', 184 | 'keydown', 185 | KEY_CODES.COMMA 186 | ); 187 | 188 | assert.dom('.js-ember-tag-input-new').includesText(''); 189 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 190 | assert.equal( 191 | findAll('.emberTagInput-tag')[0].textContent.trim(), 192 | 'Scrabble' 193 | ); 194 | }); 195 | 196 | test('Tags can contain commas when allowCommaInTags is set to true', async function (assert) { 197 | const tags = A(); 198 | 199 | this.addTag = function (tag) { 200 | tags.pushObject(tag); 201 | }; 202 | this.set('tags', tags); 203 | 204 | await render(hbs` 205 | 212 | {{tag}} 213 | 214 | `); 215 | 216 | await typeIn('.js-ember-tag-input-new', 'Scrabble, Words With Friends,'); 217 | await blur('.js-ember-tag-input-new'); 218 | 219 | assert.dom('.js-ember-tag-input-new').includesText(''); 220 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 221 | assert.equal( 222 | findAll('.emberTagInput-tag')[0].textContent.trim(), 223 | 'Scrabble, Words With Friends,' 224 | ); 225 | }); 226 | 227 | test("Tags can't be added or removed in read only mode", async function (assert) { 228 | const tags = A(['hamburger', 'cheeseburger']); 229 | this.set('tags', tags); 230 | 231 | await render(hbs` 232 | 237 | {{tag}} 238 | 239 | `); 240 | 241 | assert.dom('.emberTagInput-tag').exists({ count: 2 }); 242 | assert.dom('.emberTagInput-remove').exists({ count: 0 }); 243 | assert.dom('.emberTagInput-new').exists({ count: 1 }); 244 | assert.dom('input').hasText(''); 245 | assert.dom('input').isDisabled(); 246 | }); 247 | 248 | test('send input value when typing', async function (assert) { 249 | const tags = A(); 250 | 251 | this.addTag = function (tag) { 252 | tags.pushObject(tag); 253 | }; 254 | 255 | this.set('tags', tags); 256 | 257 | let inputValue; 258 | 259 | this.onKeyUp = function (value) { 260 | inputValue = value; 261 | }; 262 | 263 | await render(hbs` 264 | 270 | {{tag}} 271 | 272 | `); 273 | 274 | await typeIn('.js-ember-tag-input-new', 't'); 275 | assert.equal(inputValue, 't'); 276 | await typeIn('.js-ember-tag-input-new', 'e'); 277 | assert.equal(inputValue, 'te'); 278 | await typeIn('.js-ember-tag-input-new', 's'); 279 | assert.equal(inputValue, 'tes'); 280 | await blur('.js-ember-tag-input-new'); 281 | 282 | assert.dom('.emberTagInput-tag').exists({ count: 1 }); 283 | assert.equal(findAll('.emberTagInput-tag')[0].textContent.trim(), 'tes'); 284 | assert.equal(inputValue, ''); 285 | }); 286 | 287 | test('Tags can be added after readOnly changes to false', async function (assert) { 288 | const tags = A(); 289 | 290 | this.addTag = function (tag) { 291 | tags.pushObject(tag); 292 | }; 293 | 294 | this.set('tags', tags); 295 | this.set('readOnly', true); 296 | 297 | await render(hbs` 298 | 304 | {{tag}} 305 | 306 | `); 307 | 308 | assert.dom('input').isDisabled(); 309 | 310 | await this.set('readOnly', false); 311 | 312 | await typeIn('.js-ember-tag-input-new', 'some tag '); 313 | 314 | assert.dom('.emberTagInput-tag').exists({ count: 2 }); 315 | assert.equal(findAll('.emberTagInput-tag')[0].textContent.trim(), 'some'); 316 | assert.equal(findAll('.emberTagInput-tag')[1].textContent.trim(), 'tag'); 317 | }); 318 | 319 | test('input is disabled after readOnly changes from false to true', async function (assert) { 320 | const tags = A(['hamburger', 'cheeseburger']); 321 | 322 | this.set('tags', tags); 323 | this.set('readOnly', false); 324 | 325 | this.removeTagAtIndex = function (index) { 326 | tags.removeAt(index); 327 | }; 328 | 329 | this.addTag = function (tag) { 330 | tags.pushObject(tag); 331 | }; 332 | 333 | await render(hbs` 334 | 341 | {{tag}} 342 | 343 | `); 344 | 345 | assert.dom('input').isNotDisabled(); 346 | 347 | this.set('readOnly', true); 348 | 349 | assert.dom('input').isDisabled(); 350 | }); 351 | 352 | test('Tags as objects rely on property @modifiers to get custom classes', async function (assert) { 353 | const tags = A([ 354 | { 355 | label: 'hamburger', 356 | modifiers: 'burger-style meat-style' 357 | }, 358 | { 359 | label: 'cheeseburger', 360 | modifiers: 'burger-style cheese-style' 361 | } 362 | ]); 363 | 364 | this.set('tags', tags); 365 | 366 | await render(hbs` 367 | 371 | {{tag.label}} 372 | 373 | `); 374 | 375 | const tagsElements = findAll('.emberTagInput-tag'); 376 | assert.ok(tagsElements[0].className.includes('burger-style meat-style')); 377 | assert.ok( 378 | tagsElements[1].className.includes('burger-style cheese-style') 379 | ); 380 | }); 381 | } 382 | ); 383 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/tests/unit/.gitkeep -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calvinlough/ember-tag-input/1b867edc23eb857ec179d52f23e846e89e1689b9/vendor/.gitkeep --------------------------------------------------------------------------------