最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

html - Close elements by clicking anywhere on the page using pure Javascript - Stack Overflow

programmeradmin1浏览0评论

I have a menu which opens a sub-navigation on clicking a header which I am trying to get to close by clicking anywhere on the page except an open element.

My Code Snippet is as follows:

function showSubMenu(show, hide1, hide2, hide3, hide4) {
  document.getElementById(show).className = "subNavShow";
  document.getElementById(hide1).className = "subNavHide";
  document.getElementById(hide2).className = "subNavHide";
  document.getElementById(hide3).className = "subNavHide";
  document.getElementById(hide4).className = "subNavHide";
}
.subNavHide {
  display: none;
}

.subNavShow {
  display: block;
}
<ul class="topnavList" id="siteTopnavList">
  <li>
    <a onclick="showSubMenu('text1','text2','text3','text4','text5')" href="javascript:void(0);">Nav 1</a>
    <article id="text1" class="subNavHide">
      <ul>
        <li><a href="#">Sub Nav 1</a></li>
      </ul>
    </article>
  </li>
  <li>
    <a onclick="showSubMenu('text2','text1','text3','text4','text5')" href="javascript:void(0);">Nav 2</a>
    <article id="text2" class="subNavHide"> text2 </article>
  </li>
  <li>
    <a onclick="showSubMenu('text3','text1','text2','text4','text5')" href="javascript:void(0);">Nav 3</a>
    <article id="text3" class="subNavHide"> text3 </article>
  </li>
  <li>
    <a onclick="showSubMenu('text4','text1','text2','text3','text5')" href="javascript:void(0);">Nav 4</a>
    <article id="text4" class="subNavHide"> text4 </article>
  </li>
  <li>
    <a onclick="showSubMenu('text5','text1','text2','text3','text4')" href="javascript:void(0);">Nav 5</a>
    <article id="text5" class="subNavHide"> text5 </article>
  </li>
</ul>

I have a menu which opens a sub-navigation on clicking a header which I am trying to get to close by clicking anywhere on the page except an open element.

My Code Snippet is as follows:

function showSubMenu(show, hide1, hide2, hide3, hide4) {
  document.getElementById(show).className = "subNavShow";
  document.getElementById(hide1).className = "subNavHide";
  document.getElementById(hide2).className = "subNavHide";
  document.getElementById(hide3).className = "subNavHide";
  document.getElementById(hide4).className = "subNavHide";
}
.subNavHide {
  display: none;
}

.subNavShow {
  display: block;
}
<ul class="topnavList" id="siteTopnavList">
  <li>
    <a onclick="showSubMenu('text1','text2','text3','text4','text5')" href="javascript:void(0);">Nav 1</a>
    <article id="text1" class="subNavHide">
      <ul>
        <li><a href="#">Sub Nav 1</a></li>
      </ul>
    </article>
  </li>
  <li>
    <a onclick="showSubMenu('text2','text1','text3','text4','text5')" href="javascript:void(0);">Nav 2</a>
    <article id="text2" class="subNavHide"> text2 </article>
  </li>
  <li>
    <a onclick="showSubMenu('text3','text1','text2','text4','text5')" href="javascript:void(0);">Nav 3</a>
    <article id="text3" class="subNavHide"> text3 </article>
  </li>
  <li>
    <a onclick="showSubMenu('text4','text1','text2','text3','text5')" href="javascript:void(0);">Nav 4</a>
    <article id="text4" class="subNavHide"> text4 </article>
  </li>
  <li>
    <a onclick="showSubMenu('text5','text1','text2','text3','text4')" href="javascript:void(0);">Nav 5</a>
    <article id="text5" class="subNavHide"> text5 </article>
  </li>
</ul>

Ideally I would like to use pure Javascript for this but if Jquery is absolutely necessary then I would be OK with that too

Share Improve this question edited Mar 6, 2018 at 15:12 F0XS 1,2693 gold badges16 silver badges19 bronze badges asked Mar 6, 2018 at 13:21 CJNottsCJNotts 2872 gold badges6 silver badges18 bronze badges
Add a ment  | 

3 Answers 3

Reset to default 2

The easiest way to do this with your current implementation, in my opinion, is to add a click event listener to the document and use .closest to determine if the element clicked is the element open:

document.addEventListener(`click`, hideSubMenus);

function hideSubMenus(event) {
    if (!event.target.closest(`.topnavList li a, .subNavShow`)) {
        document.getElementById(`text1`).className = `subNavHide`;
        document.getElementById(`text2`).className = `subNavHide`;
        document.getElementById(`text3`).className = `subNavHide`;
        document.getElementById(`text4`).className = `subNavHide`;
        document.getElementById(`text5`).className = `subNavHide`;
    }
}

closest is however not patible with older browsers: https://developer.mozilla/en-US/docs/Web/API/Element/closest

But I would probably add classes to the links and add event listeners to them instead of using the "onclick" attribute. That way, for example, if you add the "subNavLink" class to each link, you can use a loop to deal with the links, instead of repeating the same line for each link:

let links, i, n;
links = document.getElementsByClassName(`subNavLink`);
for (i = 0, n = links.length; i < n; i++) {
    links[i].addEventListener(`click`, showSubMenu);
}

