c45e83132fac731c6634c343ebccc400dca5369418d8dec71a74d57d334be00bcaf60e10722bc9eff6909a774654bc2572519977fe420139fcfdb802767e96-exec 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. <script>
  2. import { MENU_BUFFER } from '../constants'
  3. import { watchSize, setupResizeAndScrollEventListeners } from '../utils'
  4. import Option from './Option'
  5. import Tip from './Tip'
  6. const directionMap = {
  7. top: 'top',
  8. bottom: 'bottom',
  9. above: 'top',
  10. below: 'bottom',
  11. }
  12. export default {
  13. name: 'vue-treeselect--menu',
  14. inject: [ 'instance' ],
  15. computed: {
  16. menuStyle() {
  17. const { instance } = this
  18. return {
  19. maxHeight: instance.maxHeight + 'px',
  20. }
  21. },
  22. menuContainerStyle() {
  23. const { instance } = this
  24. return {
  25. zIndex: instance.appendToBody ? null : instance.zIndex,
  26. }
  27. },
  28. },
  29. watch: {
  30. 'instance.menu.isOpen'(newValue) {
  31. if (newValue) {
  32. // In case `openMenu()` is just called and the menu is not rendered yet.
  33. this.$nextTick(this.onMenuOpen)
  34. } else {
  35. this.onMenuClose()
  36. }
  37. },
  38. },
  39. created() {
  40. this.menuSizeWatcher = null
  41. this.menuResizeAndScrollEventListeners = null
  42. },
  43. mounted() {
  44. const { instance } = this
  45. if (instance.menu.isOpen) this.$nextTick(this.onMenuOpen)
  46. },
  47. destroyed() {
  48. this.onMenuClose()
  49. },
  50. methods: {
  51. renderMenu() {
  52. const { instance } = this
  53. if (!instance.menu.isOpen) return null
  54. return (
  55. <div ref="menu" class="vue-treeselect__menu" onMousedown={instance.handleMouseDown} style={this.menuStyle}>
  56. {this.renderBeforeList()}
  57. {instance.async
  58. ? this.renderAsyncSearchMenuInner()
  59. : instance.localSearch.active
  60. ? this.renderLocalSearchMenuInner()
  61. : this.renderNormalMenuInner()}
  62. {this.renderAfterList()}
  63. </div>
  64. )
  65. },
  66. renderBeforeList() {
  67. const { instance } = this
  68. const beforeListRenderer = instance.$scopedSlots['before-list']
  69. return beforeListRenderer
  70. ? beforeListRenderer()
  71. : null
  72. },
  73. renderAfterList() {
  74. const { instance } = this
  75. const afterListRenderer = instance.$scopedSlots['after-list']
  76. return afterListRenderer
  77. ? afterListRenderer()
  78. : null
  79. },
  80. renderNormalMenuInner() {
  81. const { instance } = this
  82. if (instance.rootOptionsStates.isLoading) {
  83. return this.renderLoadingOptionsTip()
  84. } else if (instance.rootOptionsStates.loadingError) {
  85. return this.renderLoadingRootOptionsErrorTip()
  86. } else if (instance.rootOptionsStates.isLoaded && instance.forest.normalizedOptions.length === 0) {
  87. return this.renderNoAvailableOptionsTip()
  88. } else {
  89. return this.renderOptionList()
  90. }
  91. },
  92. renderLocalSearchMenuInner() {
  93. const { instance } = this
  94. if (instance.rootOptionsStates.isLoading) {
  95. return this.renderLoadingOptionsTip()
  96. } else if (instance.rootOptionsStates.loadingError) {
  97. return this.renderLoadingRootOptionsErrorTip()
  98. } else if (instance.rootOptionsStates.isLoaded && instance.forest.normalizedOptions.length === 0) {
  99. return this.renderNoAvailableOptionsTip()
  100. } else if (instance.localSearch.noResults) {
  101. return this.renderNoResultsTip()
  102. } else {
  103. return this.renderOptionList()
  104. }
  105. },
  106. renderAsyncSearchMenuInner() {
  107. const { instance } = this
  108. const entry = instance.getRemoteSearchEntry()
  109. const shouldShowSearchPromptTip = instance.trigger.searchQuery === '' && !instance.defaultOptions
  110. const shouldShowNoResultsTip = shouldShowSearchPromptTip
  111. ? false
  112. : entry.isLoaded && entry.options.length === 0
  113. if (shouldShowSearchPromptTip) {
  114. return this.renderSearchPromptTip()
  115. } else if (entry.isLoading) {
  116. return this.renderLoadingOptionsTip()
  117. } else if (entry.loadingError) {
  118. return this.renderAsyncSearchLoadingErrorTip()
  119. } else if (shouldShowNoResultsTip) {
  120. return this.renderNoResultsTip()
  121. } else {
  122. return this.renderOptionList()
  123. }
  124. },
  125. renderOptionList() {
  126. const { instance } = this
  127. return (
  128. <div class="vue-treeselect__list">
  129. {instance.forest.normalizedOptions.map(rootNode => (
  130. <Option node={rootNode} key={rootNode.id} />
  131. ))}
  132. </div>
  133. )
  134. },
  135. renderSearchPromptTip() {
  136. const { instance } = this
  137. return (
  138. <Tip type="search-prompt" icon="warning">{ instance.searchPromptText }</Tip>
  139. )
  140. },
  141. renderLoadingOptionsTip() {
  142. const { instance } = this
  143. return (
  144. <Tip type="loading" icon="loader">{ instance.loadingText }</Tip>
  145. )
  146. },
  147. renderLoadingRootOptionsErrorTip() {
  148. const { instance } = this
  149. return (
  150. <Tip type="error" icon="error">
  151. { instance.rootOptionsStates.loadingError }
  152. <a class="vue-treeselect__retry" onClick={instance.loadRootOptions} title={instance.retryTitle}>
  153. { instance.retryText }
  154. </a>
  155. </Tip>
  156. )
  157. },
  158. renderAsyncSearchLoadingErrorTip() {
  159. const { instance } = this
  160. const entry = instance.getRemoteSearchEntry()
  161. // TODO: retryTitle?
  162. return (
  163. <Tip type="error" icon="error">
  164. { entry.loadingError }
  165. <a class="vue-treeselect__retry" onClick={instance.handleRemoteSearch} title={instance.retryTitle}>
  166. { instance.retryText }
  167. </a>
  168. </Tip>
  169. )
  170. },
  171. renderNoAvailableOptionsTip() {
  172. const { instance } = this
  173. return (
  174. <Tip type="no-options" icon="warning">{ instance.noOptionsText }</Tip>
  175. )
  176. },
  177. renderNoResultsTip() {
  178. const { instance } = this
  179. return (
  180. <Tip type="no-results" icon="warning">{ instance.noResultsText }</Tip>
  181. )
  182. },
  183. onMenuOpen() {
  184. this.adjustMenuOpenDirection()
  185. this.setupMenuSizeWatcher()
  186. this.setupMenuResizeAndScrollEventListeners()
  187. },
  188. onMenuClose() {
  189. this.removeMenuSizeWatcher()
  190. this.removeMenuResizeAndScrollEventListeners()
  191. },
  192. adjustMenuOpenDirection() {
  193. const { instance } = this
  194. if (!instance.menu.isOpen) return
  195. const $menu = instance.getMenu()
  196. const $control = instance.getControl()
  197. const menuRect = $menu.getBoundingClientRect()
  198. const controlRect = $control.getBoundingClientRect()
  199. const menuHeight = menuRect.height
  200. const viewportHeight = window.innerHeight
  201. const spaceAbove = controlRect.top
  202. const spaceBelow = window.innerHeight - controlRect.bottom
  203. const isControlInViewport = (
  204. (controlRect.top >= 0 && controlRect.top <= viewportHeight) ||
  205. (controlRect.top < 0 && controlRect.bottom > 0)
  206. )
  207. const hasEnoughSpaceBelow = spaceBelow > menuHeight + MENU_BUFFER
  208. const hasEnoughSpaceAbove = spaceAbove > menuHeight + MENU_BUFFER
  209. if (!isControlInViewport) {
  210. instance.closeMenu()
  211. } else if (instance.openDirection !== 'auto') {
  212. instance.menu.placement = directionMap[instance.openDirection]
  213. } else if (hasEnoughSpaceBelow || !hasEnoughSpaceAbove) {
  214. instance.menu.placement = 'bottom'
  215. } else {
  216. instance.menu.placement = 'top'
  217. }
  218. },
  219. setupMenuSizeWatcher() {
  220. const { instance } = this
  221. const $menu = instance.getMenu()
  222. // istanbul ignore next
  223. if (this.menuSizeWatcher) return
  224. this.menuSizeWatcher = {
  225. remove: watchSize($menu, this.adjustMenuOpenDirection),
  226. }
  227. },
  228. setupMenuResizeAndScrollEventListeners() {
  229. const { instance } = this
  230. const $control = instance.getControl()
  231. // istanbul ignore next
  232. if (this.menuResizeAndScrollEventListeners) return
  233. this.menuResizeAndScrollEventListeners = {
  234. remove: setupResizeAndScrollEventListeners($control, this.adjustMenuOpenDirection),
  235. }
  236. },
  237. removeMenuSizeWatcher() {
  238. if (!this.menuSizeWatcher) return
  239. this.menuSizeWatcher.remove()
  240. this.menuSizeWatcher = null
  241. },
  242. removeMenuResizeAndScrollEventListeners() {
  243. if (!this.menuResizeAndScrollEventListeners) return
  244. this.menuResizeAndScrollEventListeners.remove()
  245. this.menuResizeAndScrollEventListeners = null
  246. },
  247. },
  248. render() {
  249. return (
  250. <div ref="menu-container" class="vue-treeselect__menu-container" style={this.menuContainerStyle}>
  251. <transition name="vue-treeselect__menu--transition">
  252. {this.renderMenu()}
  253. </transition>
  254. </div>
  255. )
  256. },
  257. }
  258. </script>