Phát hiện nhấp bên ngoài thành phần React


410

Tôi đang tìm cách để phát hiện nếu một sự kiện nhấp xảy ra bên ngoài một thành phần, như được mô tả trong bài viết này . jQuery near () được sử dụng để xem liệu mục tiêu từ một sự kiện nhấp có yếu tố dom là một trong những cha mẹ của nó không. Nếu có sự trùng khớp, sự kiện nhấp thuộc về một trong những đứa trẻ và do đó không được coi là nằm ngoài thành phần.

Vì vậy, trong thành phần của tôi, tôi muốn đính kèm một trình xử lý nhấp chuột vào cửa sổ. Khi xử lý bắn tôi cần so sánh mục tiêu với con dom của thành phần của tôi.

Sự kiện nhấp chứa các thuộc tính như "đường dẫn" dường như giữ đường dẫn dom mà sự kiện đã đi. Tôi không chắc nên so sánh cái gì hoặc làm thế nào để vượt qua nó một cách tốt nhất và tôi nghĩ ai đó chắc chắn đã đưa nó vào một chức năng tiện ích thông minh ... Không?


Bạn có thể đính kèm trình xử lý nhấp chuột vào cha mẹ chứ không phải cửa sổ?
J. Mark Stevens

Nếu bạn đính kèm một trình xử lý nhấp chuột cho cha mẹ bạn biết khi nào phần tử đó hoặc một trong số các con của họ được nhấp, nhưng tôi cần phát hiện tất cả các vị trí khác được nhấp, do đó trình xử lý cần được gắn vào cửa sổ.
Thijs Koerselman

Tôi đã xem bài báo sau phản ứng trước đó. Làm thế nào về việc thiết lập một clickState trong thành phần hàng đầu và chuyển các hành động nhấp chuột từ những đứa trẻ. Sau đó, bạn sẽ kiểm tra đạo cụ ở trẻ em để quản lý trạng thái đóng mở.
J. Mark Stevens

Thành phần hàng đầu sẽ là ứng dụng của tôi. Nhưng thành phần nghe sâu một vài cấp độ và không có vị trí nghiêm ngặt trong dom. Tôi không thể thêm trình xử lý nhấp chuột vào tất cả các thành phần trong ứng dụng của mình chỉ vì một trong số chúng quan tâm để biết liệu bạn có nhấp vào đâu đó bên ngoài ứng dụng đó không. Các thành phần khác không nên nhận thức về logic này bởi vì điều đó sẽ tạo ra sự phụ thuộc khủng khiếp và mã soạn sẵn.
Thijs Koerselman

5
Tôi muốn giới thiệu cho bạn một lib rất tốt đẹp. được tạo bởi AirBnb: github.com/airbnb/react-outside-click-handler
Osoian Marcel

Câu trả lời:


683

Thay đổi cách sử dụng trong React 16.3+.

Giải pháp sau đây sử dụng ES6 và tuân theo các thực tiễn tốt nhất để ràng buộc cũng như thiết lập ref thông qua một phương thức.

Để xem nó trong hành động:

Thực hiện lớp học:

import React, { Component } from 'react';

/**
 * Component that alerts if you click outside of it
 */
export default class OutsideAlerter extends Component {
    constructor(props) {
        super(props);

        this.wrapperRef = React.createRef();
        this.setWrapperRef = this.setWrapperRef.bind(this);
        this.handleClickOutside = this.handleClickOutside.bind(this);
    }

    componentDidMount() {
        document.addEventListener('mousedown', this.handleClickOutside);
    }

    componentWillUnmount() {
        document.removeEventListener('mousedown', this.handleClickOutside);
    }

    /**
     * Alert if clicked on outside of element
     */
    handleClickOutside(event) {
        if (this.wrapperRef && !this.wrapperRef.current.contains(event.target)) {
            alert('You clicked outside of me!');
        }
    }

    render() {
        return <div ref={this.wrapperRef}>{this.props.children}</div>;
    }
}

OutsideAlerter.propTypes = {
    children: PropTypes.element.isRequired,
};

Móc thực hiện:

import React, { useRef, useEffect } from "react";

/**
 * Hook that alerts clicks outside of the passed ref
 */
