I didn't clearly explain what I was trying to do. Hopefully this will be better.
Currently I'm using this Fiddle to toggle some divs. It acts as an accordion and shows only one div at a time. Clicking one of the titles will show the content of that div and clicking another title will hide the first div and show that one.
What I am having trouble with (and would like to do) is when opening one div I would like to hide access to the other divs until that first div is closed.
Meaning if I Click on "Content2" to show that content, I would like to hide access to Content1, Content3, and Content4 until Content 2 is closed again.
function ReverseDisplay(d) {
var els = document.querySelectorAll('.toggle.active:not(#' + d + ')');
for (var i = 0; i < els.length; i++) {
els[i].classList.remove('active');
}
document.getElementById(d).classList.toggle('active')
}
.toggle {
display: none;
}
.toggle.active {
display: block;
}
<a href="javascript:ReverseDisplay('content1')">
Content1
</a>
<a href="javascript:ReverseDisplay('content2')">
Content2
</a>
<a href="javascript:ReverseDisplay('content3')">
Content3
</a>
<a href="javascript:ReverseDisplay('content4')">
Content4
</a>
<div id="content1" class="toggle">
<p>Content 1 goes here.</p>
</div>
<div id="content2" class="toggle">
<p>Content 2 goes here.</p>
</div>
<div id="content3" class="toggle">
<p>Content 3 goes here.</p>
</div>
<div id="content4" class="toggle">
<p>Content 4 goes here.</p>
</div>
This should work... Use the id passed to identify the element and add class by checking if active exists.
function ReverseDisplay(d) {
var id = d
var el = document.getElementById(id)
var elClassList = el.classList
var [...active] = document.querySelectorAll('.toggle.active')
debugger
if (active.length === 0) {
el.classList.add('active')
} else if (id === active[0].id) {
el.classList.remove('active')
}
}
.toggle {
display: none;
margin-top: 40px;
}
.toggle.active {
display: block;
}
a {
position: fixed;
top: 10px;
}
<a href="javascript:ReverseDisplay('uniquename')">
Click to show/hide.
</a>
<div id="uniquename" class="toggle">
<p>Content 1 goes here.</p>
</div>
<a href="javascript:ReverseDisplay('uniquename1')" style="left:150px">
Click to show/hide.
</a>
<div id="uniquename1" class="toggle">
<p>Content 2 goes here.</p>
</div>
Use this javascript function in order to achieve accordion style behaviour, for as many links as you might want to have.
function ReverseDisplay(d) {
var els = document.getElementById(d);
if (els.classList.contains('active')){
els.classList.remove('active');
}else{
var activeDivs = document.getElementsByClassName('active');
for (var i = 0; i < activeDivs.length; i++) {
activeDivs[i].classList.remove('active');
}
els.classList.add('active');
}
}
Related
The intension is to have a two column accordion, without limiting the "expand" field to the left or right column. The catch is that there will be multiple on one page. This is already created, but only button 1 is working. With the way my JS is going, it will get very very repetitive - I am looking for assistance with re-writing the JS to be multiple click friendly. Fiddle: https://codepen.io/ttattini/pen/abLzaaY
EDIT: It would also be perfect if one dropdown would close as the next is opened
HTML
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="row">
<div id="column">
<button id="button">I am Button #1</button>
<button id="button">I am Button #3</button>
</div>
<div id="column">
<button id="button">I am Button #2</button>
<button id="button">I am Button #4</button>
</div>
</div>
<div id="hidden">
<p id="content"> So here I am #1</p>
</div>
<div id="hidden">
<p id="content"> So here I am #2</p>
</div>
<div id="hidden">
<p id="content"> So here I am #3</p>
</div>
<div id="hidden">
<p id="content"> So here I am #4</p>
</div>
CSS
#hidden {
background: #ccc;
margin-top: 2%;
overflow: hidden;
transition: height 200ms;
height: 0; /* <-- set this */
}
#button {
padding: 10px;
margin-top: 5px;
width:50%;
margin-left: 10%;
cursor: pointer;
}
#row {
display: flex;
}
#column {
flex: 50%;
}
JS
$(function() {
var b = $("#button");
var w = $("#hidden");
var l = $("#content");
b.click(function() {
if (w.hasClass('open')) {
w.removeClass('open');
w.height(0);
} else {
w.addClass('open');
w.height(l.outerHeight(true));
}
});
});
The biggest issue is that you're using IDs when you should be using classes. IDs must be unique to each element in a page. When you repeat an ID, JS will only target the first element using that ID. That's why only the first one is working.
The second issue is that, because of the way the script is written, it will only target a single element. What you need to do is get all the elements you want to target by something like their class name and then loop through them, applying the event listener to each one and its appropriate children.
EDIT: Here is an example from some code I wrote for a page with multiple accordions a few weeks ago in vanilla JS
//Below I establish a counting variable and find all the accordions on the page
const acc = document.getElementsByClassName( 'accordion' );
let i;
//Looping through each accordion
for ( i = 1; i <= acc.length; i++ ) {
//Identify target for the event listener. In this case, a heading for each accordion, which I've numbered e.g. "title-1"
const title = 'title-' + i;
const label = document.getElementById( title );
//Identify target content, in this case a list that has a unique ID e.g. "list-1"
const listNum = 'list-' + i;
const list = document.getElementById( listNum );
//Add event listener to heading that toggles the active classes
label.addEventListener( 'click', function() {
label.classList.toggle( 'accordion--active' );
});
}
Of course, there's more than one way to skin a cat, but this is a working example.
I have tracked the clicked event of each button and showed the corresponding hidden content with the use of data- attribute.
I have used vanilla JavaScipt instead of jQuery.
const buttons = document.querySelectorAll('.button');
const hiddens = document.querySelectorAll('.hidden');
buttons.forEach((btn) => {
btn.addEventListener('click', btnClicked)
function btnClicked(e) {
hiddens.forEach((hidden) => {
if(e.target.dataset.btn == hidden.dataset.content) {
hidden.classList.toggle('height')
} else {
hidden.classList.remove('height')
}
})
}
})
.hidden {
background: #ccc;
margin-top: 2%;
padding-left:2%;
overflow: hidden;
transition: height 200ms;
height: 0; /* <-- set this */
}
.hidden.height {
height: 50px;
}
.button {
padding: 10px;
color: white;
background-color: #2da6b5;
border: none;
margin-top: 5px;
width:90%;
margin-left: 5%;
cursor: pointer;
}
.button:hover {
filter: brightness(.9);
}
#row {
display: flex;
}
.column {
flex: 50%;
}
<div id="row">
<div class="column">
<button class="button" data-btn="one">I am Button #1</button>
<button class="button" data-btn="three">I am Button #3</button>
</div>
<div class="column">
<button class="button" data-btn="two">I am Button #2</button>
<button class="button" data-btn="four">I am Button #4</button>
</div>
</div>
<div class="hidden" data-content="one">
<p class="content"> So here I am #1</p>
</div>
<div class="hidden" data-content="two">
<p class="content"> So here I am #2</p>
</div>
<div class="hidden" data-content="three">
<p class="content"> So here I am #3</p>
</div>
<div class="hidden" data-content="four">
<p class="content"> So here I am #4</p>
</div>
Also, please do not use the same ID at multiple elements.
I have a series of anchor links at the top of my page and a series of accordions at the bottom of the page. My goal is when someone clicks the anchor link, it will drop them down to the corresponding accordion and open it automatically. The accordion elements work when you click on them directly, but I want them to also be triggered by the anchor links. *NOTE: I am using WordPress so these elements are generated using a loop. Here's a very simplified version of what I'm working with (obviously without the loop):
var directoryAcc = document.getElementsByClassName("directory-accordion");
for (var i = 0; i < directoryAcc.length; i++) {
directoryAcc[i].addEventListener("click", function() {
var panel = this.nextElementSibling;
if (panel.style.display === "flex") {
panel.style.display = "none";
} else {
panel.style.display = "flex";
}
});
}
.container {
display: flex;
flex-direction: column;
}
.directory-accordion {
cursor: pointer;
}
.panel {
display: none;
}
<div class="container">
Link1
Link2
Link3
</div>
<hr>
<div class="container">
<span class="directory-accordion">Text1</span>
<p class="panel">Accordion text1</p>
<span class="directory-accordion">Text2</span>
<p class="panel">Accordion text2</p>
<span class="directory-accordion">Text3</span>
<p class="panel">Accordion text3</p>
</div>
Any help would be appreciated, thank you!
TLDR; HTMLElement.click
Updated 14th Feb 2021 UST
A suggestion for consideration:
supply an id attribute on each of the accordion elements,
Use accordion ids within links to scroll to the accordion when clicked.
Add a new click handler to the link container division (maybe using a nav instead of div container as shown in the post) that checks if a .link element (or descendant element of one) was clicked.
If a clicked link was found in step 3, get the accordion id value from its href attribute and use HTMLElement.click to simulate a click on it.
References:
What is DOM Event delegation,
HTMLElement.click,
Example
The following snippet demonstrates the above approach in simple fashion.
The example has no code to prevent toggling accordion expansion when clicking a link.
var directoryAcc = document.getElementsByClassName("directory-accordion");
for (var i = 0; i < directoryAcc.length; i++) {
directoryAcc[i].addEventListener("click", function() {
var panel = this.nextElementSibling;
if (panel.style.display === "flex") {
panel.style.display = "none";
} else {
panel.style.display = "flex";
}
});
}
// **** NEW: set up click listener on container ****
const container = document.querySelector(".container"); // the first one
container.addEventListener("click", function(event) {
const containerNode = event.currentTarget;
// allow child elements within link:
for( let node = event.target; node != containerNode; node = node.parentNode) {
if(node.classList.contains("link")) {
let id =node.getAttribute("href");
id = id && id[0]==='#' && id.substring(1);
let expander = document.getElementById(id)
if( expander) {
expander.click();
break;
}
}
}
});
.container {
display: flex;
flex-direction: column;
}
.directory-accordion {
cursor: pointer;
}
.panel {
display: none;
}
<div class="container">
Link1
Link2
Link3
</div>
<hr>
<div class="container">
<span class="directory-accordion" id="accordion1">Text1</span>
<p class="panel" id="accordian1">Accordion text1</p>
<span class="directory-accordion" id="accordion2">Text2</span>
<p class="panel">Accordion text2</p>
<span class="directory-accordion" id="accordion3">Text3</span>
<p class="panel">Accordion text3</p>
</div>
If you meant go section by section, i have a solution:
Use an "id" for spaces of paragraphs, that id is unique and defines your text that can be linked to with a #
Example:
<div class="container">
Link1
Link2
Link3
</div>
<hr>
<div class="container">
<span class="directory-accordion" id="pasta">Text1</span>
<p class="panel">Accordion text1</p>
Link2
<span class="directory-accordion" id="brownie">Text2</span>
<p class="panel">Accordion text2</p>
Link3
<span class="directory-accordion" id="meat3ameatingbagsofcheese">me at 3am eating bags of cheese</span>
<p class="panel">Accordion text3</p>
</div>
<style>
.container {
display: flex;
flex-direction: column;
}
.directory-accordion {
cursor: pointer;
}
.panel {
display: none;
}
span {
margin-top: 600px;
margin-bottom: 10px;
}
</style>
I ran into a problem that when I click on the button, it just flips the icon but only makes the invisible fields visible on the second click. Are there any idea how to do it?
(Heres a gif to show my problem: https://ibb.co/cvz7pWC )
Also heres my code :
function moreSoc() {
var moresoc = document.getElementById("moresoc");
var btnText = document.getElementById("mbtn");
if (moresoc.style.display === "none" ) {
moresoc.style.display = "block";
mbtn.innerHTML = "More ▲";
} else {
moresoc.style.display = "none";
mbtn.innerHTML = "More ▼"
}
}
.morebutton {
border: none;
background: #fff;
color: #111;
font-size: 32px;
}
#moresoc {
display: none;
}
<div class="wrapper more">
<button class="morebutton" id="mbtn" onclick="moreSoc()">More ▲</button>
</div>
<section class="social-links" id="moresoc">
<div class="wrapper">
<h2>Others</h2>
<div class="social-link facebook">
<p>Facebook</p>
</div>
<div class="social-link instagram">
<p>Instagram</p>
</div>
<div class="social-link twitter">
<p>Twitter</p>
</div>
<div class="social-link youtube">
<p>Youtube</p>
</div>
</div>
</section>
This could be to do with you not being to read element.style.display as none the first time round. This is because it has not yet been set by JavaScript, but just by css. I suggest changing your if statement to check for not "block".
function moreSoc() {
var moresoc = document.getElementById("moresoc");
var btnText = document.getElementById("mbtn");
if (moresoc.style.display != "block" ) {
moresoc.style.display = "block";
mbtn.innerHTML = "More ▲";
} else {
moresoc.style.display = "none";
mbtn.innerHTML = "More ▼"
}
}
.morebutton {
border: none;
background: #fff;
color: #111;
font-size: 32px;
}
#moresoc {
display: none;
}
<div class="wrapper more">
<button class="morebutton" id="mbtn" onclick="moreSoc()">More ▼</button>
</div>
<section class="social-links" id="moresoc">
<div class="wrapper">
<h2>Others</h2>
<div class="social-link facebook">
<p>Facebook</p>
</div>
<div class="social-link instagram">
<p>Instagram</p>
</div>
<div class="social-link twitter">
<p>Twitter</p>
</div>
<div class="social-link youtube">
<p>Youtube</p>
</div>
</div>
</section>
ElementCSSInlineStyle.style only returns (or sets) inline styles on an element. On your first click there is no inline display property to read so your condition sets it to none. On the second click your condition finds none and sets it to block.
The answer to look for !block solves this immediate problem but it stills ties your styling to your js rather than keeping it in your CSS. This means that if the default display property of your div needs to change in your layout (inline-block, flex, etc) you would need to change it in your js as well as your CSS.
For this reason I would recommend not using inline styles at all but rather rather use Element.classList to manage applied styles from your CSS – in this case just the adding/removing of a .hidden class that sets display to none without having to know what the appropriate visible display default is.
Also, since you are querying the button element in your code anyway, it would be better to apply the click listener from your js as well rather than inline.
function moreSoc() {
const moresoc = document.getElementById("moresoc");
if (moresoc.classList.contains('hidden')) {
moresoc.classList.remove('hidden');
mbtn.innerHTML = "More ▲";
} else {
moresoc.classList.add('hidden');
mbtn.innerHTML = "More ▼"
}
}
const mbtn = document.getElementById("mbtn");
mbtn.addEventListener('click', moreSoc);
.morebutton {
border: none;
background: #fff;
color: #111;
font-size: 32px;
}
#moresoc {
}
.hidden {
display: none;
}
<div class="wrapper more">
<button class="morebutton" id="mbtn">More ▲</button>
</div>
<section class="social-links hidden" id="moresoc">
<div class="wrapper">
<h2>Others</h2>
<div class="social-link facebook">
<p>Facebook</p>
</div>
<div class="social-link instagram">
<p>Instagram</p>
</div>
<div class="social-link twitter">
<p>Twitter</p>
</div>
<div class="social-link youtube">
<p>Youtube</p>
</div>
</div>
</section>
I've created a tabbed module which works by getting content that is in the .content div (which is hidden) and displaying it in a empty div called .overview.
The idea behind this tabbed module is that, on hover (or when class active exists), the content on the right will change based on what header is being selected from the left. I.e. If I hover over a header named "Red", the .overview div on the right will spit out "red".
However, the issues I'm having are the following:
In the demo below, don't hover on any of the headers. The .overview div has no content - which is obviously not ideal. If .tabs has class .active, then I want its content displayed on the right. I have a counter running which changes class active every 5 seconds. I don't only want to show stuff on hover.
Having said the above, if I hover over another tabs div, I want the counter to stop - to prevent it from adding class active to another .tabs div (because the hovered on tabs is active.
Demo:
$(document).ready(function() {
// add class .active on li hover
$('.tabs').mouseenter(function() {
//$('.tabs').removeClass('active');
$(this).parents('.tabs').addClass('active');
});
// Change active tab every x seconds
$(function() {
var list = $(".tabs"),
currentActive = 0;
time = 5; // interval in seconds
setInterval(function() {
currentActive = (currentActive + 1) % list.length;
list.removeClass('active').eq(currentActive).addClass('active');
}, time * 1000);
});
})
var overview = $('.overview');
$('.tabs').each(function(i) {
var thisTab = $(this);
var thisContent = thisTab.find('.content').html();
// when class .active exists, change content in .overview
if ($('.tabs').hasClass('active')) {
overview.html(thisContent);
}
// on hover, change content in .overview
thisTab.on('mouseenter', function(e) {
thisTab.addClass('active');
overview.html(thisContent);
})
.on('mouseleave', function(e) {
thisTab.removeClass('active');
overview.html('');
});
});
.tabs.active {
background: none yellow;
}
.list {
flex-basis: 40%;
}
.list li {
list-style-type: none;
}
.overview {
flex-basis: 60%;
border: 1px solid blue;
}
.content {
display: none;
}
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="d-flex flex-row">
<div class="list">
<li class="tabs active">
<div class="header"><span>Header</span></div>
<div class="content">
<p>Content 1</p>
</div>
</li>
<li class="tabs">
<div class="header"><span>Header 2</span></div>
<div class="content">
<p>Content 2</p>
</div>
</li>
<li class="tabs">
<div class="header"><span>Header 3</span></div>
<div class="content">
<p>Content 3</p>
</div>
</li>
</div>
<div class="overview"> </div>
</div>
Edit:
I've managed to make some movement on issue 1. I've added:
if ($('.tabs').hasClass('active')) {
overview.html(thisContent);
}
Which now, without hover, displays content in .overview, however, the content doesn't change when another tab is .active (i.e. in the demo, don't hover over anything, wait and it just shows content 3 for all headers.
I would do the following (I have commented what I have changed)
$(document).ready(function() {
var list = $(".tabs"),
overview = $('.overview'),
autoInterval, // interval var
currentActive = 0; // make this global to this closure
overview.html(list.eq(0).find('.content').html()); // set overview content
startInterval(); // start interval straight away
// add class .active on li hover
list.mouseenter(function() {
var thisTab = $(this);
currentActive = list.index(this); // set current active
list.removeClass('active'); // remove active class
thisTab.addClass('active'); // add active class
clearInterval(autoInterval); // clear the interval whilst hovering
var thisContent = thisTab.find('.content').html(); // get content
overview.html(thisContent); // set overview content
});
list.mouseleave(function() {
startInterval(); // restart the interval on mouseleave
});
function startInterval() {
// Change active tab every x seconds
time = 5; // interval in seconds
autoInterval = setInterval(function() {
currentActive = (currentActive + 1) % list.length;
list.removeClass('active');
var currentTab = list.eq(currentActive);
currentTab.addClass('active');
overview.html(currentTab.find('.content').html()); // set overview content
}, time * 1000);
}
});
.tabs.active {
background: none yellow;
}
.list {
flex-basis: 40%;
}
.list li {
list-style-type: none;
}
.overview {
flex-basis: 60%;
border: 1px solid blue;
}
.content {
display: none;
}
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="d-flex flex-row">
<div class="list">
<li class="tabs active">
<div class="header"><span>Header</span></div>
<div class="content">
<p>Content 1</p>
</div>
</li>
<li class="tabs">
<div class="header"><span>Header 2</span></div>
<div class="content">
<p>Content 2</p>
</div>
</li>
<li class="tabs">
<div class="header"><span>Header 3</span></div>
<div class="content">
<p>Content 3</p>
</div>
</li>
</div>
<div class="overview"> </div>
</div>
As soon as you add the mouseenter event, you need to stop the interval, you have the method clearInterval to do so.
I have two div's with different content. I managed to add show-hide div function when clicking on a button. The problem is, when one div is visible and I click on the second button, they are both visible. I would like to show only one div at a time - while one div is shown and I click on another button, the previous div should hide automatically.
I would not want to use jQuery, hope it's possible with pure JavaScript only.
function horTxtFunction() {
var x = document.getElementById("horTxt");
if (x.style.display === "none") {
x.style.display = "block";
} else {
x.style.display = "none";
}
}
function verTxtFunction() {
var x = document.getElementById("verTxt");
if (x.style.display === "none") {
x.style.display = "block";
} else {
x.style.display = "none";
}
}
<button onclick="horTxtFunction()">Horisontaalne tekstiga</button>
<button onclick="verTxtFunction()">Vertikaalne tekstiga</button>
<div id="horTxt" style="display:none;">
<p>Some content here</p>
</div>
<div id="verTxt" style="display:none;">
<p>Some different content here</p>
</div>
Consider wrapping the two <div> tags in a controller <div> tag and then using "state" to control which child <div> will show.
In the example below I am using the attribute dir to hold the state and the CSS to play off the state and the children <div> classes.
var holder = document.querySelector("[dir]");
function horTxtFunction() {
holder.setAttribute('dir', 'hor');
}
function verTxtFunction() {
holder.setAttribute('dir', 'ver');
}
[dir="ver"] > :not(.verTxt),
[dir="hor"] > :not(.horTxt) {
display: none;
}
<button onclick="horTxtFunction()">Horisontaalne tekstiga</button>
<button onclick="verTxtFunction()">Vertikaalne tekstiga</button>
<div dir="hor">
<div class="horTxt">
<p>Some content here</p>
</div>
<div class="verTxt">
<p>Some different content here</p>
</div>
</div>
The major benefit of doing it this way is if you need to add additional children:
var holder = document.querySelector("[dir]");
function toggle(val) {
holder.setAttribute('dir', val);
}
[dir="ver"] > :not(.verTxt),
[dir="hor"] > :not(.horTxt),
[dir="left"] > :not(.leftTxt),
[dir="right"] > :not(.rightTxt) {
display: none;
}
<button onclick="toggle('hor')">Horizontal</button>
<button onclick="toggle('ver')">Vertical</button>
<button onclick="toggle('left')">Left</button>
<button onclick="toggle('right')">Right</button>
<div dir="hor">
<div class="horTxt">
<p>Some content here</p>
</div>
<div class="verTxt">
<p>Some different content here</p>
</div>
<div class="leftTxt">
<p>This is the left text area</p>
</div>
<div class="rightTxt">
<p>This is the right text area</p>
</div>
</div>
Here I change to a single event handler and pass in the section I want to show. Then I had to extend the CSS to handle the new <div> tags. But now growing to more children is just adding the buttons, divs and CSS.
UPDATE
To hide all <div> tags first I made a minor change:
var holder = document.querySelector(".holder");
function toggle(val) {
holder.setAttribute('dir', val);
}
.holder > div {
display: none;
}
[dir=ver] > .verTxt,
[dir=hor] > .horTxt,
[dir=left] > .leftTxt,
[dir=right] > .rightTxt {
display: block;
}
<button onclick="toggle('hor')">Horizontal</button>
<button onclick="toggle('ver')">Vertical</button>
<button onclick="toggle('left')">Left</button>
<button onclick="toggle('right')">Right</button>
<div class="holder">
<div class="horTxt">
<p>Some content here</p>
</div>
<div class="verTxt">
<p>Some different content here</p>
</div>
<div class="leftTxt">
<p>This is the left text area</p>
</div>
<div class="rightTxt">
<p>This is the right text area</p>
</div>
</div>
This hides all of the internal <div> tags and then only shows the correct one based on the value of the dir attribute. Since there is no dir attribute to start then no internal <div>s will show.
You need to make sure to hide the other div and not just show the other.
function horTxtFunction() {
var x = document.getElementById("horTxt");
var otherDiv = document.getElementById("verTxt");
if (x.style.display === "none") {
x.style.display = "block";
overDiv.style.display = "none";
} else {
x.style.display = "none";
overDiv.style.display = "block";
}
}