聊聊Web前端开发中关于JavaScript里closest()的一些基础知识。您是否曾经遇到过在JavaScript中找到DOM节点的父节点的问题,但是不确定是否要遍历多少个层次才能到达它?让我们来看一下这个HTML:
<div data-id="123">
<button>Click me</button>
</div>
那很简单,对吧?假设您要data-id
在用户单击按钮后获取的值:
var button = document.querySelector("button");
button.addEventListener("click", (evt) => {
console.log(evt.target.parentNode.dataset.id);
// prints "123"
});
在这种情况下,Node.parentNode API就足够了。它的作用是返回给定元素的父节点。在上面的示例中,evt.target
单击了按钮;它的父节点是具有data属性的div。
但是,如果HTML结构嵌套得更深呢?根据其内容,它甚至可以是动态的。
<div data-id="123">
<article>
<header>
<h1>Some title</h1>
<button>Click me</button>
</header>
<!-- ... -->
</article>
</div>
通过添加更多HTML元素,我们的工作变得更加困难。当然,我们可以做类似的事情element.parentNode.parentNode.parentNode.dataset.id,但是来吧......那不是优雅,可重用或可扩展的。
旧方法:使用while-loop
一种解决方案是利用while循环,直到找到父节点为止。
function getParentNode(el, tagName) {
while (el && el.parentNode) {
el = el.parentNode;
if (el && el.tagName == tagName.toUpperCase()) {
return el;
}
}
return null;
}
从上面再次使用相同的HTML示例,它看起来像这样:
var button = document.querySelector("button");
console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
该解决方案远非完美。想象一下,如果您要使用ID或类或任何其他类型的选择器,而不是标签名。至少它允许在父级和我们的源之间有可变数量的子节点。
还有jQuery
过去,如果您不想处理上面为每个应用程序编写的上述函数(让我们成为现实,谁想要的?),那么像jQuery这样的库就派上用场了(它仍然可以)。它提供了.closest()一种确切的方法:
$("button").closest("[data-id='123']")
新方法:使用 Element.closest()
即使jQuery仍然是一种有效的方法(嘿,我们当中有些人对此很执着),但是仅将这一种方法添加到项目中就太过头了,特别是如果您可以使用本机JavaScript拥有同样的方法。
这就是起作用的地方Element.closest:
var button = document.querySelector("button");
console.log(button.closest("div"));
// prints the HTMLDivElement
好了!这很容易,而且没有任何库或额外的代码。
Element.closest()允许我们遍历DOM,直到获得与给定选择器匹配的元素。令人敬畏的是,我们可以传递也要给Element.querySelector或的任何选择器Element.querySelectorAll。它可以是ID,类,数据属性,标签等。
element.closest("#my-id"); // yep
element.closest(".some-class"); // yep
element.closest("[data-id]:not(article)") // hell yeah
如果Element.closest根据给定的选择器找到父节点,则它会与返回相同的方式 document.querySelector。否则,如果找不到父项,它将返回null,从而易于使用if条件:
var button = document.querySelector("button");
console.log(button.closest(".i-am-in-the-dom"));
// prints HTMLElement
console.log(button.closest(".i-am-not-here"));
// prints null
if (button.closest(".i-am-in-the-dom")) {
console.log("Hello there!");
} else {
console.log(":(");
}
准备好一些真实的例子了吗?我们走吧!
用例1:下拉列表
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<meta charset="utf-8">
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>CodePen - Dropdown Example with `Element.closest`</title>
<link rel="stylesheet" media="screen" href="https://static.codepen.io/assets/fullpage/fullpage-4de243a40619a967c0bf13b95e1ac6f8de89d943b7fc8710de33f681fe287604.css" />
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,400italic,700,700italic,900,900italic" rel="stylesheet" />
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<title>CodePen - Dropdown Example with `Element.closest`</title>
<script>
if (document.location.search.match(/type=embed/gi)) {
window.parent.postMessage("resize", "*");
}
</script>
<style>
html { font-size: 15px; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body { height:100%; display: flex; flex-direction: column; }
.referer-warning {
background: black;
box-shadow: 0 2px 5px rgba(0,0,0, 0.5);
padding: 0.75em;
color: white;
text-align: center;
font-family: 'Lato', 'Lucida Grande', 'Lucida Sans Unicode', Tahoma, Sans-Serif;
line-height: 1.2;
font-size: 1rem;
position: relative;
z-index: 2;
}
.referer-warning h1 { font-size: 1.2rem; margin: 0; }
.referer-warning a { color: #56bcf9; } /* $linkColorOnBlack */
</style>
</head>
<body class="">
<div id="result-iframe-wrap" role="main">
<iframe id="result" srcdoc="
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<title>CodePen - Dropdown Example with `Element.closest`</title>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600&amp;display=swap'>
<style>
* {
box-sizing: border-box;
}
body {
font: normal 14px/1.4 "Source Sans Pro", sans-serif;
background-color: #D9E2EC;
color: #334E68;
margin: 2rem;
}
/* Navigation */
.main-navigation {
background-color: white;
padding: 1.5rem 2rem;
border-radius: .3em;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
}
.main-navigation.is-expanded {
border-bottom-left-radius: 0;
}
.main-navigation > .entry {
font: inherit;
color: inherit;
text-decoration: unset;
border: unset;
background-color: unset;
padding: unset;
font-weight: 600;
cursor: pointer;
margin-right: 2rem;
transition: color .1s linear;
position: relative;
}
.main-navigation > .entry.is-dropdown {
padding-right: 1rem;
}
.main-navigation > .entry.is-dropdown::after {
content: "";
position: absolute;
width: 0;
height: 0;
border-top: 8px solid #2CB1BC;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
top: 7px;
right: 0px;
}
.main-navigation > .entry:hover, .main-navigation > .entry:focus {
color: #2CB1BC;
}
/* Dropdown */
.menu-dropdown {
background-color: white;
position: absolute;
margin: unset;
padding: 1.5rem 2rem;
list-style: none;
border-radius: 0 0 .3em .3em;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
display: flex;
flex-wrap: wrap;
max-width: 650px;
border-top: 3px solid #2CB1BC;
}
.menu-dropdown.is-hidden {
display: none;
}
.menu-dropdown > .item {
flex: 0 0 50%;
}
.menu-dropdown > .item:nth-child(odd) {
padding-right: 1rem;
}
.menu-dropdown > .item:nth-child(even) {
padding-left: 1rem;
}
.menu-dropdown > .item:first-child, .menu-dropdown > .item:nth-child(2) {
margin-bottom: 2rem;
}
.menu-dropdown > .item > .title {
margin: unset;
}
.menu-dropdown > .item > .text {
margin-top: unset;
color: #668eb4;
}
.menu-dropdown > .item > .link {
color: #2CB1BC;
font-weight: 600;
text-decoration: unset;
transition: color .1s linear;
}
.menu-dropdown > .item > .link:hover, .menu-dropdown > .item > .link:focus {
color: #334E68;
}
</style>
<script>
window.console = window.console || function(t) {};
</script>
<script>
if (document.location.search.match(/type=embed/gi)) {
window.parent.postMessage("resize", "*");
}
</script>
</head>
<body translate="no" >
<nav class="main-navigation">
<button type="button" class="entry is-dropdown" data-dropdown-trigger>Resources</button>
<button type="button" class="entry is-dropdown" data-dropdown-trigger>Customers</button>
<a href="#" class="entry">Contact</a>
</nav>
<ul class="menu-dropdown is-hidden">
<li class="item">
<h3 class="title">Designs</h3>
<p class="text">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Soluta, amet?</p>
<a class="link" href="#">More</a>
</li>
<li class="item">
<h3 class="title">Tutorials</h3>
<p class="text">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Soluta, amet?</p>
<a class="link" href="#">More</a>
</li>
<li class="item">
<h3 class="title">Templates</h3>
<p class="text">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Soluta, amet?</p>
<a class="link" href="#">More</a>
</li>
<li class="item">
<h3 class="title">Mockups</h3>
<p class="text">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Soluta, amet?</p>
<a class="link" href="#">More</a>
</li>
</ul>
<script src="https://static.codepen.io/assets/common/stopExecutionOnTimeout-157cd5b220a5c80d4ff8e0e70ac069bffd87a61252088146915e8726e5d9f147.js"></script>
<script id="rendered-js" >
var menu = document.querySelector(".menu-dropdown");
var navigation = document.querySelector(".main-navigation");
function handleClick(evt) {
// Only if a click on a dropdown trigger happens, either close or open it.
if (evt.target.hasAttribute("data-dropdown-trigger")) {
if (menu.classList.contains("is-hidden")) {
menu.classList.remove("is-hidden");
navigation.classList.add("is-expanded");
} else {
menu.classList.add("is-hidden");
navigation.classList.remove("is-expanded");
}
return;
}
// If a click happens somewhere outside the dropdown, close it.
if (!evt.target.closest(".menu-dropdown")) {
menu.classList.add("is-hidden");
navigation.classList.remove("is-expanded");
}
}
window.addEventListener("click", handleClick);
//# sourceURL=pen.js
</script>
</body>
</html>
" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-scripts allow-top-navigation-by-user-activation" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; microphone; midi; payment; vr" allowTransparency="true" allowpaymentrequest="true" allowfullscreen="true">
</iframe>
</div>
</body>
</html>
我们的第一个演示是下拉菜单的基本(远非完美)实现,单击一个顶级菜单项后即可打开该下拉菜单。请注意,即使在下拉菜单中的任意位置单击或选择文本,菜单也如何保持打开状态?但是单击外部的某个位置,它将关闭。
该Element.closestAPI是什么检测之外点击。下拉菜单本身是<ul>带有.menu-dropdown类的元素,因此单击菜单外的任何位置都会将其关闭。这是因为for的值evt.target.closest(".menu-dropdown")将是null因为此类没有父节点。
function handleClick(evt) {
// ...
// if a click happens somewhere outside the dropdown, close it.
if (!evt.target.closest(".menu-dropdown")) {
menu.classList.add("is-hidden");
navigation.classList.remove("is-expanded");
}}
在handleClick
回调函数中,条件决定了要做什么:关闭下拉列表。如果单击无序列表中的其他位置,Element.closest
将找到并返回它,从而使下拉列表保持打开状态。
用例2:表格
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<meta charset="utf-8">
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>CodePen - Table Example with `Element.closest`</title>
<link rel="stylesheet" media="screen" href="https://static.codepen.io/assets/fullpage/fullpage-4de243a40619a967c0bf13b95e1ac6f8de89d943b7fc8710de33f681fe287604.css" />
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,400italic,700,700italic,900,900italic" rel="stylesheet" />
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<title>CodePen - Table Example with `Element.closest`</title>
<script>
if (document.location.search.match(/type=embed/gi)) {
window.parent.postMessage("resize", "*");
}
</script>
<style>
html { font-size: 15px; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body { height:100%; display: flex; flex-direction: column; }
.referer-warning {
background: black;
box-shadow: 0 2px 5px rgba(0,0,0, 0.5);
padding: 0.75em;
color: white;
text-align: center;
font-family: 'Lato', 'Lucida Grande', 'Lucida Sans Unicode', Tahoma, Sans-Serif;
line-height: 1.2;
font-size: 1rem;
position: relative;
z-index: 2;
}
.referer-warning h1 { font-size: 1.2rem; margin: 0; }
.referer-warning a { color: #56bcf9; } /* $linkColorOnBlack */
</style>
</head>
<body class="">
<div id="result-iframe-wrap" role="main">
<iframe id="result" srcdoc="
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<title>CodePen - Table Example with `Element.closest`</title>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Open+Sans:400,700&amp;display=swap'>
<style>
body {
margin: unset;
font: normal 14px/1.5 "Open Sans", sans-serif;
color: #444;
background-color: lightblue;
padding: 1rem;
}
input[type="checkbox"] {
margin: unset;
}
table {
max-width: 70vw;
margin: auto;
border-collapse: collapse;
box-shadow: 0 10px 30px rgba(0, 0, 0, .1);
}
th {
text-align: left;
padding: 10px;
background-color: rgba(0, 0, 0, .15);
}
td {
background-color: white;
padding: 10px;
}
tr:not(:last-of-type) td {
border-bottom: 1px solid #ddd;
}
</style>
<script>
window.console = window.console || function(t) {};
</script>
<script>
if (document.location.search.match(/type=embed/gi)) {
window.parent.postMessage("resize", "*");
}
</script>
</head>
<body translate="no" >
<table>
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody>
<tr data-userid="1">
<td>
<input type="checkbox" data-action="select">
</td>
<td>John Doe</td>
<td>john.doe@gmail.com</td>
<td>
<button type="button" data-action="edit">Edit</button>
<button type="button" data-action="delete">Delete</button>
</td>
</tr>
<tr data-userid="2">
<td>
<input type="checkbox" data-action="select">
</td>
<td>Jane Smith</td>
<td>jane@smith.com</td>
<td>
<button type="button" data-action="edit">Edit</button>
<button type="button" data-action="delete">Delete</button>
</td>
</tr>
<tr data-userid="3">
<td>
<input type="checkbox" data-action="select">
</td>
<td>George Westminster</td>
<td>g.westminster@googlemail.com</td>
<td>
<button type="button" data-action="edit">Edit</button>
<button type="button" data-action="delete">Delete</button>
</td>
</tr>
<tr data-userid="4">
<td>
<input type="checkbox" data-action="select">
</td>
<td>Will Johnson</td>
<td>will.johnson@yahoo.com</td>
<td>
<button type="button" data-action="edit">Edit</button>
<button type="button" data-action="delete">Delete</button>
</td>
</tr>
</tbody>
</table>
<script src="https://static.codepen.io/assets/common/stopExecutionOnTimeout-157cd5b220a5c80d4ff8e0e70ac069bffd87a61252088146915e8726e5d9f147.js"></script>
<script id="rendered-js" >
(function () {
"use strict";
function getUserId(target) {
return target.closest("[data-userid]").dataset.userid;
}
function handleClick(evt) {
var { action } = evt.target.dataset;
if (action) {
let userId = getUserId(evt.target);
if (action == "edit") {
alert(`Edit user with ID of ${userId}`);
} else if (action == "delete") {
alert(`Delete user with ID of ${userId}`);
} else if (action == "select") {
alert(`Selected user with ID of ${userId}`);
}
}
}
window.addEventListener("click", handleClick);
})();
//# sourceURL=pen.js
</script>
</body>
</html>
" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-scripts allow-top-navigation-by-user-activation" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; microphone; midi; payment; vr" allowTransparency="true" allowpaymentrequest="true" allowfullscreen="true">
</iframe>
</div>
</body>
</html>
第二个示例呈现一个显示用户信息的表,比方说它是仪表板中的组件。每个用户都有一个ID,但是我们没有显示它,而是将其另存为每个<tr>
元素的数据属性。
<table>
<!-- ... -->
<tr data-userid="1">
<td>
<input type="checkbox" data-action="select">
</td>
<td>John Doe</td>
<td>john.doe@gmail.com</td>
<td>
<button type="button" data-action="edit">Edit</button>
<button type="button" data-action="delete">Delete</button>
</td>
</tr>
</table>
最后一列包含两个按钮,用于编辑和删除表中的用户。第一个按钮的data-action属性为edit,第二个按钮的属性为delete。当我们单击它们中的任何一个时,我们都想触发一些操作(例如向服务器发送请求),但是为此,需要用户ID。
单击事件侦听器附加到全局窗口对象,因此,每当用户单击页面上的某个位置时,都会调用回调函数handleClick。
function handleClick(evt) {
var { action } = evt.target.dataset;
if (action) {
// `action` only exists on buttons and checkboxes in the table.
let userId = getUserId(evt.target);
if (action == "edit") {
alert(`Edit user with ID of ${userId}`);
} else if (action == "delete") {
alert(`Delete user with ID of ${userId}`);
} else if (action == "select") {
alert(`Selected user with ID of ${userId}`);
}
}
}
如果单击发生在这些按钮之一以外的其他位置,则不data-action存在属性,因此什么也没有发生。但是,单击任一按钮时,将确定操作(顺便说一句,称为事件委托),并且下一步,将通过调用来检索用户ID getUserId:
function getUserId(target) {
// `target` is always a button or checkbox.
return target.closest("[data-userid]").dataset.userid;
}
该函数期望将DOM节点作为唯一参数,并在调用时用于Element.closest查找包含按下按钮的表行。然后data-userid,它返回该值,该值现在可用于将请求发送到服务器。
用例3:React中的表
让我们继续使用表格示例,看看如何在React项目中处理它。这是返回表的组件的代码:
function TableView({ users }) {
function handleClick(evt) {
var userId = evt.currentTarget
.closest("[data-userid]")
.getAttribute("data-userid");
// do something with `userId`
}
return (
<table>
{users.map((user) => (
<tr key={user.id} data-userid={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<button onClick={handleClick}>Edit</button>
</td>
</tr>
))}
</table>
);
}
我发现这种用例经常出现-映射一组数据并将其显示在列表或表中,然后允许用户对其进行处理是相当普遍的。许多人使用嵌入式箭头功能,如下所示:
<button onClick={() => handleClick(user.id)}>Edit</button>
虽然这也是解决问题的有效方法,但我更喜欢使用该data-userid
技术。内联箭头功能的缺点之一是,每次React重新渲染列表时,它都需要再次创建回调函数,从而在处理大量数据时可能导致性能问题。
在回调函数中,我们只需提取目标(按钮)并获取<tr>
包含该data-userid
值的父元素来处理事件。
function handleClick(evt) {
var userId = evt.target
.closest("[data-userid]")
.getAttribute("data-userid");
// do something with `userId`
}
用例4:模态
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<meta charset="utf-8">
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>CodePen - Modal Example with `Element.closest`</title>
<link rel="stylesheet" media="screen" href="https://static.codepen.io/assets/fullpage/fullpage-4de243a40619a967c0bf13b95e1ac6f8de89d943b7fc8710de33f681fe287604.css" />
<link href="https://fonts.googleapis.com/css?family=Lato:300,400,400italic,700,700italic,900,900italic" rel="stylesheet" />
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<title>CodePen - Modal Example with `Element.closest`</title>
<script>
if (document.location.search.match(/type=embed/gi)) {
window.parent.postMessage("resize", "*");
}
</script>
<style>
html { font-size: 15px; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body { height:100%; display: flex; flex-direction: column; }
.referer-warning {
background: black;
box-shadow: 0 2px 5px rgba(0,0,0, 0.5);
padding: 0.75em;
color: white;
text-align: center;
font-family: 'Lato', 'Lucida Grande', 'Lucida Sans Unicode', Tahoma, Sans-Serif;
line-height: 1.2;
font-size: 1rem;
position: relative;
z-index: 2;
}
.referer-warning h1 { font-size: 1.2rem; margin: 0; }
.referer-warning a { color: #56bcf9; } /* $linkColorOnBlack */
</style>
</head>
<body class="">
<div id="result-iframe-wrap" role="main">
<iframe id="result" srcdoc="
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<link rel="apple-touch-icon" type="image/png" href="https://static.codepen.io/assets/favicon/apple-touch-icon-5ae1a0698dcc2402e9712f7d01ed509a57814f994c660df9f7a952f3060705ee.png" />
<meta name="apple-mobile-web-app-title" content="CodePen">
<link rel="shortcut icon" type="image/x-icon" href="https://static.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico" />
<link rel="mask-icon" type="" href="https://static.codepen.io/assets/favicon/logo-pin-8f3771b1072e3c38bd662872f6b673a722f4b3ca2421637d5596661b4e2132cc.svg" color="#111" />
<title>CodePen - Modal Example with `Element.closest`</title>
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Gelasio:400,700&amp;display=swap'>
<style>
body {
margin: unset;
font: normal 16px/1.5 Gelasio, serif;
text-align: center;
}
.open-modal {
font-family: inherit;
font-size: 23px;
background-color: tomato;
border: none;
font-weight: bold;
padding: .5rem 4rem;
border-radius: .2rem;
margin-top: 3rem;
cursor: pointer;
}
.open-modal:hover {
background-color: black;
color: white;
}
.modal-outer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(200, 200, 200, .4);
display: flex;
align-items: center;
justify-content: center;
}
.modal-outer.is-hidden {
display: none;
}
.modal-inner {
background-color: white;
position: relative;
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, .1);
width: 100%;
max-width: 70vw;
max-height: 50vh;
border-radius: .5rem;
padding: 2rem;
box-sizing: border-box;
text-align: center;
}
.modal-close {
position: absolute;
top: 0;
right: 0;
font-size: 2rem;
background-color: transparent;
border: none;
margin: unset;
width: 50px;
height: 50px;
cursor: pointer;
}
.modal-close:hover,
.modal-close:focus {
color: tomato;
}
</style>
<script>
window.console = window.console || function(t) {};
</script>
<script>
if (document.location.search.match(/type=embed/gi)) {
window.parent.postMessage("resize", "*");
}
</script>
</head>
<body translate="no" >
<button type="button" class="open-modal">Open modal</button>
<div class="modal-outer is-hidden">
<div class="modal-inner">
<button type="button" class="modal-close">&times;</button>
<h1>Modal content</h1>
<p>You can click anywhere outside the modal to close it.<br>Alternatively, there's the close button in the upper right corner.</p>
</div>
</div>
<script src="https://static.codepen.io/assets/common/stopExecutionOnTimeout-157cd5b220a5c80d4ff8e0e70ac069bffd87a61252088146915e8726e5d9f147.js"></script>
<script id="rendered-js" >
(function () {
"use strict";
var modal = document.querySelector(".modal-outer");
var open = document.querySelector(".open-modal");
var close = modal.querySelector(".modal-close");
function handleModalOpen() {
modal.classList.remove("is-hidden");
}
function handleModalClose() {
modal.classList.add("is-hidden");
}
function handleModalClick(evt) {
if (!evt.target.closest(".modal-inner")) {
handleModalClose();
}
}
function handleKeyDown(evt) {
if (evt.key == "Escape" && !modal.classList.contains("is-hidden")) {
handleModalClose();
}
}
open.addEventListener("click", handleModalOpen);
close.addEventListener("click", handleModalClose);
modal.addEventListener("click", handleModalClick);
window.addEventListener("keydown", handleKeyDown);
})();
//# sourceURL=pen.js
</script>
</body>
</html>
" sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-scripts allow-top-navigation-by-user-activation" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; microphone; midi; payment; vr" allowTransparency="true" allowpaymentrequest="true" allowfullscreen="true">
</iframe>
</div>
</body>
</html>
最后一个示例是另一个我可以肯定的组件:模态。模态的实现通常具有挑战性,因为它们需要提供许多功能,同时要易于访问和(理想情况下)美观。
我们要重点关注如何关闭模式。在此示例中,可以通过按下Esc键盘,单击模态中的按钮或单击模态之外的任何位置来实现。
在我们的JavaScript中,我们想听模式中某处的点击:
var modal = document.querySelector(".modal-outer");
modal.addEventListener("click", handleModalClick);
默认情况下,通过.is-hidden实用程序类隐藏模式。只有当用户单击红色的大按钮时,模态才会通过删除此类而打开。一旦打开了模态,单击它的任何地方(关闭按钮除外)都不会无意间将其关闭。事件侦听器回调函数负责:
function handleModalClick(evt) {
// `evt.target` is the DOM node the user clicked on.
if (!evt.target.closest(".modal-inner")) {
handleModalClose();
}
}
evt.target是被单击的DOM节点,在此示例中,是MODE后面的整个背景<div class="modal-outer">。此DOM节点不在内<div class="modal-inner">,因此Element.closest()可以使所有想要的气泡冒泡,而找不到它。条件将对此进行检查并触发handleModalClose功能。
单击节点内的某个位置(例如标题),将成<div class="modal-inner">为父节点。在这种情况下,条件不是真实的,将模式保留为打开状态。
关于浏览器支持...
与任何酷的"新"JavaScript API一样,需要考虑对浏览器的支持。好消息是,Element.closest它并不是一个新事物,并且已经在相当长的一段时间内在所有主要浏览器中得到支持,支持率高达94%。我会说这可以在生产环境中安全使用。
唯一不提供任何支持的浏览器是Internet Explorer(所有版本)。如果您必须支持IE,那么使用jQuery方法可能会更好。
如您所见,有一些非常可靠的用例Element.closest。过去,像jQuery这样使我们相对容易使用的库现在可以与原始JavaScript一起使用。
得益于良好的浏览器支持和易于使用的API,我在许多应用程序中都严重依赖此小方法,并且尚未感到失望。