0

I want to make a "popup" box activated by a button that reduces the opacity of all other elements. When the user clicks out of the box, it should disappear and the opacity should go back to normal. However, these two functions are conflicting with each other. It requires me to click the button TWICE in order for showBox() to be called. And clicking out of the box does nothing unless I reinvoke hideOnClickOutside(document.querySelector('div')); in the browser's console.

Why do I have to click "New Audio" twice and why does hideOnClickOutside() not work unless reinvoked?

function showBox() {
  document.body.style.opacity = "0.5";
  document.querySelector('div').style.display = "block";
}

document.querySelector('button').addEventListener('click', showBox);
const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
function hideOnClickOutside(element) {
  const outsideClickListener = event => {
    if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
      element.style.display = 'none';
      removeClickListener()
      document.body.style.opacity = "1";
    }
  }

  const removeClickListener = () => {
    document.removeEventListener('click', outsideClickListener)
  }

  document.addEventListener('click', outsideClickListener)
}

hideOnClickOutside(document.querySelector('div'));
<button>New Audio</button>

<div style="display: none">
  <button>Record Directly</button>
</div>

hideOnClickOutside() function was taken from another StackOverflow answer

Edit

I figured out that it requires two clicks because on the first click, showBox() is called, but immediately after, so is outsideClickListener, and at this point the element is NOW visible AND the user has clicked "outside" the element. This reverts the style changes of showBox().

Marvin
  • 853
  • 2
  • 14
  • 38

1 Answers1

1

The easiest fix is to store the reference to the "New Audio" button and check to see if that is the target of the click on document. If so, then return from the function without updating DOM.

const button = document.querySelector('button')
button.addEventListener('click', showBox);
// ..
function hideOnClickOutside(element) {
  const outsideClickListener = event => {
    if (event.target === button) return 
// ..

Keep in mind, with the current code you have, the hideOnClickOutside function only gets until the first time isVisible is true and the target is not button, since you remove the event listener on that condition.

function showBox(e) {
  document.body.style.opacity = "0.5";
  document.querySelector('div').style.display = "block";
}
const button = document.querySelector('button')
button.addEventListener('click', showBox);
const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js
function hideOnClickOutside(element) {
  const outsideClickListener = event => {
    if (event.target === button) return 
    if (!element.contains(event.target) && isVisible(element)) { // or use: event.target.closest(selector) === null
      element.style.display = 'none';
      removeClickListener()
      document.body.style.opacity = "1";
    }
  }

  const removeClickListener = () => {
    document.removeEventListener('click', outsideClickListener)
  }

  document.addEventListener('click', outsideClickListener)
}

hideOnClickOutside(document.querySelector('div'));
<button>New Audio</button>

<div style="display: none">
  <button>Record Directly</button>
</div>

The other problem is that once the showBox function is called, you actually probably want the button to be considered outside. Let's refactor your code to store references to the showButton and box, add a flag to disable the showButton and only add the event listener to the document if the showButton is clicked and remove the event listener only when the box is displayed.

You can later refactor this to fit your particular use case. The idea is to think of the various states this application can be in and create functions to manage that state.

const box = document.querySelector('#box');
const showButton = document.querySelector('#show-button');
showButton.addEventListener('click', showBox);

let isDisabled = false;
const isVisible = elem => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js

function toggleDisabled(bool) {
    showButton.attributes.disabled = bool;
    isDisabled = bool;
}

function toggleDisplay(display, opacity) {
    document.body.style.opacity = opacity;
    box.style.display = display;
}

function showBox(event) {
  if (!isDisabled) {
    event.preventDefault();
    event.stopPropagation();
    toggleDisplay("block", 0.5);
    toggleDisabled(true);
    document.addEventListener('click', outsideClickListener);
  }
}

function outsideClickListener(event) {
  if (!box.contains(event.target) && isVisible(box)) { // or use: event.target.closest(selector) === null
    toggleDisplay("none", 1);
    toggleDisabled(false);
    document.removeEventListener('click', outsideClickListener)
  }
}
<button id="show-button">New Audio</button>

<div id="box" style="display: none">
  <button>Record Directly</button>
</div>
wlh
  • 3,426
  • 1
  • 16
  • 32