├── .gitignore ├── .eslintrc ├── demo ├── assets │ ├── favicon.ico │ └── style.css ├── main.js ├── index.html └── Typeahead.vue ├── .npmignore ├── .babelrc ├── .editorconfig ├── LICENSE ├── webpack.config.js ├── package.json ├── src └── main.js ├── README.md └── dist └── vue-typeahead.common.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "vue", 3 | "rules": { 4 | "no-duplicate-imports": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /demo/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pespantelis/vue-typeahead/HEAD/demo/assets/favicon.ico -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .babelrc 3 | .editorconfig 4 | .npmignore 5 | demo/ 6 | index.html 7 | webpack.config.js 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Axios from 'axios' 3 | import Typeahead from './Typeahead.vue' 4 | 5 | Vue.prototype.$http = Axios 6 | 7 | new Vue({ 8 | el: '#demo', 9 | components: { 10 | Typeahead 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pantelis Peslis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | entry: './demo/main.js', 5 | output: { 6 | path: './demo/build', 7 | publicPath: '/build/', 8 | filename: 'build.js' 9 | }, 10 | resolve: { 11 | alias: { 12 | 'vue': 'vue/dist/vue.common' 13 | } 14 | }, 15 | module: { 16 | loaders: [ 17 | { 18 | test: /\.vue$/, 19 | loader: 'vue' 20 | }, 21 | { 22 | test: /\.js$/, 23 | loader: 'babel', 24 | exclude: /node_modules/ 25 | } 26 | ] 27 | }, 28 | devServer: { 29 | historyApiFallback: true, 30 | noInfo: true 31 | }, 32 | devtool: '#eval-source-map' 33 | } 34 | 35 | if (process.env.NODE_ENV === 'production') { 36 | module.exports.devtool = '#source-map' 37 | module.exports.plugins = (module.exports.plugins || []).concat([ 38 | new webpack.DefinePlugin({ 39 | 'process.env': { 40 | NODE_ENV: '"production"' 41 | } 42 | }), 43 | new webpack.optimize.UglifyJsPlugin({ 44 | compress: { 45 | warnings: false 46 | } 47 | }), 48 | new webpack.optimize.OccurenceOrderPlugin() 49 | ]) 50 | } 51 | -------------------------------------------------------------------------------- /demo/assets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0 20px; 4 | font-family: 'Source Sans Pro', 'Helvetica Neue', sans-serif; 5 | border-top: 2px solid #4fc08d; 6 | } 7 | 8 | h1, h4 { 9 | cursor: default; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | } 15 | 16 | h1 { 17 | font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', sans-serif; 18 | font-weight: 300; 19 | font-size: 4em; 20 | color: #2c3e50; 21 | } 22 | 23 | h4, a { 24 | font-weight: 400; 25 | font-size: 15px; 26 | color: #7f8c8d; 27 | } 28 | 29 | .container { 30 | width: 100%; 31 | max-width: 600px; 32 | margin: 0 auto; 33 | text-align: center; 34 | } 35 | 36 | .button { 37 | display: inline-block; 38 | width: 180px; 39 | margin: 0.5em; 40 | padding: 12px 14px; 41 | background-color: #4fc08d; 42 | font-weight: 700; 43 | color: #fff; 44 | border-bottom: 2px solid #3aa373; 45 | border-radius: 4px; 46 | -webkit-transition: all 0.15s ease; 47 | transition: all 0.15s ease; 48 | } 49 | 50 | .button:hover { 51 | background-color: #5dc596; 52 | -webkit-transform: translate(0, -2px); 53 | -ms-transform: translate(0, -2px); 54 | transform: translate(0, -2px); 55 | } 56 | 57 | #social { 58 | margin: 2em 0 3em; 59 | } 60 | 61 | #social iframe { 62 | margin: 0 4px; 63 | } 64 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Typeahead | Vue.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Typeahead

15 |

Vue.js component

16 | Source on GitHub 17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-typeahead", 3 | "version": "2.3.2", 4 | "author": "Pantelis Peslis ", 5 | "license": "MIT", 6 | "description": "Typeahead component for Vue.js", 7 | "keywords": [ 8 | "vue", 9 | "typeahead" 10 | ], 11 | "main": "dist/vue-typeahead.common.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/pespantelis/vue-typeahead.git" 15 | }, 16 | "bugs": "https://github.com/pespantelis/vue-typeahead/issues", 17 | "homepage": "http://pespantelis.github.io/vue-typeahead/", 18 | "scripts": { 19 | "lint": "eslint src demo", 20 | "dev": "webpack-dev-server --content-base demo/ --inline --hot", 21 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", 22 | "dist": "babel src/main.js --out-file dist/vue-typeahead.common.js" 23 | }, 24 | "peerDependencies": { 25 | "vue": "^1.0.21 || ^2.x.x" 26 | }, 27 | "dependencies": { 28 | "babel-runtime": "^6.0.0" 29 | }, 30 | "devDependencies": { 31 | "axios": "^0.15.3", 32 | "babel-cli": "^6.23.0", 33 | "babel-core": "^6.0.0", 34 | "babel-loader": "^6.0.0", 35 | "babel-plugin-transform-runtime": "^6.0.0", 36 | "babel-preset-es2015": "^6.0.0", 37 | "babel-preset-stage-2": "^6.0.0", 38 | "cross-env": "^1.0.6", 39 | "css-loader": "^0.23.0", 40 | "eslint": "^2.13.1", 41 | "eslint-config-vue": "^1.0.3", 42 | "eslint-plugin-html": "^1.5.5", 43 | "vue-hot-reload-api": "^1.2.0", 44 | "vue-html-loader": "^1.0.0", 45 | "vue-loader": "^11.1.4", 46 | "vue-style-loader": "^1.0.0", 47 | "vue-template-compiler": "^2.2.1", 48 | "webpack": "^1.12.2", 49 | "webpack-dev-server": "^1.12.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { util } from 'vue' 2 | 3 | export default { 4 | data () { 5 | return { 6 | items: [], 7 | query: '', 8 | current: -1, 9 | loading: false, 10 | selectFirst: false, 11 | queryParamName: 'q' 12 | } 13 | }, 14 | 15 | computed: { 16 | hasItems () { 17 | return this.items.length > 0 18 | }, 19 | 20 | isEmpty () { 21 | return !this.query 22 | }, 23 | 24 | isDirty () { 25 | return !!this.query 26 | } 27 | }, 28 | 29 | methods: { 30 | update () { 31 | this.cancel() 32 | 33 | if (!this.query) { 34 | return this.reset() 35 | } 36 | 37 | if (this.minChars && this.query.length < this.minChars) { 38 | return 39 | } 40 | 41 | this.loading = true 42 | 43 | this.fetch().then((response) => { 44 | if (response && this.query) { 45 | let data = response.data 46 | data = this.prepareResponseData ? this.prepareResponseData(data) : data 47 | this.items = this.limit ? data.slice(0, this.limit) : data 48 | this.current = -1 49 | this.loading = false 50 | 51 | if (this.selectFirst) { 52 | this.down() 53 | } 54 | } 55 | }) 56 | }, 57 | 58 | fetch () { 59 | if (!this.$http) { 60 | return util.warn('You need to provide a HTTP client', this) 61 | } 62 | 63 | if (!this.src) { 64 | return util.warn('You need to set the `src` property', this) 65 | } 66 | 67 | const src = this.queryParamName 68 | ? this.src 69 | : this.src + this.query 70 | 71 | const params = this.queryParamName 72 | ? Object.assign({ [this.queryParamName]: this.query }, this.data) 73 | : this.data 74 | 75 | let cancel = new Promise((resolve) => this.cancel = resolve) 76 | let request = this.$http.get(src, { params }) 77 | 78 | return Promise.race([cancel, request]) 79 | }, 80 | 81 | cancel () { 82 | // used to 'cancel' previous searches 83 | }, 84 | 85 | reset () { 86 | this.items = [] 87 | this.query = '' 88 | this.loading = false 89 | }, 90 | 91 | setActive (index) { 92 | this.current = index 93 | }, 94 | 95 | activeClass (index) { 96 | return { 97 | active: this.current === index 98 | } 99 | }, 100 | 101 | hit () { 102 | if (this.current !== -1) { 103 | this.onHit(this.items[this.current]) 104 | } 105 | }, 106 | 107 | up () { 108 | if (this.current > 0) { 109 | this.current-- 110 | } else if (this.current === -1) { 111 | this.current = this.items.length - 1 112 | } else { 113 | this.current = -1 114 | } 115 | }, 116 | 117 | down () { 118 | if (this.current < this.items.length - 1) { 119 | this.current++ 120 | } else { 121 | this.current = -1 122 | } 123 | }, 124 | 125 | onHit () { 126 | util.warn('You need to implement the `onHit` method', this) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /demo/Typeahead.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | 53 | 54 | 55 | 56 | 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueTypeahead 2 | 3 | See a live demo [here](http://pespantelis.github.io/vue-typeahead/). 4 | 5 | ## Install 6 | 7 | #### NPM 8 | Available through npm as `vue-typeahead`. 9 | ``` 10 | npm install --save vue-typeahead 11 | ``` 12 | > Also, you need to install a HTTP client like [`axios`](https://github.com/mzabriskie/axios). 13 | 14 | ## Usage 15 | If you are using `vue@1.0.22+`, you could use the new [`extends`](http://vuejs.org/api/#extends) property (see below). 16 | 17 | Otherwise, the `mixins` way also works. 18 | 19 | ```html 20 | 50 | 51 | 103 | 104 | 109 | ``` 110 | 111 | ## Key Actions 112 | **Down Arrow:** Highlight the previous item. 113 | 114 | **Up Arrow:** Highlight the next item. 115 | 116 | **Enter:** Hit on highlighted item. 117 | 118 | **Escape:** Hide the list. 119 | 120 | ## States 121 | **loading:** Indicates that awaits the data. 122 | 123 | **isEmpty:** Indicates that the input is empty. 124 | 125 | **isDirty:** Indicates that the input is not empty. 126 | > Useful if you want to add icon indicators (see the demo) 127 | 128 | ## License 129 | VueTypeahead is released under the MIT License. See the bundled LICENSE file for details. 130 | -------------------------------------------------------------------------------- /dist/vue-typeahead.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _promise = require('babel-runtime/core-js/promise'); 8 | 9 | var _promise2 = _interopRequireDefault(_promise); 10 | 11 | var _defineProperty2 = require('babel-runtime/helpers/defineProperty'); 12 | 13 | var _defineProperty3 = _interopRequireDefault(_defineProperty2); 14 | 15 | var _assign = require('babel-runtime/core-js/object/assign'); 16 | 17 | var _assign2 = _interopRequireDefault(_assign); 18 | 19 | var _vue = require('vue'); 20 | 21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 22 | 23 | exports.default = { 24 | data: function data() { 25 | return { 26 | items: [], 27 | query: '', 28 | current: -1, 29 | loading: false, 30 | selectFirst: false, 31 | queryParamName: 'q' 32 | }; 33 | }, 34 | 35 | 36 | computed: { 37 | hasItems: function hasItems() { 38 | return this.items.length > 0; 39 | }, 40 | isEmpty: function isEmpty() { 41 | return !this.query; 42 | }, 43 | isDirty: function isDirty() { 44 | return !!this.query; 45 | } 46 | }, 47 | 48 | methods: { 49 | update: function update() { 50 | var _this = this; 51 | 52 | this.cancel(); 53 | 54 | if (!this.query) { 55 | return this.reset(); 56 | } 57 | 58 | if (this.minChars && this.query.length < this.minChars) { 59 | return; 60 | } 61 | 62 | this.loading = true; 63 | 64 | this.fetch().then(function (response) { 65 | if (response && _this.query) { 66 | var data = response.data; 67 | data = _this.prepareResponseData ? _this.prepareResponseData(data) : data; 68 | _this.items = _this.limit ? data.slice(0, _this.limit) : data; 69 | _this.current = -1; 70 | _this.loading = false; 71 | 72 | if (_this.selectFirst) { 73 | _this.down(); 74 | } 75 | } 76 | }); 77 | }, 78 | fetch: function fetch() { 79 | var _this2 = this; 80 | 81 | if (!this.$http) { 82 | return _vue.util.warn('You need to provide a HTTP client', this); 83 | } 84 | 85 | if (!this.src) { 86 | return _vue.util.warn('You need to set the `src` property', this); 87 | } 88 | 89 | var src = this.queryParamName ? this.src : this.src + this.query; 90 | 91 | var params = this.queryParamName ? (0, _assign2.default)((0, _defineProperty3.default)({}, this.queryParamName, this.query), this.data) : this.data; 92 | 93 | var cancel = new _promise2.default(function (resolve) { 94 | return _this2.cancel = resolve; 95 | }); 96 | var request = this.$http.get(src, { params: params }); 97 | 98 | return _promise2.default.race([cancel, request]); 99 | }, 100 | cancel: function cancel() {}, 101 | reset: function reset() { 102 | this.items = []; 103 | this.query = ''; 104 | this.loading = false; 105 | }, 106 | setActive: function setActive(index) { 107 | this.current = index; 108 | }, 109 | activeClass: function activeClass(index) { 110 | return { 111 | active: this.current === index 112 | }; 113 | }, 114 | hit: function hit() { 115 | if (this.current !== -1) { 116 | this.onHit(this.items[this.current]); 117 | } 118 | }, 119 | up: function up() { 120 | if (this.current > 0) { 121 | this.current--; 122 | } else if (this.current === -1) { 123 | this.current = this.items.length - 1; 124 | } else { 125 | this.current = -1; 126 | } 127 | }, 128 | down: function down() { 129 | if (this.current < this.items.length - 1) { 130 | this.current++; 131 | } else { 132 | this.current = -1; 133 | } 134 | }, 135 | onHit: function onHit() { 136 | _vue.util.warn('You need to implement the `onHit` method', this); 137 | } 138 | } 139 | }; 140 | --------------------------------------------------------------------------------