Эх сурвалжийг харах

修复列表中下拉框最后一个选项,鼠标手势频闪

cyy 2 өдөр өмнө
parent
commit
05564c1d9f

+ 169 - 0
src/components/ibps-tree-select/dropdown.vue

@@ -0,0 +1,169 @@
+<template>
+  <div
+    v-show="visible"
+    ref="root"
+    class="el-tree-select-dropdown"
+    :class="{ 'is-top': placement === 'top' }"
+    :style="panelStyle"
+    @mousedown.stop
+    @click.stop
+  >
+    <div class="el-tree-select-dropdown__wrap" :style="wrapStyle">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<script>
+import PopupManager from '@/plugins/element-ui/src/utils/popup/popup-manager'
+
+const GAP = 5
+const VIEWPORT_PADDING = 8
+const DEFAULT_MAX_HEIGHT = 274
+const MIN_HEIGHT = 100
+
+export default {
+  name: 'IbpsTreeSelectDropdown',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    minWidth: {
+      type: Number,
+      default: 0
+    }
+  },
+  data() {
+    return {
+      zIndex: PopupManager.nextZIndex(),
+      top: 0,
+      left: 0,
+      width: 0,
+      maxHeight: DEFAULT_MAX_HEIGHT,
+      placement: 'bottom'
+    }
+  },
+  computed: {
+    panelStyle() {
+      const style = {
+        position: 'fixed',
+        left: `${this.left}px`,
+        minWidth: this.width ? `${this.width}px` : undefined,
+        zIndex: this.zIndex
+      }
+      if (this.placement === 'top') {
+        style.bottom = `${this.bottom}px`
+      } else {
+        style.top = `${this.top}px`
+      }
+      return style
+    },
+    wrapStyle() {
+      return {
+        maxHeight: `${this.maxHeight}px`
+      }
+    },
+    bottom() {
+      return window.innerHeight - this.top + GAP
+    }
+  },
+  watch: {
+    visible(val) {
+      if (val) {
+        this.$nextTick(() => {
+          this.mountToBody()
+        })
+      }
+    }
+  },
+  beforeDestroy() {
+    this.unmountFromBody()
+  },
+  methods: {
+    mountToBody() {
+      const el = this.$refs.root
+      if (el && el.parentNode !== document.body) {
+        document.body.appendChild(el)
+      }
+    },
+    unmountFromBody() {
+      const el = this.$refs.root
+      if (el && el.parentNode === document.body) {
+        document.body.removeChild(el)
+      }
+    },
+    updatePosition(referenceEl) {
+      if (!referenceEl || !this.visible) return
+
+      const rect = referenceEl.getBoundingClientRect()
+      const viewportWidth = window.innerWidth
+      const viewportHeight = window.innerHeight
+
+      const spaceBelow = viewportHeight - rect.bottom - VIEWPORT_PADDING
+      const spaceAbove = rect.top - VIEWPORT_PADDING
+      const width = Math.max(rect.width, this.minWidth || 0)
+
+      let left = rect.left
+      if (left + width > viewportWidth - VIEWPORT_PADDING) {
+        left = Math.max(VIEWPORT_PADDING, viewportWidth - VIEWPORT_PADDING - width)
+      }
+      if (left < VIEWPORT_PADDING) {
+        left = VIEWPORT_PADDING
+      }
+
+      // 下方空间不足时翻转到上方
+      const openToTop =
+        spaceBelow < MIN_HEIGHT ||
+        (spaceBelow < DEFAULT_MAX_HEIGHT && spaceAbove > spaceBelow)
+
+      if (openToTop) {
+        this.placement = 'top'
+        this.maxHeight = Math.min(
+          DEFAULT_MAX_HEIGHT,
+          Math.max(MIN_HEIGHT, spaceAbove - GAP)
+        )
+        this.top = rect.top
+        this.left = left
+        this.width = width
+      } else {
+        this.placement = 'bottom'
+        this.maxHeight = Math.min(
+          DEFAULT_MAX_HEIGHT,
+          Math.max(MIN_HEIGHT, spaceBelow - GAP)
+        )
+        this.top = rect.bottom + GAP
+        this.left = left
+        this.width = width
+      }
+
+      this.mountToBody()
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+.el-tree-select-dropdown {
+  box-sizing: border-box;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  background-color: #fff;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+
+  .el-tree-select-dropdown__wrap {
+    overflow-x: hidden;
+    overflow-y: auto;
+    margin-bottom: 0 !important;
+    margin-right: 0 !important;
+  }
+
+  .el-tree {
+    padding: 5px 0 8px;
+  }
+
+  .el-tree-node__content {
+    cursor: pointer;
+  }
+}
+</style>

+ 133 - 151
src/components/ibps-tree-select/index.vue

@@ -3,7 +3,8 @@
     v-if="editable"
     v-clickoutside="handleClose"
     class="el-tree-select"
-    @click="toggleTree"
+    :class="{ 'is-open': visible }"
+    @click="handleRootClick"
   >
     <div
       v-if="multiple"
@@ -63,47 +64,41 @@
       </template>
       <i slot="suffix" :class="suffixIconClass" @click="handleIconClick" />
     </el-input>
-    <transition name="el-zoom-in-top">
-      <div
-        v-show="visible"
-        ref="popper"
-        :style="{ minWidth: inputWidth + 'px' }"
-        class="el-tree-select-dropdown el-popper"
+    <ibps-tree-select-dropdown
+      ref="dropdown"
+      :visible="visible"
+      :min-width="inputWidth"
+    >
+      <el-tree
+        ref="tree"
+        :data="data"
+        :lazy="lazy"
+        :load="load"
+        :check-on-click-node="checkOnClickNode"
+        :props="treeProps"
+        :show-checkbox="showCheckbox"
+        :expand-on-click-node="false"
+        :check-strictly="checkStrictly"
+        :filter-node-method="filterNodeMethod"
+        :default-checked-keys="checkedKeys"
+        :node-key="nodeKey"
+        :empty-text="emptyText"
+        :current-node-key="currentNodeKey"
+        default-expand-all
+        highlight-current
+        @check="handleCheck"
+        @node-click="handleNodeClick"
       >
-        <el-scrollbar wrap-class="el-tree-select-dropdown__wrap">
-          <el-tree
-            ref="tree"
-            :data="data"
-            :lazy="lazy"
-            :load="load"
-            :check-on-click-node="checkOnClickNode"
-            :props="treeProps"
-            :show-checkbox="showCheckbox"
-            :expand-on-click-node="false"
-            :check-strictly="checkStrictly"
-            :filter-node-method="filterNodeMethod"
-            :default-checked-keys="checkedKeys"
-            :node-key="nodeKey"
-            :empty-text="emptyText"
-            :current-node-key="currentNodeKey"
-            default-expand-all
-            highlight-current
-            @check="handleCheck"
-            @node-click="handleNodeClick"
-          >
-            <template v-slot="scope">
-              <span class="el-tree-node__label">
-                <i v-if="icon" :class="icon(scope.data)" />
-                {{ scope.node.label }}
-              </span>
-            </template>
-          </el-tree>
-        </el-scrollbar>
-      </div>
-    </transition>
+        <template v-slot="scope">
+          <span class="el-tree-node__label">
+            <i v-if="icon" :class="icon(scope.data)" />
+            {{ scope.node.label }}
+          </span>
+        </template>
+      </el-tree>
+    </ibps-tree-select-dropdown>
   </div>
 
-  <!--只读 文本样式-->
   <div v-else class="el-tree-select">
     <template v-if="$utils.isNotEmpty(selected)">
       <div v-if="multiple" class="el-tree-select__tags_readonly">
@@ -132,43 +127,35 @@
 </template>
 
 <script>
-// 参考 https://github.com/ElemeFE/element/blob/29e76cda035bb8a951e6792a33ba4ff5056515a0/packages/tree-select/src/main.vue
-// 可能下个版本出现,再进行修复
-// API https://deploy-preview-12104--element.netlify.com/#/zh-CN/component/tree-select
-// import ElInput from 'element-ui/packages/input'
-// import ElTree from 'element-ui/packages/tree/src/tree.vue'
 import Clickoutside from '@/plugins/element-ui/src/utils/clickoutside'
-import Popper from '@/plugins/element-ui/src/utils/vue-popper'
 import { valueEquals } from '@/plugins/element-ui/src/utils/util'
 import {
   addResizeListener,
   removeResizeListener
 } from '@/plugins/element-ui/src/utils/resize-event'
 import emitter from '@/plugins/element-ui/src/mixins/emitter'
-
-import PopupManager from '@/utils/popup'
-
+import IbpsTreeSelectDropdown from './dropdown.vue'
 // TODO: 等 vue-popper 合并后,这里还需要做出调整
-const popperMixin = {
-  props: {
-    placement: {
-      type: String,
-      default: 'bottom-start'
-    },
-    appendToBody: false,
-    arrowOffset: Popper.props.arrowOffset,
-    offset: Popper.props.offset,
-    boundariesPadding: Popper.props.boundariesPadding,
-    popperOptions: Popper.props.popperOptions,
-    visibleArrow: {
-      type: Boolean,
-      default: true
-    }
-  },
-  methods: Popper.methods,
-  data: Popper.data,
-  beforeDestroy: Popper.beforeDestroy
-}
+// const popperMixin = {
+//   props: {
+//     placement: {
+//       type: String,
+//       default: 'bottom-start'
+//     },
+//     appendToBody: false,
+//     arrowOffset: Popper.props.arrowOffset,
+//     offset: Popper.props.offset,
+//     boundariesPadding: Popper.props.boundariesPadding,
+//     popperOptions: Popper.props.popperOptions,
+//     visibleArrow: {
+//       type: Boolean,
+//       default: true
+//     }
+//   },
+//   methods: Popper.methods,
+//   data: Popper.data,
+//   beforeDestroy: Popper.beforeDestroy
+// }
 const sizeMap = {
   medium: 36,
   small: 32,
@@ -177,34 +164,23 @@ const sizeMap = {
 
 export default {
   name: 'ibps-tree-select',
-  // components: {
-  //   ElInput,
-  //   ElTree
-  // },
+  components: {
+    IbpsTreeSelectDropdown
+  },
   directives: { Clickoutside },
-  mixins: [popperMixin, emitter],
+  mixins: [emitter],
   provide() {
     return {
       elTreeSelect: this
     }
   },
   inject: {
-    elForm: {
-      default: ''
-    },
-    elFormItem: {
-      default: ''
-    }
+    elForm: { default: '' },
+    elFormItem: { default: '' }
   },
   props: {
-    data: {
-      type: Array,
-      required: true
-    },
-    value: {
-      type: [String, Number, Array, Object],
-      default: ''
-    },
+    data: { type: Array, required: true },
+    value: { type: [String, Number, Array, Object], default: '' },
     multiple: Boolean,
     disabled: Boolean,
     readonly: {
@@ -212,7 +188,6 @@ export default {
       default: false
     },
     readonlyText: {
-      // 只读样式 【text ,original】
       type: String,
       default: 'original',
       validator(val) {
@@ -226,11 +201,7 @@ export default {
         return ['medium', 'small', 'mini'].indexOf(val) > -1
       }
     },
-    nodeKey: {
-      type: String,
-      default: 'id'
-    },
-
+    nodeKey: { type: String, default: 'id' },
     props: Object,
     placeholder: {
       type: String,
@@ -239,31 +210,22 @@ export default {
       }
     },
     selectMode: {
-      // 选值模式 leaf、any
       type: String,
       default: 'any',
-      validator: function (value) {
+      validator(value) {
         return ['any', 'leaf'].indexOf(value) !== -1
       }
     },
     displayMode: {
-      // 显示模式 path 、name
       type: String,
       default: 'name',
-      validator: function (value) {
+      validator(value) {
         return ['name', 'path'].indexOf(value) !== -1
       }
     },
-    separator: {
-      // 树形选项分隔符
-      type: String,
-      default: '/'
-    },
-    warningText: {
-      type: String,
-      default: '请选择叶子节点'
-    },
-    allowSelection: Function, // 允许的节点
+    separator: { type: String, default: '/' },
+    warningText: { type: String, default: '请选择叶子节点' },
+    allowSelection: Function,
     lazy: Boolean,
     load: Function,
     showCheckbox: Boolean,
@@ -272,7 +234,6 @@ export default {
     filterMethod: Function,
     emptyText: String,
     showCheckedStrategy: {
-      // 显示多选按钮
       type: String,
       default: 'child',
       validator(val) {
@@ -349,27 +310,20 @@ export default {
     checkedKeys() {
       if (this.multiple && this.showCheckbox) {
         return this.value || []
-      } else {
-        return []
       }
+      return []
     },
     currentNodeKey() {
       if (this.multiple) {
         return this.value && this.value.length > 0 ? this.value[0].value : ''
-      } else {
-        if (this.value && Array.isArray(this.value)) {
-          return this.value[0] || ''
-        } else {
-          return this.value || ''
-        }
       }
+      if (this.value && Array.isArray(this.value)) {
+        return this.value[0] || ''
+      }
+      return this.value || ''
     },
     currentPlaceholder() {
-      if (this.$utils.isEmpty(this.value)) {
-        return this.placeholder
-      } else {
-        return ''
-      }
+      return this.$utils.isEmpty(this.value) ? this.placeholder : ''
     },
     labelKey() {
       return this.treeProps['label'] || 'name'
@@ -378,7 +332,10 @@ export default {
   watch: {
     visible(val) {
       if (val) {
-        this.updatePopper()
+        this.$nextTick(() => {
+          this.syncDropdown()
+          this.bindPositionEvents()
+        })
         if (this.multiple && this.filterable) {
           this.$refs.input && this.$refs.input.focus()
         }
@@ -387,7 +344,7 @@ export default {
           this.broadcast('ElInput', 'inputSelect')
         }
       } else {
-        this.destroyPopper()
+        this.unbindPositionEvents()
         this.$refs.input && this.$refs.input.blur()
         this.$emit('blur', this)
         if (!this.multiple) {
@@ -413,7 +370,7 @@ export default {
       this.setSelected()
     },
     props: {
-      handler(val, oldVal) {
+      handler(val) {
         this.treeProps = val
       },
       deep: true,
@@ -425,25 +382,50 @@ export default {
       this.checkOnClickNode = true
     }
     if (this.editable) {
-      this.referenceElm = this.$refs.reference.$el
-      this.popperElm = this.$refs.popper
       this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width
       addResizeListener(this.$el, this.handleResize)
       this.setSelected()
-      this.fixZIndex()
     }
   },
   beforeDestroy() {
+    this.unbindPositionEvents()
     if (this.editable) {
       removeResizeListener(this.$el, this.handleResize)
     }
   },
   methods: {
-    /**
-     * zxh 修复zindex 不是最高的被遮住
-     */
-    fixZIndex() {
-      PopupManager.getZIndex()
+    getReferenceEl() {
+      return this.$refs.reference && this.$refs.reference.$el
+    },
+    syncDropdown() {
+      const dropdown = this.$refs.dropdown
+      const referenceEl = this.getReferenceEl()
+      if (!dropdown || !referenceEl) return
+      dropdown.updatePosition(referenceEl)
+      this.popperElm = dropdown.$refs.root
+    },
+    bindPositionEvents() {
+      if (this._positionHandler) return
+      this._positionHandler = () => {
+        if (this.visible) {
+          this.syncDropdown()
+        }
+      }
+      window.addEventListener('scroll', this._positionHandler, true)
+      window.addEventListener('resize', this._positionHandler)
+    },
+    unbindPositionEvents() {
+      if (!this._positionHandler) return
+      window.removeEventListener('scroll', this._positionHandler, true)
+      window.removeEventListener('resize', this._positionHandler)
+      this._positionHandler = null
+    },
+    handleRootClick(event) {
+      const root = this.$refs.dropdown && this.$refs.dropdown.$refs.root
+      if (root && root.contains(event.target)) {
+        return
+      }
+      this.toggleTree()
     },
     handleFocus(event) {
       this.treeVisibleOnFocus = true
@@ -479,6 +461,11 @@ export default {
     },
     handleQueryChange(val) {
       this.$refs.tree && this.$refs.tree.filter(val)
+      this.$nextTick(() => {
+        if (this.visible) {
+          this.syncDropdown()
+        }
+      })
     },
     handleNodeClick(data, node, tree) {
       if (this.showCheckbox) return
@@ -495,9 +482,9 @@ export default {
         this.$message.warning(this.warningText)
         return false
       }
-      this.setSelectedNode(data, node, tree)
+      this.setSelectedNode(data)
     },
-    setSelectedNode(data, node, tree) {
+    setSelectedNode(data) {
       const value = data[this.nodeKey]
       if (this.multiple) {
         const valueCopy = this.value.slice()
@@ -511,9 +498,6 @@ export default {
         this.$emit('input', valueCopy)
         this.emitChange(valueCopy)
       } else {
-        // if (value === this.value) {
-        //   value = ''
-        // }
         this.$emit('input', value)
         this.emitChange(value)
         this.visible = false
@@ -593,20 +577,16 @@ export default {
     },
     filterNodeMethod(value, data) {
       if (!value) return true
-      this.$nextTick(this.updatePopper)
       if (typeof this.filterMethod === 'function') {
         return this.filterMethod(value, data)
-      } else {
-        if (data[this.labelKey]) {
-          return data[this.labelKey].indexOf(value) !== -1
-        } else {
-          if (data.label) {
-            return data.label.indexOf(value) !== -1
-          } else {
-            return true
-          }
-        }
       }
+      if (data[this.labelKey]) {
+        return data[this.labelKey].indexOf(value) !== -1
+      }
+      if (data.label) {
+        return data.label.indexOf(value) !== -1
+      }
+      return true
     },
     resetInputHeight() {
       this.$nextTick(() => {
@@ -622,7 +602,7 @@ export default {
         }
         inputEl.style.height = `${height}px`
         if (this.visible) {
-          this.updatePopper()
+          this.syncDropdown()
         }
       })
     },
@@ -632,9 +612,8 @@ export default {
         if (item === value) {
           index = i
           return true
-        } else {
-          return false
         }
+        return false
       })
       return index
     },
@@ -661,6 +640,9 @@ export default {
     handleResize() {
       this.resetInputWidth()
       if (this.multiple) this.resetInputHeight()
+      if (this.visible) {
+        this.syncDropdown()
+      }
     },
     setSelected() {
       if (this.multiple) {