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

javascript - How to hide a dropdown when the user clicks outside of it - Stack Overflow

programmeradmin7浏览0评论

Below is a simplified version of the input dropdown I am working with.

A basic summary of what it does is: if you focus on the input, a dropdown appears. If you click one of the options in the dropdown, the option populates the input and the dropdown disappears. This is achieved using onfocus and a functions I called dropdown(); and undropdown();.

I'm in a dilemma, where I'm unable to make the dropdown disappear when someone clicks elsewhere. If I use onblur, it successfully hides the dropdown, but if you click on an option it doesn't populate the input, this is because, the onblur function runs first and, therefore, the input(); function doesn't not run because the dropdown is already hidden.

If you put an onclick on the body tag, or other parent, it considers the onfocus as a click, where it run's the dropdown(); function then the undropdown(); function immediately so the dropdown never appears since the functions overlap.

I would appreciate help on figuring out how to order the functions so that they are executed in the right order without overlapping with each other.

JSFiddle available here.

function input(pos) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");
  document.getElementsByTagName('input')[0].value = li[pos].innerHTML;
  undropdown(0);
}
function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "block"
}
function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "none";
}
.drop {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: visible;
}
.content {
  display: none;
  list-style-type: none;
  border: 1px solid #000;
  padding: 0;
  margin: 0;
  position: absolute;
  z-index: 2;
  width: 100%;
  max-height: 190px;
  overflow-y: scroll;
}
.content li {
  padding: 12px 16px;
  display: block;
  margin: 0;
}
<div class="drop">
  <input type="text" name="class" placeholder="Class" onfocus="dropdown(0)"/>
  <ul class="content">
    <li onclick="input(0)">Option 1</li>
    <li onclick="input(1)">Option 2</li>
    <li onclick="input(2)">Option 3</li>
    <li onclick="input(3)">Option 4</li>
  </ul>
</div>

Below is a simplified version of the input dropdown I am working with.

A basic summary of what it does is: if you focus on the input, a dropdown appears. If you click one of the options in the dropdown, the option populates the input and the dropdown disappears. This is achieved using onfocus and a functions I called dropdown(); and undropdown();.

I'm in a dilemma, where I'm unable to make the dropdown disappear when someone clicks elsewhere. If I use onblur, it successfully hides the dropdown, but if you click on an option it doesn't populate the input, this is because, the onblur function runs first and, therefore, the input(); function doesn't not run because the dropdown is already hidden.

If you put an onclick on the body tag, or other parent, it considers the onfocus as a click, where it run's the dropdown(); function then the undropdown(); function immediately so the dropdown never appears since the functions overlap.

I would appreciate help on figuring out how to order the functions so that they are executed in the right order without overlapping with each other.

JSFiddle available here.

function input(pos) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");
  document.getElementsByTagName('input')[0].value = li[pos].innerHTML;
  undropdown(0);
}
function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "block"
}
function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "none";
}
.drop {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: visible;
}
.content {
  display: none;
  list-style-type: none;
  border: 1px solid #000;
  padding: 0;
  margin: 0;
  position: absolute;
  z-index: 2;
  width: 100%;
  max-height: 190px;
  overflow-y: scroll;
}
.content li {
  padding: 12px 16px;
  display: block;
  margin: 0;
}
<div class="drop">
  <input type="text" name="class" placeholder="Class" onfocus="dropdown(0)"/>
  <ul class="content">
    <li onclick="input(0)">Option 1</li>
    <li onclick="input(1)">Option 2</li>
    <li onclick="input(2)">Option 3</li>
    <li onclick="input(3)">Option 4</li>
  </ul>
</div>

PS: In addition to the above problem, I would appreciate suggestion for edits to get a better title for this question such that someone experiencing a similar problem could find it more easily.

Share Improve this question edited Dec 8, 2016 at 15:53 Alexandru Severin 6,22812 gold badges50 silver badges73 bronze badges asked Dec 8, 2016 at 13:58 user7267879user7267879 3
  • 1 Might be helpful to repro the problem in www.jsfiddle.net (easier to work with and come up with a solution) - title is probably fine, generic enough to work with many problems but describes yours pretty accurately. – tymeJV Commented Dec 8, 2016 at 14:03
  • 1 Any particular reason why you can't use <select>? – Dave K. Commented Dec 8, 2016 at 14:27
  • 1 Usually you trap clicks on (or propagated to) the document element and close the dropdown if necessary. – Salman Arshad Commented Dec 9, 2016 at 12:00
