webcomponents - скрыть выпадающее меню на window.onclick

Выпадающее меню создано в shadowDOM

почти идеально, но проблема в том, как скрыть выпадающее меню при нажатии на любое другое место в окне

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)

пожалуйста, обратитесь к этой демонстрации для полного кода

2 ответа

Решение

Несвязанное предложение: вы, вероятно, должны отделить .dropdown в свое собственное <app-nav-dropdown> компонент и назначить прослушиватели событий "click" в его "конструкторе" или "connectedCallback".

Лучшая идея для вашей проблемы -

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, ваш e.target будет app-nav элемент, из-за Shadow DOM. Что затрудняет переключение. Я не знаю, как вы решите эту проблему, может быть, вы сможете придумать что-нибудь необычное или прекратить использовать Shadow DOM.

Кроме того, ваш код имеет некоторые красные флаги... Например, вы используете let ключевое слово в вашем цикле for, что нормально. Поддержка для let все еще очень ограничен, так что вы, скорее всего, отправитесь в путь. Транспортер будет просто менять каждый let в var, Однако, если вы используете var в вашем цикле, назначение обработчиков в цикле, как это больше не будет работать, потому что index каждый обработчик будет ссылаться на последний выпадающий список (потому что теперь индекс будет глобальным в контексте функции, а не локальным для каждого экземпляра цикла).

Наилучшим решением, как предложил @guitarino, является определение пользовательского элемента выпадающего меню.

При щелчке по меню вызовите (первый) обработчик событий, который отобразит / скроет меню, а также добавьте / удалите (второй) dropdown обработчик событий включен window,

В свою очередь, (второй) 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 )   
}

Работает с или без 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>

Другие вопросы по тегам