| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- <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>
|