Mike P
Mike P

Reputation: 51

When to Call JavaScript Toggle Function?

I have a drop down menu I need to make appear and disappear using pure JavaScript (no libraries/jQuery). Thus I am developing a toggle function. However despite trying several approaches, nothing seems to work. My current idea is to create a variable to hold the state of the menu (open or closed). Once the display of the menu changes from "none" to "block", the variable should change from "closed" to "open". Then an event listener would be added to the body element so when anything is clicked, the menu closes (i.e. the display property is changed back to "none").

Unfortunately the above doesn't seem work. When I put the If/else block outside of an event listener it fires when the page loads, but not when the menuToggle variable changes. If I put it or a function inside the menuPlaceholder event listener the menu won't open, probably due to the open and close code being called basically at the same time.

Clearly I am missing something, probably related to program control or function calling. Does anyone have any insights?

The code I am working with is below. Note the alert functions peppered throughout the code are for testing purposes only.

//Puts IDs for search preference selection box into variables
	var menuPlaceholder = document.getElementById('searchSelection');
	var menuDisplay = document.getElementById('searchOptions');
	var boxLabel = document.getElementById('searchLabel');
	
	//Puts IDs for text input box and submission into variables
	var searchBoxPlaceholder = document.getElementById('searchInput');
	var searchInput = document.getElementById('searchBox');
	var submitButton = document.getElementById('submit');
	
	//Adds class to each search option and puts ID of hidde field into variable
	var searchPrefSubmission = document.getElementsByClassName('buttonSearch');
	var hiddenInput = document.getElementById('searchChoice');
	
	//Global variable to indicate whether searchOptions menu is opened or closed
	var menuToggle = "closed";

	//Closes element when one clicks outside of it.
	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()
			}
		}

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

		document.addEventListener('click', outsideClickListener)
	}

	const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length )
	
	//When the placeholder box is clicked, the option menu appears
	menuPlaceholder.addEventListener('click', function (event){
		menuDisplay.style.display = "block";
		menuToggle = "open";
		
		//Add click event to searchPref buttons
		for (i = 0; i < searchPrefSubmission.length; i++) {
			//Assigns value of the button to both the hidden input field and the placeholder box
			searchPrefSubmission[i].addEventListener('click', function(event) {
				hiddenInput.value=this.value;
				boxLabel.innerHTML = this.value;
				
				menuDisplay.style.display = "none";
				menuPlaceholder.style.display = "inline-block";
				
			});
		}
		
	}); 
	
	
	//This code causes the text input box of the search form to appear when the background box is clicked
	searchBoxPlaceholder.addEventListener('click', function(event){
		searchInput.style.display = "inline";
		submitButton.style.display = "inline";
		//hideOnClickOutside(menuDisplay);
	});
	
	if (menuToggle == "open"){
		document.body.addEventListener('click', function(event){
			alert('Foo!');
		})
	}else{
		alert('Boo!');
	}
	
	/*function toggleMenu () {
		//menuDisplay.style.display = "none";
		alert('Boo!');
		menuToggle = "closed";
	}*/
body {
	font-family:Montserrat, sans-serif;
}

#searchOptionPlaceholder {
	display: inline-block;
}

#searchSelection {
	padding: 10px 20px;
	margin-right: 10px;
	background-color: #F0F3F5;
	display: inline-block;
	color: #000000;
	width: 140px;
	max-width: 200px;
	max-height: 35px;
	border: 2px solid black;
	vertical-align: middle;
}

#searchSelection img {
	float: right;
}

#searchLabel {
	display: inline-block;
	padding-top: 10px;
	vertical-align: top;
}

#searchOptions {
	display: none;
	background-color: #F0F3F5;
	position: absolute;
	z-index: 2;
}

#searchOptions ul {
	background-color: #F0F3F5;
	padding: 5px;
}

#searchOptions li {
	list-style-type: none;
	border-bottom: 2px solid black;
}

#searchOptions li:hover {
	background-color: #706868;
	color: #ffffff;
}

.buttonSearch {
	background-color: transparent;
	border: none;
	padding: 10px;
	font-size: 14px;
}

.searchSubHeading {
	font-size: 12px;
}

#searchInput {
	display: inline-block;
	background-color: #F0F3F5;
	padding: 10px 100px;
	position: relative;
	top: 0px;
	max-width: 350px;
	border: 2px solid black;
	vertical-align: middle;
}

#searchInput img {
	position: relative;
	left: 80px;
}