function useOutsideAlerter(ref) {
    useEffect(() => {
        /**
         * Alert if clicked on outside of element
         */
        function handleClickOutside(event) {
            if (ref.current && !ref.current.contains(event.target)) {
                alert("You clicked outside of me!");
            }
        }

        // Bind the event listener
        document.addEventListener("mousedown", handleClickOutside);
        return () => {
            // Unbind the event listener on clean up
            document.removeEventListener("mousedown", handleClickOutside);
        };
    }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function OutsideAlerter(props) {
    const wrapperRef = useRef(null);
    useOutsideAlerter(wrapperRef);

    return <div ref={wrapperRef}>{props.children}</div>;
}

3
Làm thế nào để bạn kiểm tra điều này mặc dù? Làm cách nào để bạn gửi một event.target hợp lệ cho hàm handleClickOutside?
csilk

5
Làm thế nào bạn có thể sử dụng các sự kiện tổng hợp của React, thay vì document.addEventListenerở đây?
Ralph David Abernathy

9
@ polkovnikov.ph 1. contextchỉ cần thiết làm đối số trong hàm tạo nếu được sử dụng. Nó không được sử dụng trong trường hợp này. Lý do nhóm phản ứng khuyến nghị nên có propsmột đối số trong hàm tạo là vì việc sử dụng this.propstrước khi gọi super(props)sẽ không được xác định và có thể dẫn đến lỗi. contextvẫn có sẵn trên các nút bên trong, đó là toàn bộ mục đích của context. Bằng cách này, bạn không phải truyền nó từ thành phần này sang thành phần khác như bạn làm với đạo cụ. 2. Đây chỉ là một sở thích phong cách, và không đảm bảo tranh luận trong trường hợp này.
Ben Bud

7
Tôi nhận được lỗi sau: "this.wrapperRef.contains không phải là một chức năng"
Bogdan

5
@Bogdan, tôi đã gặp lỗi tương tự khi sử dụng một thành phần theo kiểu. Cân nhắc sử dụng <div> ở cấp cao nhất
user3040068

149

Đây là giải pháp phù hợp nhất với tôi mà không cần đính kèm các sự kiện vào container:

Một số phần tử HTML nhất định có thể có cái được gọi là " tiêu điểm ", ví dụ như các phần tử đầu vào. Những yếu tố đó cũng sẽ phản ứng với sự kiện mờ , khi họ mất trọng tâm đó.

Để cung cấp cho bất kỳ yếu tố nào khả năng tập trung, chỉ cần đảm bảo thuộc tính tabindex của nó được đặt thành bất kỳ thứ gì ngoài -1. Trong HTML thông thường sẽ bằng cách đặt tabindexthuộc tính, nhưng trong React bạn phải sử dụng tabIndex(lưu ý viết hoa I).

Bạn cũng có thể làm điều đó thông qua JavaScript với element.setAttribute('tabindex',0)

Đây là những gì tôi đã sử dụng nó để làm menu DropDown tùy chỉnh.

var DropDownMenu = React.createClass({
    getInitialState: function(){
        return {
            expanded: false
        }
    },
    expand: function(){
        this.setState({expanded: true});
    },
    collapse: function(){
        this.setState({expanded: false});
    },
    render: function(){
        if(this.state.expanded){
            var dropdown = ...; //the dropdown content
        } else {
            var dropdown = undefined;
        }

        return (
            <div className="dropDownMenu" tabIndex="0" onBlur={ this.collapse } >
                <div className="currentValue" onClick={this.expand}>
                    {this.props.displayValue}
                </div>
                {dropdown}
            </div>
        );
    }
});

10
Điều này sẽ không tạo ra vấn đề với khả năng tiếp cận? Vì bạn tạo một phần tử phụ có thể tập trung chỉ để phát hiện khi nó đi vào / ra tiêu điểm, nhưng tương tác có nên nằm trong các giá trị thả xuống không?
Thijs Koerselman

2
Đây là một cách tiếp cận rất thú vị. Hoạt động hoàn hảo cho tình huống của tôi, nhưng tôi muốn tìm hiểu làm thế nào điều này có thể ảnh hưởng đến khả năng tiếp cận.
klinore

17
Tôi đã "làm việc" ở một mức độ nào đó, đó là một bản hack nhỏ thú vị. Vấn đề của tôi là nội dung thả xuống của tôi là display:absolutedo việc thả xuống sẽ không ảnh hưởng đến kích thước của div cha. Điều này có nghĩa là khi tôi nhấp vào một mục trong danh sách thả xuống, onblur sẽ kích hoạt.
Dave

2
Tôi gặp vấn đề với cách tiếp cận này, bất kỳ yếu tố nào trong nội dung thả xuống đều không kích hoạt các sự kiện như onClick.
AnimaSola

8
Bạn có thể phải đối mặt với một số vấn đề khác nếu nội dung thả xuống có chứa một số yếu tố có thể tập trung như đầu vào biểu mẫu chẳng hạn. Họ sẽ đánh cắp sự tập trung của bạn, onBlur sẽ được kích hoạt trên thùng chứa thả xuống của bạn và expanded sẽ được đặt thành false.
Kamagatos

106

Tôi đã bị mắc kẹt trong cùng một vấn đề. Tôi đến bữa tiệc muộn một chút, nhưng với tôi đây là một giải pháp thực sự tốt. Hy vọng nó sẽ giúp ích cho người khác. Bạn cần nhập findDOMNodetừreact-dom

import ReactDOM from 'react-dom';
// ... ✂

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

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

handleClickOutside = event => {
    const domNode = ReactDOM.findDOMNode(this);

    if (!domNode || !domNode.contains(event.target)) {
        this.setState({
            visible: false
        });
    }
}

Phương pháp tiếp cận móc phản ứng (16.8 +)

Bạn có thể tạo ra một cái móc tái sử dụng được gọi là useComponentVisible.

import { useState, useEffect, useRef } from 'react';

export default function useComponentVisible(initialIsVisible) {
    const [isComponentVisible, setIsComponentVisible] = useState(initialIsVisible);
    const ref = useRef(null);

    const handleClickOutside = (event) => {
        if (ref.current && !ref.current.contains(event.target)) {
            setIsComponentVisible(false);
        }
    };

    useEffect(() => {
        document.addEventListener('click', handleClickOutside, true);
        return () => {
            document.removeEventListener('click', handleClickOutside, true);
        };
    });

    return { ref, isComponentVisible, setIsComponentVisible };
}

Sau đó, trong thành phần bạn muốn thêm chức năng để làm như sau:

const DropDown = () => {
    const { ref, isComponentVisible } = useComponentVisible(true);
    return (
       <div ref={ref}>
          {isComponentVisible && (<p>Dropdown Component</p>)}
       </div>
    );

}

Tìm một ví dụ về mã và hộp ở đây.


1
@LeeHanKyeol Không hoàn toàn - câu trả lời này gọi trình xử lý sự kiện trong giai đoạn bắt giữ xử lý sự kiện, trong khi câu trả lời liên quan đến việc gọi trình xử lý sự kiện trong giai đoạn bong bóng.
stevejay

3
Đây phải là câu trả lời được chấp nhận. Hoạt động hoàn hảo cho các menu thả xuống với định vị tuyệt đối.
máy rửa

1
ReactDOM.findDOMNodevà không được dùng nữa, nên sử dụng các refcuộc gọi lại: github.com/yannickcr/eslint-plugin-react/issues/ Kẻ
dain

2
giải pháp tuyệt vời và sạch sẽ
Karim

1
Điều này làm việc cho tôi mặc dù tôi đã phải hack thành phần của bạn để thiết lập lại hiển thị trở lại 'true' sau 200ms (nếu không menu của tôi không bao giờ hiển thị lại). Cảm ơn bạn!
Lee Moe

87

Sau khi thử nhiều phương pháp ở đây, tôi quyết định sử dụng github.com/Pomax/react-onclickoutside vì mức độ hoàn thiện của nó.

Tôi đã cài đặt mô-đun qua npm và nhập nó vào thành phần của mình:

import onClickOutside from 'react-onclickoutside'

Sau đó, trong lớp thành phần của tôi, tôi đã định nghĩa handleClickOutsidephương thức:

handleClickOutside = () => {
  console.log('onClickOutside() method called')
}

Và khi xuất thành phần của tôi, tôi gói nó vào onClickOutside():

export default onClickOutside(NameOfComponent)

Đó là nó.


2
Có bất kỳ lợi thế cụ thể nào cho vấn đề này so với tabIndex/ onBlurphương pháp được đề xuất bởi Pablo không? Làm thế nào để thực hiện công việc, và hành vi của nó khác với onBlurcách tiếp cận như thế nào?
Đánh dấu Amery

4
Tốt hơn hết là sử dụng một thành phần chuyên dụng sau đó sử dụng hack tab Index. Nâng cao nó!
Atul Yadav

5
@MarkAmery - Tôi đưa ra nhận xét về tabIndex/onBlurcách tiếp cận. Nó không hoạt động khi thả xuống position:absolute, chẳng hạn như một menu lơ lửng trên các nội dung khác.
Beau Smith

4
Một ưu điểm khác của việc sử dụng tính năng này trên tabindex là giải pháp tabindex cũng kích hoạt sự kiện mờ nếu bạn tập trung vào một phần tử con
Jemar Jones

1
@BeauSmith - Cảm ơn rất nhiều! Cứu ngày của tôi!
Mika

38

Tôi đã tìm thấy một giải pháp nhờ Ben Alpert trên thảo luận.reactjs.org . Cách tiếp cận được đề xuất gắn một trình xử lý vào tài liệu nhưng hóa ra lại có vấn đề. Nhấp vào một trong các thành phần trong cây của tôi dẫn đến việc đăng ký lại đã loại bỏ phần tử được nhấp khi cập nhật. Bởi vì việc đăng ký lại từ React xảy ra trước khi trình xử lý thân tài liệu được gọi, phần tử không được phát hiện là "bên trong" cây.

Giải pháp cho vấn đề này là thêm trình xử lý vào phần tử gốc của ứng dụng.

chủ yếu:

window.__myapp_container = document.getElementById('app')
React.render(<App/>, window.__myapp_container)

thành phần:

import { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';

export default class ClickListener extends Component {

  static propTypes = {
    children: PropTypes.node.isRequired,
    onClickOutside: PropTypes.func.isRequired
  }

  componentDidMount () {
    window.__myapp_container.addEventListener('click', this.handleDocumentClick)
  }

  componentWillUnmount () {
    window.__myapp_container.removeEventListener('click', this.handleDocumentClick)
  }

  /* using fat arrow to bind to instance */
  handleDocumentClick = (evt) => {
    const area = ReactDOM.findDOMNode(this.refs.area);

    if (!area.contains(evt.target)) {
      this.props.onClickOutside(evt)
    }
  }

  render () {
    return (
      <div ref='area'>
       {this.props.children}
      </div>
    )
  }
}

8
Điều này không còn hiệu quả với tôi nữa với React 0.14.7 - có thể React đã thay đổi điều gì đó hoặc có thể tôi đã mắc lỗi khi điều chỉnh mã cho tất cả các thay đổi đối với React. Thay vào đó, tôi đang sử dụng github.com/Pomax/react-onclickoutside hoạt động như một bùa mê.
Nicole

1
Hừm. Tôi không thấy bất kỳ lý do tại sao điều này nên làm việc. Tại sao một trình xử lý trên nút DOM gốc của ứng dụng phải được đảm bảo kích hoạt trước khi trình xử lý lại được kích hoạt bởi một trình xử lý khác nếu một trình xử lý không được kích hoạt document?
Đánh dấu Amery

1
Một điểm UX thuần túy: mousedowncó lẽ sẽ là một xử lý tốt hơn clickở đây. Trong hầu hết các ứng dụng, hành vi đóng menu bằng cách nhấp vào bên ngoài xảy ra ngay khi bạn thả chuột xuống, không phải khi bạn phát hành. Hãy thử nó, ví dụ, với Stack Overflow của cờ hoặc phần lời thoại, với một trong những Dropdowns từ thanh menu trên cùng của trình duyệt.
Mark Amery

ReactDOM.findDOMNodevà chuỗi refkhông được dùng nữa, nên sử dụng các refcuộc gọi lại: github.com/yannickcr/eslint-plugin-react/issues/ Kẻ
dain

đây là giải pháp đơn giản nhất và hoạt động hoàn hảo với tôi ngay cả khi gắn vàodocument
Flion

37

Triển khai hook dựa trên bài nói chuyện xuất sắc của Tanner Linsley tại JSConf Hawaii 2020 :

useOuterClick API móc

const Client = () => {
  const innerRef = useOuterClick(ev => { /* what you want do on outer click */ });
  return <div ref={innerRef}> Inside </div> 
};

Thực hiện

/*
  Custom Hook
*/
function useOuterClick(callback) {
  const innerRef = useRef();
  const callbackRef = useRef();

  // set current callback in ref, before second useEffect uses it
  useEffect(() => { // useEffect wrapper to be safe for concurrent mode
    callbackRef.current = callback;
  });

  useEffect(() => {
    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);

    // read most recent callback and innerRef dom node from refs
    function handleClick(e) {
      if (
        innerRef.current && 
        callbackRef.current &&
        !innerRef.current.contains(e.target)
      ) {
        callbackRef.current(e);
      }
    }
  }, []); // no need for callback + innerRef dep
  
  return innerRef; // return ref; client can omit `useRef`
}

/*
  Usage 
*/
const Client = () => {
  const [counter, setCounter] = useState(0);
  const innerRef = useOuterClick(e => {
    // counter state is up-to-date, when handler is called
    alert(`Clicked outside! Increment counter to ${counter + 1}`);
    setCounter(c => c + 1);
  });
  return (
    <div>
      <p>Click outside!</p>
      <div id="container" ref={innerRef}>
        Inside, counter: {counter}
      </div>
    </div>
  );
};

ReactDOM.render(<Client />, document.getElementById("root"));
#container { border: 1px solid red; padding: 20px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js" integrity="sha256-Ef0vObdWpkMAnxp39TYSLVS/vVUokDE8CDFnx7tjY6U=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js" integrity="sha256-p2yuFdE8hNZsQ31Qk+s8N+Me2fL5cc6NKXOC0U9uGww=" crossorigin="anonymous"></script>
<script> var {useRef, useEffect, useCallback, useState} = React</script>
<div id="root"></div>

useOuterClicksử dụng các ref có thể thay đổi để tạo API nạc cho Client. Một cuộc gọi lại có thể được thiết lập mà không cần phải ghi nhớ nó thông qua useCallback. Phần thân gọi lại vẫn có quyền truy cập vào các đạo cụ và trạng thái gần đây nhất (không có giá trị đóng cũ).


Lưu ý cho người dùng iOS

iOS chỉ xử lý một số yếu tố nhất định là có thể nhấp. Để tránh hành vi này, hãy chọn một trình nghe nhấp chuột bên ngoài khác document- không có gì bao gồm cả body. Ví dụ: bạn có thể thêm một trình lắng nghe vào gốc React divtrong ví dụ trên và mở rộng chiều cao của nó ( height: 100vhhoặc tương tự) để bắt tất cả các nhấp chuột bên ngoài. Nguồn: quirksmode.orgcâu trả lời này


không hoạt động trên thiết bị di động nên không tự nguyện
Omar

@Omar vừa thử nghiệm Codeandbox trong bài đăng của tôi với Firefox 67.0.3 trên thiết bị Android - hoạt động tốt. Bạn có thể giải thích những gì trình duyệt bạn sử dụng, những gì là lỗi vv?
ford04

Không có lỗi nhưng không hoạt động trên chrome, iphone 7+. Nó không phát hiện ra các vòi bên ngoài. Trong các công cụ phát triển chrome trên thiết bị di động, nó hoạt động nhưng trong một thiết bị ios thực sự thì nó không hoạt động.
Omar

1
@ ford04 cảm ơn bạn đã đào nó ra, tôi tình cờ thấy một chủ đề khác đang gợi ý thủ thuật sau đây. @media (hover: none) and (pointer: coarse) { body { cursor:pointer } }Thêm điều này trong phong cách toàn cầu của tôi, dường như nó đã khắc phục vấn đề.
Kostas Sarantopoulos

1
@ ford04 yeah, btw theo chủ đề này thậm chí còn có một truy vấn phương tiện cụ thể hơn @supports (-webkit-overflow-scrolling: touch)chỉ nhắm mục tiêu đến iOS mặc dù tôi không biết nhiều hơn bao nhiêu! Tôi chỉ thử nó và nó hoạt động.
Kostas Sarantopoulos

30

Không có câu trả lời nào khác ở đây làm việc cho tôi. Tôi đã cố gắng để ẩn một cửa sổ bật lên trên mờ, nhưng vì nội dung được định vị tuyệt đối, onBlur đã bắn ngay cả khi nhấp vào nội dung bên trong.

Đây là một cách tiếp cận đã làm việc cho tôi:

// Inside the component:
onBlur(event) {
    // currentTarget refers to this component.
    // relatedTarget refers to the element where the user clicked (or focused) which
    // triggered this event.
    // So in effect, this condition checks if the user clicked outside the component.
    if (!event.currentTarget.contains(event.relatedTarget)) {
        // do your thing.
    }
},

Hi vọng điêu nay co ich.


Rất tốt! Cảm ơn. Nhưng trong trường hợp của tôi, đó là một vấn đề để bắt "vị trí hiện tại" với vị trí tuyệt đối cho div, vì vậy tôi sử dụng "OnMouseLeave" cho div với đầu vào và thả xuống lịch để chỉ vô hiệu hóa tất cả div khi chuột rời khỏi div.
Alex

1
Cảm ơn! Giúp tôi rất nhiều.
Ângelo Lucas

1
Tôi thích điều này tốt hơn những phương pháp đính kèm document. Cảm ơn.
kyw

Để làm việc này, bạn cần đảm bảo rằng phần tử được nhấp (có liên quan đến mục tiêu) có thể tập trung được. Xem stackoverflow.com/questions/42764494/
Mạnh

21

[Cập nhật] Giải pháp với Phản ứng ^ 16.8 bằng cách sử dụng Móc

CodeSandbox

import React, { useEffect, useRef, useState } from 'react';

const SampleComponent = () => {
    const [clickedOutside, setClickedOutside] = useState(false);
    const myRef = useRef();

    const handleClickOutside = e => {
        if (!myRef.current.contains(e.target)) {
            setClickedOutside(true);
        }
    };

    const handleClickInside = () => setClickedOutside(false);

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => document.removeEventListener('mousedown', handleClickOutside);
    });

    return (
        <button ref={myRef} onClick={handleClickInside}>
            {clickedOutside ? 'Bye!' : 'Hello!'}
        </button>
    );
};