Add a comment  | 

4 Answers 4

Reset to default 6

In this case, On onblur you could call a function which fires the undropdown(0); after a very tiny setTimeout almost instantly. Like so:

function set() {
  setTimeout(function(){ 
    undropdown(0);
  }, 100);
}

HTML

<input type="text" name="class" placeholder="Class" onfocus="dropdown(0)" onblur="set()" />

No other change is required.

function input(pos) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");

  document.getElementsByTagName('input')[0].value = li[pos].innerHTML;
  undropdown(0);
}

function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display= "block"
}

function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display= "none";
}

function set() {
  setTimeout(function(){ 
    undropdown(0);
  }, 100);
}
        .drop {
            position: relative;
            display: inline-block;
            vertical-align:top;
            overflow: visible;
        }

        .content {
            display: none;
            list-style-type: none;
            border: 1px solid #000; 
            padding: 0;
            margin: 0;
            position: absolute;
            z-index: 2;
            width: 100%;
            max-height: 190px;
            overflow-y: scroll;
        }

        .content li {
            padding: 12px 16px;
            display: block;
            margin: 0;
        }
   <div class="drop">
        <input type="text" name="class" placeholder="Class" onfocus="dropdown(0)" onblur="set()" />
        <ul class="content">
            <li onclick="input(0)">Option 1</li>
            <li onclick="input(1)">Option 2</li>
            <li onclick="input(2)">Option 3</li>
            <li onclick="input(3)">Option 4</li>
        </ul>
    </div>

You could make the dropdown focusable with tabindex, and in the input's blur event listener only hide the dropdown if the focus didn't go to the dropdown (see When onblur occurs, how can I find out which element focus went to?)

<ul class="content" tabindex="-1"></ul>
input.addEventListener('blur', function(e) {
  if(!e.relatedTarget || !e.relatedTarget.classList.contains('content')) {
    undropdown(0);
  }
});

function input(e) {
  var dropdown = document.getElementsByClassName('drop');
  var li = dropdown[0].getElementsByTagName("li");
  document.getElementsByTagName('input')[0].value = e.target.textContent;
  undropdown(0);
}
[].forEach.call(document.getElementsByTagName('li'), function(el) {
  el.addEventListener('click', input);
});
function dropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "block"
}
function undropdown(pos) {
  document.getElementsByClassName('content')[pos].style.display = "none";
}
var input = document.getElementsByTagName('input')[0];
input.addEventListener('focus', function(e) {
  dropdown(0);
});
input.addEventListener('blur', function(e) {
  if(!e.relatedTarget || !e.relatedTarget.classList.contains('content')) {
    undropdown(0);
  }
});
.drop {
  position: relative;
  display: inline-block;
  vertical-align: top;
  overflow: visible;
}
.content {
  display: none;
  list-style-type: none;
  border: 1px solid #000;
  padding: 0;
  margin: 0;
  position: absolute;
  z-index: 2;
  width: 100%;
  max-height: 190px;
  overflow-y: scroll;
  outline: none;
}
.content li {
  padding: 12px 16px;
  display: block;
  margin: 0;
}
<div class="drop">
  <input type="text" name="class" placeholder="Class" />
  <ul class="content" tabindex="-1">
    <li>Option 1</li>
    <li>Option 2</li>
    <li>Option 3</li>
    <li>Option 4</li>
  </ul>
</div>

Accepted answer is a naive and unreliable approach. I had a hard-to-catch bug in a complex application because I used a setTimeout to give ~200ms delay so the browser can process dropdown click before blur event happens. While it worked great on every setup I tested it with, some users did have issues, in particular users with slower machines.

The correct way is to test relatedTarget on focusout event:

input.addEventListener('focusout', function(event) {
  if(!isDropdownElement(event.relatedTarget)) {
    // hide dropdown
  }
});

relatedTarget for focusout contains an element reference, which is receiving focus. This reliably works in every browser I've tested so far (I didn't test IE10 and lower, only IE11 and Edge).

W3Schools has a nice example of how to create a custom dropdown: https://www.w3schools.com/howto/howto_custom_select.asp

This is how the focus is handled in that example:

A global click-handler handles clicks outside of the dropdown list: document.addEventListener("click", closeAllSelect);. So as soon as the user clicks anywhere in the document, all dropdowns are closed.

But when the user selects an element of the dropdown list, the click-event is stopped by e.stopPropagation(); inside of the selection-handler.

This way, you don´t need the timer workaround.

发布评论

评论列表(0)

  1. 暂无评论