closing dropdown by clicking outside - javascript

I want to be able to close my dropdown menu not only by clicking the x, but by clicking outside of it aswell. My js code doesnt seem to work. The Javascript is copied from a template i had left somewhere but im actually not able to fix it in order for it to work.
window.onclick = function closeMenu() {
if(document.getElementById("dropdown-content").style.left != "-300px") {
var dropdown = document.getElementById("dropdown-content");
var i;
for(i = 0; i < dropdown.length; i++) {
var openDropdown = dropdown[i];
if(openDropdown.style.left != ('-300px')) {
openDropdown.style.left = ('-300px');
}
}
}
}
.dropdown-content {
position: fixed;
background-color: rgb(255, 255, 255);
width: 300px;
height: 100%;
/*box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);*/
z-index: 600;
transition: 0.3s;
}
#dropdown-content {
left: -300px;
z-index: 600;
}
<div class="dropdown-container">
<div class="dropdown-content" id="dropdown-content">
<div class="menubutton" onclick="menu(this)">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</div>
<div class="menulist">
Angebot des Tages
Alle Angebote
Technik
Hardware
Mode
Automobil
</div>
</div>
</div>

const x = document.querySelector('.x');
const ul = document.querySelector('ul');
x.addEventListener('click', () => {
ul.classList.toggle('show');
});
document.addEventListener('click', ({
target
}) => {
if (target.matches('.x') === false) {
ul.classList.remove('show');
}
});
ul {
display: none;
}
ul.show {
display: block;
}
<div class="x">X</div>
<ul>
<li>Test</li>
<li>Test</li>
<li>Test</li>
<li>Test</li>
</ul>
Here, we track the X and just use toggle(), for any other click we ensure it is not X and then just remove() our show class.

If you are using just vanilla javascript, you can add a eventListener to the document object that listens to click event and checking some condition of the dropdown to check if it's opened, if it is, then closes it, if it's not, do nothing.
Something like:
document.addEventListener('click', () => {
const dropdown = ... // grab dropdown element
if (isOpened(dropdown)) closeDropdown()
})
EDIT: Also you should check if the click happened outside your dropdown since it will also be triggered if it is clicked. For that you can use the Node API.
Leaving it as:
document.addEventListener('click', (e) => {
const dropdown = ... // grab dropdown element
if (dropdown.contains(e.target)) return; // if the clicked element is inside the dropdown or it's the dropdown itself, do nothing
if (isOpened(dropdown)) closeDropdown()
})

Related

How to make navigation menu disappear when clicking anywhere on the screen with HTML, CSS, and javascript

