webcomponents - hide dropdown menu on [removed]

2019-03-05 07:34发布

问题:

dropdown menu is created in shadowDOM

almost perfect, but the problem is how to hide the dropdown menu when click on any where else in 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)

please refer this demo for complete code

回答1:

An unrelated suggestion: you should probably separate .dropdown into its own <app-nav-dropdown> component and assign the 'click' event listeners in its 'constructor' or 'connectedCallback'.

The best idea for your problem is to

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);
    }
});

Note: I use addEventListener with true, such that the event happens at capture, so that it's will not happen immediately after the .dropdown click handler.

And your handleDropdownUnfocus will look like

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);
}

There's a problem with this solution though, that if you click on the menu item again after opening it, it will call both .dropdown and window handlers, and the net result will be that the dropdown will remain open. To fix that, you would normally add a check in handleDropdownUnfocus:

if(e.target.closest('.dropdown')) return;

However, it will not work. Even when you click on .dropdown, your e.target will be app-nav element, due to Shadow DOM. Which makes it more difficult to do the toggling. I don't know how you'd address this issue, maybe you can come up with something fancy, or stop using Shadow DOM.

Also, your code has some red flags... For example, you use let keyword in your for-loop, which is fine. The support for let is very limited still, so you're more likely going to transpile. The transpiler will just change every let to var. However, if you use var in your loop, assigning the handlers in a loop like that will not work anymore, because index for every handler will refer to the last dropdown (because index will now be global within the function's context and not local for every loop instance).



回答2:

The best solution, as @guitarino suggested is to define a dropdown menu custom element.

When the menu is clicked, call a (first) event handler that will show/hide the menu, and also add/remove a (second) dropdown event handler on window.

At its turn, the (second) dropdown event handler will call the first event handler only if the action is outside the custom element itself.

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 )   
}

It works with or without Shadow 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>