解决如果父组件位于滚动容器内,Vuetify“v-menu”显示为固定的问题

问题描述 投票:0回答:2

Vuetify 中的 v-menu 组件存在一个长期存在的

问题

  1. 默认情况下,弹出窗口在物理上与激活器“分离”,并创建为
    v-app
    的子级,从而避免在某些父 DOM 节点具有
    overflow: hidden
    样式时被剪裁;然而,这会导致这样的问题:当激活器位于滚动容器内时,弹出窗口的行为为“位置:固定” - 也就是说,它不会与激活器一起滚动,并且看起来在视觉上断开连接,只是“悬挂”在页面上。
  2. Vuetify 维护者承认这一事实并建议使用“attach”属性 - 然而,当使用“attach”时,弹出窗口的位置计算出十分之九的错误。

经过 2 个小时的调试,我最终放弃了使用“attach”属性,并决定简单地跟踪激活器所在的父容器的滚动位置,并在计算弹出窗口的位置时将其考虑在内。我在下面分享我对这个问题的解决方案,希望它能被纳入主流 Vuetify 中。

scroll menu vuetify.js fixed
2个回答
1
投票

这是解决上述问题以及其他一些问题的补丁文件。在项目中创建一个名为

patches
的文件夹,并将补丁文件保存为
patches/vuetify#2.6.4.patch
。然后将新脚本添加到
scripts
 中的 
package.json

组中
"scripts":
{
   ....
   "prepare": "custompatch"
}

然后运行

npm -i -D custompatch
(为您的 CI/CD 安装修补程序)和
npx custompatch
(在您的开发环境中实际修补 Vuetify)。

Index: \vuetify\lib\components\VDialog\VDialog.js
===================================================================
--- \vuetify\lib\components\VDialog\VDialog.js
+++ \vuetify\lib\components\VDialog\VDialog.js
@@ -43,17 +43,21 @@
     transition: {
       type: [String, Boolean],
       default: 'dialog-transition'
     },
-    width: [String, Number]
+    width: [String, Number],
+    zIndex: {
+      type: [String, Number],
+      default: 200
+    }
   },
 
   data() {
     return {
       activatedBy: null,
       animate: false,
       animateTimeout: -1,
-      stackMinZIndex: 200,
+      stackMinZIndex: this.zIndex || 200,
       previousActiveElement: null
     };
   },
 
Index: \vuetify\lib\components\VMenu\VMenu.js
===================================================================
--- \vuetify\lib\components\VMenu\VMenu.js
+++ \vuetify\lib\components\VMenu\VMenu.js
@@ -120,10 +120,10 @@
       return {
         maxHeight: this.calculatedMaxHeight,
         minWidth: this.calculatedMinWidth,
         maxWidth: this.calculatedMaxWidth,
-        top: this.calculatedTop,
-        left: this.calculatedLeft,
+        top: `calc(${this.calculatedTop} - ${this.scrollY}px + ${this.originalScrollY}px)`,
+        left: `calc(${this.calculatedLeft} - ${this.scrollX}px + ${this.originalScrollX}px)`,
         transformOrigin: this.origin,
         zIndex: this.zIndex || this.activeZIndex
       };
     }