This webpage has a navigation menu that is functioning properly. When the navigation icon is clicked, the menu will appear and the icon will be replaced with an "X" icon. When the "X" is clicked the navigation menu disappears.
However, it would nice to make it so that while the navigation menu is present, clicking anywhere outside of it will make it disappear as well.
Here is a picture for context:
HMTL
<html>
<div id="sideNav">
<nav>
<ul>
<li>HOME</li>
<li>COVID-19</li>
<li>SERVICES</li>
<li>REVIEWS</li>
<li>CONTACT</li>
</ul>
</nav>
</div>
<div id="menuBtn">
<img src="images/menu.PNG" id="menu">
</div>
</html>
CSS
#sideNav{
width: 200px;
height: 100vh;
position: fixed;
right: -250px;
top:0;
background: #009688;
z-index: 2;
transition: 0.33s;
}
javascript
var menuBtn = document.getElementById("menuBtn")
var sideNav = document.getElementById("sideNav")
var menu = document.getElementById("menu")
sideNav.style.right = "-250px";
menuBtn.onclick = function(){
if(sideNav.style.right == "-250px"){
sideNav.style.right = "0";
menu.src = "images/close.PNG";
}
else{
sideNav.style.right = "-250px";
menu.src = "images/menu.PNG";
}
}
use event.stopPropagation() and add click event to the document
var menuBtn = document.getElementById("menuBtn")
var sideNav = document.getElementById("sideNav")
var menu = document.getElementById("menu")
menuBtn.onclick = function(e) {
//stop propagation of document click
e.stopPropagation()
//toggle side nav
if (!sideNav.classList.contains("open")) {
sideNav.classList.add("open");
menu.src = "images/close.PNG";
} else {
sideNav.classList.remove("open");
menu.src = "images/menu.PNG";
}
}
//stop propagation on the side nav element
sideNav.onclick = function(e) {
e.stopPropagation()
}
//close menu when document is clicked
document.onclick = function() {
sideNav.classList.remove("open");
menu.src = "images/menu.PNG";
}
#sideNav {
width: 200px;
height: 100vh;
position: fixed;
right: -250px;
top: 0;
background: #009688;
z-index: 2;
transition: 0.33s;
}
/*set the right to 0 for class open*/
#sideNav.open {
right: 0;
}
<html>
<div id="sideNav">
<nav>
<ul>
<li>HOME</li>
<li>COVID-19</li>
<li>SERVICES</li>
<li>REVIEWS</li>
<li>CONTACT</li>
</ul>
</nav>
</div>
<div id="menuBtn">
<img src="images/menu.PNG" id="menu">
</div>
</html>
To able to click anywhere on the screen and have something, then you should listen for clicks anywhere on the page. Attach an event listener to the document where you check if the clicked element is not a child of the side nav element.
You can check this with the closest() method which is like a querySelector() that runs up the DOM and evaluates each parent.
If there is no hit, then it means that you're not clicking inside the side navigation, so it must be outside the side navigation. Now you know that you can close the menu.
document.addEventListener('click', event => {
// If the clicked element is not a child of #sideNav..
if (event.target.closest('#sideNav') === null) {
// ..then close navigation..
// { your code here }
}
});
With jQuery:
$(document).mouseup(function (e){
let sideNav = $("#sideNav");
if (!sideNav.is(e.target) && sideNav.has(e.target).length === 0) {
sideNav.addClass("hide");
$("#menu").removeClass("close");
}
});
Pure js:
window.addEventListener('mouseup', function(e){
var sideNav = document.getElementById("sideNav");
var menu = document.getElementById("menu")
if (e.target.id != 'moreDrop') {
sideNav.classList.add('hide');
}
});

How to remove unwanted counting-increment from forEach method?

