Skip to content

Commit d018550

Browse files
committed
feat: Autocomplete component (not done yet)
1 parent cdb0b67 commit d018550

7 files changed

Lines changed: 236 additions & 3 deletions

File tree

build/script.dev.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ app.use(hotMiddleware)
5050

5151
// serve pure static assets
5252
var staticsPath = path.posix.join(webpackConfig.output.publicPath, 'statics/')
53-
app.use(staticsPath, express.static('./src/statics'))
53+
app.use(staticsPath, express.static('./dev/statics'))
5454

5555
// try to serve Cordova statics for Play App
5656
app.use(express.static(env.platform.cordovaAssets))
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<template>
2+
<div>
3+
<div class="layout-padding" style="max-width: 600px;">
4+
<q-autocomplete v-model="terms" @search="search" :minCharacters="3" set-width>
5+
<q-search v-model="terms" />
6+
</q-autocomplete>
7+
8+
<br><br>
9+
10+
<q-autocomplete v-model="terms" @search="search" :minCharacters="3">
11+
<input v-model="terms" class="full-width"/>
12+
</q-autocomplete>
13+
14+
<br><br>
15+
16+
<q-autocomplete v-model="terms" @search="search" :minCharacters="3" />
17+
</div>
18+
</div>
19+
</template>
20+
21+
<script>
22+
export default {
23+
data () {
24+
return {
25+
terms: ''
26+
}
27+
},
28+
methods: {
29+
search (terms, done) {
30+
console.log('triggered', terms)
31+
setTimeout(() => {
32+
done([
33+
{label: 'alarm', secondLabel: 'Mail alarm', icon: 'alarm', stamp: '10 min', value: 'Alarm'},
34+
{label: 'email 1 Email1', secondLabel: 'Some email, huh?', icon: 'email', secondImg: '/statics/mountains.jpg', value: 'Email1'},
35+
{label: 'email 2', img: '/statics/mountains.jpg', value: 'Email2'},
36+
{label: 'email 3', icon: 'email', secondIcon: 'email', value: 'Email3'},
37+
{label: 'email 4', icon: 'email', value: 'Email4'},
38+
{label: 'email 5', value: 'Email5'},
39+
{label: 'email 6', value: 'Email6'},
40+
{label: 'email 7', icon: 'email', value: 'Email7'},
41+
{label: 'email 8', icon: 'email', value: 'Email8'},
42+
{label: 'email 9', icon: 'email', value: 'Email9'},
43+
{label: 'email 10', icon: 'email', value: 'Email10'},
44+
{label: 'email 11', icon: 'email', value: 'Email11'}
45+
])
46+
}, 2000)
47+
}
48+
}
49+
}
50+
</script>
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<template>
2+
<span>
3+
<slot>
4+
<input
5+
type="text"
6+
v-model="model"
7+
/>
8+
</slot>
9+
<q-popover ref="popover" :anchorClick="false">
10+
<div v-if="searching" class="row justify-center" :style="{width: width, padding: '3px 10px'}">
11+
<spinner name="dots" :size="40"></spinner>
12+
</div>
13+
<div v-else class="list no-border" :style="computedWidth">
14+
<div
15+
class="item item-link"
16+
v-for="(result, index) in computedResults"
17+
@click="setValue(result.value)"
18+
:class="{active: selectedIndex === index, 'two-lines': result.secondLabel}"
19+
>
20+
<i v-if="result.icon" class="item-primary">{{result.icon}}</i>
21+
<img v-if="result.img" class="item-primary thumbnail" :src="result.img" />
22+
23+
<div class="item-content" :class="{'has-secondary': result.secondIcon || result.secondImg || result.stamp}">
24+
<div v-html="result.label"></div>
25+
<div v-if="result.secondLabel" v-html="result.secondLabel"></div>
26+
</div>
27+
28+
<div v-if="result.stamp" class="item-secondary stamp" v-html="result.stamp"></div>
29+
<i v-if="result.secondIcon" class="item-secondary">{{result.secondIcon}}</i>
30+
<img v-if="result.secondImg" class="item-secondary thumbnail" :src="result.secondImg" />
31+
</div>
32+
</div>
33+
</q-popover>
34+
</span>
35+
</template>
36+
37+
<script>
38+
import Utils from '../../utils'
39+
40+
export default {
41+
props: {
42+
value: {
43+
type: String,
44+
required: true
45+
},
46+
minCharacters: {
47+
type: Number,
48+
default: 1
49+
},
50+
resultsLimit: {
51+
type: Number,
52+
default: 6
53+
},
54+
delay: {
55+
type: Number,
56+
default: 500
57+
},
58+
setWidth: Boolean
59+
},
60+
data () {
61+
return {
62+
searchId: '',
63+
results: [],
64+
selectedIndex: -1,
65+
width: 0
66+
}
67+
},
68+
watch: {
69+
delay (val) {
70+
this.__updateDelay(val)
71+
}
72+
},
73+
computed: {
74+
model: {
75+
get () {
76+
return this.value
77+
},
78+
set (val) {
79+
this.$emit('input', val)
80+
}
81+
},
82+
computedResults () {
83+
if (this.resultsLimit && this.results.length > 0) {
84+
return this.results.slice(0, this.resultsLimit)
85+
}
86+
},
87+
computedWidth () {
88+
if (this.setWidth) {
89+
return {width: this.width}
90+
}
91+
},
92+
searching () {
93+
return this.searchId.length > 0
94+
}
95+
},
96+
methods: {
97+
trigger () {
98+
this.width = Utils.dom.width(this.inputEl) + 'px'
99+
this.$nextTick(() => {
100+
const searchId = Utils.uid()
101+
this.searchId = searchId
102+
if (this.model.length < this.minCharacters) {
103+
this.searchId = ''
104+
this.close()
105+
return
106+
}
107+
108+
this.$refs.popover.open()
109+
this.$emit('search', this.model, results => {
110+
if (results && this.searchId === searchId) {
111+
this.searchId = ''
112+
this.results = results
113+
}
114+
})
115+
})
116+
},
117+
close () {
118+
this.$refs.popover.close()
119+
this.results = []
120+
this.selectedIndex = -1
121+
},
122+
setValue (val) {
123+
this.model = val
124+
this.close()
125+
},
126+
move (offset) {
127+
this.selectedIndex = Math.max(-1, Math.min(this.computedResults.length - 1, this.selectedIndex + offset))
128+
},
129+
setCurrentSelection () {
130+
if (this.selectedIndex >= 0) {
131+
this.setValue(this.results[this.selectedIndex].value)
132+
}
133+
},
134+
__updateDelay () {
135+
this.inputEl.removeEventListener('input', this.delayedTrigger)
136+
this.delayedTrigger = this.delay ? Utils.debounce(this.trigger, this.delay) : this.trigger
137+
this.inputEl.addEventListener('input', this.delayedTrigger)
138+
},
139+
handleKeydown (e) {
140+
switch (e.keyCode || e.which) {
141+
case 38: // up
142+
this.move(-1)
143+
break
144+
case 40: // down
145+
if (!this.$refs.popover.opened) {
146+
this.trigger()
147+
}
148+
else {
149+
this.move(1)
150+
}
151+
break
152+
case 13: // enter
153+
this.setCurrentSelection()
154+
break
155+
}
156+
}
157+
},
158+
mounted () {
159+
this.$nextTick(() => {
160+
this.inputEl = this.$el.querySelector('input')
161+
if (!this.inputEl) {
162+
console.error('Autocomplete needs to contain one input field in its slot.')
163+
return
164+
}
165+
this.__updateDelay()
166+
this.inputEl.addEventListener('keydown', this.handleKeydown)
167+
})
168+
},
169+
beforeDestroy () {
170+
this.close()
171+
this.inputEl.removeEventListener('input', this.delayedTrigger)
172+
this.inputEl.removeEventListener('keydown', this.handleKeydown)
173+
}
174+
}
175+
</script>

