JS 判斷元素父子關係

在工作中遇到一個問題:如果界面元素很多,如何判斷當前點擊的元素,和某個元素是否有父子關係?

在這裏插入圖片描述

下面這個截圖中,可以看到有20個DIV在嵌套。如果界面發生一個點擊事件,在document 監聽到這個點擊事件,怎樣判斷點擊位於哪個父元素下面(PS 這個HTML代碼寫的語義不明確,層級很複雜)

查找資料後,有下面幾個解決方案

原生 JS

原生JS中可以使用 dom.contains(dom) 來判斷是否存在父子關係。

var father = document.getElementById('#wrap');
var son = event.target;
if (father.contains(son)) {
  console.log('點擊的元素是父元素的子元素');
} else {
  console.log('點擊的元素不是父元素的子元素');
}

注意:這個方法在舊版火狐或者IE瀏覽器中存在兼容性問題,詳情可以參考 https://www.cnblogs.com/cuixi/archive/2013/11/06/3409918.html 兼容寫法

// 早期 Firefox 兼容寫法
console.log(father.compareDocumentPosition(son));

// 下面是兼容寫法
var contains = function(a, b, itself){
  // 第一個節點是否包含第二個節點
  //contains 方法支持情況:chrome+ firefox9+ ie5+, opera9.64+,safari5.1.7+
  if (itself && a == b){
    return true
  }
  if (a.contains){
    if (a.nodeType === 9) {
      return true;
    }
    return a.contains(b);
  }
  else if (a.compareDocumentPosition){
    return !!(a.compareDocumentPosition(b) & 16);
  }
  while ((b = b.parentNode))
    if (a === b) {
      return true;
    }
  return false;
}

jquery

jquery 中給我們提供了一個方法,可以直接判斷父子關係(現在jquery使用已經不是很多了)

$.contains()方法用於判斷指定元素內是否包含另一個元素,即另一個DOM元素是否是指定DOM元素的後代。

$.contains( container, contained )
參數 描述
container Element類型 指定可能包含其他元素的祖輩容器元素。
contained Element類型 指定可能被其他元素包含的後代元素。

例子

$.contains(father, son)); // true
$.contains(son, father)); // false

詳情可參考:https://www.runoob.com/jquery/misc-contains.html

React

React 存在的特殊問題:React 父子組件實際渲染後可能位於不同的根組件中

React 渲染有兩種情況:第一個情況是傳統的父子組件,那麼使用傳統的 dom.contains(e.target) 可以判斷點擊對象的父子關係;第二種情況是 portals,例如使用 modal 或者 mask, 組件的邏輯是父子關係,但是渲染後的界面中,會把子組件掛在到根節點的某個節點,界面上顯示成不同的圖層,這樣使用傳統的 dom.contains 無法判斷組件的父子層級。下面是具體的解決方案:

在這裏插入圖片描述

Detecting outside click on a react component is surprisingly hard. A general approach is to have a global click handler on the document which checks if the click target is inside the editor container or not using editorContainer.contains(e.target). This approach works well until portals(入口站點) are used for editors. Portals render children into a DOM node that exists outside the DOM hierarchy of the parent component so editorContainer.contains(e.target) does not work. Here are some examples of the DOM structure with different types of editors。

很難檢測到React組件的外部的點擊事件。通常是在document上綁定全局點擊處理函數,以檢查點擊的對象(即event.target)是否位於編輯器容器內,或不使用editorContainer.contains(e.target)。在使用入口站點進行編輯之前,此方法效果很好。入口站點將子組件,渲染到父組件的DOM層次結構之外的DOM節點中,因此editorContainer.contains(e.target)不起作用。這是使用不同類型的編輯器的DOM結構的一些示例。

// SimpleEditor for example Texbox (No Portals)
<div react-data-grid>..<div>
  <div portal-created-by-the-grid-for-editors>
     <div editor-container>
       <div simple-editor>..</div>
     </div>
  <div>

// ComplexEditor for example Modals (using Portals)
  <div react-data-grid>..<div>
  <div portal-created-by-the-grid-for-editors>
     <div editor-container>
       // Nothing here
     </div>
  <div>
  <div portal-created-by-the-editor>
    <div complex-editor>..</div>
  </div>

One approach to detect outside click is to use event bubbling through portals. An event fired from inside a portal will propagate to ancestors in the containing React tree, even if those elements are not ancestors in the DOM tree. This means a click handler can be attached on the document and on the editor container. The editor container can set a flag to notify that the click was inside the editor and the document click handler can use this flag to call onClickOutside. This approach however has a few caveats

檢測外部點擊的一種方法是通過 portals 使用事件冒泡。即使這些元素不是DOM樹中的祖先,從portals內部觸發的事件也將傳播到包含的 React 樹中的祖先。這意味着可以在文檔和編輯器容器上附加單擊處理程序。編輯器容器可以設置一個標誌,以通知該單擊是在編輯器內部的,而文檔單擊處理程序可以使用此標誌來調用onClickOutside。但是,此方法有一些警告

  • Click handler on the document is set using document.addEventListener
  • Click handler on the editor container is set using onClick prop

-使用document.addEventListener設置文檔上的單擊處理程序
-使用onClick屬性設置編輯器容器上的單擊處理程序

This means if a child component inside the editor calls e.stopPropagation then the click handler on the editor container will not be called whereas document click handler will be called.
https://github.com/facebook/react/issues/12518 To solve this issue onClickCapture event is used.

這意味着,如果編輯器中的子組件調用e.stopPropagation,則不會調用編輯器容器上的單擊處理程序,而將調用文檔單擊處理程序。https://github.com/facebook/react/issues/12518。爲了解決此問題,使用了onClickCapture事件。

下面是具體的使用:在一個組件外部嵌套一個 ClickOutside 組件,這個組件會捕獲組件外部的點擊事件。

import React from 'react';

class ClickOutside extends React.Component {
  isClickedInside = false;

  componentDidMount() {
    document.addEventListener('click', this.handleDocumentClick);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (e) => {
    if (this.isClickedInside) {
      this.isClickedInside = false;
      return;
    }
    this.props.onClickOutside(e);
  };

  handleClick = () => {
    this.isClickedInside = true;
  };

  render() {
    return React.cloneElement(React.Children.only(this.props.children), {
        onClickCapture: this.handleClick
     }
   );
  }
}

export default ClickOutside;

後續繼續分析這個組件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章