#searchBox {
	display: none;
	width: 80%;
	background-color: #F0F3F5;
	border: none;
	font-size: 1.5em;
	position: relative;
	right: 50px;
	vertical-align: middle;
}

#submit {
	border: none;
	background-image: url('https://library.domains.skidmore.edu/search/magnifyingGlass.png');
	background-repeat: no-repeat;
	background-size: contain;
	width: 50px;
	height: 30px;
	position: relative;
	right: -80px;
	vertical-align: middle;
}

#otherLinks {
	margin-top: 10px;
}

#otherLinks a{
	color: #000000;
}

#otherLinks a:hover{
	color: #006a52;
}
<h1>Library Search</h1>
<form method="post" action="https://library.domains.skidmore.edu/search/searchBox.php" id="librarySearch">
<div id="searchSelection"><span id="searchLabel">Catalog</span><img src="down.png" height="30px" width="30px" /></div>
<div id="searchOptions">
	<ul>
		<li><button type="button" name="searchPref" value="Catalog" class="buttonSearch">Catalog<br /><br /><span class="searchSubHeading">Search books and DVDs</span></button></li>
		<li><button type="button" name="searchPref" value="SearchMore" class="buttonSearch">SearchMore<br /><br /><span class="searchSubHeading">Search everything</span></button></li>
		<li><button type="button" name="searchPref" value="Journals" class="buttonSearch">Journals<br /><br /><span class="searchSubHeading">Search journals</span></button></li>
	</ul>
</div>
<div id="searchInput">
<input type="hidden" id="searchChoice" name="searchPref" value="catalog" />
<input type="search" id="searchBox" size="60" name="searchText" placeholder="Search our holdings"/><button type="submit" id="submit"></button></div>
<div id="otherLinks"><a href="https://lucy2.skidmore.edu/vwebv/searchAdvanced">Advanced Catalog Search |</a><a href="https://lib-proxy01.skidmore.edu/login?url=https://firstsearch.oclc.org/dbname=WorldCat;done=referer;FSIP"> WorldCat |</a><a href="http://libguides.skidmore.edu/c.php?g=35919&amp;p=1378258"> eBooks</a></div>
</form>

Upvotes: 0

Views: 211

Answers (1)

trincot
trincot

Reputation: 351084

Some issues:

  • Adding event listeners within an event listener is in most cases a code smell: this will add those inner listeners each time the outer event is triggered. Those listeners remain attached, and so they accumulate. So, attach all event handlers in the top-level script, i.e. on page load, and then never again.

  • The if ... else at the end will execute on page load, and then never again. So the value of menuToggle is guaranteed to be "closed". You need to put that if...else switch inside the handler, so that it executes every time the event triggers, at which time the menuToggle variable will possibly have a modified value.

  • The body element does not stretch (by default) over the whole window. If you want to detect a click anywhere on the page, you should attach the listener on the document element itself, not on document.body.

  • When the click on the menu placeholder is handled, you should avoid that this event "bubbles" up the DOM tree up to the document, because there you have the other handler that wants to hide the menu again. You can do this with event.stopPropagation().

  • The global variable is not absolutely necessary, but if you use it, then I would call it menuVisible and give it a boolean value: false at first, and possibly true later.

  • For actually toggling the menu, I would create a function, which takes the desired visibility (false or true) as argument, and then performs the toggle.

  • Do not use undeclared variables, like the for loop variable i. Define it with let.

Here is your code with those changes implemented. Of course, there is still a lot that could be improved, but I believe that goes beyond the scope of this question:

var menuPlaceholder = document.getElementById('searchSelection');
var menuDisplay = document.getElementById('searchOptions');
var boxLabel = document.getElementById('searchLabel');

var searchBoxPlaceholder = document.getElementById('searchInput');
var searchInput = document.getElementById('searchBox');
var submitButton = document.getElementById('submit');

var searchPrefSubmission = document.getElementsByClassName('buttonSearch');
var hiddenInput = document.getElementById('searchChoice');

// Changed name and type of global variable:
var menuVisible = false;

// Removed some functions ...

menuPlaceholder.addEventListener('click', function (event){
    // Use new function for actually setting the visibility 
    toggleMenu(!menuVisible);
    // Avoid that click event bubbles up to the document level
    event.stopPropagation();
}); 

