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"> | ||||
|         <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"> | ||||
|               <span class="font-normal ml-3 block truncate">{{ item }}</span> | ||||
|             </div> | ||||
| @ -62,7 +62,9 @@ export default { | ||||
|       currentSearch: null, | ||||
|       typingTimeout: null, | ||||
|       isFocused: false, | ||||
|       menu: null | ||||
|       menu: null, | ||||
|       filteredItems: null, | ||||
|       selectedMenuItemIndex: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
| @ -91,24 +93,66 @@ 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) | ||||
|           } | ||||
|           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) | ||||
|       this.typingTimeout = setTimeout(() => { | ||||
|         this.currentSearch = this.textInput | ||||
|         this.search() | ||||
|       }, 100) | ||||
|       this.setInputWidth() | ||||
|     }, | ||||
| @ -120,6 +164,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() | ||||
| @ -219,6 +281,7 @@ export default { | ||||
|       } | ||||
|       this.textInput = null | ||||
|       this.currentSearch = null | ||||
|       this.selectedMenuItemIndex = null | ||||
|       this.$emit('input', newSelected) | ||||
|       this.$nextTick(() => { | ||||
|         this.recalcMenuPos() | ||||
| @ -245,6 +308,7 @@ export default { | ||||
|       this.$emit('newItem', item) | ||||
|       this.textInput = null | ||||
|       this.currentSearch = null | ||||
|       this.selectedMenuItemIndex = null | ||||
|       this.$nextTick(() => { | ||||
|         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"> | ||||
|         <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"> | ||||
|               <span class="font-normal ml-3 block truncate">{{ item.name }}</span> | ||||
|             </div> | ||||
| @ -63,7 +63,8 @@ export default { | ||||
|       typingTimeout: null, | ||||
|       isFocused: false, | ||||
|       menu: null, | ||||
|       items: [] | ||||
|       items: [], | ||||
|       selectedMenuItemIndex: null | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
| @ -122,7 +123,35 @@ 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() | ||||
| @ -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() | ||||
| @ -244,6 +291,7 @@ export default { | ||||
|       } | ||||
|       this.textInput = null | ||||
|       this.currentSearch = null | ||||
|       this.selectedMenuItemIndex = null | ||||
| 
 | ||||
|       this.$emit('input', newSelected) | ||||
|       this.$nextTick(() => { | ||||
| @ -271,6 +319,7 @@ export default { | ||||
|       this.$emit('newItem', item) | ||||
|       this.textInput = null | ||||
|       this.currentSearch = null | ||||
|       this.selectedMenuItemIndex = null | ||||
|       this.$nextTick(() => { | ||||
|         this.blur() | ||||
|       }) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user