webcomponents - 在 window.onclick 上隐藏下拉菜单
webcomponents - hide dropdown menu on window.onclick
在 shadowDOM 中创建下拉菜单
几近完美,
但问题是如何在单击 window
中的任何其他位置时隐藏下拉菜单
class NavComponent extends HTMLElement {
constructor() {
super();
let template = document.currentScript.ownerDocument.querySelector('template');
let clone = document.importNode(template.content, true);
let root = this.attachShadow({ mode: 'open' });
root.appendChild(clone);
}
connectedCallback() {
let ddc = this.shadowRoot.querySelectorAll('.dropdowncontainer')
let dd = this.shadowRoot.querySelectorAll('.dropdown');
let ddc_length = ddc.length
for (let index = 0; index < ddc_length; index++) {
ddc[index].addEventListener('click', e => {
dd[index].classList.toggle('show');
});
}
/** have to update the code ............ */
window.onclick = function (event) {
} /** END - have to update the code ............ */
}
}
customElements.define('app-nav', NavComponent)
完整代码请参考此demo
一个不相关的建议:您应该将 .dropdown
分离到它自己的 <app-nav-dropdown>
组件中,并在其 'constructor' 或 'connectedCallback' 中分配 'click' 事件侦听器.
解决你的问题的最好办法是
ddc[index].addEventListener('click', e => {
if(dd[index].classList.contains('show')) {
dd[index].classList.remove('show');
window.removeEventListener('click', handleDropdownUnfocus, true);
}
else {
dd[index].classList.add('show');
window.addEventListener('click', handleDropdownUnfocus, true);
}
});
注意:我将 addEventListener 与 true
一起使用,以便事件在捕获时发生,这样它就不会在 .dropdown 单击处理程序之后立即发生。
你的 handleDropdownUnfocus
看起来像
function handleDropdownUnfocus(e) {
Array.from(document.querySelectorAll('app-nav')).forEach(function(appNav) {
Array.from(appNav.shadowRoot.querySelectorAll('.dropdown')).forEach(function(dd) {
dd.classList.remove('show');
});
});
window.removeEventListener('click', handleDropdownUnfocus, true);
}
虽然这个解决方案有一个问题,如果你在打开菜单项后再次点击它,它会同时调用 .dropdown
和 window
处理程序,最终结果将是下拉菜单将保持打开状态。要解决这个问题,您通常 添加检查 handleDropdownUnfocus
:
if(e.target.closest('.dropdown')) return;
但是,它将不起作用。即使您单击 .dropdown
,由于阴影 DOM,您的 e.target
也将是 app-nav
元素。这使得切换变得更加困难。我不知道你会如何解决这个问题,也许你可以想出一些花哨的东西,或者停止使用 Shadow DOM.
此外,您的代码有一些危险信号...例如,您在 for 循环中使用了 let
关键字,这很好。对 let
的支持仍然非常有限,因此您更有可能转译。转译器只会将每个 let
更改为 var
。但是,如果您在循环中使用 var
,在循环中分配处理程序 将不再有效 ,因为每个处理程序的 index
将引用最后一个下拉列表(因为索引现在在函数的上下文中是全局的,而不是每个循环实例的局部索引)。
正如@guitarino 所建议的那样,最好的解决方案是定义一个下拉菜单自定义元素。
单击菜单时,调用将 show/hide 菜单的(第一个)事件处理程序,并且 add/remove [=15] 上的(第二个)dropdown
事件处理程序=].
轮到(第二个)dropdown
事件处理程序时,仅当操作在自定义元素本身之外时才会调用第一个事件处理程序。
connectedCallback()
{
//mousedown anywhere
this.mouse_down = ev => !this.contains( ev.target ) && toggle_menu()
//toggle menu and window listener
var toggle_menu = () =>
{
if ( this.classList.toggle( 'show' ) )
window.addEventListener( 'mousedown', this.mouse_down )
else
window.removeEventListener( 'mousedown', this.mouse_down )
}
//click on menu
this.addEventListener( 'click', toggle_menu )
}
有或没有影子都可以使用 DOM:
customElements.define( 'drop-menu', class extends HTMLElement
{
constructor ()
{
super()
this.attachShadow( { mode: 'open'} )
.innerHTML = '<slot></slot>'
}
connectedCallback()
{
//mousedown anywhere
this.mouse_down = ev => !this.contains( ev.target ) && toggle_menu()
//toggle menu and window listener
var toggle_menu = () =>
{
if ( this.classList.toggle( 'show' ) )
window.addEventListener( 'mousedown', this.mouse_down )
else
window.removeEventListener( 'mousedown', this.mouse_down )
}
//click on menu
this.addEventListener( 'click', toggle_menu )
}
disconnectedCallback ()
{
this.removeEventListener( 'mousedown', this.mouse_down )
}
} )
drop-menu {
position: relative ;
cursor: pointer ;
display: inline-block ;
}
drop-menu > output {
border: 1px solid #ccc ;
padding: 2px 5px ;
}
drop-menu > ul {
box-sizing: content-box ;
position: absolute ;
top: 2px ; left: 5px ;
width: 200px;
list-style: none ;
border: 1px solid #ccc ;
padding: 0 ;
opacity: 0 ;
transition: all 0.2s ease-in-out ;
background: white ;
visibility: hidden ;
z-index: 2 ;
}
drop-menu.show > ul {
opacity: 1 ;
visibility: visible ;
}
drop-menu > ul > li {
overflow: hidden ;
transition: font 0.2s ease-in-out ;
padding: 2px 5px ;
background-color: #e7e7e7;
}
drop-menu:hover {
cursor: pointer;
background-color: #f2f2f2;
}
drop-menu ul li:hover {
background-color: #e0e0e0;
}
drop-menu ul li span {
float: right;
color: #f9f9f9;
background-color: #f03861;
padding: 2px 5px;
border-radius: 3px;
text-align: center;
font-size: .8rem;
}
drop-menu ul li:hover span {
background-color: #ee204e;
}
<drop-menu><output>Services</output>
<ul>
<li>Graphic desing</li>
<li>web design</li>
<li>app design</li>
<li>theme</li>
</ul>
</drop-menu>
<drop-menu><output>tutorial</output>
<ul>
<li>css <span>12 available</span></li>
<li>php <span>10 available</span></li>
<li>javascript <span>40 available</span></li>
<li>html <span>20 available</span></li>
</ul>
</drop-menu>
在 shadowDOM 中创建下拉菜单
几近完美, 但问题是如何在单击 window
中的任何其他位置时隐藏下拉菜单class NavComponent extends HTMLElement {
constructor() {
super();
let template = document.currentScript.ownerDocument.querySelector('template');
let clone = document.importNode(template.content, true);
let root = this.attachShadow({ mode: 'open' });
root.appendChild(clone);
}
connectedCallback() {
let ddc = this.shadowRoot.querySelectorAll('.dropdowncontainer')
let dd = this.shadowRoot.querySelectorAll('.dropdown');
let ddc_length = ddc.length
for (let index = 0; index < ddc_length; index++) {
ddc[index].addEventListener('click', e => {
dd[index].classList.toggle('show');
});
}
/** have to update the code ............ */
window.onclick = function (event) {
} /** END - have to update the code ............ */
}
}
customElements.define('app-nav', NavComponent)
完整代码请参考此demo
一个不相关的建议:您应该将 .dropdown
分离到它自己的 <app-nav-dropdown>
组件中,并在其 'constructor' 或 'connectedCallback' 中分配 'click' 事件侦听器.
解决你的问题的最好办法是
ddc[index].addEventListener('click', e => {
if(dd[index].classList.contains('show')) {
dd[index].classList.remove('show');
window.removeEventListener('click', handleDropdownUnfocus, true);
}
else {
dd[index].classList.add('show');
window.addEventListener('click', handleDropdownUnfocus, true);
}
});
注意:我将 addEventListener 与 true
一起使用,以便事件在捕获时发生,这样它就不会在 .dropdown 单击处理程序之后立即发生。
你的 handleDropdownUnfocus
看起来像
function handleDropdownUnfocus(e) {
Array.from(document.querySelectorAll('app-nav')).forEach(function(appNav) {
Array.from(appNav.shadowRoot.querySelectorAll('.dropdown')).forEach(function(dd) {
dd.classList.remove('show');
});
});
window.removeEventListener('click', handleDropdownUnfocus, true);
}
虽然这个解决方案有一个问题,如果你在打开菜单项后再次点击它,它会同时调用 .dropdown
和 window
处理程序,最终结果将是下拉菜单将保持打开状态。要解决这个问题,您通常 添加检查 handleDropdownUnfocus
:
if(e.target.closest('.dropdown')) return;
但是,它将不起作用。即使您单击 .dropdown
,由于阴影 DOM,您的 e.target
也将是 app-nav
元素。这使得切换变得更加困难。我不知道你会如何解决这个问题,也许你可以想出一些花哨的东西,或者停止使用 Shadow DOM.
此外,您的代码有一些危险信号...例如,您在 for 循环中使用了 let
关键字,这很好。对 let
的支持仍然非常有限,因此您更有可能转译。转译器只会将每个 let
更改为 var
。但是,如果您在循环中使用 var
,在循环中分配处理程序 将不再有效 ,因为每个处理程序的 index
将引用最后一个下拉列表(因为索引现在在函数的上下文中是全局的,而不是每个循环实例的局部索引)。
正如@guitarino 所建议的那样,最好的解决方案是定义一个下拉菜单自定义元素。
单击菜单时,调用将 show/hide 菜单的(第一个)事件处理程序,并且 add/remove [=15] 上的(第二个)dropdown
事件处理程序=].
轮到(第二个)dropdown
事件处理程序时,仅当操作在自定义元素本身之外时才会调用第一个事件处理程序。
connectedCallback()
{
//mousedown anywhere
this.mouse_down = ev => !this.contains( ev.target ) && toggle_menu()
//toggle menu and window listener
var toggle_menu = () =>
{
if ( this.classList.toggle( 'show' ) )
window.addEventListener( 'mousedown', this.mouse_down )
else
window.removeEventListener( 'mousedown', this.mouse_down )
}
//click on menu
this.addEventListener( 'click', toggle_menu )
}
有或没有影子都可以使用 DOM:
customElements.define( 'drop-menu', class extends HTMLElement
{
constructor ()
{
super()
this.attachShadow( { mode: 'open'} )
.innerHTML = '<slot></slot>'
}
connectedCallback()
{
//mousedown anywhere
this.mouse_down = ev => !this.contains( ev.target ) && toggle_menu()
//toggle menu and window listener
var toggle_menu = () =>
{
if ( this.classList.toggle( 'show' ) )
window.addEventListener( 'mousedown', this.mouse_down )
else
window.removeEventListener( 'mousedown', this.mouse_down )
}
//click on menu
this.addEventListener( 'click', toggle_menu )
}
disconnectedCallback ()
{
this.removeEventListener( 'mousedown', this.mouse_down )
}
} )
drop-menu {
position: relative ;
cursor: pointer ;
display: inline-block ;
}
drop-menu > output {
border: 1px solid #ccc ;
padding: 2px 5px ;
}
drop-menu > ul {
box-sizing: content-box ;
position: absolute ;
top: 2px ; left: 5px ;
width: 200px;
list-style: none ;
border: 1px solid #ccc ;
padding: 0 ;
opacity: 0 ;
transition: all 0.2s ease-in-out ;
background: white ;
visibility: hidden ;
z-index: 2 ;
}
drop-menu.show > ul {
opacity: 1 ;
visibility: visible ;
}
drop-menu > ul > li {
overflow: hidden ;
transition: font 0.2s ease-in-out ;
padding: 2px 5px ;
background-color: #e7e7e7;
}
drop-menu:hover {
cursor: pointer;
background-color: #f2f2f2;
}
drop-menu ul li:hover {
background-color: #e0e0e0;
}
drop-menu ul li span {
float: right;
color: #f9f9f9;
background-color: #f03861;
padding: 2px 5px;
border-radius: 3px;
text-align: center;
font-size: .8rem;
}
drop-menu ul li:hover span {
background-color: #ee204e;
}
<drop-menu><output>Services</output>
<ul>
<li>Graphic desing</li>
<li>web design</li>
<li>app design</li>
<li>theme</li>
</ul>
</drop-menu>
<drop-menu><output>tutorial</output>
<ul>
<li>css <span>12 available</span></li>
<li>php <span>10 available</span></li>
<li>javascript <span>40 available</span></li>
<li>html <span>20 available</span></li>
</ul>
</drop-menu>