<script> import { MENU_BUFFER } from '../constants' import { watchSize, setupResizeAndScrollEventListeners } from '../utils' import Option from './Option' import Tip from './Tip' const directionMap = { top: 'top', bottom: 'bottom', above: 'top', below: 'bottom', } export default { name: 'vue-treeselect--menu', inject: [ 'instance' ], computed: { menuStyle() { const { instance } = this return { maxHeight: instance.maxHeight + 'px', } }, menuContainerStyle() { const { instance } = this return { zIndex: instance.appendToBody ? null : instance.zIndex, } }, }, watch: { 'instance.menu.isOpen'(newValue) { if (newValue) { // In case `openMenu()` is just called and the menu is not rendered yet. this.$nextTick(this.onMenuOpen) } else { this.onMenuClose() } }, }, created() { this.menuSizeWatcher = null this.menuResizeAndScrollEventListeners = null }, mounted() { const { instance } = this if (instance.menu.isOpen) this.$nextTick(this.onMenuOpen) }, destroyed() { this.onMenuClose() }, methods: { renderMenu() { const { instance } = this if (!instance.menu.isOpen) return null return ( <div ref="menu" class="vue-treeselect__menu" onMousedown={instance.handleMouseDown} style={this.menuStyle}> {this.renderBeforeList()} {instance.async ? this.renderAsyncSearchMenuInner() : instance.localSearch.active ? this.renderLocalSearchMenuInner() : this.renderNormalMenuInner()} {this.renderAfterList()} </div> ) }, renderBeforeList() { const { instance } = this const beforeListRenderer = instance.$scopedSlots['before-list'] return beforeListRenderer ? beforeListRenderer() : null }, renderAfterList() { const { instance } = this const afterListRenderer = instance.$scopedSlots['after-list'] return afterListRenderer ? afterListRenderer() : null }, renderNormalMenuInner() { const { instance } = this if (instance.rootOptionsStates.isLoading) { return this.renderLoadingOptionsTip() } else if (instance.rootOptionsStates.loadingError) { return this.renderLoadingRootOptionsErrorTip() } else if (instance.rootOptionsStates.isLoaded && instance.forest.normalizedOptions.length === 0) { return this.renderNoAvailableOptionsTip() } else { return this.renderOptionList() } }, renderLocalSearchMenuInner() { const { instance } = this if (instance.rootOptionsStates.isLoading) { return this.renderLoadingOptionsTip() } else if (instance.rootOptionsStates.loadingError) { return this.renderLoadingRootOptionsErrorTip() } else if (instance.rootOptionsStates.isLoaded && instance.forest.normalizedOptions.length === 0) { return this.renderNoAvailableOptionsTip() } else if (instance.localSearch.noResults) { return this.renderNoResultsTip() } else { return this.renderOptionList() } }, renderAsyncSearchMenuInner() { const { instance } = this const entry = instance.getRemoteSearchEntry() const shouldShowSearchPromptTip = instance.trigger.searchQuery === '' && !instance.defaultOptions const shouldShowNoResultsTip = shouldShowSearchPromptTip ? false : entry.isLoaded && entry.options.length === 0 if (shouldShowSearchPromptTip) { return this.renderSearchPromptTip() } else if (entry.isLoading) { return this.renderLoadingOptionsTip() } else if (entry.loadingError) { return this.renderAsyncSearchLoadingErrorTip() } else if (shouldShowNoResultsTip) { return this.renderNoResultsTip() } else { return this.renderOptionList() } }, renderOptionList() { const { instance } = this return ( <div class="vue-treeselect__list"> {instance.forest.normalizedOptions.map(rootNode => ( <Option node={rootNode} key={rootNode.id} /> ))} </div> ) }, renderSearchPromptTip() { const { instance } = this return ( <Tip type="search-prompt" icon="warning">{ instance.searchPromptText }</Tip> ) }, renderLoadingOptionsTip() { const { instance } = this return ( <Tip type="loading" icon="loader">{ instance.loadingText }</Tip> ) }, renderLoadingRootOptionsErrorTip() { const { instance } = this return ( <Tip type="error" icon="error"> { instance.rootOptionsStates.loadingError } <a class="vue-treeselect__retry" onClick={instance.loadRootOptions} title={instance.retryTitle}> { instance.retryText } </a> </Tip> ) }, renderAsyncSearchLoadingErrorTip() { const { instance } = this const entry = instance.getRemoteSearchEntry() // TODO: retryTitle? return ( <Tip type="error" icon="error"> { entry.loadingError } <a class="vue-treeselect__retry" onClick={instance.handleRemoteSearch} title={instance.retryTitle}> { instance.retryText } </a> </Tip> ) }, renderNoAvailableOptionsTip() { const { instance } = this return ( <Tip type="no-options" icon="warning">{ instance.noOptionsText }</Tip> ) }, renderNoResultsTip() { const { instance } = this return ( <Tip type="no-results" icon="warning">{ instance.noResultsText }</Tip> ) }, onMenuOpen() { this.adjustMenuOpenDirection() this.setupMenuSizeWatcher() this.setupMenuResizeAndScrollEventListeners() }, onMenuClose() { this.removeMenuSizeWatcher() this.removeMenuResizeAndScrollEventListeners() }, adjustMenuOpenDirection() { const { instance } = this if (!instance.menu.isOpen) return const $menu = instance.getMenu() const $control = instance.getControl() const menuRect = $menu.getBoundingClientRect() const controlRect = $control.getBoundingClientRect() const menuHeight = menuRect.height const viewportHeight = window.innerHeight const spaceAbove = controlRect.top const spaceBelow = window.innerHeight - controlRect.bottom const isControlInViewport = ( (controlRect.top >= 0 && controlRect.top <= viewportHeight) || (controlRect.top < 0 && controlRect.bottom > 0) ) const hasEnoughSpaceBelow = spaceBelow > menuHeight + MENU_BUFFER const hasEnoughSpaceAbove = spaceAbove > menuHeight + MENU_BUFFER if (!isControlInViewport) { instance.closeMenu() } else if (instance.openDirection !== 'auto') { instance.menu.placement = directionMap[instance.openDirection] } else if (hasEnoughSpaceBelow || !hasEnoughSpaceAbove) { instance.menu.placement = 'bottom' } else { instance.menu.placement = 'top' } }, setupMenuSizeWatcher() { const { instance } = this const $menu = instance.getMenu() // istanbul ignore next if (this.menuSizeWatcher) return this.menuSizeWatcher = { remove: watchSize($menu, this.adjustMenuOpenDirection), } }, setupMenuResizeAndScrollEventListeners() { const { instance } = this const $control = instance.getControl() // istanbul ignore next if (this.menuResizeAndScrollEventListeners) return this.menuResizeAndScrollEventListeners = { remove: setupResizeAndScrollEventListeners($control, this.adjustMenuOpenDirection), } }, removeMenuSizeWatcher() { if (!this.menuSizeWatcher) return this.menuSizeWatcher.remove() this.menuSizeWatcher = null }, removeMenuResizeAndScrollEventListeners() { if (!this.menuResizeAndScrollEventListeners) return this.menuResizeAndScrollEventListeners.remove() this.menuResizeAndScrollEventListeners = null }, }, render() { return ( <div ref="menu-container" class="vue-treeselect__menu-container" style={this.menuContainerStyle}> <transition name="vue-treeselect__menu--transition"> {this.renderMenu()} </transition> </div> ) }, } </script>