export default SampleComponent;

Giải pháp với Phản ứng ^ 16.3 :

CodeSandbox

import React, { Component } from "react";

class SampleComponent extends Component {
  state = {
    clickedOutside: false
  };

  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  myRef = React.createRef();

  handleClickOutside = e => {
    if (!this.myRef.current.contains(e.target)) {
      this.setState({ clickedOutside: true });
    }
  };

  handleClickInside = () => this.setState({ clickedOutside: false });

  render() {
    return (
      <button ref={this.myRef} onClick={this.handleClickInside}>
        {this.state.clickedOutside ? "Bye!" : "Hello!"}
      </button>
    );
  }
}

export default SampleComponent;

3
Đây là giải pháp đi đến bây giờ. Nếu bạn đang gặp lỗi .contains is not a function, có thể là do bạn chuyển ref prop cho một thành phần tùy chỉnh chứ không phải là một thành phần DOM thực sự như<div>
willlma

Đối với những người đang cố gắng chuyển refprop sang một thành phần tùy chỉnh, bạn có thể muốn xem quaReact.forwardRef
onoya

4

Đây là cách tiếp cận của tôi (bản demo - https://jsfiddle.net/agymay93/4/ ):

Tôi đã tạo thành phần đặc biệt được gọi WatchClickOutsidevà nó có thể được sử dụng như (tôi giả sử JSXcú pháp):

<WatchClickOutside onClickOutside={this.handleClose}>
  <SomeDropdownEtc>
</WatchClickOutside>

Đây là mã WatchClickOutsidethành phần:

import React, { Component } from 'react';

export default class WatchClickOutside extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentWillMount() {
    document.body.addEventListener('click', this.handleClick);
  }

  componentWillUnmount() {
    // remember to remove all events to avoid memory leaks
    document.body.removeEventListener('click', this.handleClick);
  }

  handleClick(event) {
    const {container} = this.refs; // get container that we'll wait to be clicked outside
    const {onClickOutside} = this.props; // get click outside callback
    const {target} = event; // get direct click event target

    // if there is no proper callback - no point of checking
    if (typeof onClickOutside !== 'function') {
      return;
    }

    // if target is container - container was not clicked outside
    // if container contains clicked target - click was not outside of it
    if (target !== container && !container.contains(target)) {
      onClickOutside(event); // clicked outside - fire callback
    }
  }

  render() {
    return (
      <div ref="container">
        {this.props.children}
      </div>
    );
  }
}