Index: \vuetify\lib\components\VSelect\VSelect.js
===================================================================
--- \vuetify\lib\components\VSelect\VSelect.js
+++ \vuetify\lib\components\VSelect\VSelect.js
@@ -678,12 +678,17 @@
     },
 
     onScroll() {
       if (!this.isMenuActive) {
-        requestAnimationFrame(() => this.getContent().scrollTop = 0);
+        requestAnimationFrame(() => 
+        {
+          const content = this.getContent();
+          if (content) content.scrollTop = 0;
+        });
       } else {
         if (this.lastItem > this.computedItems.length) return;
-        const showMoreItems = this.getContent().scrollHeight - (this.getContent().scrollTop + this.getContent().clientHeight) < 200;
+        const content = this.getContent();
+        const showMoreItems = content ? this.getContent().scrollHeight - (this.getContent().scrollTop + this.getContent().clientHeight) < 200 : false;
 
         if (showMoreItems) {
           this.lastItem += 20;
         }
Index: \vuetify\lib\components\VSlideGroup\VSlideGroup.js
===================================================================
--- \vuetify\lib\components\VSlideGroup\VSlideGroup.js
+++ \vuetify\lib\components\VSlideGroup\VSlideGroup.js
@@ -181,9 +181,9 @@
   },
 
   methods: {
     onScroll() {
-      this.$refs.wrapper.scrollLeft = 0;
+      if (this.$refs.wrapper) this.$refs.wrapper.scrollLeft = 0; // TMCDOS
     },
 
     onFocusin(e) {
       if (!this.isOverflowing) return; // Focused element is likely to be the root of an item, so a
Index: \vuetify\lib\components\VTextField\VTextField.js
===================================================================
--- \vuetify\lib\components\VTextField\VTextField.js
+++ \vuetify\lib\components\VTextField\VTextField.js
@@ -441,8 +441,9 @@
       this.$refs.input.focus();
     },
 
     onFocus(e) {
+      this.onResize();
       if (!this.$refs.input) return;
       const root = attachedRoot(this.$el);
       if (!root) return;
 
Index: \vuetify\lib\directives\click-outside\index.js
===================================================================
--- \vuetify\lib\directives\click-outside\index.js
+++ \vuetify\lib\directives\click-outside\index.js
@@ -35,10 +35,11 @@
 }
 
 function directive(e, el, binding, vnode) {
   const handler = typeof binding.value === 'function' ? binding.value : binding.value.handler;
+  const target = e.target;
   el._clickOutside.lastMousedownWasOutside && checkEvent(e, el, binding) && setTimeout(() => {
-    checkIsActive(e, binding) && handler && handler(e);
+    checkIsActive({...e, target}, binding) && handler && handler({...e, target}); 
   }, 0);
 }
 
 function handleShadow(el, callback) {
Index: \vuetify\lib\directives\ripple\index.js
===================================================================
--- \vuetify\lib\directives\ripple\index.js
+++ \vuetify\lib\directives\ripple\index.js
@@ -119,9 +119,9 @@
           el.style.position = el.dataset.previousPosition;
           delete el.dataset.previousPosition;
         }
 
-        animation.parentNode && el.removeChild(animation.parentNode);
+        animation.parentNode && /* el */animation.parentNode.parentNode.removeChild(animation.parentNode);
       }, 300);
     }, delay);
   }
 
Index: \vuetify\lib\mixins\detachable\index.js
===================================================================
--- \vuetify\lib\mixins\detachable\index.js
+++ \vuetify\lib\mixins\detachable\index.js
@@ -28,13 +28,23 @@
     },
     contentClass: {
       type: String,
       default: ''
+    },
+    scroller:
+    {
+      default: null,
+      validator: validateAttachTarget
     }
   },
   data: () => ({
     activatorNode: null,
-    hasDetached: false
+    hasDetached: false,
+    scrollingNode: null,
+    scrollX: 0,
+    scrollY: 0,
+    originalScrollX: 0,
+    originalScrollY: 0 
   }),
   watch: {
     attach() {
       this.hasDetached = false;
@@ -42,10 +52,38 @@
     },
 
     hasContent() {
       this.$nextTick(this.initDetach);
+    },
+    isActive(val)
+    {
+      if (val)
+      {
+        if (typeof this.scroller === 'string') {
+          // CSS selector
+          this.scrollingNode = document.querySelector(this.scroller);
+        } else if (this.scroller && typeof this.scroller === 'object') {
+          // DOM Element
+          this.scrollingNode = this.scroller;
+        }
+        if (this.scrollingNode)
+        {
+          this.originalScrollX = this.scrollingNode.scrollLeft;
+          this.originalScrollY = this.scrollingNode.scrollTop;
+          this.scrollX = this.originalScrollX;
+          this.scrollY = this.originalScrollY;
+          this.scrollingNode.addEventListener('scroll', this.setScrollOffset, {passive: true});
+        }
+      }
+      else
+      {
+        if (this.scrollingNode)
+        {
+          this.scrollingNode.removeEventListener('scroll', this.setScrollOffset, {passive: true});
+        }
+        this.scrollingNode = null;
+      }
     }
-
   },
 
   beforeMount() {
     this.$nextTick(() => {
@@ -95,8 +133,12 @@
       } else {
         removeActivator(activator);
       }
     }
+    if (this.scrollingNode)
+    {
+      this.scrollingNode.removeEventListener('scroll', this.setScrollOffset, {passive: true});
+    }
   },
 
   methods: {
     getScopeIdAttrs() {
@@ -132,9 +174,13 @@
       }
 
       target.appendChild(this.$refs.content);
       this.hasDetached = true;
+    },
+    setScrollOffset(event)
+    {
+      this.scrollX = event.target.scrollLeft;
+      this.scrollY = event.target.scrollTop;
     }
-
   }
 });
 //# sourceMappingURL=index.js.map