// Add these event handlers on page load, not within another handler
// Define loop variable with let
for (let i = 0; i < searchPrefSubmission.length; i++) {
    //Assigns value of the button to both the hidden input field and the placeholder box
    searchPrefSubmission[i].addEventListener('click', function(event) {
        hiddenInput.value = this.value;
        boxLabel.innerHTML = this.value;
        // Use the new function for setting the visibility            
        toggleMenu(false);
        menuPlaceholder.style.display = "inline-block";     
    });
}

searchBoxPlaceholder.addEventListener('click', function(event){
    searchInput.style.display = "inline";
    submitButton.style.display = "inline";
});

// Bind handler on document itself, and call new function
document.addEventListener('click', function(event) {
    toggleMenu(false);
});

// new function to perform the toggle
function toggleMenu(show) {
    menuDisplay.style.display = show ? "block" : "none";
    menuVisible = show;
}
body {
	font-family:Montserrat, sans-serif;
}

#searchOptionPlaceholder {
	display: inline-block;
}

#searchSelection {
	padding: 10px 20px;
	margin-right: 10px;
	background-color: #F0F3F5;
	display: inline-block;
	color: #000000;
	width: 140px;
	max-width: 200px;
	max-height: 35px;
	border: 2px solid black;
	vertical-align: middle;
}

#searchSelection img {
	float: right;
}

#searchLabel {
	display: inline-block;
	padding-top: 10px;
	vertical-align: top;
}

#searchOptions {
	display: none;
	background-color: #F0F3F5;
	position: absolute;
	z-index: 2;
}

#searchOptions ul {
	background-color: #F0F3F5;
	padding: 5px;
}

#searchOptions li {
	list-style-type: none;
	border-bottom: 2px solid black;
}

#searchOptions li:hover {
	background-color: #706868;
	color: #ffffff;
}

.buttonSearch {
	background-color: transparent;
	border: none;
	padding: 10px;
	font-size: 14px;
}

.searchSubHeading {
	font-size: 12px;
}

#searchInput {
	display: inline-block;
	background-color: #F0F3F5;
	padding: 10px 100px;
	position: relative;
	top: 0px;
	max-width: 350px;
	border: 2px solid black;
	vertical-align: middle;
}

#searchInput img {
	position: relative;
	left: 80px;
}

#searchBox {
	display: none;
	width: 80%;
	background-color: #F0F3F5;
	border: none;
	font-size: 1.5em;
	position: relative;
	right: 50px;
	vertical-align: middle;
}

#submit {
	border: none;
	background-image: url('https://library.domains.skidmore.edu/search/magnifyingGlass.png');
	background-repeat: no-repeat;
	background-size: contain;
	width: 50px;
	height: 30px;
	position: relative;
	right: -80px;
	vertical-align: middle;
}

#otherLinks {
	margin-top: 10px;
}

#otherLinks a{
	color: #000000;
}

#otherLinks a:hover{
	color: #006a52;
}
<h1>Library Search</h1>
<form method="post" action="https://library.domains.skidmore.edu/search/searchBox.php" id="librarySearch">
    <div id="searchSelection">
        <span id="searchLabel">Catalog</span>
        <img src="down.png" height="30px" width="30px" />
    </div>
    <div id="searchOptions">
        <ul>
            <li>
                <button type="button" name="searchPref" value="Catalog" class="buttonSearch">
                    Catalog<br /><br /><span class="searchSubHeading">Search books and DVDs</span>
                </button>
            </li>
            <li>
                <button type="button" name="searchPref" value="SearchMore" class="buttonSearch">
                    SearchMore<br /><br /><span class="searchSubHeading">Search everything</span>
                </button>
            </li>
            <li>
                <button type="button" name="searchPref" value="Journals" class="buttonSearch">
                    Journals<br /><br /><span class="searchSubHeading">Search journals</span>
                </button>
            </li>
        </ul>
    </div>
    <div id="searchInput">
        <input type="hidden" id="searchChoice" name="searchPref" value="catalog" />
        <input type="search" id="searchBox" size="60" name="searchText" placeholder="Search our holdings"/>
        <button type="submit" id="submit"></button>
    </div>
    <div id="otherLinks">
        <a href="https://lucy2.skidmore.edu/vwebv/searchAdvanced">Advanced Catalog Search |</a>
        <a href="https://lib-proxy01.skidmore.edu/login?url=https://firstsearch.oclc.org/dbname=WorldCat;done=referer;FSIP"> WorldCat |</a>
        <a href="http://libguides.skidmore.edu/c.php?g=35919&amp;p=1378258"> eBooks</a>
    </div>
</form>

Upvotes: 1

Related Questions