最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 检测元素点击在指定范围之外指令实现,vue3+ts,文末有非ts实现,看elementPlus源码所得

    正文概述 掘金(海天酱油_沃利贝尔)   2020-12-22   1123

    闲言碎语

    自己在写组件库,目前已经写了popper组件,popup组件,然后现在在写颜色选择器。全部都是用的vue3和ts。

    缘起于popper组件的细节优化。我的颜色选择器组件,基于popper为底层组件。然后除了一般的参数控制关闭之外。我需要做到点击在非组件绑定的元素区域要关闭弹窗。

    引入使用

    要结果的直接最后复制源码

    import ClickOutside from '../ClickOutside'
    export default defineComponent({
      directives: {
        'dht-click-outside': ClickOutside.directive,
      },
     }
    )
    <span v-dht-click-outside={clickOutside}></span>
    

    基础原理实现

    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <title>js实现的点击div区域外隐藏div区域</title>
        <style type="text/css">
            #myDiv{
                border:1px solid #000000;
                width:200px;
                height:100px;
                background:#FF0000;
            }
        </style>
        <script type="text/javascript">
            window.onload=function(){
                var myDiv = document.getElementById("myDiv");
                document.addEventListener("click",function(){
                    console.log(11111)
                    myDiv.style.display="none";
                });
                myDiv.addEventListener("click",function(event){
                    console.log(2222)
                    event=event||window.event;
                    event.stopPropagation();
                });
            };
        </script>
    </head>
    <body>
    <div id="myDiv"></div>
    </body>
    

    这部分代码就是核心原理了。或者我们可以根据元素的xy轴判断等等。

    翻看elemenPlus源码,addEventListener细节

    我的组件也是有些模仿elementUI的,所以就翻看他怎么实现的。然后我发现elementUI自己实现了一个自定义指令。ClickOutside

    其中有一个关键的细节。指令可以绑定到不同的元素上面,但是elementUI做了一个操作,他把绑定之后的数据存到了Map下面,我想本身已经绑定到元素下面了,为什么还需要存放到一个数组一样的结构中呢。

    实验之后,发现

    下面这种代码方式,addEventListener只会触发一次

     <span v-dht-click-outside={clickOutside}</span>
    <span v-dht-click-outside={clickOutside}></span>
    

    源码

    我自己也懒,把elementUI的源码小改改直接放项目里面使用了。

    下面是完整源码,使用参考上面的引入使用

    // 该代码抄自elementUI
    import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue'
    
    type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void
    
    type FlushList = Map<
      HTMLElement,
      {
        documentHandler: DocumentHandler
        bindingFn: (...args: unknown[]) => unknown
      }
    >
    
    const on = function (
      element: HTMLElement | Document | Window,
      event: string,
      handler: EventListenerOrEventListenerObject,
      useCapture = false,
    ): void {
      if (element && event && handler) {
        element.addEventListener(event, handler, useCapture)
      }
    }
    
    const nodeList: FlushList = new Map()
    
    let startClick: MouseEvent
    
    on(document, 'mousedown', (e) => (startClick = e as MouseEvent))
    on(document, 'mouseup', (e) => {
      for (const { documentHandler } of nodeList.values()) {
        documentHandler(e as MouseEvent, startClick)
      }
    })
    
    function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
      let excludes: HTMLElement[] = []
      if (Array.isArray(binding.arg)) {
        excludes = binding.arg
      } else {
        // due to current implementation on binding type is wrong the type casting is necessary here
        excludes.push((binding.arg as unknown) as HTMLElement)
      }
      return function (mouseup, mousedown) {
        const popperRef = (binding.instance as ComponentPublicInstance<{
          popperRef: HTMLElement
        }>).popperRef
        const mouseUpTarget = mouseup.target as Node
        const mouseDownTarget = mousedown.target as Node
        const isBound = !binding || !binding.instance
        const isTargetExists = !mouseUpTarget || !mouseDownTarget
        const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
        const isSelf = el === mouseUpTarget
    
        const isTargetExcluded =
          (excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
          (excludes.length && excludes.includes(mouseDownTarget as HTMLElement))
        const isContainedByPopper =
          popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget))
        if (
          isBound ||
          isTargetExists ||
          isContainedByEl ||
          isSelf ||
          isTargetExcluded ||
          isContainedByPopper
        ) {
          return
        }
        binding.value()
      }
    }
    
    const ClickOutside: ObjectDirective = {
      beforeMount(el, binding) {
        nodeList.set(el, {
          documentHandler: createDocumentHandler(el, binding),
          bindingFn: binding.value,
        })
      },
      updated(el, binding) {
        nodeList.set(el, {
          documentHandler: createDocumentHandler(el, binding),
          bindingFn: binding.value,
        })
      },
      unmounted(el) {
        nodeList.delete(el)
      },
    }
    
    export default ClickOutside
    

    非ts代码实现

    const on = function (element, event, handler, useCapture = false) {
        if (element && event && handler) {
            element.addEventListener(event, handler, useCapture);
        }
    };
    const nodeList = new Map();
    let startClick;
    on(document, 'mousedown', (e) => (startClick = e));
    on(document, 'mouseup', (e) => {
        for (const { documentHandler } of nodeList.values()) {
            documentHandler(e, startClick);
        }
    });
    function createDocumentHandler(el, binding) {
        let excludes = [];
        if (Array.isArray(binding.arg)) {
            excludes = binding.arg;
        }
        else {
            // due to current implementation on binding type is wrong the type casting is necessary here
            excludes.push(binding.arg);
        }
        return function (mouseup, mousedown) {
            const popperRef = binding.instance.popperRef;
            const mouseUpTarget = mouseup.target;
            const mouseDownTarget = mousedown.target;
            const isBound = !binding || !binding.instance;
            const isTargetExists = !mouseUpTarget || !mouseDownTarget;
            const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
            const isSelf = el === mouseUpTarget;
            const isTargetExcluded = (excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
                (excludes.length && excludes.includes(mouseDownTarget));
            const isContainedByPopper = popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
            if (isBound ||
                isTargetExists ||
                isContainedByEl ||
                isSelf ||
                isTargetExcluded ||
                isContainedByPopper) {
                return;
            }
            binding.value();
        };
    }
    const ClickOutside = {
        beforeMount(el, binding) {
            nodeList.set(el, {
                documentHandler: createDocumentHandler(el, binding),
                bindingFn: binding.value,
            });
        },
        updated(el, binding) {
            nodeList.set(el, {
                documentHandler: createDocumentHandler(el, binding),
                bindingFn: binding.value,
            });
        },
        unmounted(el) {
            nodeList.delete(el);
        },
    };
    export default ClickOutside;
    //# sourceMappingURL=directive.js.map
    

    起源地下载网 » 检测元素点击在指定范围之外指令实现,vue3+ts,文末有非ts实现,看elementPlus源码所得

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元