\ No newline at end of file
Index: \vuetify\lib\mixins\menuable\index.js
===================================================================
--- \vuetify\lib\mixins\menuable\index.js
+++ \vuetify\lib\mixins\menuable\index.js
@@ -96,9 +96,9 @@
   computed: {
     computedLeft() {
       const a = this.dimensions.activator;
       const c = this.dimensions.content;
-      const activatorLeft = (this.attach !== false ? a.offsetLeft : a.left) || 0;
+      const activatorLeft = (this.attach !== false ? this.getActivatorLeft() : a.left) || 0;
       const minWidth = Math.max(a.width, c.width);
       let left = 0;
       left += activatorLeft;
       if (this.left || this.$vuetify.rtl && !this.right) left -= minWidth - a.width;
@@ -117,9 +117,9 @@
       const a = this.dimensions.activator;
       const c = this.dimensions.content;
       let top = 0;
       if (this.top) top += a.height - c.height;
-      if (this.attach !== false) top += a.offsetTop;else top += a.top + this.pageYOffset;
+      if (this.attach !== false) top += this.getActivatorTop(); else top += a.top + this.pageYOffset;
       if (this.offsetY) top += this.top ? -a.height : a.height;
       if (this.nudgeTop) top -= parseInt(this.nudgeTop);
       if (this.nudgeBottom) top += parseInt(this.nudgeBottom);
       return top;
@@ -130,10 +130,13 @@
     },
 
     absoluteYOffset() {
       return this.pageYOffset - this.relativeYOffset;
+    },
+
+    windowContainer() {
+      return typeof this.attach === 'string' ? document.querySelector(this.attach) || document.body : typeof this.attach === 'object' ? this.attach : document.body;
     }
-
   },
   watch: {
     disabled(val) {
       val && this.callDeactivate();
@@ -274,19 +277,19 @@
     },
 
     getInnerHeight() {
       if (!this.hasWindow) return 0;
-      return window.innerHeight || document.documentElement.clientHeight;
+      return this.attach !== false ? this.windowContainer.clientHeight : window.innerHeight || document.documentElement.clientHeight;
     },
 
     getOffsetLeft() {
       if (!this.hasWindow) return 0;
-      return window.pageXOffset || document.documentElement.scrollLeft;
+      return this.attach !== false ? this.windowContainer.scrollLeft : window.pageXOffset || document.documentElement.scrollLeft;
     },
 
     getOffsetTop() {
       if (!this.hasWindow) return 0;
-      return window.pageYOffset || document.documentElement.scrollTop;
+      return this.attach !== false ? this.windowContainer.scrollTop : window.pageYOffset || document.documentElement.scrollTop;
     },
 
     getRoundedBoundedClientRect(el) {
       const rect = el.getBoundingClientRect();
@@ -368,19 +371,38 @@
       this.sneakPeek(() => {
         if (this.$refs.content) {
           if (this.$refs.content.offsetParent) {
             const offsetRect = this.getRoundedBoundedClientRect(this.$refs.content.offsetParent);
-            this.relativeYOffset = window.pageYOffset + offsetRect.top;
+            this.relativeYOffset = (this.attach !== false ? this.windowContainer.scrollTop : window.pageYOffset) + offsetRect.top;
             dimensions.activator.top -= this.relativeYOffset;
-            dimensions.activator.left -= window.pageXOffset + offsetRect.left;
+            dimensions.activator.left -= (this.attach !== false ? this.windowContainer.scrollLeft : window.pageXOffset) + offsetRect.left;
           }
 
           dimensions.content = this.measure(this.$refs.content);
         }
 
         this.dimensions = dimensions;
       });
+    },
+
+    getActivatorTop() {
+      let result = 0;
+      let elem = this.getActivator();
+      while (elem && elem !== this.windowContainer && this.windowContainer.contains(elem)) {
+        result += elem.offsetTop;
+        elem = elem.offsetParent;
+      }
+      return result;
+    },
+
+    getActivatorLeft() {
+      let result = 0;
+      let elem = this.getActivator();
+      while (elem && elem !== this.windowContainer && this.windowContainer.contains(elem)) {
+        result += elem.offsetLeft;
+        elem = elem.offsetParent;
+      }
+      return result;
     }
-
   }
 });
 //# sourceMappingURL=index.js.map
\ No newline at end of file 

0
投票

对于 Vuetify 3 用户: 您可以通过将滚动策略属性设置为“无”来解决此问题:

<v-menu v-model="menu" scroll-strategy="none">
    <template #activator="{ props: activatorProps }">
      ...
    </template>
    ...
 </v-menu>

如 Vuetify 的 v-menu API 文档中所述,默认设置为“重新定位”,这会导致这种悬挂效果。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.