function showSubMenu(event) {
    let currentLink, i, link, n;
    currentLink = event.currentTarget;
    for (i = 0, n = links.length; i < n; i++) {
        link = links[i];
        if (link === currentLink) {
            // this link was clicked, so we have to show its submenu
            link.nextElementSibling.className = `subNavShow`;
        } else {
            // this link was not clicked, so we have to hide its submenu
            link.nextElementSibling.className = `subNavHide`;
        }
    }
}

By doing this you can change the hideSubMenus function to:

function hideSubMenus(event) {
    let i, n;
    if (!event.target.closest(`.subNavLink, .subNavShow`)) {
        for (i = 0, n = links.length; i < n; i++) {
            links[i].nextElementSibling.className = `subNavHide`;
        }
    }
}

I've found that the easiest way to pull this off is to create a layer, underneath the menu (or more monly a modal window). And then use that layer as the element to test if it has been clicked (versus the element sitting on top of it).

(The example uses a grayed out background to show the overlay's presence, but it could just as easily be a transparent DIV and still have the same effect)

// Get the elements that will show/hide
const overlay = document.getElementById('overlay');
const menu = document.getElementById('menu');

// Change the className to have the CSS that will hide
// the elements
// Since the 'menu' element is on top of the 'overlay'
// element, clicking on the 'menu' should not click
// through the 'overlay' -- thus ignoring this section
// of code to hide things
overlay.onclick = function(){
  menu.className = 'hide';
  overlay.className = 'hide';
};

// Quick and dirty code to reset the page and display 
// the 'menu' and 'overlay' DIVs
function open(){
  menu.className = '';
  overlay.className = '';
}
#overlay{
  display: block;
  position: fixed;
  top: 0; left: 0;
  height: 100%; height: 100vh;
  width: 100%; width: 100vw;
  background-color: rgba( 0, 0, 0, 0.25 );
}
#overlay.hide{ display: none; }

#menu{
  position: absolute;
  background-color: white;
  padding: 15px; border-radius: 5px;
}
  #menu ul, #menu li{
    margin: 0; padding: 0;
    list-style: none;
  }
#menu.hide{ display: none; }
<a href="javascript:open();">OPEN</a>

<div id="overlay"></div>

<div id="menu">
  <ul>
    <li>Menu Item</li>
    <li>Menu Item</li>
    <li>Menu Item</li>
    <li>Menu Item</li>
  </ul>
</div>

With the bubble and how elements are stacked, clicking on the menu won't close it -- but clicking anywhere outside of it will.

The more general the code is, the better.

Using an eventListener set on the document lets you listen to all "click" events (that bubbles up the DOM tree) on the page. You can close all articles no matter what, then display the clicked entry (and its ancestors) if appropriate.

The code below, yet short has many benefits:

  • It is dynamic. Meaning it can handle any amount of sub-levels. article elements neither require id attributes nor show/hide classes at first render. The code bees loosely coupled.
  • Only a single handler function will live in memory instead of one per menu entry.
  • It will handle entries added later (after eventListener registration) to the menu.
  • Your code is factorized which makes it easier to read and reuse.

let topNavList = document.querySelector('#siteTopnavList');

document.addEventListener('click', function (e) {
    let t = e.target;
    
    // At this point, close menu entries anyway
    topNavList.querySelectorAll('a ~ article').forEach(el => {
       el.classList.add('subNavHide'); el.classList.remove('subNavShow');
    });
    
    // Drop clicks on the "active" link or any element that is outside the `#siteTopnavList` menu
    if (!t.nextElementSibling || t.nextElementSibling.classList.contains('subNavShow')) {
      return;
    }
    
    if (t.nodeName.toLowerCase() == 'a' && topNavList.contains(t)) {
       topNavList.querySelectorAll('article').forEach(x => {
           if(x.contains(t) || x === t.nextElementSibling) {
            x.classList.remove('subNavHide');
            x.classList.add('subNavShow');
           }
       });

       // Prevent the browser to process the anchor href attribute
       e.preventDefault();
    }
});
#siteTopnavList article {display:none}
#siteTopnavList .subNavShow {display:block}
<ul class="topnavList" id="siteTopnavList">
    <li>
        <a href="#">Nav 1</a>
        <article> 
            <ul>
                <li><a href="#">Sub Nav 1</a></li> 
            </ul>
        </article>
    </li>
    <li>
        <a href="#">Nav 2</a>
        <article> TEXT2 </article>
    </li>
    <li>
        <a href="#">Multi level</a>
        <article>
            <ul>
                <li>
                  <a href="#">Sub Nav 1</a>
                  <article>
                      <ul>
                          <li><a href="http://nowhere.">Deep 1</a></li>
                          <li><a href="http://nowhere.">Deep 2</a></li>
                          <li>
                              <a href="#">Even deeper 3</a>
                              <article> 
                                  <ul>
                                      <li><a href="#">Even deeper 1</a></li> 
                                  </ul>
                              </article>
                          </li>
                      </ul>
                  </article>
                </li> 
            </ul>
        </article>
    </li>
</ul>

发布评论

评论列表(0)

  1. 暂无评论