Commit fed3e09d154d9da3cba89b5872358cea864d7243
1 parent
1183836a
add AutoComplete component
Showing
11 changed files
with
255 additions
and
31 deletions
Show diff stats
examples/app.vue
| ... | ... | @@ -56,6 +56,7 @@ li + li { border-left: solid 1px #bbb; padding-left: 10px; margin-left: 10px; } |
| 56 | 56 | <li><router-link to="/notice">Notice</router-link></li> |
| 57 | 57 | <li><router-link to="/avatar">Avatar</router-link></li> |
| 58 | 58 | <li><router-link to="/color-picker">ColorPicker</router-link></li> |
| 59 | + <li><router-link to="/auto-complete">AutoComplete</router-link></li> | |
| 59 | 60 | </ul> |
| 60 | 61 | </nav> |
| 61 | 62 | <router-view></router-view> | ... | ... |
examples/main.js
| ... | ... | @@ -188,6 +188,10 @@ const router = new VueRouter({ |
| 188 | 188 | { |
| 189 | 189 | path: '/color-picker', |
| 190 | 190 | component: require('./routers/color-picker.vue') |
| 191 | + }, | |
| 192 | + { | |
| 193 | + path: '/auto-complete', | |
| 194 | + component: require('./routers/auto-complete.vue') | |
| 191 | 195 | } |
| 192 | 196 | ] |
| 193 | 197 | }); | ... | ... |
| 1 | +<template> | |
| 2 | + <div style="margin: 100px;width: 200px;"> | |
| 3 | + <AutoComplete transfer v-model="value" :data="data" @on-change="hc" :filter-method="fm"> | |
| 4 | + <!--<Option v-for="item in data" :value="item" :label="item" :key="item">--> | |
| 5 | + <!--<span style="color: red">{{ item }}</span>--> | |
| 6 | + <!--</Option>--> | |
| 7 | + </AutoComplete> | |
| 8 | + </div> | |
| 9 | +</template> | |
| 10 | +<script> | |
| 11 | + | |
| 12 | + export default { | |
| 13 | + props: { | |
| 14 | + | |
| 15 | + }, | |
| 16 | + data () { | |
| 17 | + return { | |
| 18 | + value: '', | |
| 19 | +// data: [], | |
| 20 | + data: ['Burns Bay Road', 'Downing Street', 'Wall Street'] | |
| 21 | + }; | |
| 22 | + }, | |
| 23 | + computed: {}, | |
| 24 | + methods: { | |
| 25 | + handleSearch (value) { | |
| 26 | + this.data = !value ? [] : [ | |
| 27 | + value + '@qq.com', | |
| 28 | + value + '@sina.com', | |
| 29 | + value + '@163.com' | |
| 30 | + ] | |
| 31 | + }, | |
| 32 | + hc (v) { | |
| 33 | + console.log(v) | |
| 34 | + }, | |
| 35 | + fm (value, item) { | |
| 36 | + return item.toUpperCase().indexOf(value.toUpperCase()) !== -1; | |
| 37 | + } | |
| 38 | + } | |
| 39 | + }; | |
| 40 | +</script> | |
| 0 | 41 | \ No newline at end of file | ... | ... |
| 1 | +<template> | |
| 2 | + <i-select | |
| 3 | + ref="select" | |
| 4 | + class="ivu-auto-complete" | |
| 5 | + :label="label" | |
| 6 | + :disabled="disabled" | |
| 7 | + :clearable="clearable" | |
| 8 | + :placeholder="placeholder" | |
| 9 | + :size="size" | |
| 10 | + filterable | |
| 11 | + remote | |
| 12 | + auto-complete | |
| 13 | + :remote-method="remoteMethod" | |
| 14 | + @on-change="handleChange" | |
| 15 | + :transfer="transfer"> | |
| 16 | + <slot name="input"> | |
| 17 | + <i-input | |
| 18 | + ref="input" | |
| 19 | + slot="input" | |
| 20 | + v-model="currentValue" | |
| 21 | + :placeholder="placeholder" | |
| 22 | + :disabled="disabled" | |
| 23 | + :size="size" | |
| 24 | + :icon="closeIcon" | |
| 25 | + @on-click="handleClear" | |
| 26 | + @on-focus="handleFocus" | |
| 27 | + @on-blur="handleBlur"></i-input> | |
| 28 | + </slot> | |
| 29 | + <slot> | |
| 30 | + <i-option v-for="item in filteredData" :value="item" :key="item">{{ item }}</i-option> | |
| 31 | + </slot> | |
| 32 | + </i-select> | |
| 33 | +</template> | |
| 34 | +<script> | |
| 35 | + import iSelect from '../select/select.vue'; | |
| 36 | + import iOption from '../select/option.vue'; | |
| 37 | + import iInput from '../input/input.vue'; | |
| 38 | + import { oneOf } from '../../utils/assist'; | |
| 39 | + | |
| 40 | + export default { | |
| 41 | + name: 'AutoComplete', | |
| 42 | + components: { iSelect, iOption, iInput }, | |
| 43 | + props: { | |
| 44 | + value: { | |
| 45 | + type: [String, Number], | |
| 46 | + default: '' | |
| 47 | + }, | |
| 48 | + label: { | |
| 49 | + type: [String, Number], | |
| 50 | + default: '' | |
| 51 | + }, | |
| 52 | + data: { | |
| 53 | + type: Array, | |
| 54 | + default: () => [] | |
| 55 | + }, | |
| 56 | + disabled: { | |
| 57 | + type: Boolean, | |
| 58 | + default: false | |
| 59 | + }, | |
| 60 | + clearable: { | |
| 61 | + type: Boolean, | |
| 62 | + default: false | |
| 63 | + }, | |
| 64 | + placeholder: { | |
| 65 | + type: String | |
| 66 | + }, | |
| 67 | + size: { | |
| 68 | + validator (value) { | |
| 69 | + return oneOf(value, ['small', 'large', 'default']); | |
| 70 | + } | |
| 71 | + }, | |
| 72 | + filterMethod: { | |
| 73 | + type: [Function, Boolean], | |
| 74 | + default: false | |
| 75 | + }, | |
| 76 | + transfer: { | |
| 77 | + type: Boolean, | |
| 78 | + default: false | |
| 79 | + } | |
| 80 | + }, | |
| 81 | + data () { | |
| 82 | + return { | |
| 83 | + currentValue: this.value | |
| 84 | + }; | |
| 85 | + }, | |
| 86 | + computed: { | |
| 87 | + closeIcon () { | |
| 88 | + return this.clearable && this.currentValue ? 'ios-close' : ''; | |
| 89 | + }, | |
| 90 | + filteredData () { | |
| 91 | + if (this.filterMethod) { | |
| 92 | + return this.data.filter(item => this.filterMethod(this.currentValue, item)); | |
| 93 | + } else { | |
| 94 | + return this.data; | |
| 95 | + } | |
| 96 | + } | |
| 97 | + }, | |
| 98 | + watch: { | |
| 99 | + value (val) { | |
| 100 | + this.currentValue = val; | |
| 101 | + }, | |
| 102 | + currentValue (val) { | |
| 103 | + this.$refs.select.query = val; | |
| 104 | + this.$emit('input', val); | |
| 105 | + this.$emit('on-change', val); | |
| 106 | + } | |
| 107 | + }, | |
| 108 | + methods: { | |
| 109 | + remoteMethod (query) { | |
| 110 | + this.$emit('on-search', query); | |
| 111 | + }, | |
| 112 | + handleChange (val) { | |
| 113 | + this.currentValue = val; | |
| 114 | + this.$refs.select.model = val; | |
| 115 | + this.$refs.input.blur(); | |
| 116 | + this.$emit('on-select', val); | |
| 117 | + }, | |
| 118 | + handleFocus () { | |
| 119 | + this.$refs.select.visible = true; | |
| 120 | + }, | |
| 121 | + handleBlur () { | |
| 122 | + this.$refs.select.visible = false; | |
| 123 | + }, | |
| 124 | + handleClear () { | |
| 125 | + this.currentValue = ''; | |
| 126 | + this.$refs.select.model = ''; | |
| 127 | + } | |
| 128 | + } | |
| 129 | + }; | |
| 130 | +</script> | |
| 0 | 131 | \ No newline at end of file | ... | ... |
src/components/input/input.vue
| ... | ... | @@ -212,12 +212,19 @@ |
| 212 | 212 | |
| 213 | 213 | this.textareaStyles = calcTextareaHeight(this.$refs.textarea, minRows, maxRows); |
| 214 | 214 | }, |
| 215 | - focus() { | |
| 215 | + focus () { | |
| 216 | 216 | if (this.type === 'textarea') { |
| 217 | 217 | this.$refs.textarea.focus(); |
| 218 | 218 | } else { |
| 219 | 219 | this.$refs.input.focus(); |
| 220 | 220 | } |
| 221 | + }, | |
| 222 | + blur () { | |
| 223 | + if (this.type === 'textarea') { | |
| 224 | + this.$refs.textarea.blur(); | |
| 225 | + } else { | |
| 226 | + this.$refs.input.blur(); | |
| 227 | + } | |
| 221 | 228 | } |
| 222 | 229 | }, |
| 223 | 230 | watch: { | ... | ... |
src/components/select/option.vue
| ... | ... | @@ -3,6 +3,7 @@ |
| 3 | 3 | </template> |
| 4 | 4 | <script> |
| 5 | 5 | import Emitter from '../../mixins/emitter'; |
| 6 | + import { findComponentUpward } from '../../utils/assist'; | |
| 6 | 7 | |
| 7 | 8 | const prefixCls = 'ivu-select-item'; |
| 8 | 9 | |
| ... | ... | @@ -29,7 +30,8 @@ |
| 29 | 30 | index: 0, // for up and down to focus |
| 30 | 31 | isFocus: false, |
| 31 | 32 | hidden: false, // for search |
| 32 | - searchLabel: '' // the value is slot,only for search | |
| 33 | + searchLabel: '', // the value is slot,only for search | |
| 34 | + autoComplete: false | |
| 33 | 35 | }; |
| 34 | 36 | }, |
| 35 | 37 | computed: { |
| ... | ... | @@ -38,7 +40,7 @@ |
| 38 | 40 | `${prefixCls}`, |
| 39 | 41 | { |
| 40 | 42 | [`${prefixCls}-disabled`]: this.disabled, |
| 41 | - [`${prefixCls}-selected`]: this.selected, | |
| 43 | + [`${prefixCls}-selected`]: this.selected && !this.autoComplete, | |
| 42 | 44 | [`${prefixCls}-focus`]: this.isFocus |
| 43 | 45 | } |
| 44 | 46 | ]; |
| ... | ... | @@ -72,6 +74,9 @@ |
| 72 | 74 | this.$on('on-query-change', (val) => { |
| 73 | 75 | this.queryChange(val); |
| 74 | 76 | }); |
| 77 | + | |
| 78 | + const Select = findComponentUpward(this, 'iSelect'); | |
| 79 | + if (Select) this.autoComplete = Select.autoComplete; | |
| 75 | 80 | }, |
| 76 | 81 | beforeDestroy () { |
| 77 | 82 | this.dispatch('iSelect', 'remove'); | ... | ... |
src/components/select/select.vue
| 1 | 1 | <template> |
| 2 | 2 | <div :class="classes" v-clickoutside="handleClose"> |
| 3 | 3 | <div |
| 4 | - :class="[prefixCls + '-selection']" | |
| 4 | + :class="selectionCls" | |
| 5 | 5 | ref="reference" |
| 6 | 6 | @click="toggleMenu"> |
| 7 | - <div class="ivu-tag" v-for="(item, index) in selectedMultiple"> | |
| 8 | - <span class="ivu-tag-text">{{ item.label }}</span> | |
| 9 | - <Icon type="ios-close-empty" @click.native.stop="removeTag(index)"></Icon> | |
| 10 | - </div> | |
| 11 | - <span :class="[prefixCls + '-placeholder']" v-show="showPlaceholder && !filterable">{{ localePlaceholder }}</span> | |
| 12 | - <span :class="[prefixCls + '-selected-value']" v-show="!showPlaceholder && !multiple && !filterable">{{ selectedSingle }}</span> | |
| 13 | - <input | |
| 14 | - type="text" | |
| 15 | - v-if="filterable" | |
| 16 | - v-model="query" | |
| 17 | - :disabled="disabled" | |
| 18 | - :class="[prefixCls + '-input']" | |
| 19 | - :placeholder="showPlaceholder ? localePlaceholder : ''" | |
| 20 | - :style="inputStyle" | |
| 21 | - @blur="handleBlur" | |
| 22 | - @keydown="resetInputState" | |
| 23 | - @keydown.delete="handleInputDelete" | |
| 24 | - ref="input"> | |
| 25 | - <Icon type="ios-close" :class="[prefixCls + '-arrow']" v-show="showCloseIcon" @click.native.stop="clearSingleSelect"></Icon> | |
| 26 | - <Icon type="arrow-down-b" :class="[prefixCls + '-arrow']" v-if="!remote"></Icon> | |
| 7 | + <slot name="input"> | |
| 8 | + <div class="ivu-tag" v-for="(item, index) in selectedMultiple"> | |
| 9 | + <span class="ivu-tag-text">{{ item.label }}</span> | |
| 10 | + <Icon type="ios-close-empty" @click.native.stop="removeTag(index)"></Icon> | |
| 11 | + </div> | |
| 12 | + <span :class="[prefixCls + '-placeholder']" v-show="showPlaceholder && !filterable">{{ localePlaceholder }}</span> | |
| 13 | + <span :class="[prefixCls + '-selected-value']" v-show="!showPlaceholder && !multiple && !filterable">{{ selectedSingle }}</span> | |
| 14 | + <input | |
| 15 | + type="text" | |
| 16 | + v-if="filterable" | |
| 17 | + v-model="query" | |
| 18 | + :disabled="disabled" | |
| 19 | + :class="[prefixCls + '-input']" | |
| 20 | + :placeholder="showPlaceholder ? localePlaceholder : ''" | |
| 21 | + :style="inputStyle" | |
| 22 | + @blur="handleBlur" | |
| 23 | + @keydown="resetInputState" | |
| 24 | + @keydown.delete="handleInputDelete" | |
| 25 | + ref="input"> | |
| 26 | + <Icon type="ios-close" :class="[prefixCls + '-arrow']" v-show="showCloseIcon" @click.native.stop="clearSingleSelect"></Icon> | |
| 27 | + <Icon type="arrow-down-b" :class="[prefixCls + '-arrow']" v-if="!remote"></Icon> | |
| 28 | + </slot> | |
| 27 | 29 | </div> |
| 28 | 30 | <transition :name="transitionName"> |
| 29 | 31 | <Drop |
| ... | ... | @@ -33,7 +35,7 @@ |
| 33 | 35 | ref="dropdown" |
| 34 | 36 | :data-transfer="transfer" |
| 35 | 37 | v-transfer-dom> |
| 36 | - <ul v-show="notFountShow" :class="[prefixCls + '-not-found']"><li>{{ localeNotFoundText }}</li></ul> | |
| 38 | + <ul v-show="notFoundShow" :class="[prefixCls + '-not-found']"><li>{{ localeNotFoundText }}</li></ul> | |
| 37 | 39 | <ul v-show="(!notFound && !remote) || (remote && !loading && !notFound)" :class="[prefixCls + '-dropdown-list']"><slot></slot></ul> |
| 38 | 40 | <ul v-show="loading" :class="[prefixCls + '-loading']">{{ localeLoadingText }}</ul> |
| 39 | 41 | </Drop> |
| ... | ... | @@ -123,6 +125,11 @@ |
| 123 | 125 | transfer: { |
| 124 | 126 | type: Boolean, |
| 125 | 127 | default: false |
| 128 | + }, | |
| 129 | + // Use for AutoComplete | |
| 130 | + autoComplete: { | |
| 131 | + type: Boolean, | |
| 132 | + default: false | |
| 126 | 133 | } |
| 127 | 134 | }, |
| 128 | 135 | data () { |
| ... | ... | @@ -161,7 +168,13 @@ |
| 161 | 168 | dropdownCls () { |
| 162 | 169 | return { |
| 163 | 170 | [prefixCls + '-dropdown-transfer']: this.transfer, |
| 164 | - [prefixCls + '-multiple']: this.multiple && this.transfer | |
| 171 | + [prefixCls + '-multiple']: this.multiple && this.transfer, | |
| 172 | + ['ivu-auto-complete']: this.autoComplete, | |
| 173 | + }; | |
| 174 | + }, | |
| 175 | + selectionCls () { | |
| 176 | + return { | |
| 177 | + [`${prefixCls}-selection`]: !this.autoComplete | |
| 165 | 178 | }; |
| 166 | 179 | }, |
| 167 | 180 | showPlaceholder () { |
| ... | ... | @@ -225,16 +238,19 @@ |
| 225 | 238 | let status = true; |
| 226 | 239 | const options = this.$slots.default || []; |
| 227 | 240 | if (!this.loading && this.remote && this.query === '' && !options.length) status = false; |
| 241 | + | |
| 242 | + if (this.autoComplete && !options.length) status = false; | |
| 243 | + | |
| 228 | 244 | return this.visible && status; |
| 229 | 245 | }, |
| 230 | - notFountShow () { | |
| 246 | + notFoundShow () { | |
| 231 | 247 | const options = this.$slots.default || []; |
| 232 | 248 | return (this.notFound && !this.remote) || (this.remote && !this.loading && !options.length); |
| 233 | 249 | } |
| 234 | 250 | }, |
| 235 | 251 | methods: { |
| 236 | 252 | toggleMenu () { |
| 237 | - if (this.disabled) { | |
| 253 | + if (this.disabled || this.autoComplete) { | |
| 238 | 254 | return false; |
| 239 | 255 | } |
| 240 | 256 | this.visible = !this.visible; |
| ... | ... | @@ -543,6 +559,7 @@ |
| 543 | 559 | }, |
| 544 | 560 | handleBlur () { |
| 545 | 561 | setTimeout(() => { |
| 562 | + if (this.autoComplete) return; | |
| 546 | 563 | const model = this.model; |
| 547 | 564 | |
| 548 | 565 | if (this.multiple) { |
| ... | ... | @@ -737,7 +754,7 @@ |
| 737 | 754 | if (this.multiple) { |
| 738 | 755 | this.$refs.input.focus(); |
| 739 | 756 | } else { |
| 740 | - this.$refs.input.select(); | |
| 757 | + if (!this.autoComplete) this.$refs.input.select(); | |
| 741 | 758 | } |
| 742 | 759 | if (this.remote) { |
| 743 | 760 | this.findChild(child => { |
| ... | ... | @@ -753,7 +770,7 @@ |
| 753 | 770 | this.broadcast('Drop', 'on-update-popper'); |
| 754 | 771 | } else { |
| 755 | 772 | if (this.filterable) { |
| 756 | - this.$refs.input.blur(); | |
| 773 | + if (!this.autoComplete) this.$refs.input.blur(); | |
| 757 | 774 | // #566 reset options visible |
| 758 | 775 | setTimeout(() => { |
| 759 | 776 | this.broadcastQuery(''); | ... | ... |
src/index.js
| ... | ... | @@ -3,6 +3,7 @@ import 'core-js/fn/array/find-index'; |
| 3 | 3 | |
| 4 | 4 | import Affix from './components/affix'; |
| 5 | 5 | import Alert from './components/alert'; |
| 6 | +import AutoComplete from './components/auto-complete'; | |
| 6 | 7 | import Avatar from './components/avatar'; |
| 7 | 8 | import BackTop from './components/back-top'; |
| 8 | 9 | import Badge from './components/badge'; |
| ... | ... | @@ -51,6 +52,7 @@ import locale from './locale'; |
| 51 | 52 | const iview = { |
| 52 | 53 | Affix, |
| 53 | 54 | Alert, |
| 55 | + AutoComplete, | |
| 54 | 56 | Avatar, |
| 55 | 57 | BackTop, |
| 56 | 58 | Badge, | ... | ... |
| 1 | +@auto-complete-prefix-cls: ~"@{css-prefix}auto-complete"; | |
| 2 | + | |
| 3 | +.@{auto-complete-prefix-cls} { | |
| 4 | + .@{select-prefix-cls} { | |
| 5 | + &-not-found{ | |
| 6 | + display: none; | |
| 7 | + } | |
| 8 | + } | |
| 9 | + .@{icon-prefix-cls}-ios-close{ | |
| 10 | + display: none; | |
| 11 | + } | |
| 12 | + &:hover .@{icon-prefix-cls}-ios-close{ | |
| 13 | + display: inline-block; | |
| 14 | + } | |
| 15 | +} | |
| 0 | 16 | \ No newline at end of file | ... | ... |
src/styles/components/index.less