mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add keyboard navigation to multi-select components
This commit is contained in:
		
							parent
							
								
									b4eed3bad2
								
							
						
					
					
						commit
						a5772f6b66
					
				| @ -17,7 +17,7 @@ | |||||||
| 
 | 
 | ||||||
|       <ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> |       <ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> | ||||||
|         <template v-for="item in itemsToShow"> |         <template v-for="item in itemsToShow"> | ||||||
|           <li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> |           <li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-sky-400' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> | ||||||
|             <div class="flex items-center"> |             <div class="flex items-center"> | ||||||
|               <span class="font-normal ml-3 block truncate">{{ item }}</span> |               <span class="font-normal ml-3 block truncate">{{ item }}</span> | ||||||
|             </div> |             </div> | ||||||
| @ -62,7 +62,9 @@ export default { | |||||||
|       currentSearch: null, |       currentSearch: null, | ||||||
|       typingTimeout: null, |       typingTimeout: null, | ||||||
|       isFocused: false, |       isFocused: false, | ||||||
|       menu: null |       menu: null, | ||||||
|  |       filteredItems: null, | ||||||
|  |       selectedMenuItemIndex: null | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
| @ -91,24 +93,66 @@ export default { | |||||||
|       return classes.join(' ') |       return classes.join(' ') | ||||||
|     }, |     }, | ||||||
|     itemsToShow() { |     itemsToShow() { | ||||||
|       if (!this.currentSearch || !this.textInput) { |       if (!this.currentSearch || !this.textInput || !this.filteredItems) { | ||||||
|         return this.items |         return this.items | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       return this.items.filter((i) => { |       return this.filteredItems | ||||||
|         var iValue = String(i).toLowerCase() |  | ||||||
|         return iValue.includes(this.currentSearch.toLowerCase()) |  | ||||||
|       }) |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     editItem(item) { |     editItem(item) { | ||||||
|       this.$emit('edit', 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) | ||||||
|  |           } | ||||||
|  |           console.log('ArrowDown. this.selectedMenuItemIndex=', this.selectedMenuItemIndex) | ||||||
|  |         } else if (event.key === 'ArrowUp') { | ||||||
|  |           if (this.selectedMenuItemIndex === null) { | ||||||
|  |             this.selectedMenuItemIndex = items.length - 1 | ||||||
|  |           } else { | ||||||
|  |             this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0) | ||||||
|  |           } | ||||||
|  |           console.log('ArrowUp. this.selectedMenuItemIndex=', this.selectedMenuItemIndex) | ||||||
|  |         } | ||||||
|  |         this.recalcScroll() | ||||||
|  |         return | ||||||
|  |       }  else if (event.key === 'Enter') { | ||||||
|  |         if (this.selectedMenuItemIndex !== null) { | ||||||
|  |           this.clickedOption(event, items[this.selectedMenuItemIndex]) | ||||||
|  |         } else { | ||||||
|  |           console.log('Enter. this.textInput=', this.textInput) | ||||||
|  |           this.submitForm() | ||||||
|  |         } | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       this.selectedMenuItemIndex = null | ||||||
|       clearTimeout(this.typingTimeout) |       clearTimeout(this.typingTimeout) | ||||||
|       this.typingTimeout = setTimeout(() => { |       this.typingTimeout = setTimeout(() => { | ||||||
|         this.currentSearch = this.textInput |         this.search() | ||||||
|       }, 100) |       }, 100) | ||||||
|       this.setInputWidth() |       this.setInputWidth() | ||||||
|     }, |     }, | ||||||
| @ -120,6 +164,24 @@ export default { | |||||||
|         this.recalcMenuPos() |         this.recalcMenuPos() | ||||||
|       }, 50) |       }, 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() { |     recalcMenuPos() { | ||||||
|       if (!this.menu || !this.$refs.inputWrapper) return |       if (!this.menu || !this.$refs.inputWrapper) return | ||||||
|       var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() |       var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() | ||||||
| @ -219,6 +281,7 @@ export default { | |||||||
|       } |       } | ||||||
|       this.textInput = null |       this.textInput = null | ||||||
|       this.currentSearch = null |       this.currentSearch = null | ||||||
|  |       this.selectedMenuItemIndex = null | ||||||
|       this.$emit('input', newSelected) |       this.$emit('input', newSelected) | ||||||
|       this.$nextTick(() => { |       this.$nextTick(() => { | ||||||
|         this.recalcMenuPos() |         this.recalcMenuPos() | ||||||
| @ -245,6 +308,7 @@ export default { | |||||||
|       this.$emit('newItem', item) |       this.$emit('newItem', item) | ||||||
|       this.textInput = null |       this.textInput = null | ||||||
|       this.currentSearch = null |       this.currentSearch = null | ||||||
|  |       this.selectedMenuItemIndex = null | ||||||
|       this.$nextTick(() => { |       this.$nextTick(() => { | ||||||
|         this.blur() |         this.blur() | ||||||
|       }) |       }) | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ | |||||||
| 
 | 
 | ||||||
|       <ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> |       <ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label"> | ||||||
|         <template v-for="item in itemsToShow"> |         <template v-for="item in itemsToShow"> | ||||||
|           <li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> |           <li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-sky-400' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent> | ||||||
|             <div class="flex items-center"> |             <div class="flex items-center"> | ||||||
|               <span class="font-normal ml-3 block truncate">{{ item.name }}</span> |               <span class="font-normal ml-3 block truncate">{{ item.name }}</span> | ||||||
|             </div> |             </div> | ||||||
| @ -63,7 +63,8 @@ export default { | |||||||
|       typingTimeout: null, |       typingTimeout: null, | ||||||
|       isFocused: false, |       isFocused: false, | ||||||
|       menu: null, |       menu: null, | ||||||
|       items: [] |       items: [], | ||||||
|  |       selectedMenuItemIndex: null | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
| @ -122,7 +123,35 @@ export default { | |||||||
| 
 | 
 | ||||||
|       this.items = results || [] |       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) |       clearTimeout(this.typingTimeout) | ||||||
|       this.typingTimeout = setTimeout(() => { |       this.typingTimeout = setTimeout(() => { | ||||||
|         this.search() |         this.search() | ||||||
| @ -137,6 +166,24 @@ export default { | |||||||
|         this.recalcMenuPos() |         this.recalcMenuPos() | ||||||
|       }, 50) |       }, 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() { |     recalcMenuPos() { | ||||||
|       if (!this.menu || !this.$refs.inputWrapper) return |       if (!this.menu || !this.$refs.inputWrapper) return | ||||||
|       var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() |       var boundingBox = this.$refs.inputWrapper.getBoundingClientRect() | ||||||
| @ -244,6 +291,7 @@ export default { | |||||||
|       } |       } | ||||||
|       this.textInput = null |       this.textInput = null | ||||||
|       this.currentSearch = null |       this.currentSearch = null | ||||||
|  |       this.selectedMenuItemIndex = null | ||||||
| 
 | 
 | ||||||
|       this.$emit('input', newSelected) |       this.$emit('input', newSelected) | ||||||
|       this.$nextTick(() => { |       this.$nextTick(() => { | ||||||
| @ -271,6 +319,7 @@ export default { | |||||||
|       this.$emit('newItem', item) |       this.$emit('newItem', item) | ||||||
|       this.textInput = null |       this.textInput = null | ||||||
|       this.currentSearch = null |       this.currentSearch = null | ||||||
|  |       this.selectedMenuItemIndex = null | ||||||
|       this.$nextTick(() => { |       this.$nextTick(() => { | ||||||
|         this.blur() |         this.blur() | ||||||
|       }) |       }) | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user