diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue
index f1625de0..516b062d 100644
--- a/client/components/ui/MultiSelect.vue
+++ b/client/components/ui/MultiSelect.vue
@@ -17,7 +17,7 @@
- -
+
-
{{ item }}
@@ -54,7 +54,7 @@ export default {
menuDisabled: {
type: Boolean,
default: false
- },
+ }
},
data() {
return {
@@ -62,7 +62,9 @@ export default {
currentSearch: null,
typingTimeout: null,
isFocused: false,
- menu: null
+ menu: null,
+ filteredItems: null,
+ selectedMenuItemIndex: null
}
},
watch: {
@@ -91,24 +93,63 @@ export default {
return classes.join(' ')
},
itemsToShow() {
- if (!this.currentSearch || !this.textInput) {
+ if (!this.currentSearch || !this.textInput || !this.filteredItems) {
return this.items
}
- return this.items.filter((i) => {
- var iValue = String(i).toLowerCase()
- return iValue.includes(this.currentSearch.toLowerCase())
- })
+ return this.filteredItems
}
},
methods: {
editItem(item) {
this.$emit('edit', item)
},
- keydownInput() {
+ search() {
+ if (!this.textInput) {
+ this.filteredItems = null
+ return
+ }
+ this.currentSearch = this.textInput
+
+ const results = this.items.filter((i) => {
+ var iValue = String(i).toLowerCase()
+ return iValue.includes(this.currentSearch.toLowerCase())
+ })
+
+ this.filteredItems = results || []
+ },
+ keydownInput(event) {
+ let items = this.itemsToShow
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault()
+ if (!items.length) return
+ if (event.key === 'ArrowDown') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = 0
+ } else {
+ this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
+ }
+ } else if (event.key === 'ArrowUp') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = items.length - 1
+ } else {
+ this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
+ }
+ }
+ this.recalcScroll()
+ return
+ } else if (event.key === 'Enter') {
+ if (this.selectedMenuItemIndex !== null) {
+ this.clickedOption(event, items[this.selectedMenuItemIndex])
+ } else {
+ this.submitForm()
+ }
+ return
+ }
+ this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
- this.currentSearch = this.textInput
+ this.search()
}, 100)
this.setInputWidth()
},
@@ -120,6 +161,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
+ recalcScroll() {
+ if (!this.menu) return
+ var menuItems = this.menu.querySelectorAll('li')
+ if (!menuItems.length) return
+ var selectedItem = menuItems[this.selectedMenuItemIndex]
+ if (!selectedItem) return
+ var menuHeight = this.menu.offsetHeight
+ var itemHeight = selectedItem.offsetHeight
+ var itemTop = selectedItem.offsetTop
+ var itemBottom = itemTop + itemHeight
+ if (itemBottom > this.menu.scrollTop + menuHeight) {
+ let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
+ this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
+ } else if (itemTop < this.menu.scrollTop) {
+ let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
+ this.menu.scrollTop = itemTop - menuPaddingTop
+ }
+ },
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -208,7 +267,7 @@ export default {
e.stopPropagation()
e.preventDefault()
}
- if (this.$refs.input) {
+ if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
@@ -222,6 +281,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
this.recalcMenuPos()
@@ -248,6 +308,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue
index 65cc8a63..6e9c0f10 100644
--- a/client/components/ui/MultiSelectQueryInput.vue
+++ b/client/components/ui/MultiSelectQueryInput.vue
@@ -20,7 +20,7 @@
- -
+
-
{{ item.name }}
@@ -63,7 +63,8 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
- items: []
+ items: [],
+ selectedMenuItemIndex: null
}
},
watch: {
@@ -122,11 +123,39 @@ export default {
this.items = results || []
},
- keydownInput() {
+ keydownInput(event) {
+ let items = this.itemsToShow
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault()
+ if (!items.length) return
+ if (event.key === 'ArrowDown') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = 0
+ } else {
+ this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
+ }
+ } else if (event.key === 'ArrowUp') {
+ if (this.selectedMenuItemIndex === null) {
+ this.selectedMenuItemIndex = items.length - 1
+ } else {
+ this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
+ }
+ }
+ this.recalcScroll()
+ return
+ } else if (event.key === 'Enter') {
+ if (this.selectedMenuItemIndex !== null) {
+ this.clickedOption(event, items[this.selectedMenuItemIndex])
+ } else {
+ this.submitForm()
+ }
+ return
+ }
+ this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
- }, 250)
+ }, 250)
this.setInputWidth()
},
setInputWidth() {
@@ -137,6 +166,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
+ recalcScroll() {
+ if (!this.menu) return
+ var menuItems = this.menu.querySelectorAll('li')
+ if (!menuItems.length) return
+ var selectedItem = menuItems[this.selectedMenuItemIndex]
+ if (!selectedItem) return
+ var menuHeight = this.menu.offsetHeight
+ var itemHeight = selectedItem.offsetHeight
+ var itemTop = selectedItem.offsetTop
+ var itemBottom = itemTop + itemHeight
+ if (itemBottom > this.menu.scrollTop + menuHeight) {
+ let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
+ this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
+ } else if (itemTop < this.menu.scrollTop) {
+ let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
+ this.menu.scrollTop = itemTop - menuPaddingTop
+ }
+ },
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -247,6 +294,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
@@ -274,6 +322,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
+ this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})