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