src/vue-components/popover/Popover.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@
1212
import Utils from '../../utils'
1313
import EscapeKey from '../../features/escape-key'
1414
15+
function getEl (ref) {
16+
return ref.$el || ref
17+
}
18+
1519
export default {
1620
props: {
1721
anchor: {
1822
type: String,
1923
default: 'bottom left',
2024
validator: Utils.popup.positionValidator
2125
},
26+
anchorRef: Object,
2227
self: {
2328
type: String,
2429
default: 'top left',
@@ -57,6 +62,9 @@ export default {
5762
this.$nextTick(() => {
5863
this.anchorEl = this.$el.parentNode
5964
this.anchorEl.removeChild(this.$el)
65+
if (this.anchorRef) {
66+
this.anchorEl = getEl(this.anchorRef)
67+
}
6068
if (this.anchorClick) {
6169
this.anchorEl.classList.add('cursor-pointer')
6270
this.anchorEl.addEventListener('click', this.toggle)

src/vue-components/search/search.ios.styl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
.q-search
4141
display flex
4242
align-items center
43-
padding 3px 8px
4443
width 100%
4544
button
4645
text-shadow none

src/vue-components/search/search.mat.styl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
.q-search
3636
display flex
3737
align-items center
38-
padding 3px
3938
background inherit
4039
width 100%
4140
color $grey-5

src/vue-install.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import dTouchPan from './vue-directives/touch-pan'
1414
import dTouchSwipe from './vue-directives/touch-swipe'
1515

1616
import AjaxBar from './vue-components/ajax-bar/AjaxBar.vue'
17+
import Autocomplete from './vue-components/autocomplete/Autocomplete.vue'
1718
import Checkbox from './vue-components/checkbox/Checkbox.vue'
1819
import Chips from './vue-components/chips/Chips.vue'
1920
import Collapsible from './vue-components/collapsible/Collapsible.vue'
@@ -81,6 +82,7 @@ function registerComponents (_Vue) {
8182

8283
;[
8384
['ajax-bar', AjaxBar],
85+
['autocomplete', Autocomplete],
8486
['checkbox', Checkbox],
8587
['chips', Chips],
8688
['collapsible', Collapsible],

0 commit comments

Comments
 (0)