├── .browserslistrc ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .postcssrc.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── README.md ├── assets └── screenshot.png ├── babel.config.js ├── deploy.sh ├── docs ├── .vuepress │ ├── components │ │ ├── APIExample.vue │ │ ├── CustomSuggestion.vue │ │ ├── HomePageDemo.vue │ │ └── PendingAppendingExample.vue │ └── config.js ├── README.md ├── examples │ └── examples.md └── guide │ ├── gettingStarted.md │ └── reference.md ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── countries.json ├── favicon.ico └── index.html ├── src └── components │ ├── VueTypeaheadBootstrap.vue │ ├── VueTypeaheadBootstrapList.vue │ └── VueTypeaheadBootstrapListItem.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── VueTypeaheadBootstrap.spec.js │ ├── VueTypeaheadBootstrapList.spec.js │ └── VueTypeaheadBootstrapListItem.spec.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 9 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'space-before-function-paren': 'off' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: [push, pull_request] 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [ 12.x, 14.x ] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Lint & Test 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm run lint 25 | - run: npm run test:unit 26 | 27 | release: 28 | runs-on: ubuntu-latest 29 | needs: test 30 | strategy: 31 | matrix: 32 | node-version: [ 12.x, 13.x ] 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Build & Release 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: ${{ matrix.node }} 39 | - run: npm ci 40 | - run: npm run build 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | /docs/.vuepress/dist 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | .vscode 3 | assets 4 | public 5 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.12.0 - 26 Aug 2021 2 | - Revert localization changes 3 | - Update CI build targets 4 | 5 | ## 2.11.1 - 23 Apr 2021 6 | - Improve screen reader text support 7 | 8 | ## 2.11.0 - 7 Apr 2021 9 | - Rebuild dist files that were missed in previous two versions (2.9.0 & 2.10.0) 10 | 11 | ## 2.10.0 - 5 Apr 2021 12 | - Provide for customizable screen reader text 13 | 14 | ## 2.9.0 - 5 Apr 2021 15 | - Support 'enter' selecting the first item of the list 16 | 17 | ## 2.8.0 - 9 Feb 2021 18 | - Issues blur event when leaving the dropdown navigated to with arrow keys 19 | 20 | ## 2.7.3 - 9 Feb 2021 21 | - Fix internationalization support. Bug with the `z` characters 22 | 23 | ## 2.7.2 - 31 Dec 2020 24 | - Update highlight.js for security vulnerability 25 | - Update missing doc link 26 | 27 | ## 2.7.1 - 31 Dec 2020 28 | - Update docs to match new usage of `list-group-item-${context}` 29 | 30 | ## 2.7.0 - 31 Dec 2020 31 | - Add support for different background colors per list item 32 | - NOTE: also changing from `bg-${context}` to `list-group-item-${context}` to match bootstrap docs and allow text colors to change automatically 33 | 34 | ## 2.6.1 - 28 Dec 2020 35 | - Add internationalization support, specifically for diacritics (accents, etc) 36 | - Allow `md` as a size option 37 | 38 | ## 2.5.6 - 28 Dec 2020 39 | - Formalize IE close fix 40 | 41 | ## 2.5.5 - 19 Dec 2020 42 | - Include lodash in dependencies 43 | 44 | ## 2.5.4 - 11 Nov 2020 45 | - Reduced package size 46 | 47 | ## 2.5.3.beta - 28 Sep 2020 48 | - Attempted a11y improvements to use standard combobox aria tags 49 | 50 | ## 2.5.2 - 28 Sep 2020 51 | - Fix IE dropdowns closing 52 | 53 | ## 2.5.1 - 08 Aug 2020 54 | - Fix broken key handling events 55 | 56 | ## 2.5.0 - 07 Jul 2020 57 | - Propagate keyup events from the input. 58 | 59 | ## 2.4.1 - 05 Jul 2020 60 | - Fix bug when given a null query 61 | 62 | ## 2.4.0 - 05 Jul 2020 63 | - Add the ability to disable list items 64 | 65 | ## 2.3.0 - 18 Jun 2020 66 | - Add support for inputName 67 | 68 | ## 2.2.0 - 18 Jun 2020 69 | - Fix IE support for click handling 70 | - Clean up tests 71 | 72 | ## 2.1.0 - 26 May 2020 73 | - Add disabled option 74 | 75 | ## 2.0.2 - 26 May 2020 76 | - Rename file 77 | 78 | ## 2.0.1 - 22 May 2020 79 | - Add migration instructions 80 | 81 | ## 2.0.0 - 22 May 2020 82 | - Breaking change: Rename this component fully. To continue using this component, you'll need to change all 83 | your usages from `vue-bootstrap-typeahead` to `vue-typeahead-bootstrap`. 84 | 85 | - Bug Fix: Handle `ESC` keypress more appropriately 86 | - Feature: Add `autoclose` to allow for the component to hide upon item selection 87 | - Feature: Migrate to VuePress style documentation 88 | 89 | ## 1.0.3 - 2 Mar 2020 90 | - Allow up/down arrow keys to wrap without an extra key push. 91 | 92 | ## 1.0.2 - 1 Mar 2020 93 | - Added `disableSort`. No sorting occurs and the list is presented to the user as it is given to the component. 94 | - Fix IE 11 display issue. Fixes issue listed here: 95 | https://github.com/alexurquhart/vue-bootstrap-typeahead/issues/2#issuecomment-418142023 96 | 97 | ## 1.0.1 - 22 Feb 2020 98 | - Add documentation. 99 | 100 | ## 1.0.0 - 22 Feb 2020 101 | - Restarted maintenance of this project. Needed to rename because I was unable to contact the original 102 | developer. 103 | 104 | Merge PRs from previous repo: 105 | - Keyboard support for arrow keys 106 | - Added `showOnFocus`. Show results as soon as the input gains focus before the user has typed anything. 107 | - Added `showAllResults`. Show all results even ones that highlighting doesn't match. 108 | - Initialize input field correctly 109 | 110 | ## 0.1.2 - 28 Aug 2018 111 | - Fixed #3 & #4 112 | 113 | ## 0.2.0 - 6 Sept 2018 114 | - Added a scoped slot for custom suggestion list items 115 | - Added library build + unpkg tags 116 | - Updated documentation site (working on gh-pages) 117 | - Added basic unit tests 118 | 119 | ## 0.2.1 - 7 Sept 2018 120 | - Fixed positioning bug for the typeahead list when the prepend slot was used 121 | 122 | ## 0.2.2 - 7 Sept 2018 123 | - Forgot to update the `dist/` folder with new build from last release. 124 | - Added updated documentation. `docs` folder now to be published to gh-pages 125 | - Updated readme 126 | - Added `.npmignore` 127 | 128 | ## 0.2.3 - 21 Sept 2018 129 | - Fixed Safari bug (issue #14) 130 | - Fixed error when `v-model` is not used on component (issue #18) 131 | 132 | ## 0.2.4 - 21 Sept 2018 133 | - Re-fixed error when `v-model` is not used on component (issue #18) 134 | 135 | ## 0.2.5 - 21 Sept 2018 136 | - Re-fixed error when `v-model` is not used on component (issue #18) 137 | - More comprehensive unit testing is now a priority, edge cases are harder to find than I thought :joy: 138 | 139 | ## 0.2.6 - 30 Sept 2018 140 | - Fixed `maxMatches` bug. Thanks to @jimfisher 141 | -------------------------------------------------------------------------------- /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 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at vue.typeahead.bootstrap@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 Alex Urquhart 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # No Longer Maintained, Moved 2 | This version of the project is no longer maintained. Graciously, [@drikusroor](https://github.com/drikusroor) has taken over development and maintenance at [this repository](https://github.com/drikusroor/vue-bootstrap-autocomplete) and it is published on [NPM here](https://www.npmjs.com/package/@vue-bootstrap-components/vue-bootstrap-autocomplete). 3 | 4 | ## Lineage 5 | I want to specifically acknowledge the original repository by Alex Urquhart for this work: https://github.com/alexurquhart/vue-bootstrap-typeahead. He brought this project into reality, we're simply trying to help keep it moving forward. Thanks, Alex! 6 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattzollinhofer/vue-typeahead-bootstrap/279ff4c37a4a8c3c920dd9d19ec8175594df89ea/assets/screenshot.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Build latest docs 4 | yarn docs:build 5 | 6 | # navigate to docs output directory 7 | cd docs/.vuepress/dist 8 | 9 | # create a new blank git repository 10 | git init 11 | git add -A 12 | git commit -m 'deploy' 13 | 14 | # force push to docs repo 15 | git push -f git@github.com:mattzollinhofer/vue-typeahead-bootstrap-docs.git master:gh-pages 16 | 17 | cd - 18 | -------------------------------------------------------------------------------- /docs/.vuepress/components/APIExample.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 55 | 56 | 59 | 60 | -------------------------------------------------------------------------------- /docs/.vuepress/components/CustomSuggestion.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 71 | 72 | 84 | 85 | -------------------------------------------------------------------------------- /docs/.vuepress/components/HomePageDemo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /docs/.vuepress/components/PendingAppendingExample.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 59 | 60 | 63 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: '/vue-typeahead-bootstrap-docs/', 3 | chainWebpack(config) { 4 | for (const lang of ["sass", "scss"]) { 5 | for (const name of ["modules", "normal"]) { 6 | const rule = config.module.rule(lang).oneOf(name); 7 | rule.uses.delete("sass-loader"); 8 | 9 | rule 10 | .use("sass-loader") 11 | .loader("sass-loader") 12 | .options({ 13 | implementation: require("sass"), 14 | }); 15 | } 16 | } 17 | }, 18 | themeConfig: { 19 | nav: [ 20 | { 21 | text: 'Home', link: '/', 22 | }, { 23 | text: 'Guide', link: '/guide/gettingStarted', 24 | }, { 25 | text: 'Examples', link: '/examples/examples' 26 | }, { 27 | text: 'Github', link: 'https://github.com/mattzollinhofer/vue-typeahead-bootstrap/' 28 | } 29 | ], 30 | sidebar: [ 31 | '/', 32 | '/guide/gettingStarted', 33 | '/guide/reference', 34 | '/examples/examples' 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: Vue Typeahead Bootstrap 4 | tagline: A simple typeahead for Vue 2 using Bootstrap 4 5 | actionText: Get Started 6 | features: 7 | - title: bootstrap-vue compatible 8 | details: Works with the popular bootstrap-vue bootstrap wrapper. 9 | - title: Configurable 10 | details: Can be configured for many different use cases 11 | - title: Works well with JSON API's 12 | details: Easy to integrate with any JSON based API's 13 | --- 14 | 15 | ## Try It Out 16 | 17 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /docs/examples/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Basic 4 | 5 | 6 | ```vue 7 | 18 | 19 | 28 | ``` 29 | 30 | ## API Example 31 | 32 | 33 | ```vue 34 | 54 | 55 | 81 | ``` 82 | 83 | ## Prepend/Append 84 | 85 | 86 | ```vue 87 | 108 | 109 | 136 | ``` 137 | 138 | ## Custom Suggestion Slot 139 | 140 | 141 | ```vue 142 | 177 | 178 | 179 | 204 | 205 | 217 | ``` 218 | -------------------------------------------------------------------------------- /docs/guide/gettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | ```js 5 | // yarn 6 | yarn add vue-typeahead-bootstrap 7 | 8 | // npm 9 | npm install vue-typeahead-bootstrap --save 10 | ``` 11 | 12 | ## Registration 13 | 14 | ```js 15 | import VueTypeaheadBootstrap from 'vue-typeahead-bootstrap'; 16 | 17 | // Required dependency of bootstrap css/scss files 18 | import 'bootstrap/scss/bootstrap.scss'; 19 | 20 | // Global registration 21 | Vue.component('vue-typeahead-bootstrap', VueTypeaheadBootstrap) 22 | 23 | // or 24 | 25 | // Local Registration 26 | export default { 27 | components: { 28 | VueTypeaheadBootstrap 29 | } 30 | } 31 | ``` 32 | 33 | ## Basic Usage 34 | The only required props are a `v-model` and a `data` array. 35 | 36 | ```html 37 | 41 | ``` 42 | 43 | ## Nuxt.js 44 | 45 | Configure the [build transpile](https://nuxtjs.org/api/configuration-build/#transpile) option in `nuxt.config.js`. 46 | ```js 47 | { 48 | build: { 49 | transpile: [ 50 | ({ isServer }) => 'vue-typeahead-bootstrap' 51 | ] 52 | } 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/guide/reference.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | ## Props 4 | 5 | | Name | type | Default | Description | 6 | | ---- |:----:| ------------- | ---- | 7 | | append | String | | Text to be appended to the `input-group` 8 | | autoClose | `Boolean` | true | Whether the autocomplete should hide upon item selection 9 | | backgroundVariant | String | | Background color for the autocomplete result `list-group` items. [See values here.][1] 10 | | backgroundVariantResolver | Function | input => null | Function which accepts the current list item data and returns a background color for the current autocomplete result `list-group` item. The non-null/non-empty string value returned from this function will supersede the value specified in `backgroundVariant`. 11 | | data | Array | | Array of data to be available for querying. **Required**| 12 | | disabled | `Boolean` | false | Enable or disable input field 13 | | disabledValues| `Array` | false | The dropdown items to `disable`. 14 | | disableSort | `Boolean` | false | If set to true, no sorting occurs and the list is presented to the user as it is given to the component. Use this if you sort the list before giving it to the component. Ex: an elasticsearch result being passed to Vue. 15 | | highlightClass | `String` | `vbt-matched-text` | CSS class to style highlighted text 16 | | ieCloseFix | Boolean | true | Adds (imperfect) handling for auto closing the typeahead list on focus out in IE 17 | | inputClass | String | | Class to be added to the `input` tag for validation, etc. 18 | | inputName | String | | Name to be added to the `input` tag. 19 | | maxMatches | Number | 10 | Maximum amount of list items to appear. 20 | | minMatchingChars | Number | 2 | Minimum matching characters in query before the typeahead list appears 21 | | prepend | String | | Text to be prepended to the `input-group` 22 | | screenReaderTextSerializer | Function | `input => input`| Function used to convert the entries in the data array into the screen reader text string. Falls back to the value of serializer.| 23 | | serializer | Function | `input => input`| Function used to convert the entries in the data array into a text string. | 24 | | showAllResults | `Boolean` | false | Show all results even ones that highlighting doesn't match. This is useful when interacting with a API that returns results based on different values than what is displayed. Ex: user searches for "USA" and the service returns "United States of America". 25 | | showOnFocus | `Boolean` | false | Show results as soon as the input gains focus before the user has typed anything. 26 | | size | String | | Size of the `input-group`. Valid values: `sm`, `md`, or `lg` | 27 | | textVariant | String | | Text color for autocomplete result `list-group` items. [See values here.][2] 28 | 29 | ## Events 30 | 31 | Name | Description 32 | | --- | --- | 33 | hit | Triggered when an autocomplete item is selected. The entry in the input data array that was selected is returned. If no autocomplete item is selected, the first entry matching the query is selected and returned. 34 | input | The component can be used with `v-model` 35 | keyup | Triggered when any keyup event is fired in the input. Often used for catching `keyup.enter`. 36 | focus | Triggered when the input element receives focus. 37 | blur | Triggered when the input field loses focus, except when pressing the `tab` key to focus the dropdown list. 38 | 39 | ## Slots 40 | 41 | There are `prepend` and `append` slots available for adding buttons or other markup. Overrides the prepend and append props. 42 | 43 | ### Scoped Slot 44 | 45 | You can use a [scoped slot][3] called `suggestion` to define custom content for the suggestion `list-item`'s. You can use bound variables `data`, which holds the data from the input array, and `htmlText`, which is the highlighted text that is used for the suggestion. 46 | 47 | See the [custom suggestion slot example][4] for more info. 48 | 49 | [1]: https://getbootstrap.com/docs/4.1/components/list-group/#contextual-classes 50 | [2]: https://getbootstrap.com/docs/4.1/utilities/colors/#color 51 | [3]: https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots 52 | [4]: https://mattzollinhofer.github.io/vue-typeahead-bootstrap-docs/examples/examples.html#custom-suggestion-slot 53 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | 'js', 4 | 'jsx', 5 | 'json', 6 | 'vue' 7 | ], 8 | transform: { 9 | '^.+\\.vue$': 'vue-jest', 10 | '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', 11 | '^.+\\.jsx?$': 'babel-jest' 12 | }, 13 | moduleNameMapper: { 14 | '^@/(.*)$': '/src/$1' 15 | }, 16 | snapshotSerializers: [ 17 | 'jest-serializer-vue' 18 | ], 19 | testEnvironment: 'jsdom', 20 | testMatch: [ 21 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 22 | ], 23 | testURL: 'http://localhost/', 24 | collectCoverageFrom: [ 25 | 'src/components/*.vue' 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-typeahead-bootstrap", 3 | "version": "2.13.0", 4 | "private": false, 5 | "description": "A typeahead/autocomplete component for Vue 2 using Bootstrap 4", 6 | "keywords": [ 7 | "vue", 8 | "autocomplete", 9 | "bootstrap", 10 | "bootstrap 4", 11 | "omnisearch", 12 | "typeahead" 13 | ], 14 | "homepage": "https://github.com/mattzollinhofer/vue-typeahead-bootstrap", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/mattzollinhofer/vue-typeahead-bootstrap" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/mattzollinhofer/vue-typeahead-bootstrap/issues" 21 | }, 22 | "license": "MIT", 23 | "author": "Matt Zollinhofer ", 24 | "main": "src/components/VueTypeaheadBootstrap.vue", 25 | "scripts": { 26 | "serve": "vue-cli-service serve", 27 | "build": "vue-cli-service build --target lib --name VueTypeaheadBootstrap src/components/VueTypeaheadBootstrap.vue", 28 | "docs:dev": "vuepress dev docs", 29 | "docs:build": "vuepress build docs", 30 | "docs:deploy": "./deploy.sh", 31 | "lint": "vue-cli-service lint", 32 | "test:unit": "vue-cli-service test:unit", 33 | "test:unit:watch": "vue-cli-service test:unit --watch", 34 | "test:debug": "echo '\n\n\n\n*******\n*****************\nlaunch chrome://inspect!!!!!!\n****************\n******\n\n\n\n\n';sleep 5;node --inspect ./node_modules/@vue/cli-service/bin/vue-cli-service.js test:unit --runInBand --no-cache --watch" 35 | }, 36 | "dependencies": { 37 | "lodash": "^4.17.20", 38 | "resize-observer-polyfill": "^1.5.0", 39 | "vue": "^2.7.8" 40 | }, 41 | "devDependencies": { 42 | "@vue/cli-plugin-babel": "^5.0.8", 43 | "@vue/cli-plugin-eslint": "^5.0.8", 44 | "@vue/cli-plugin-unit-jest": "^5.0.8", 45 | "@vue/cli-service": "^5.0.8", 46 | "@vue/eslint-config-standard": "^3.0.1", 47 | "@vue/test-utils": "^1.1.1", 48 | "babel-core": "7.0.0-bridge.0", 49 | "babel-jest": "^23.0.1", 50 | "bootstrap": "^4.1.3", 51 | "bootstrap-vue": "^2.0.0-rc.11", 52 | "core-js": "^3.24.0", 53 | "coveralls": "^3.0.2", 54 | "eslint": "^8.20.0", 55 | "eslint-plugin-vue": "^9.3.0", 56 | "highlight.js": "^10.4.1", 57 | "sass": "^1.54.0", 58 | "sass-loader": "^7.0.1", 59 | "underscore": "^1.9.1", 60 | "vue-gtm": "^2.0.0", 61 | "vue-hljs": "^2.0.0", 62 | "vue-jest": "^3.0.7", 63 | "vue-router": "^3.0.1", 64 | "vue-template-compiler": "^2.7.8", 65 | "vuepress": "^1.9.7", 66 | "whatwg-fetch": "^2.0.4" 67 | }, 68 | "directories": { 69 | "doc": "docs", 70 | "test": "tests" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /public/countries.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "Afghanistan", "code": "AF"}, 3 | {"name": "Åland Islands", "code": "AX"}, 4 | {"name": "Albania", "code": "AL"}, 5 | {"name": "Algeria", "code": "DZ"}, 6 | {"name": "American Samoa", "code": "AS"}, 7 | {"name": "AndorrA", "code": "AD"}, 8 | {"name": "Angola", "code": "AO"}, 9 | {"name": "Anguilla", "code": "AI"}, 10 | {"name": "Antarctica", "code": "AQ"}, 11 | {"name": "Antigua and Barbuda", "code": "AG"}, 12 | {"name": "Argentina", "code": "AR"}, 13 | {"name": "Armenia", "code": "AM"}, 14 | {"name": "Aruba", "code": "AW"}, 15 | {"name": "Australia", "code": "AU"}, 16 | {"name": "Austria", "code": "AT"}, 17 | {"name": "Azerbaijan", "code": "AZ"}, 18 | {"name": "Bahamas", "code": "BS"}, 19 | {"name": "Bahrain", "code": "BH"}, 20 | {"name": "Bangladesh", "code": "BD"}, 21 | {"name": "Barbados", "code": "BB"}, 22 | {"name": "Belarus", "code": "BY"}, 23 | {"name": "Belgium", "code": "BE"}, 24 | {"name": "Belize", "code": "BZ"}, 25 | {"name": "Benin", "code": "BJ"}, 26 | {"name": "Bermuda", "code": "BM"}, 27 | {"name": "Bhutan", "code": "BT"}, 28 | {"name": "Bolivia", "code": "BO"}, 29 | {"name": "Bosnia and Herzegovina", "code": "BA"}, 30 | {"name": "Botswana", "code": "BW"}, 31 | {"name": "Bouvet Island", "code": "BV"}, 32 | {"name": "Brazil", "code": "BR"}, 33 | {"name": "British Indian Ocean Territory", "code": "IO"}, 34 | {"name": "Brunei Darussalam", "code": "BN"}, 35 | {"name": "Bulgaria", "code": "BG"}, 36 | {"name": "Burkina Faso", "code": "BF"}, 37 | {"name": "Burundi", "code": "BI"}, 38 | {"name": "Cambodia", "code": "KH"}, 39 | {"name": "Cameroon", "code": "CM"}, 40 | {"name": "Canada", "code": "CA"}, 41 | {"name": "Cape Verde", "code": "CV"}, 42 | {"name": "Cayman Islands", "code": "KY"}, 43 | {"name": "Central African Republic", "code": "CF"}, 44 | {"name": "Chad", "code": "TD"}, 45 | {"name": "Chile", "code": "CL"}, 46 | {"name": "China", "code": "CN"}, 47 | {"name": "Christmas Island", "code": "CX"}, 48 | {"name": "Cocos (Keeling) Islands", "code": "CC"}, 49 | {"name": "Colombia", "code": "CO"}, 50 | {"name": "Comoros", "code": "KM"}, 51 | {"name": "Congo", "code": "CG"}, 52 | {"name": "Congo, The Democratic Republic of the", "code": "CD"}, 53 | {"name": "Cook Islands", "code": "CK"}, 54 | {"name": "Costa Rica", "code": "CR"}, 55 | {"name": "Cote D'Ivoire", "code": "CI"}, 56 | {"name": "Croatia", "code": "HR"}, 57 | {"name": "Cuba", "code": "CU"}, 58 | {"name": "Cyprus", "code": "CY"}, 59 | {"name": "Czech Republic", "code": "CZ"}, 60 | {"name": "Denmark", "code": "DK"}, 61 | {"name": "Djibouti", "code": "DJ"}, 62 | {"name": "Dominica", "code": "DM"}, 63 | {"name": "Dominican Republic", "code": "DO"}, 64 | {"name": "Ecuador", "code": "EC"}, 65 | {"name": "Egypt", "code": "EG"}, 66 | {"name": "El Salvador", "code": "SV"}, 67 | {"name": "Equatorial Guinea", "code": "GQ"}, 68 | {"name": "Eritrea", "code": "ER"}, 69 | {"name": "Estonia", "code": "EE"}, 70 | {"name": "Ethiopia", "code": "ET"}, 71 | {"name": "Falkland Islands (Malvinas)", "code": "FK"}, 72 | {"name": "Faroe Islands", "code": "FO"}, 73 | {"name": "Fiji", "code": "FJ"}, 74 | {"name": "Finland", "code": "FI"}, 75 | {"name": "France", "code": "FR"}, 76 | {"name": "French Guiana", "code": "GF"}, 77 | {"name": "French Polynesia", "code": "PF"}, 78 | {"name": "French Southern Territories", "code": "TF"}, 79 | {"name": "Gabon", "code": "GA"}, 80 | {"name": "Gambia", "code": "GM"}, 81 | {"name": "Georgia", "code": "GE"}, 82 | {"name": "Germany", "code": "DE"}, 83 | {"name": "Ghana", "code": "GH"}, 84 | {"name": "Gibraltar", "code": "GI"}, 85 | {"name": "Greece", "code": "GR"}, 86 | {"name": "Greenland", "code": "GL"}, 87 | {"name": "Grenada", "code": "GD"}, 88 | {"name": "Guadeloupe", "code": "GP"}, 89 | {"name": "Guam", "code": "GU"}, 90 | {"name": "Guatemala", "code": "GT"}, 91 | {"name": "Guernsey", "code": "GG"}, 92 | {"name": "Guinea", "code": "GN"}, 93 | {"name": "Guinea-Bissau", "code": "GW"}, 94 | {"name": "Guyana", "code": "GY"}, 95 | {"name": "Haiti", "code": "HT"}, 96 | {"name": "Heard Island and Mcdonald Islands", "code": "HM"}, 97 | {"name": "Holy See (Vatican City State)", "code": "VA"}, 98 | {"name": "Honduras", "code": "HN"}, 99 | {"name": "Hong Kong", "code": "HK"}, 100 | {"name": "Hungary", "code": "HU"}, 101 | {"name": "Iceland", "code": "IS"}, 102 | {"name": "India", "code": "IN"}, 103 | {"name": "Indonesia", "code": "ID"}, 104 | {"name": "Iran, Islamic Republic Of", "code": "IR"}, 105 | {"name": "Iraq", "code": "IQ"}, 106 | {"name": "Ireland", "code": "IE"}, 107 | {"name": "Isle of Man", "code": "IM"}, 108 | {"name": "Israel", "code": "IL"}, 109 | {"name": "Italy", "code": "IT"}, 110 | {"name": "Jamaica", "code": "JM"}, 111 | {"name": "Japan", "code": "JP"}, 112 | {"name": "Jersey", "code": "JE"}, 113 | {"name": "Jordan", "code": "JO"}, 114 | {"name": "Kazakhstan", "code": "KZ"}, 115 | {"name": "Kenya", "code": "KE"}, 116 | {"name": "Kiribati", "code": "KI"}, 117 | {"name": "Korea, Democratic People's Republic of", "code": "KP"}, 118 | {"name": "Korea, Republic of", "code": "KR"}, 119 | {"name": "Kuwait", "code": "KW"}, 120 | {"name": "Kyrgyzstan", "code": "KG"}, 121 | {"name": "Lao People's Democratic Republic", "code": "LA"}, 122 | {"name": "Latvia", "code": "LV"}, 123 | {"name": "Lebanon", "code": "LB"}, 124 | {"name": "Lesotho", "code": "LS"}, 125 | {"name": "Liberia", "code": "LR"}, 126 | {"name": "Libyan Arab Jamahiriya", "code": "LY"}, 127 | {"name": "Liechtenstein", "code": "LI"}, 128 | {"name": "Lithuania", "code": "LT"}, 129 | {"name": "Luxembourg", "code": "LU"}, 130 | {"name": "Macao", "code": "MO"}, 131 | {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"}, 132 | {"name": "Madagascar", "code": "MG"}, 133 | {"name": "Malawi", "code": "MW"}, 134 | {"name": "Malaysia", "code": "MY"}, 135 | {"name": "Maldives", "code": "MV"}, 136 | {"name": "Mali", "code": "ML"}, 137 | {"name": "Malta", "code": "MT"}, 138 | {"name": "Marshall Islands", "code": "MH"}, 139 | {"name": "Martinique", "code": "MQ"}, 140 | {"name": "Mauritania", "code": "MR"}, 141 | {"name": "Mauritius", "code": "MU"}, 142 | {"name": "Mayotte", "code": "YT"}, 143 | {"name": "Mexico", "code": "MX"}, 144 | {"name": "Micronesia, Federated States of", "code": "FM"}, 145 | {"name": "Moldova, Republic of", "code": "MD"}, 146 | {"name": "Monaco", "code": "MC"}, 147 | {"name": "Mongolia", "code": "MN"}, 148 | {"name": "Montserrat", "code": "MS"}, 149 | {"name": "Morocco", "code": "MA"}, 150 | {"name": "Mozambique", "code": "MZ"}, 151 | {"name": "Myanmar", "code": "MM"}, 152 | {"name": "Namibia", "code": "NA"}, 153 | {"name": "Nauru", "code": "NR"}, 154 | {"name": "Nepal", "code": "NP"}, 155 | {"name": "Netherlands", "code": "NL"}, 156 | {"name": "Netherlands Antilles", "code": "AN"}, 157 | {"name": "New Caledonia", "code": "NC"}, 158 | {"name": "New Zealand", "code": "NZ"}, 159 | {"name": "Nicaragua", "code": "NI"}, 160 | {"name": "Niger", "code": "NE"}, 161 | {"name": "Nigeria", "code": "NG"}, 162 | {"name": "Niue", "code": "NU"}, 163 | {"name": "Norfolk Island", "code": "NF"}, 164 | {"name": "Northern Mariana Islands", "code": "MP"}, 165 | {"name": "Norway", "code": "NO"}, 166 | {"name": "Oman", "code": "OM"}, 167 | {"name": "Pakistan", "code": "PK"}, 168 | {"name": "Palau", "code": "PW"}, 169 | {"name": "Palestinian Territory, Occupied", "code": "PS"}, 170 | {"name": "Panama", "code": "PA"}, 171 | {"name": "Papua New Guinea", "code": "PG"}, 172 | {"name": "Paraguay", "code": "PY"}, 173 | {"name": "Peru", "code": "PE"}, 174 | {"name": "Philippines", "code": "PH"}, 175 | {"name": "Pitcairn", "code": "PN"}, 176 | {"name": "Poland", "code": "PL"}, 177 | {"name": "Portugal", "code": "PT"}, 178 | {"name": "Puerto Rico", "code": "PR"}, 179 | {"name": "Qatar", "code": "QA"}, 180 | {"name": "Reunion", "code": "RE"}, 181 | {"name": "Romania", "code": "RO"}, 182 | {"name": "Russian Federation", "code": "RU"}, 183 | {"name": "RWANDA", "code": "RW"}, 184 | {"name": "Saint Helena", "code": "SH"}, 185 | {"name": "Saint Kitts and Nevis", "code": "KN"}, 186 | {"name": "Saint Lucia", "code": "LC"}, 187 | {"name": "Saint Pierre and Miquelon", "code": "PM"}, 188 | {"name": "Saint Vincent and the Grenadines", "code": "VC"}, 189 | {"name": "Samoa", "code": "WS"}, 190 | {"name": "San Marino", "code": "SM"}, 191 | {"name": "Sao Tome and Principe", "code": "ST"}, 192 | {"name": "Saudi Arabia", "code": "SA"}, 193 | {"name": "Senegal", "code": "SN"}, 194 | {"name": "Serbia and Montenegro", "code": "CS"}, 195 | {"name": "Seychelles", "code": "SC"}, 196 | {"name": "Sierra Leone", "code": "SL"}, 197 | {"name": "Singapore", "code": "SG"}, 198 | {"name": "Slovakia", "code": "SK"}, 199 | {"name": "Slovenia", "code": "SI"}, 200 | {"name": "Solomon Islands", "code": "SB"}, 201 | {"name": "Somalia", "code": "SO"}, 202 | {"name": "South Africa", "code": "ZA"}, 203 | {"name": "South Georgia and the South Sandwich Islands", "code": "GS"}, 204 | {"name": "Spain", "code": "ES"}, 205 | {"name": "Sri Lanka", "code": "LK"}, 206 | {"name": "Sudan", "code": "SD"}, 207 | {"name": "Suriname", "code": "SR"}, 208 | {"name": "Svalbard and Jan Mayen", "code": "SJ"}, 209 | {"name": "Swaziland", "code": "SZ"}, 210 | {"name": "Sweden", "code": "SE"}, 211 | {"name": "Switzerland", "code": "CH"}, 212 | {"name": "Syrian Arab Republic", "code": "SY"}, 213 | {"name": "Taiwan, Province of China", "code": "TW"}, 214 | {"name": "Tajikistan", "code": "TJ"}, 215 | {"name": "Tanzania, United Republic of", "code": "TZ"}, 216 | {"name": "Thailand", "code": "TH"}, 217 | {"name": "Timor-Leste", "code": "TL"}, 218 | {"name": "Togo", "code": "TG"}, 219 | {"name": "Tokelau", "code": "TK"}, 220 | {"name": "Tonga", "code": "TO"}, 221 | {"name": "Trinidad and Tobago", "code": "TT"}, 222 | {"name": "Tunisia", "code": "TN"}, 223 | {"name": "Turkey", "code": "TR"}, 224 | {"name": "Turkmenistan", "code": "TM"}, 225 | {"name": "Turks and Caicos Islands", "code": "TC"}, 226 | {"name": "Tuvalu", "code": "TV"}, 227 | {"name": "Uganda", "code": "UG"}, 228 | {"name": "Ukraine", "code": "UA"}, 229 | {"name": "United Arab Emirates", "code": "AE"}, 230 | {"name": "United Kingdom", "code": "GB"}, 231 | {"name": "United States", "code": "US"}, 232 | {"name": "United States Minor Outlying Islands", "code": "UM"}, 233 | {"name": "Uruguay", "code": "UY"}, 234 | {"name": "Uzbekistan", "code": "UZ"}, 235 | {"name": "Vanuatu", "code": "VU"}, 236 | {"name": "Venezuela", "code": "VE"}, 237 | {"name": "Viet Nam", "code": "VN"}, 238 | {"name": "Virgin Islands, British", "code": "VG"}, 239 | {"name": "Virgin Islands, U.S.", "code": "VI"}, 240 | {"name": "Wallis and Futuna", "code": "WF"}, 241 | {"name": "Western Sahara", "code": "EH"}, 242 | {"name": "Yemen", "code": "YE"}, 243 | {"name": "Zambia", "code": "ZM"}, 244 | {"name": "Zimbabwe", "code": "ZW"} 245 | ] -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattzollinhofer/vue-typeahead-bootstrap/279ff4c37a4a8c3c920dd9d19ec8175594df89ea/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-typeahead-bootstrap - A simple typeahead using Vue and Bootstrap 4 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/VueTypeaheadBootstrap.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 313 | 314 | 327 | -------------------------------------------------------------------------------- /src/components/VueTypeaheadBootstrapList.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 272 | -------------------------------------------------------------------------------- /src/components/VueTypeaheadBootstrapListItem.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 85 | 86 | 95 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | }, 5 | rules: { 6 | 'import/no-extraneous-dependencies': 'off' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/unit/VueTypeaheadBootstrap.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VueTypeaheadBootstrap from '@/components/VueTypeaheadBootstrap.vue' 3 | import VueTypeaheadBootstrapList from '@/components/VueTypeaheadBootstrapList.vue' 4 | 5 | describe('VueTypeaheadBootstrap', () => { 6 | let wrapper 7 | 8 | const demoData = [ 9 | 'Canada', 10 | 'United States', 11 | 'Mexico', 12 | 'Japan', 13 | 'China', 14 | 'United Kingdom' 15 | ] 16 | 17 | beforeEach(() => { 18 | wrapper = mount(VueTypeaheadBootstrap, { 19 | propsData: { 20 | data: demoData 21 | } 22 | }) 23 | }) 24 | 25 | it('Should mount and render a hidden typeahead list', () => { 26 | let child = wrapper.findComponent(VueTypeaheadBootstrapList) 27 | expect(child).toBeTruthy() 28 | expect(child.isVisible()).toBe(false) 29 | }) 30 | 31 | it('Formats the input data properly', () => { 32 | expect(wrapper.vm.formattedData[0].id).toBe(0) 33 | expect(wrapper.vm.formattedData[0].data).toBe('Canada') 34 | expect(wrapper.vm.formattedData[0].text).toBe('Canada') 35 | }) 36 | 37 | it('Defaults the screenReaderTextSerializer to the text for arrays', () => { 38 | wrapper = mount(VueTypeaheadBootstrap, { 39 | propsData: { 40 | data: ['Canada', 'CA'] 41 | } 42 | }) 43 | expect(wrapper.vm.formattedData[0].screenReaderText).toBe('Canada') 44 | expect(wrapper.vm.formattedData[1].screenReaderText).toBe('CA') 45 | }) 46 | 47 | it('Defaults the screenReaderTextSerializer to the value of the serializer', () => { 48 | wrapper = mount(VueTypeaheadBootstrap, { 49 | propsData: { 50 | data: [{ 51 | name: 'Canada', 52 | code: 'CA' 53 | }], 54 | value: 'Can', 55 | serializer: t => t.name 56 | } 57 | }) 58 | expect(wrapper.vm.formattedData[0].id).toBe(0) 59 | expect(wrapper.vm.formattedData[0].data.code).toBe('CA') 60 | expect(wrapper.vm.formattedData[0].screenReaderText).toBe('Canada') 61 | }) 62 | 63 | it('Uses a custom screenReaderTextSerializer properly', () => { 64 | wrapper = mount(VueTypeaheadBootstrap, { 65 | propsData: { 66 | data: [{ 67 | name: 'Canada', 68 | screenReaderText: 'Canada button', 69 | code: 'CA' 70 | }], 71 | value: 'Can', 72 | screenReaderTextSerializer: t => t.screenReaderText, 73 | serializer: t => t.name 74 | } 75 | }) 76 | expect(wrapper.vm.formattedData[0].id).toBe(0) 77 | expect(wrapper.vm.formattedData[0].data.code).toBe('CA') 78 | expect(wrapper.vm.formattedData[0].screenReaderText).toBe('Canada button') 79 | }) 80 | 81 | it('Uses a custom serializer properly', () => { 82 | wrapper = mount(VueTypeaheadBootstrap, { 83 | propsData: { 84 | data: [{ 85 | name: 'Canada', 86 | code: 'CA' 87 | }], 88 | value: 'Can', 89 | serializer: t => t.name 90 | } 91 | }) 92 | expect(wrapper.vm.formattedData[0].id).toBe(0) 93 | expect(wrapper.vm.formattedData[0].data.code).toBe('CA') 94 | expect(wrapper.vm.formattedData[0].text).toBe('Canada') 95 | }) 96 | 97 | it('Allows for a name to be provided for the input', () => { 98 | wrapper = mount(VueTypeaheadBootstrap, { 99 | propsData: { 100 | data: demoData, 101 | inputName: 'name-is-provided-for-this-input' 102 | } 103 | }) 104 | expect(wrapper.find('input').attributes().name).toBe('name-is-provided-for-this-input') 105 | }) 106 | 107 | it('Show the list when given a query', async () => { 108 | let child = wrapper.findComponent(VueTypeaheadBootstrapList) 109 | expect(child.isVisible()).toBe(false) 110 | wrapper.find('input').setValue('Can') 111 | await wrapper.vm.$nextTick() 112 | expect(child.isVisible()).toBe(true) 113 | }) 114 | 115 | it('Hides the list when focus is lost', async () => { 116 | let child = wrapper.findComponent(VueTypeaheadBootstrapList) 117 | wrapper.setData({ inputValue: 'Can' }) 118 | wrapper.find('input').trigger('focus') 119 | await wrapper.vm.$nextTick() 120 | expect(child.isVisible()).toBe(true) 121 | 122 | wrapper.find('input').trigger('blur') 123 | await wrapper.vm.$nextTick() 124 | expect(child.isVisible()).toBe(false) 125 | }) 126 | 127 | it('Renders the list in different sizes', () => { 128 | expect(wrapper.vm.inputGroupClasses).toBe('input-group') 129 | wrapper.setProps({ 130 | size: 'lg' 131 | }) 132 | expect(wrapper.vm.inputGroupClasses).toBe('input-group input-group-lg') 133 | }) 134 | 135 | describe('key press handling', () => { 136 | it('Emits a keyup.enter event when enter is pressed on the input field', () => { 137 | let input = wrapper.find('input') 138 | input.trigger('keyup.enter') 139 | expect(wrapper.emitted().keyup).toBeTruthy() 140 | expect(wrapper.emitted().keyup.length).toBe(1) 141 | expect(wrapper.emitted().keyup[0][0].keyCode).toBe(13) 142 | }) 143 | 144 | it('triggers the correct event when hitting enter', () => { 145 | let child = wrapper.findComponent(VueTypeaheadBootstrapList) 146 | const hitActive = jest.spyOn(child.vm, 'hitActiveListItem') 147 | let input = wrapper.find('input') 148 | 149 | input.trigger('keyup.enter') 150 | 151 | expect(hitActive).toHaveBeenCalledWith() 152 | }) 153 | 154 | it('triggers the correct event when hitting the down arrow', () => { 155 | let child = wrapper.findComponent(VueTypeaheadBootstrapList) 156 | const selectNextListItem = jest.spyOn(child.vm, 'selectNextListItem') 157 | let input = wrapper.find('input') 158 | 159 | input.trigger('keyup.down') 160 | 161 | expect(selectNextListItem).toHaveBeenCalledWith() 162 | }) 163 | 164 | it('triggers the correct event when hitting up arrow', () => { 165 | let child = wrapper.findComponent(VueTypeaheadBootstrapList) 166 | const selectPreviousListItem = jest.spyOn(child.vm, 'selectPreviousListItem') 167 | let input = wrapper.find('input') 168 | 169 | input.trigger('keyup.up') 170 | 171 | expect(selectPreviousListItem).toHaveBeenCalledWith() 172 | }) 173 | 174 | it('Emits a blur event when the underlying input field blurs', async () => { 175 | let input = wrapper.find('input') 176 | await input.trigger('blur') 177 | expect(wrapper.emitted().blur).toBeTruthy() 178 | }) 179 | 180 | it('Does not emit a blur event if the focus shifted to the dropdown list', async () => { 181 | let input = wrapper.find('input') 182 | wrapper.setData({ inputValue: 'Can' }) 183 | await input.trigger('focus') 184 | 185 | let listItem = wrapper.get('.vbst-item').element 186 | await input.trigger('blur', {relatedTarget: listItem}) 187 | 188 | expect(wrapper.emitted().blur).toBeFalsy() 189 | }) 190 | 191 | it('Emits a focus event when the underlying input field receives focus', async () => { 192 | let input = wrapper.find('input') 193 | await input.trigger('focus') 194 | expect(wrapper.emitted().focus).toBeTruthy() 195 | }) 196 | }) 197 | }) 198 | -------------------------------------------------------------------------------- /tests/unit/VueTypeaheadBootstrapList.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VueTypeaheadBootstrapList from '@/components/VueTypeaheadBootstrapList.vue' 3 | import VueTypeaheadBootstrapListItem from '@/components/VueTypeaheadBootstrapListItem.vue' 4 | 5 | describe('VueBootstrapTypeaheadList', () => { 6 | let wrapper 7 | 8 | const demoData = [ 9 | { 10 | id: 0, 11 | data: 'Canada', 12 | text: 'Canada' 13 | }, 14 | { 15 | id: 1, 16 | data: 'USA', 17 | text: 'USA' 18 | }, 19 | { 20 | id: 2, 21 | data: 'Mexico', 22 | text: 'Mexico' 23 | }, 24 | { 25 | id: 3, 26 | data: 'Canadiana', 27 | text: 'Canadiana' 28 | }, 29 | { 30 | id: 4, 31 | data: 'Canada (CA)', 32 | text: 'Canada (CA)' 33 | } 34 | ] 35 | 36 | beforeEach(() => { 37 | wrapper = mount(VueTypeaheadBootstrapList, { 38 | propsData: { 39 | data: demoData, 40 | vbtUniqueId: 123456789 41 | } 42 | }) 43 | }) 44 | 45 | it('Mounts and renders a list-group div', () => { 46 | expect(wrapper.element.tagName.toLowerCase()).toBe('div') 47 | expect(wrapper.classes()).toContain('list-group') 48 | }) 49 | 50 | it('Matches items when there is a query', async () => { 51 | expect(wrapper.vm.matchedItems.length).toBe(0) 52 | wrapper.setProps({ 53 | query: 'Can' 54 | }) 55 | await wrapper.vm.$nextTick() 56 | expect(wrapper.vm.matchedItems.length).toBe(3) 57 | expect(wrapper.findAllComponents(VueTypeaheadBootstrapListItem).length).toBe(3) 58 | wrapper.setProps({ 59 | query: 'Canada' 60 | }) 61 | await wrapper.vm.$nextTick() 62 | expect(wrapper.vm.matchedItems.length).toBe(2) 63 | expect(wrapper.findAllComponents(VueTypeaheadBootstrapListItem).length).toBe(2) 64 | }) 65 | 66 | it('Matches no items when there is no query', () => { 67 | expect(wrapper.vm.matchedItems.length).toBe(0) 68 | wrapper.setProps({ 69 | query: '' 70 | }) 71 | expect(wrapper.vm.matchedItems.length).toBe(0) 72 | expect(wrapper.findAllComponents(VueTypeaheadBootstrapListItem).length).toBe(0) 73 | }) 74 | 75 | it('Limits the number of matches with maxMatches', () => { 76 | wrapper.setProps({ 77 | query: 'can' 78 | }) 79 | expect(wrapper.vm.matchedItems.length).toBe(3) 80 | wrapper.setProps({ 81 | maxMatches: 1 82 | }) 83 | expect(wrapper.vm.matchedItems.length).toBe(1) 84 | }) 85 | 86 | it('Uses minMatchingChars to filter the number of matches', async () => { 87 | wrapper.setProps({ 88 | query: 'c', 89 | minMatchingChars: 1 90 | }) 91 | await wrapper.vm.$nextTick() 92 | expect(wrapper.findAllComponents(VueTypeaheadBootstrapListItem).length).toBe(4) 93 | }) 94 | 95 | it('Highlights text matches properly by default', async () => { 96 | wrapper.setProps({ 97 | query: 'Cana' 98 | }) 99 | await wrapper.vm.$nextTick() 100 | expect(wrapper.findComponent(VueTypeaheadBootstrapListItem).vm.htmlText).toBe(`Canada`) 101 | }) 102 | 103 | it('Highlights text matches when query text contains regex escape characters', async () => { 104 | wrapper.setProps({ 105 | query: 'Canada (C' 106 | }) 107 | await wrapper.vm.$nextTick() 108 | expect(wrapper.findComponent(VueTypeaheadBootstrapListItem).vm.htmlText).toBe(`Canada (CA)`) 109 | }) 110 | 111 | describe('providing accessible text for screen readers', () => { 112 | it('renders screen reader text if provided', async () => { 113 | wrapper.setProps({ 114 | data: [ 115 | { 116 | id: 0, 117 | data: 'Canada', 118 | text: 'Canada', 119 | screenReaderText: 'my screen reader text' 120 | } 121 | ], 122 | query: 'Can' 123 | }) 124 | await wrapper.vm.$nextTick() 125 | expect(wrapper.findComponent(VueTypeaheadBootstrapListItem).vm.screenReaderText).toBe('my screen reader text') 126 | }) 127 | 128 | it("defaults the screen reader text to the item's text if not provided ", async () => { 129 | wrapper.setProps({ 130 | data: [ 131 | { 132 | id: 0, 133 | data: 'Canada', 134 | text: 'Canada' 135 | } 136 | ], 137 | query: 'Can' 138 | }) 139 | await wrapper.vm.$nextTick() 140 | expect(wrapper.findComponent(VueTypeaheadBootstrapListItem).vm.screenReaderText).toBe('Canada') 141 | }) 142 | }) 143 | 144 | describe('selecting items with the keyboard', () => { 145 | beforeEach(() => { 146 | wrapper.setProps({ 147 | data: [ 148 | { 149 | id: 0, 150 | data: 'Canada', 151 | text: 'Canada' 152 | }, 153 | { 154 | id: 1, 155 | data: 'Canada1', 156 | text: 'Canada1' 157 | }, 158 | { 159 | id: 2, 160 | data: 'Canada2', 161 | text: 'Canada2' 162 | } 163 | ], 164 | query: 'Cana' 165 | }) 166 | }) 167 | 168 | describe('using the down arrow', () => { 169 | it('cycles through all options', () => { 170 | wrapper.vm.selectNextListItem() 171 | expect(wrapper.vm.activeListItem).toBe(0) 172 | wrapper.vm.selectNextListItem() 173 | expect(wrapper.vm.activeListItem).toBe(1) 174 | wrapper.vm.selectNextListItem() 175 | expect(wrapper.vm.activeListItem).toBe(2) 176 | }) 177 | it('returns the first item when nothing is disabled', () => { 178 | wrapper.vm.selectNextListItem() 179 | expect(wrapper.vm.activeListItem).toBe(0) 180 | }) 181 | it('returns the second item when the first is disabled', () => { 182 | wrapper.setProps({ disabledValues: ['Canada'] }) 183 | wrapper.vm.selectNextListItem() 184 | expect(wrapper.vm.activeListItem).toBe(1) 185 | }) 186 | it('returns the third item when the first and second are disabled', () => { 187 | wrapper.setProps({ disabledValues: ['Canada', 'Canada1'] }) 188 | wrapper.vm.selectNextListItem() 189 | expect(wrapper.vm.activeListItem).toBe(2) 190 | }) 191 | it('returns -1 when everything is disabled', () => { 192 | wrapper.setProps({ disabledValues: ['Canada', 'Canada1', 'Canada2'] }) 193 | wrapper.vm.selectNextListItem() 194 | expect(wrapper.vm.activeListItem).toBe(-1) 195 | wrapper.vm.selectNextListItem() 196 | expect(wrapper.vm.activeListItem).toBe(-1) 197 | }) 198 | it('wraps back to the beginning from the end', () => { 199 | wrapper.vm.activeListItem = 1 200 | wrapper.vm.selectNextListItem() 201 | expect(wrapper.vm.activeListItem).toBe(2) 202 | wrapper.vm.selectNextListItem() 203 | expect(wrapper.vm.activeListItem).toBe(0) 204 | }) 205 | it('wrapping accounts for disabled items', () => { 206 | wrapper.setProps({ disabledValues: ['Canada'] }) 207 | wrapper.vm.activeListItem = 2 208 | wrapper.vm.selectNextListItem() 209 | expect(wrapper.vm.activeListItem).toBe(1) 210 | }) 211 | }) 212 | 213 | describe('using the up arrow', () => { 214 | it('returns the last item when nothing is disabled', () => { 215 | wrapper.vm.selectPreviousListItem() 216 | expect(wrapper.vm.activeListItem).toBe(2) 217 | }) 218 | it('returns the second item when the last is disabled', () => { 219 | wrapper.setProps({ disabledValues: ['Canada2'] }) 220 | wrapper.vm.selectPreviousListItem() 221 | expect(wrapper.vm.activeListItem).toBe(1) 222 | }) 223 | it('returns the second item when the third and fourth are disabled', () => { 224 | wrapper.setProps({ disabledValues: ['Canada3', 'Canada2'] }) 225 | wrapper.vm.selectPreviousListItem() 226 | expect(wrapper.vm.activeListItem).toBe(1) 227 | }) 228 | it('returns -1 when everything is disabled', () => { 229 | wrapper.setProps({ disabledValues: ['Canada', 'Canada1', 'Canada2', 'Canada3'] }) 230 | wrapper.vm.selectPreviousListItem() 231 | expect(wrapper.vm.activeListItem).toBe(-1) 232 | wrapper.vm.selectPreviousListItem() 233 | expect(wrapper.vm.activeListItem).toBe(-1) 234 | }) 235 | it('cycles through all options', () => { 236 | wrapper.vm.selectPreviousListItem() 237 | expect(wrapper.vm.activeListItem).toBe(2) 238 | wrapper.vm.selectPreviousListItem() 239 | expect(wrapper.vm.activeListItem).toBe(1) 240 | wrapper.vm.selectPreviousListItem() 241 | expect(wrapper.vm.activeListItem).toBe(0) 242 | }) 243 | it('wraps back to the end from the beginning', () => { 244 | wrapper.vm.activeListItem = 1 245 | wrapper.vm.selectPreviousListItem() 246 | expect(wrapper.vm.activeListItem).toBe(0) 247 | wrapper.vm.selectPreviousListItem() 248 | expect(wrapper.vm.activeListItem).toBe(2) 249 | }) 250 | it('wrapping accounts for disabled items', () => { 251 | wrapper.setProps({ disabledValues: ['Canada2'] }) 252 | wrapper.vm.activeListItem = 0 253 | wrapper.vm.selectPreviousListItem() 254 | expect(wrapper.vm.activeListItem).toBe(1) 255 | }) 256 | }) 257 | }) 258 | 259 | describe('Selecting on Enter Key', () => { 260 | beforeEach(() => { 261 | wrapper.setProps({ 262 | data: [ 263 | { 264 | id: 0, 265 | data: 'Canada', 266 | text: 'Canada' 267 | }, 268 | { 269 | id: 1, 270 | data: 'Canada1', 271 | text: 'Canada1' 272 | }, 273 | { 274 | id: 2, 275 | data: 'Canada2', 276 | text: 'Canada2' 277 | } 278 | ] 279 | }) 280 | }) 281 | 282 | it('does not return a hit with no matches', async () => { 283 | wrapper.setProps({ 284 | query: ';lskdj' 285 | }) 286 | await wrapper.vm.$nextTick() 287 | wrapper.vm.handleParentInputKeyup({ keyCode: 13 }) // simulate enter key 288 | await wrapper.vm.$nextTick() 289 | expect(wrapper.emitted('hit')).toBeFalsy() 290 | }) 291 | 292 | describe('with some matches', () => { 293 | beforeEach(() => { 294 | wrapper.setProps({ 295 | query: 'Cana' 296 | }) 297 | }) 298 | it('returns the selected item when one is selected', async () => { 299 | wrapper.vm.selectNextListItem() 300 | wrapper.vm.selectNextListItem() 301 | await wrapper.vm.$nextTick() 302 | wrapper.vm.handleParentInputKeyup({ keyCode: 13 }) // simulate enter key 303 | await wrapper.vm.$nextTick() 304 | expect(wrapper.emitted().hit).toBeTruthy() 305 | expect(wrapper.emitted().hit[0][0].data).toBe('Canada1') 306 | }) 307 | 308 | it('returns the first item when no item is selected', async () => { 309 | await wrapper.vm.$nextTick() 310 | wrapper.vm.handleParentInputKeyup({ keyCode: 13 }) // simulate enter key 311 | await wrapper.vm.$nextTick() 312 | expect(wrapper.emitted().hit).toBeTruthy() 313 | expect(wrapper.emitted().hit[0][0].data).toBe('Canada') 314 | }) 315 | 316 | it('returns the first enabled item when no item is selected', async () => { 317 | wrapper.setProps({ 318 | disabledValues: ['Canada'] 319 | }) 320 | wrapper.vm.handleParentInputKeyup({ keyCode: 13 }) // simulate enter key 321 | await wrapper.vm.$nextTick() 322 | expect(wrapper.emitted().hit).toBeTruthy() 323 | expect(wrapper.emitted().hit[0][0].data).toBe('Canada1') 324 | }) 325 | }) 326 | 327 | it('returns the only non-disabled item as a hit with only one enabled match', async () => { 328 | wrapper.setProps({ 329 | disabledValues: ['Canada', 'Canada2'], 330 | query: 'Cana' 331 | }) 332 | await wrapper.vm.$nextTick() 333 | wrapper.vm.handleParentInputKeyup({ keyCode: 13 }) // simulate enter key 334 | await wrapper.vm.$nextTick() 335 | expect(wrapper.emitted().hit).toBeTruthy() 336 | expect(wrapper.emitted().hit[0][0].data).toBe('Canada1') 337 | }) 338 | 339 | it('does not return a hit with only disabled matches', async () => { 340 | wrapper.setProps({ 341 | disabledValues: ['Canada', 'Canada1', 'Canada2'], 342 | query: 'Cana' 343 | }) 344 | await wrapper.vm.$nextTick() 345 | wrapper.vm.handleParentInputKeyup({ keyCode: 13 }) // simulate enter key 346 | await wrapper.vm.$nextTick() 347 | expect(wrapper.emitted('hit')).toBeFalsy() 348 | }) 349 | }) 350 | 351 | it('Highlights text matches properly with highlightClass prop', async () => { 352 | wrapper.setProps({ 353 | query: 'Canada', 354 | highlightClass: 'myStyle' 355 | }) 356 | await wrapper.vm.$nextTick() 357 | expect(wrapper.findComponent(VueTypeaheadBootstrapListItem).vm.htmlText).toBe(`Canada`) 358 | }) 359 | }) 360 | -------------------------------------------------------------------------------- /tests/unit/VueTypeaheadBootstrapListItem.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import VueTypeaheadBootstrapListItem from '@/components/VueTypeaheadBootstrapListItem.vue' 3 | 4 | describe('VueTypeaheadBootstrapListItem.vue', () => { 5 | let wrapper 6 | beforeEach(() => { 7 | wrapper = shallowMount(VueTypeaheadBootstrapListItem) 8 | }) 9 | 10 | it('Mounts and renders an tag', () => { 11 | expect(wrapper.exists()).toBe(true) 12 | expect(wrapper.element.tagName.toLowerCase()).toBe('a') 13 | }) 14 | 15 | it('Renders textVariant classes properly', async () => { 16 | wrapper.setProps({ textVariant: 'dark' }) 17 | await wrapper.vm.$nextTick() 18 | expect(wrapper.classes()).toEqual(expect.arrayContaining(['text-dark'])) 19 | }) 20 | 21 | it('Renders backgroundVariant classes properly', async () => { 22 | wrapper.setProps({ backgroundVariant: 'light' }) 23 | await wrapper.vm.$nextTick() 24 | expect(wrapper.classes()).toEqual(expect.arrayContaining(['list-group-item-light'])) 25 | }) 26 | 27 | it('Renders text classes properly when backgroundVariantResolver is not specified', async () => { 28 | const baseClasses = [...wrapper.vm.baseTextClasses] 29 | expect(wrapper.classes()).toHaveLength(baseClasses.length) 30 | expect(wrapper.classes()).toEqual(expect.arrayContaining([...baseClasses])) 31 | }) 32 | 33 | it('Renders text classes properly when backgroundVariantResolver returns non-string', async () => { 34 | const baseClasses = [...wrapper.vm.baseTextClasses] 35 | const resolver1 = () => [ 'light', 'dark' ] 36 | wrapper.setProps({ backgroundVariantResolver: resolver1 }) 37 | await wrapper.vm.$nextTick() 38 | // the first resolver returns an array when it should be returning a string; 39 | // therefore the classes assigned to the wrapper should be the same as the 40 | // base classes. 41 | expect(wrapper.classes()).toHaveLength(baseClasses.length) 42 | expect(wrapper.classes()).toEqual(expect.arrayContaining([...baseClasses])) 43 | const resolver2 = () => ({ 'class1': 'light', 'class2': 'dark' }) 44 | wrapper.setProps({ backgroundVariantResolver: resolver2 }) 45 | await wrapper.vm.$nextTick() 46 | // the second resolver returns an object, therefore the classes 47 | // assigned to the wrapper should also be the same as the base classes. 48 | expect(wrapper.classes()).toHaveLength(baseClasses.length) 49 | expect(wrapper.classes()).toEqual(expect.arrayContaining([...baseClasses])) 50 | }) 51 | 52 | it('emits "listItemBlur" when the list items lose focus', async () => { 53 | wrapper.find('.vbst-item').trigger('blur') 54 | expect(wrapper.emitted('listItemBlur')).toBeTruthy() 55 | }) 56 | 57 | it('Renders backgroundVariantResolver classes properly', async () => { 58 | const baseClasses = [...wrapper.vm.baseTextClasses] 59 | const testItems = [ 60 | { 61 | data: { 'prop': 'light' }, 62 | resolver: (d) => d.prop 63 | }, 64 | { 65 | data: { 'prop': 'dark' }, 66 | resolver: (d) => d.prop 67 | }, 68 | { 69 | data: { 'prop': 'warning' }, 70 | resolver: (d) => d.prop 71 | } 72 | ] 73 | // iterate through the test items which specify a classname to add 74 | // and a resolver to use. 75 | for (let i = 0; i < testItems.length; i++) { 76 | wrapper.setProps({ backgroundVariantResolver: testItems[i].resolver, data: testItems[i].data }) 77 | await wrapper.vm.$nextTick() 78 | // this should be adding a single class to the baseClasses, should not be appending 79 | expect(wrapper.classes()).toHaveLength(baseClasses.length + 1) 80 | expect(wrapper.classes()).toEqual(expect.arrayContaining([`list-group-item-${[testItems[i].data.prop]}`])) 81 | } 82 | }) 83 | 84 | it('Favors backgroundVariantResolver class over backgroundVariant class', async () => { 85 | const baseClasses = [...wrapper.vm.baseTextClasses] 86 | const resolver1 = () => 'dark' 87 | // we are specifying both a backgroungVariant and a backgroundVariantResolver 88 | wrapper.setProps({ backgroundVariant: 'light', backgroundVariantResolver: resolver1 }) 89 | await wrapper.vm.$nextTick() 90 | expect(wrapper.classes()).toHaveLength(baseClasses.length + 1) 91 | expect(wrapper.classes()).toEqual(expect.arrayContaining([`list-group-item-dark`])) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: process.env.NODE_ENV === 'production' ? 'vue-typeahead-bootstrap/' : '/' 3 | } 4 | --------------------------------------------------------------------------------