探索clickout指令

探索clickout指令

隨着3大框架的風靡,咱們從之前的layer等UI庫遷移到了更增強大的UI庫,好比vue的好夥伴element,組件庫的做用是封裝一些經常使用的功能,將HTML、CSS、JS做爲一個功能單元封裝爲一個總體,向外界暴露合理的接口,它極大地提高了咱們的開發效率,最近遇到一個要本身寫一個select(選擇器)場景,如下的場景一會兒讓我懵了javascript

KylgHg.png

好比上圖的選擇器,咱們除了點擊輸入框時,會切換列表展開狀態,點擊列表項會收起列表,同時,咱們須要在點擊其餘區域時,也要關閉列表,本文基於此需求展開html

DOM判斷

這個需求最重要的點就是須要判斷點擊區域在指定區域以外,執行指定的邏輯,沿着這個思路,我居然想去了去計算當前點擊的座標是否在指定區域,這顯然是不行的,從視覺上難以判斷,有沒有可以從編碼上判斷的方法呢,好比,判斷點擊的DOM不是指定的DOM,因而有了初版的方案vue

// 給元素綁定click事件
element.addEventListener("click",(e) => {
  const { target } = e
  
  // 判斷target是否是在指定DOM
}, false);
複製代碼

這裏有兩個嚴重的問題java

  • 按照上述代碼,須要爲頁面上的每一個DOM元素都綁定一個事件,不管在代碼量和性能上,都十分很差node

  • 指定DOM只能是知足條件的,要是比較多,會致使這部分邏輯很複雜express

事件委託

原先的代碼,會致使綁定和事件在每一個DOM節點上重複,其實,程序只須要知道本次點擊的是誰,不須要關注綁定事件的是誰,這個時候,咱們的事件委託就上場了。框架

// 給元素綁定click事件
document.addEventListener("click",(e) => {
  const { target } = e
  
  // 判斷target是否是在指定DOM
}, false);
複製代碼

咱們將事件綁定在document上,默認狀況下,事件是遵循冒泡模型,從事件源往document上觸發對應類型的事件,因此事件點擊時候,可以在doucument上統一接受到事件源,另外,這裏測試了一下,由於在某些場景下,可能會使用到捕獲模型ide

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id="button"></button>
</body>
<script> // 註冊捕獲階段觸發的事件 document.getElementById("button").addEventListener("click", () => { }, true) // 代理document內元素的click  document.addEventListener("click", (e)=> { const { target } = e console.log(target); }) </script>
</html>
複製代碼

Ky8Qhj.png

能夠看到,即便使用了捕獲模型,咱們的事件源也是同樣能夠正確獲取,固然,這只是一個嘗試,通常來講,這個不用嘗試都知道是這樣的,利用事件委託,咱們很好地減小了事件綁定重複,有一個微不足道的缺點,就是阻止了事件傳播,如阻止事件冒泡的元素不能正確觸發咱們的document事件,可是這個能夠接受性能

contains

咱們一開始是判斷DOM節點特有的標誌來執行咱們的程序測試

// 判斷指定節點
if($(target).attr("id") === xxx)
複製代碼

這樣子太受限了,須要寫不少條件,何況咱們須要的是區域,因此最好可以有個API能判斷是否在一個區域,正好,有一個API

node.contains( otherNode ) 

node 是否包含otherNode節點.
otherNode 是不是node的後代節點.
複製代碼

contains這個API,能夠判斷一個節點是否包含在另一個節點以內,這個內部是指是否爲判斷節點自己或者其後臺節點,因而,咱們利用此API,就能夠完美判斷一個節點是否在一個區域以外

// 觸發事件節點在區域外
!node.contains(target)
複製代碼

最終的代碼是

document.addEventListener("click",(e) => {
  const { target } = e
  
  if(!node.contains(target)) {
    // 點擊區域以外的事情
  }
}, false);
複製代碼

複用拓展

上述的最終代碼已經能夠用了,可是對於多個元素來講,他們須要不一樣的callback,這裏咱們咱們進行一個改造

let nodeList = []

document.addEventListener("click",(e) => {
  const { target } = e
  nodeList.map(({node, cb}) => {
    if(!node.contains(target)) {
      cb()
      // 點擊區域以外的事情
    }
  })

 
}, false);


// 將你須要實現點擊區域以外的邏輯置入nodeList之中
nodeList.push({
  node: node,
  cb: function () {

  }
})

複製代碼

因爲節點和callback是每一個須要此交互的都不一樣,這裏講節點和callback存儲到一個全局的列表中去,而後點擊頁面時,去觸發列表中點擊元素不在其指定範圍的callback,使得邏輯得以複用,而每一個元素本身的業務邏輯能夠分離,不過這裏有個小問題就是要注意在指定節點移除時,要及時手動移除nodeList中對應的邏輯

v-clickout指令實現

這部分原本想着本身寫的,而後忽然想到element可能實現了,藉助Vue的指令,咱們能更方便的管理DOM的生命週期,輕鬆添加、銷燬callback,下面是element的clickout指令源碼,思路大體相同,只是基於vue實現

const on = (function() {
  if (document.addEventListener) {
    return function(element, event, handler) {
      if (element && event && handler) {
        element.addEventListener(event, handler, false);
      }
    };
  } else {
    return function(element, event, handler) {
      if (element && event && handler) {
        element.attachEvent('on' + event, handler);
      }
    };
  }
})();

const nodeList = [];
const ctx = '@@clickoutsideContext';

let startClick;
let seed = 0;

on(document, 'mousedown', e => (startClick = e));

on(document, 'mouseup', e => {
  nodeList.forEach(node => node[ctx].documentHandler(e, startClick));
});

function createDocumentHandler(el, binding, vnode) {
  return function(mouseup = {}, mousedown = {}) {
    if (!vnode ||
      !vnode.context ||
      !mouseup.target ||
      !mousedown.target ||
      el.contains(mouseup.target) ||
      el.contains(mousedown.target) ||
      el === mouseup.target ||
      (vnode.context.popperElm &&
      (vnode.context.popperElm.contains(mouseup.target) ||
      vnode.context.popperElm.contains(mousedown.target)))) return;

    if (binding.expression &&
      el[ctx].methodName &&
      vnode.context[el[ctx].methodName]) {
      vnode.context[el[ctx].methodName]();
    } else {
      el[ctx].bindingFn && el[ctx].bindingFn();
    }
  };
}

/** * v-clickoutside * @desc 點擊元素外面纔會觸發的事件 * @example * ```vue * <div v-element-clickoutside="handleClose"> * ``` */
export default {
  bind(el, binding, vnode) {
    nodeList.push(el);
    const id = seed++;
    el[ctx] = {
      id,
      documentHandler: createDocumentHandler(el, binding, vnode),
      methodName: binding.expression,
      bindingFn: binding.value
    };
  },

  update(el, binding, vnode) {
    el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
    el[ctx].methodName = binding.expression;
    el[ctx].bindingFn = binding.value;
  },

  unbind(el) {
    let len = nodeList.length;

    for (let i = 0; i < len; i++) {
      if (nodeList[i][ctx].id === el[ctx].id) {
        nodeList.splice(i, 1);
        break;
      }
    }
    delete el[ctx];
  }
};

複製代碼
相關文章
相關標籤/搜索