I'm having a trouble to get rid of unwanted counting-increment from forEach method in my CodePen.
The algorithm is simple:
EventManager() registers an event called mouseenter to every each of menuCells.
menuCount() gets an current index of the targeted cell. Next, matches it between a new node's index for showing or hiding a slateCell.
slateCount() gets the targeted item from the menuCount(), and using forEach() for getting li's index.
The problem is every time when I restart the event, an increment of the forEach() itself is increasing time to time like this: (couldn't imagine better describing words. Limited vocabulary problem :|)
This may not be a big problem because what the function does is actually just getting an index. But since I noticed this was abnormal, I wanted to know why and how to get rid of that unwanted increment counting.
I've been trying to find how to resolve my case or such as mine but still haven't found any of infos or articles.
Are there any solutions to fix this problem?
CodePen
'use strict';
const Slater = (function() {
let menu = document.querySelector('.menu'),
slate = document.querySelector('.slate');
let node_menuCells = menu.querySelectorAll('.cell'),
node_slateCells = slate.querySelectorAll('.grid.first > .cell');
let menuCells = Array.from(node_menuCells);
function EventManager(array, node) {
array.reduce((init, length, current) => {
node[current].addEventListener('mouseenter', (e) => menuCount(e, current, node_slateCells));
}, 0);
}
function menuCount(event, index, node) {
console.log(`menuCell count is: ${index}`);
node.forEach((item, i) => {
let comparing = (i == index) ? item.classList.add('shown') : item.classList.remove('shown');
slateCount(item);
})
}
function slateCount(item) {
let node_cellItems = item.querySelectorAll('li');
node_cellItems.forEach((listItem, n) => {
listItem.addEventListener('mouseenter', (e) => {
console.log(`slateCell count is: ${n}`);
})
})
}
return {
initialize: EventManager(menuCells, node_menuCells)
}
}());
* {
margin: 0;
padding: 0;
color: white;
}
ul li {
list-style: none;
text-decoration: none;
padding: 20px 0;
}
.layout {
width: 900px;
display: flex;
flex-flow: row;
align-items: center;
background-color: #414141;
}
.menu {
height: 60px;
}
.cell {
margin: 0 20px;
font-family:'Helvetica';
}
.slate {
border-top: 1px solid rgb(160, 117, 0);
height: 20rem;
}
.grid {
width: 50%;
height: 100%;
border: 1px solid rgb(160, 117, 0);
}
.grid > .cell {
display: none;
position: absolute;
color: rgb(36, 88, 21);
}
.shown {
display: block !important;
}
<div class="menu layout">
<div class="cell">Lorem</div>
<div class="cell">Ipsum Dolor</div>
<div class="cell">Consectetur</div>
<div class="cell">Similique</div>
</div>
<div class="slate layout">
<div class="grid first">
<ul class="cell">
<li>Sample Text 001</li>
<li>Sample Text 002</li>
</ul>
<ul class="cell">
<li>Sample Text 003</li>
<li>Sample Text 004</li>
</ul>
</div>
<div class="grid second">
<ul class="cell">
<li>Sample Text 001</li>
<li>Sample Text 002</li>
</ul>
<ul class="cell">
<li>Sample Text 003</li>
<li>Sample Text 004</li>
</ul>
</div>
</div>
From your code, every time you hover the top menu, a for-loop is ran to add event listeners on the slate items. So if you hover the slate items for the first time, the behavior is the same as you would expect, logging just once. However, if you repeat the action of hovering the menu, more and more of the same event listeners will be added to the slate items, so the log starts to blow up quickly, causing memory leaks.
To solve this, extract the logic of adding event listeners into the init function so that it will only be executed once.
function EventManager(array, node) {
array.reduce((init, length, current) => {
node[current].addEventListener('mouseenter', (e) => menuCount(e, current, node_slateCells));
}, 0);
// add the event listeners here
node_slateCells.forEach(item => slateCount(item));
}
function menuCount(event, index, node) {
console.log(`menuCell count is: ${index}`);
node.forEach((item, i) => {
let comparing = (i == index) ? item.classList.add('shown') : item.classList.remove('shown');
// slateCount(item);
})
}
function slateCount(item) {
let node_cellItems = item.querySelectorAll('li');
node_cellItems.forEach((listItem, n) => {
listItem.addEventListener('mouseenter', (e) => {
console.log(`slateCell count is: ${n}`);
})
})
}

Javascript tabs using data attributes rather than IDs to link button and tab

I'm wanting to create a variation of Javascript tabs using data attributes rather than IDs to link the tab and the content.
Here's how it should work:
Clicking a <button class="tab" data-tab-trigger="1"> adds a class of is-active and removes any is-active classes from all other button elements
The value of data-tab-trigger matches the value of data-tab-content on the corresponding <div class="tab-content" data-tab-content="1"> and should add a class of is-open to it
The is-active class highlights the active tab and the is-open class shows the related tab content
Here's the JS I'm currently working which isn't working as expected:
var tabTriggerBtns = document.querySelectorAll('.tabs li button');
tabTriggerBtns.forEach(function(tabTriggerBtn, index){
tabTriggerBtn.addEventListener('click', function(){
var tabTrigger = this;
var tabTriggerData = tabTrigger.getAttribute('data-tab-trigger');
var tabContent = document.querySelector('.tab-content');
var currentTabData = document.querySelector('.tab-content[data-tab-content="' + tabTriggerData + '"]').classList.add('is-open');
if(tabContent !== currentTabData) {
tabContent.classList.toggle('is-open');
}
if(tabTrigger.classList.contains('is-active')) {
tabTrigger.classList.remove('is-active');
}
else {
tabTriggerBtn.classList.remove('is-active');
tabTrigger.classList.add('is-active');
}
});
});
Here's a Codepen with my ongoing script: https://codepen.io/abbasarezoo/pen/752f24fc896e6f9fcce8b590b64b37bc
I'm having difficulty finding what's going wrong here. I'm relatively comfortable writing jQuery, but quite raw when it comes to vanilla JS so any help would be very much appreciated.
One of your main issue is in this line:
tabContent !== currentTabData
You may use dataset in order to access data attributes.
Moreover, you may simplify your code in few steps:
remove classess
add classess
The snippet:
var tabTriggerBtns = document.querySelectorAll('.tabs li button');
tabTriggerBtns.forEach(function(tabTriggerBtn, index){
tabTriggerBtn.addEventListener('click', function(){
var currentTabData = document.querySelector('.tab-content[data-tab-content="' + this.dataset.tabTrigger + '"]');
// remove classess
document.querySelector('.tab-content.is-open').classList.remove('is-open');
document.querySelector('.tabs li button.is-active').classList.remove('is-active');
// add classes
currentTabData.classList.add('is-open');
this.classList.add('is-active');
});
});
* {
margin: 0;
padding: 0;
}
body {
display: flex;
}
.tabs {
width: 25%;
border: 2px solid red;
}
button.is-active {
background-color: red;
}
.tab-content__outer {
width: 75%;
}
.tab-content {
display: none;
}
.tab-content.is-open {
display: block;
background-color: yellow;
}
<ul class="tabs">
<li>
<button class="tab is-active" data-tab-trigger="1">First</button>
</li>
<li>
<button class="tab" data-tab-trigger="2">Second</button>
</li>
<li>
<button class="tab" data-tab-trigger="3">Third</button>
</li>
</ul>
<div class="tab-content__outer">
<div class="tab-content is-open" data-tab-content="1">
First
</div>
<div class="tab-content" data-tab-content="2">
Second
</div>
<div class="tab-content" data-tab-content="3">
Third
</div>
</div>

Tooltipster content doubling up each time it is opened

I'm using Tooltipster to show a list of items that the user can click so as to enter the item into a textarea. When a tooltip is created, I get its list of items with selectors = $("ul.alternates > li");
However, each time a tooltip is opened the item clicked will be inserted a corresponding number of times; for example if I've opened a tooltip 5 times then the item clicked will be inserted 5 times. I've tried deleting the variable's value after a tooltip is closed with functionAfter: function() {selectors = null;} but that had no effect.
I have a Codepen of the error here that should make it clearer.
// set list to be tooltipstered
$(".commands > li").tooltipster({
interactive: true,
theme: "tooltipster-light",
functionInit: function(instance, helper) {
var content = $(helper.origin).find(".tooltip_content").detach();
instance.content(content);
},
functionReady: function() {
selectors = $("ul.alternates > li");
$(selectors).click(function() {
var sampleData = $(this).text();
insertText(sampleData);
});
},
// this doesn't work
functionAfter: function() {
selectors = null;
}
});
// Begin inputting of clicked text into editor
function insertText(data) {
var cm = $(".CodeMirror")[0].CodeMirror;
var doc = cm.getDoc();
var cursor = doc.getCursor(); // gets the line number in the cursor position
var line = doc.getLine(cursor.line); // get the line contents
var pos = {
line: cursor.line
};
if (line.length === 0) {
// check if the line is empty
// add the data
doc.replaceRange(data, pos);
} else {
// add a new line and the data
doc.replaceRange("\n" + data, pos);
}
}
var code = $(".codemirror-area")[0];
var editor = CodeMirror.fromTextArea(code, {
mode: "simplemode",
lineNumbers: true,
theme: "material",
scrollbarStyle: "simple",
extraKeys: { "Ctrl-Space": "autocomplete" }
});
body {
margin: 1em auto;
font-size: 16px;
}
.commands {
display: inline-block;
}
.tooltip {
position: relative;
opacity: 1;
color: inherit;
}
.alternates {
display: inline;
margin: 5px 10px;
padding-left: 0;
}
.tooltipster-content .alternates {
li {
list-style: none;
pointer-events: all;
padding: 15px 0;
cursor: pointer;
color: #333;
border-bottom: 1px solid #d3d3d3;
span {
font-weight: 600;
}
&:last-of-type {
border-bottom: none;
}
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/theme/material.min.css" rel="stylesheet"/>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/235651/jquery-3.2.1.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/235651/tooltipster.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/codemirror.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/addon/mode/simple.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/addon/hint/show-hint.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.25.2/addon/scroll/simplescrollbars.js"></script>
<div class="container">
<div class="row">
<div class="col-md-6">
<ul class="commands">
<li><span class="command">Hover for my list</span><div class="tooltip_content">
<ul class="alternates">
<li>Lorep item</li>
<li>Ipsum item</li>
<li>Dollar item</li>
</ul>
</li>
</div>
</ul>
</div>
<div class="col-md-6">
<textarea class="codemirror-area"></textarea>
</div>
</div>
</div>
Tooltipster's functionReady fires every time the tooltip is added to the DOM, which means every time a user hovers over the list, you are binding the event again.
Here are two ways to prevent this from happening:
Attach a click handler to anything that exists in the DOM before the tooltip is displayed. (Put it outside of tooltipspter(). No need to use functionReady.)
Example:
$(document).on('click','ul.alternates li', function(){
var sampleText = $(this).text();
insertText(sampleText);
})
Here's a Codepen.
Unbind and bind the event each time functionReady is triggered.
Example:
functionReady: function() {
selectors = $("ul.alternates > li");
$(selectors).off('click').on('click', function() {
var sampleData = $(this).text();
insertText(sampleData);
});
}
Here's a Codpen.
You are binding new clicks every time.
I would suggest different code style but in that format you can just add before the click event
$(selectors).unbind('click');
Then do the click again..

Creating Drop down page

I am after creating a drop down page like on my examples below:
This is how I would like it to show when the arrow on the side is cliked.
How would I make something like this and is there any examples any where for me to study to help me make this ?
If you can use jquery you can play with hasClass, addClass and removeClass to change the height of the submenu
Working Demo.
$(".btn").click(function() {
if ($(".menu").hasClass("dropped")) {
$(".menu").removeClass("dropped");
} else {
$(".menu").addClass("dropped");
}
});
.menu {
height: 0;
overflow: hidden;
transition: all 0.5s ease 0s;
}
.dropped {
height: inherit;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<button class="btn">
Dropdown
</button>
<div class="menu">
<p>Stufss...</p>
<p>Stufss...</p>
<p>Stufss...</p>
<p>Stufss...</p>
<p>Stufss...</p>
<p>Stufss...</p>
</div>
With 3 div elements you can get a result like the one pictured. From the picture it looks like one div is wrapping around two other div elements, a div element that already has some information and a div element that will grow/shrink in size through appending/removing elements when the user presses the dropdown button.
Here is a working example:
var extraInformation = document.getElementById('infoLong');
var dropdown = document.getElementById('dropdown');
// The extra info that will be appended into the infoLong div
var someHeading = document.createElement('h4');
someHeading.innerHTML = 'Detailed Game Information';
someHeading.style.background = '#C58AC5';
var teamOneInfo = document.createElement('p');
teamOneInfo.innerHTML = 'Team 1: Lost';
teamOneInfo.style.background = '#FF516B';
var teamTwoInfo = document.createElement('p');
teamTwoInfo.innerHTML = 'Team 2: Won';
teamTwoInfo.style.background = '#3FBFBF';
// Should add more detailed information when the dropdown button
// is pressed only if the infoLong div is empty
dropdown.addEventListener('click', function(){
if(extraInformation.children.length === 0){
extraInformation.appendChild(someHeading);
extraInformation.appendChild(teamOneInfo);
extraInformation.appendChild(teamTwoInfo);
}else{
while(extraInformation.firstChild){
extraInformation.removeChild(extraInformation.firstChild);
}
}
});
#infoShort {
background: #3FBFBF;
}
p {
margin: 0;
}
h4 {
margin: 0;
}
<div id='gameInfoContainer'>
<div id='infoShort'>
<h3>Game Summary</h3>
<button id='dropdown'>Dropdown</button>
</div>
<div id='infoLong'></div>
</div>

Categories