Giải pháp tuyệt vời - để sử dụng, tôi đã thay đổi để nghe tài liệu thay vì cơ thể trong trường hợp các trang có nội dung ngắn và thay đổi ref từ chuỗi thành tham chiếu dom: jsfiddle.net/agymay93/9
Billy Moon

và sử dụng span thay vì div vì nó ít có khả năng thay đổi bố cục và thêm bản demo xử lý các nhấp chuột bên ngoài cơ thể: jsfiddle.net/agymay93/10
Billy Moon

Chuỗi refkhông được dùng nữa, nên sử dụng các refcuộc gọi lại: Reacjs.org/docs/refs-and-the-dom.html
dain

4

Điều này đã có nhiều câu trả lời nhưng họ không giải quyết e.stopPropagation()và ngăn nhấp vào các liên kết phản ứng bên ngoài yếu tố bạn muốn đóng.

Do thực tế là React có trình xử lý sự kiện nhân tạo của riêng bạn, bạn không thể sử dụng tài liệu làm cơ sở cho người nghe sự kiện. Bạn cần phải làm e.stopPropagation()điều này vì React sử dụng tài liệu của chính nó. Nếu bạn sử dụng ví dụ document.querySelector('body')thay thế. Bạn có thể ngăn chặn nhấp chuột từ liên kết React. Sau đây là một ví dụ về cách tôi thực hiện nhấp bên ngoài và đóng.
Điều này sử dụng ES6React 16.3 .

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      isOpen: false,
    };

    this.insideContainer = React.createRef();
  }

  componentWillMount() {
    document.querySelector('body').addEventListener("click", this.handleClick, false);
  }

  componentWillUnmount() {
    document.querySelector('body').removeEventListener("click", this.handleClick, false);
  }

  handleClick(e) {
    /* Check that we've clicked outside of the container and that it is open */
    if (!this.insideContainer.current.contains(e.target) && this.state.isOpen === true) {
      e.preventDefault();
      e.stopPropagation();
      this.setState({
        isOpen: false,
      })
    }
  };

  togggleOpenHandler(e) {
    e.preventDefault();

    this.setState({
      isOpen: !this.state.isOpen,
    })
  }

  render(){
    return(
      <div>
        <span ref={this.insideContainer}>
          <a href="#open-container" onClick={(e) => this.togggleOpenHandler(e)}>Open me</a>
        </span>
        <a href="/" onClick({/* clickHandler */})>
          Will not trigger a click when inside is open.
        </a>
      </div>
    );
  }
}

export default App;

3

Đối với những người cần định vị tuyệt đối, một tùy chọn đơn giản mà tôi đã chọn là thêm một thành phần trình bao bọc được tạo kiểu để bao phủ toàn bộ trang bằng một nền trong suốt. Sau đó, bạn có thể thêm một onClick vào thành phần này để đóng thành phần bên trong của bạn.

<div style={{
        position: 'fixed',
        top: '0', right: '0', bottom: '0', left: '0',
        zIndex: '1000',
      }} onClick={() => handleOutsideClick()} >
    <Content style={{position: 'absolute'}}/>
</div>

Vì hiện tại nếu bạn thêm một trình xử lý nhấp chuột vào nội dung, sự kiện cũng sẽ được truyền đến div trên và do đó kích hoạt handlerOutsideClick. Nếu đây không phải là hành vi mong muốn của bạn, chỉ cần dừng chương trình sự kiện trên trình xử lý của bạn.

<Content style={{position: 'absolute'}} onClick={e => {
                                          e.stopPropagation();
                                          desiredFunctionCall();
                                        }}/>

`


Vấn đề với cách tiếp cận này là bạn không thể có bất kỳ nhấp chuột nào vào Nội dung - div sẽ nhận được nhấp chuột thay thế.
rbonick

Vì nội dung nằm trong div nếu bạn không dừng việc truyền bá sự kiện, cả hai sẽ nhận được nhấp chuột. Đây thường là một hành vi mong muốn nhưng nếu bạn không muốn nhấp chuột được truyền đến div, chỉ cần dừng eventPropagation trên nội dung của bạn trên trình xử lý onClick. Tôi đã cập nhật câu trả lời của tôi để hiển thị như thế nào.
Anthony Garant

3

Để mở rộng câu trả lời được chấp nhận của Ben Bud , nếu bạn đang sử dụng các thành phần theo kiểu, việc chuyển các tham chiếu theo cách đó sẽ gây ra lỗi cho bạn như "this.wrapperRef.contains không phải là một chức năng".

Các sửa chữa được đề xuất, trong các ý kiến, để bọc thành phần theo kiểu với một div và vượt qua ref ở đó, hoạt động. Có nói rằng, trong các tài liệu của họ, họ đã giải thích lý do cho việc này và việc sử dụng refs đúng cách trong các thành phần theo kiểu:

Truyền một ref prop cho một thành phần được tạo kiểu sẽ cung cấp cho bạn một thể hiện của trình bao bọc StyledComponent, nhưng không phải cho nút DOM bên dưới. Điều này là do cách thức làm việc của refs. Không thể gọi các phương thức DOM, như tiêu điểm, trên các hàm bao của chúng tôi trực tiếp. Để có được một tham chiếu đến nút DOM được bao bọc thực tế, thay vào đó hãy chuyển cuộc gọi lại đến prop bên trong.

Thích như vậy:

<StyledDiv innerRef={el => { this.el = el }} />

Sau đó, bạn có thể truy cập nó trực tiếp trong chức năng "handleClickOutside":

handleClickOutside = e => {
    if (this.el && !this.el.contains(e.target)) {
        console.log('clicked outside')
    }
}

Điều này cũng áp dụng cho phương pháp "onBlur":

componentDidMount(){
    this.el.focus()
}
blurHandler = () => {
    console.log('clicked outside')
}
render(){
    return(
        <StyledDiv
            onBlur={this.blurHandler}
            tabIndex="0"
            innerRef={el => { this.el = el }}
        />
    )
}

3

Material-UI có một thành phần nhỏ để giải quyết vấn đề này: https : // m vật liệu- comp.com/entsonents / click -away-listener / mà bạn có thể chọn nó. Nó có trọng lượng 1,5 kB được nén, nó hỗ trợ di động, IE 11 và cổng.


2

Mối quan tâm lớn nhất của tôi với tất cả các câu trả lời khác là phải lọc các sự kiện nhấp chuột từ gốc / gốc xuống. Tôi thấy cách dễ nhất là chỉ cần đặt phần tử anh chị em với vị trí: cố định, chỉ số z 1 phía sau thả xuống và xử lý sự kiện nhấp chuột trên phần tử cố định bên trong cùng một thành phần. Giữ mọi thứ tập trung vào một thành phần nhất định.

Mã ví dụ

#HTML
<div className="parent">
  <div className={`dropdown ${this.state.open ? open : ''}`}>
    ...content
  </div>
  <div className="outer-handler" onClick={() => this.setState({open: false})}>
  </div>
</div>

#SASS
.dropdown {
  display: none;
  position: absolute;
  top: 0px;
  left: 0px;
  z-index: 100;
  &.open {
    display: block;
  }
}
.outer-handler {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    opacity: 0;
    z-index: 99;
    display: none;
    &.open {
      display: block;
    }
}

1
Hừm. Nó sẽ làm việc với nhiều trường hợp? Hoặc mỗi yếu tố cố định có khả năng chặn nhấp chuột cho các yếu tố khác?
Thijs Koerselman

2
componentWillMount(){

  document.addEventListener('mousedown', this.handleClickOutside)
}

handleClickOutside(event) {

  if(event.path[0].id !== 'your-button'){
     this.setState({showWhatever: false})
  }
}

Sự kiện path[0]là mục cuối cùng được nhấp


3
Đảm bảo bạn xóa trình lắng nghe sự kiện khi thành phần ngắt kết nối để ngăn các sự cố quản lý bộ nhớ, v.v.
agm1984

2

Tôi đã sử dụng mô-đun này (tôi không có liên kết với tác giả)

npm install react-onclickout --save

const ClickOutHandler = require('react-onclickout');
 
class ExampleComponent extends React.Component {
 
  onClickOut(e) {
    if (hasClass(e.target, 'ignore-me')) return;
    alert('user clicked outside of the component!');
  }
 
  render() {
    return (
      <ClickOutHandler onClickOut={this.onClickOut}>
        <div>Click outside of me!</div>
      </ClickOutHandler>
    );
  }
}

Nó đã làm công việc độc đáo.


Điều này làm việc tuyệt vời! Đơn giản hơn nhiều so với nhiều câu trả lời khác ở đây và không giống như ít nhất 3 giải pháp khác ở đây, nơi mà tất cả chúng đều nhận được "Support for the experimental syntax 'classProperties' isn't currently enabled"điều này chỉ cần làm việc ngay lập tức. Đối với bất kỳ ai đang thử giải pháp này, hãy nhớ xóa mọi mã còn sót lại mà bạn có thể đã sao chép khi thử các giải pháp khác. ví dụ: thêm người nghe sự kiện dường như mâu thuẫn với điều này.
Frikster

2

Tôi đã làm điều này một phần bằng cách làm theo điều này và bằng cách làm theo các tài liệu chính thức của React về việc xử lý các ref yêu cầu phản ứng ^ 16.3. Đây là điều duy nhất hiệu quả với tôi sau khi thử một số gợi ý khác ở đây ...

class App extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillMount() {
    document.addEventListener("mousedown", this.handleClick, false);
  }
  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClick, false);
  }
  handleClick = e => {
    if (this.inputRef.current === e.target) {
      return;
    }
    this.handleclickOutside();
  };
handleClickOutside(){
...***code to handle what to do when clicked outside***...
}
render(){
return(
<div>
...***code for what's outside***...
<span ref={this.inputRef}>
...***code for what's "inside"***...
</span>
...***code for what's outside***
)}}

1

Một ví dụ với Chiến lược

Tôi thích các giải pháp được cung cấp sử dụng để làm điều tương tự bằng cách tạo một trình bao bọc xung quanh thành phần.

Vì đây là một hành vi nhiều hơn, tôi nghĩ về Chiến lược và đã đưa ra những điều sau đây.

Tôi mới sử dụng React và tôi cần một chút trợ giúp để lưu một số bản tóm tắt trong các trường hợp sử dụng

Vui lòng xem lại và cho tôi biết những gì bạn nghĩ.

ClickOutsideBehavior

import ReactDOM from 'react-dom';

export default class ClickOutsideBehavior {

  constructor({component, appContainer, onClickOutside}) {

    // Can I extend the passed component's lifecycle events from here?
    this.component = component;
    this.appContainer = appContainer;
    this.onClickOutside = onClickOutside;
  }

  enable() {

    this.appContainer.addEventListener('click', this.handleDocumentClick);
  }

  disable() {

    this.appContainer.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = (event) => {

    const area = ReactDOM.findDOMNode(this.component);

    if (!area.contains(event.target)) {
        this.onClickOutside(event)
    }
  }
}

Sử dụng mẫu

import React, {Component} from 'react';
import {APP_CONTAINER} from '../const';
import ClickOutsideBehavior from '../ClickOutsideBehavior';

export default class AddCardControl extends Component {

  constructor() {
    super();

    this.state = {
      toggledOn: false,
      text: ''
    };

    this.clickOutsideStrategy = new ClickOutsideBehavior({
      component: this,
      appContainer: APP_CONTAINER,
      onClickOutside: () => this.toggleState(false)
    });
  }

  componentDidMount () {

    this.setState({toggledOn: !!this.props.toggledOn});
    this.clickOutsideStrategy.enable();
  }

  componentWillUnmount () {
    this.clickOutsideStrategy.disable();
  }

  toggleState(isOn) {

    this.setState({toggledOn: isOn});
  }

  render() {...}
}

Ghi chú

Tôi đã nghĩ đến việc lưu trữ các componentmóc vòng đời đã qua và ghi đè chúng bằng các phương thức tương tự như sau:

const baseDidMount = component.componentDidMount;

component.componentDidMount = () => {
  this.enable();
  baseDidMount.call(component)
}

componentlà thành phần được truyền cho hàm tạo của ClickOutsideBehavior.
Điều này sẽ loại bỏ bật / tắt bảng ghi từ người dùng của hành vi này nhưng nó trông không đẹp lắm


1

Trong trường hợp DROPDOWN của tôi , giải pháp của Ben Bud hoạt động tốt, nhưng tôi có một nút chuyển đổi riêng với trình xử lý onClick. Vì vậy, logic nhấp chuột bên ngoài đã xung đột với nút onClick toggler. Đây là cách tôi giải quyết nó bằng cách chuyển qua ref của nút:

import React, { useRef, useEffect, useState } from "react";

/**
 * Hook that triggers onClose when clicked outside of ref and buttonRef elements
 */
function useOutsideClicker(ref, buttonRef, onOutsideClick) {
  useEffect(() => {

    function handleClickOutside(event) {
      /* clicked on the element itself */
      if (ref.current && !ref.current.contains(event.target)) {
        return;
      }

      /* clicked on the toggle button */
      if (buttonRef.current && !buttonRef.current.contains(event.target)) {
        return;
      }

      /* If it's something else, trigger onClose */
      onOutsideClick();
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [ref]);
}

/**
 * Component that alerts if you click outside of it
 */
export default function DropdownMenu(props) {
  const wrapperRef = useRef(null);
  const buttonRef = useRef(null);
  const [dropdownVisible, setDropdownVisible] = useState(false);

  useOutsideClicker(wrapperRef, buttonRef, closeDropdown);

  const toggleDropdown = () => setDropdownVisible(visible => !visible);

  const closeDropdown = () => setDropdownVisible(false);

  return (
    <div>
      <button onClick={toggleDropdown} ref={buttonRef}>Dropdown Toggler</button>
      {dropdownVisible && <div ref={wrapperRef}>{props.children}</div>}
    </div>
  );
}

0

Nếu bạn muốn sử dụng một thành phần nhỏ bé (466 Byte gzipped) đã tồn tại cho chức năng này thì bạn có thể kiểm tra thư viện này phản ứng outclick .

Điểm hay của thư viện là nó cũng cho phép bạn phát hiện các nhấp chuột bên ngoài một thành phần và bên trong một thành phần khác. Nó cũng hỗ trợ phát hiện các loại sự kiện khác.


0

Nếu bạn muốn sử dụng một thành phần nhỏ bé (466 Byte gzipped) đã tồn tại cho chức năng này thì bạn có thể kiểm tra thư viện này phản ứng outclick . Nó ghi lại các sự kiện bên ngoài thành phần phản ứng hoặc phần tử jsx.

Điểm hay của thư viện là nó cũng cho phép bạn phát hiện các nhấp chuột bên ngoài một thành phần và bên trong một thành phần khác. Nó cũng hỗ trợ phát hiện các loại sự kiện khác.


0

Tôi biết đây là một câu hỏi cũ, nhưng tôi tiếp tục gặp phải vấn đề này và tôi đã gặp nhiều rắc rối khi tìm ra điều này trong một định dạng đơn giản. Vì vậy, nếu điều này sẽ giúp mọi người dễ dàng hơn một chút, hãy sử dụng InsideClickHandler bằng airbnb. Nó là một plugin đơn giản nhất để thực hiện nhiệm vụ này mà không cần viết mã của riêng bạn.

Thí dụ:

hideresults(){
   this.setState({show:false})
}
render(){
 return(
 <div><div onClick={() => this.setState({show:true})}>SHOW</div> {(this.state.show)? <OutsideClickHandler onOutsideClick={() => 
  {this.hideresults()}} > <div className="insideclick"></div> </OutsideClickHandler> :null}</div>
 )
}

0
import { useClickAway } from "react-use";

useClickAway(ref, () => console.log('OUTSIDE CLICKED'));

0

bạn có thể cho bạn một cách đơn giản để giải quyết vấn đề của bạn, tôi chỉ cho bạn:

....

const [dropDwonStatus , setDropDownStatus] = useState(false)

const openCloseDropDown = () =>{
 setDropDownStatus(prev => !prev)
}

const closeDropDown = ()=> {
 if(dropDwonStatus){
   setDropDownStatus(false)
 }
}
.
.
.
<parent onClick={closeDropDown}>
 <child onClick={openCloseDropDown} />
</parent>

Điều này làm việc cho tôi, chúc may mắn;)


-1

Tôi tìm thấy điều này từ bài viết dưới đây:

render () {return ({this.node = node;}}> Toggle Popover {this.state.popupVisible && (Tôi là một popover!)}); }}

Đây là một bài viết tuyệt vời về vấn đề này: "Xử lý các nhấp chuột bên ngoài các thành phần React" https://larsgraubner.com/handle-outside-clicks-react/


Chào mừng bạn đến với Stack Overflow! Câu trả lời chỉ là một liên kết thường được tán thành: meta.stackoverflow.com/questions/251006/ mẹo . Trong tương lai, bao gồm một trích dẫn hoặc ví dụ mã với thông tin liên quan đến câu hỏi. Chúc mừng!
Fred Stark

-1

Thêm một onClicktrình xử lý vào thùng chứa cấp cao nhất của bạn và tăng giá trị trạng thái bất cứ khi nào người dùng nhấp vào bên trong. Truyền giá trị đó cho thành phần có liên quan và bất cứ khi nào giá trị thay đổi, bạn có thể thực hiện công việc.

Trong trường hợp này, chúng tôi gọi this.closeDropdown()bất cứ khi nào clickCountgiá trị thay đổi.

Các incrementClickCountvụ cháy phương pháp trong .appthùng chứa nhưng không phải là .dropdownvì chúng tôi sử dụng event.stopPropagation()để ngăn chặn sự kiện bọt.

Mã của bạn có thể sẽ trông giống như thế này:

class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            clickCount: 0
        };
    }
    incrementClickCount = () => {
        this.setState({
            clickCount: this.state.clickCount + 1
        });
    }
    render() {
        return (
            <div className="app" onClick={this.incrementClickCount}>
                <Dropdown clickCount={this.state.clickCount}/>
            </div>
        );
    }
}
class Dropdown extends Component {
    constructor(props) {
        super(props);
        this.state = {
            open: false
        };
    }
    componentDidUpdate(prevProps) {
        if (this.props.clickCount !== prevProps.clickCount) {
            this.closeDropdown();
        }
    }
    toggleDropdown = event => {
        event.stopPropagation();
        return (this.state.open) ? this.closeDropdown() : this.openDropdown();
    }
    render() {
        return (
            <div className="dropdown" onClick={this.toggleDropdown}>
                ...
            </div>
        );
    }
}

Không chắc ai là người hạ thấp nhưng giải pháp này khá sạch so với người nghe sự kiện ràng buộc / không ràng buộc. Tôi đã không có vấn đề với việc thực hiện này.
wdm

-1

Để làm cho giải pháp 'tập trung' hoạt động cho trình đơn thả xuống với người nghe sự kiện, bạn có thể thêm chúng bằng sự kiện onMouseDown thay vì onClick . Bằng cách đó, sự kiện sẽ kích hoạt và sau đó cửa sổ bật lên sẽ đóng như vậy:

<TogglePopupButton
                    onClick = { this.toggleDropup }
                    tabIndex = '0'
                    onBlur = { this.closeDropup }
                />
                { this.state.isOpenedDropup &&
                <ul className = { dropupList }>
                    { this.props.listItems.map((item, i) => (
                        <li
                            key = { i }
                            onMouseDown = { item.eventHandler }
                        >
                            { item.itemName}
                        </li>
                    ))}
                </ul>
                }

-1
import ReactDOM from 'react-dom' ;

class SomeComponent {

  constructor(props) {
    // First, add this to your constructor
    this.handleClickOutside = this.handleClickOutside.bind(this);
  }

  componentWillMount() {
    document.addEventListener('mousedown', this.handleClickOutside, false); 
  }

  // Unbind event on unmount to prevent leaks
  componentWillUnmount() {
    window.removeEventListener('mousedown', this.handleClickOutside, false);
  }

  handleClickOutside(event) {
    if(!ReactDOM.findDOMNode(this).contains(event.path[0])){
       console.log("OUTSIDE");
    }
  }
}

Và đừng quênimport ReactDOM from 'react-dom';
dipole_moment

-1

Tôi đã thực hiện một giải pháp cho tất cả các dịp.

Bạn nên sử dụng Thành phần bậc cao để bọc thành phần mà bạn muốn lắng nghe các nhấp chuột bên ngoài nó.

Ví dụ thành phần này chỉ có một prop: "onClickyOutside" nhận chức năng.

ClickedOutside.js
import React, { Component } from "react";

export default class ClickedOutside extends Component {
  componentDidMount() {
    document.addEventListener("mousedown", this.handleClickOutside);
  }

  componentWillUnmount() {
    document.removeEventListener("mousedown", this.handleClickOutside);
  }

  handleClickOutside = event => {
    // IF exists the Ref of the wrapped component AND his dom children doesnt have the clicked component 
    if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
      // A props callback for the ClikedClickedOutside
      this.props.onClickedOutside();
    }
  };

  render() {
    // In this piece of code I'm trying to get to the first not functional component
    // Because it wouldn't work if use a functional component (like <Fade/> from react-reveal)
    let firstNotFunctionalComponent = this.props.children;
    while (typeof firstNotFunctionalComponent.type === "function") {
      firstNotFunctionalComponent = firstNotFunctionalComponent.props.children;
    }

    // Here I'm cloning the element because I have to pass a new prop, the "reference" 
    const children = React.cloneElement(firstNotFunctionalComponent, {
      ref: node => {
        this.wrapperRef = node;
      },
      // Keeping all the old props with the new element
      ...firstNotFunctionalComponent.props
    });

    return <React.Fragment>{children}</React.Fragment>;
  }
}

-1

UseOnClickOutside Hook - Phản ứng 16.8 +

Tạo một hàm useOnOutsideClick chung

export const useOnOutsideClick = handleOutsideClick => {
  const innerBorderRef = useRef();

  const onClick = event => {
    if (
      innerBorderRef.current &&
      !innerBorderRef.current.contains(event.target)
    ) {
      handleOutsideClick();
    }
  };

  useMountEffect(() => {
    document.addEventListener("click", onClick, true);
    return () => {
      document.removeEventListener("click", onClick, true);
    };
  });

  return { innerBorderRef };
};

const useMountEffect = fun => useEffect(fun, []);

Sau đó sử dụng móc trong bất kỳ thành phần chức năng.

const OutsideClickDemo = ({ currentMode, changeContactAppMode }) => {

  const [open, setOpen] = useState(false);
  const { innerBorderRef } = useOnOutsideClick(() => setOpen(false));

  return (
    <div>
      <button onClick={() => setOpen(true)}>open</button>
      {open && (
        <div ref={innerBorderRef}>
           <SomeChild/>
        </div>
      )}
    </div>
  );

};

Liên kết đến bản demo

Lấy cảm hứng một phần từ câu trả lời @ pau1fitzgerald.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.