I want to set up a JS based tab navigation but somehow the Tab content doesnt show properly.
The JS code seems off but I cannot find the error.
I just want the Tab Content to show that belongs to one tab.
The Code looks like this:
function openPage(e, pageName) {
// Declare all variables
let i, content, tabLinks;
// Get all elements with class="tabs_content" and hide them
content = document.getElementsByClassName("tabs_content");
for (i = 0; i < content.length; i++) {
content[i].style.display = "none";
}
// Get all elements with class="tabs_button" and remove the class "active"
tabLinks = document.getElementsByClassName("tabs_button");
for (i = 0; i < tabLinks.length; i++) {
tabLinks[i].className = tabLinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the link that opened the tab
document.getElementById(pageName).style.display = "block";
e.currentTarget.className += " active";
}
// Get the element with id="defaultOpen" and click on it
document.getElementById("defaultOpen").click();
.wrapper{
display: grid;
grid-template-columns: 150px 1fr;
border:1px solid #cccccc;
padding: 15px;
margin: 0 auto;
grid-template-areas:
'nav main '
'nav main'
}
.tabs_sidebar {
grid-area: nav;
display: flex;
flex-direction: column;
background: #cccccc;
}
section {
display: flex;
flex-direction: row;
flex-shrink: 0;
min-height: 400px;
}
.tabs_content {
grid-area: main;
background: #f6e3e3;
padding-left: 15px;
font-size: 1rem;
}
.tabs_button {
display: block;
padding: 10px;
background: #eeeeee;
border: none;
width: 100%;
outline: none;
cursor: pointer;
font-size: 1rem;
}
.tabs_button:active {
background: #dddddd;
}
.tabs_button:not(:last-of-type){
border-bottom: 1px solid #cccccc;
}
<div class="wrapper">
<nav class="tabs_sidebar">
<button class="tabs_button" onclick="openPage(e, 'Home')" id="defaultOpen">Home</button>
<button class="tabs_button" onclick="openPage(e, 'News')">News</button>
<button class="tabs_button" onclick="openPage(e, 'Contact')">Contact</button>
</nav>
<section class="tabs_content tabs_content--active">
<div id="Home" class="tabs_content">
<h2>Tab #1</h2>
<p>Content Page 1 </p>
</div>
<div class="tabs_content">
<h2>Tab #2</h2>
<p>Content Page 2 </p>
</div>
<div class="tabs_content">
<h2>Tab #3</h2>
<p>Content Page 3</p>
</div>
</section>
</div>
I cannot find the error, but the problem seems to be the <section>. Any help is much appreciated.
Your code has more than one problem.
The one you're currently stuck on is that e is not defined in this template bit:
<button class="tabs_button" onclick="openPage(e, 'Home')" id="defaultOpen">Home</button>
The default name of the event when you want to pass it to the function is event, not e (and you can't change it, it's a convention - you can only change its name in the function). In other words, even if in the function it's named e, you still have to reference it as event in the template.
Here are the rest of the problems, in the order I encountered them as I worked my way into making it work:
the .tabs_button:active selector is wrong. You probably want to use .tabs_button.active (since you're applying the class active to it).
you have tabs_content class on the tabs wrapper, so the entire wrapper is hidden, when you probably only want to set display:none onto its children.
you're missing the ids on second and third tab
you need width: 100% on .tabs_content
See it here:
function openPage(e, pageName) {
let i, content, tabLinks;
content = document.getElementsByClassName("tabs_content");
for (i = 0; i < content.length; i++) {
content[i].style.display = "none";
}
tabLinks = document.getElementsByClassName("tabs_button");
for (i = 0; i < tabLinks.length; i++) {
tabLinks[i].className = tabLinks[i].className.replace(" active", "");
}
document.getElementById(pageName).style.display = "block";
e.currentTarget.className += " active";
}
document.getElementById("defaultOpen").click();
.wrapper {
display: grid;
grid-template-columns: 150px 1fr;
border: 1px solid #cccccc;
padding: 15px;
margin: 0 auto;
grid-template-areas: 'nav main ' 'nav main'
}
.tabs_sidebar {
grid-area: nav;
display: flex;
flex-direction: column;
background: #cccccc;
}
section {
display: flex;
flex-direction: row;
flex-shrink: 0;
min-height: 400px;
}
.tabs_content {
grid-area: main;
background: #f6e3e3;
padding-left: 15px;
font-size: 1rem;
width: 100%;
}
.tabs_button {
display: block;
padding: 10px;
background: #eeeeee;
border: none;
width: 100%;
outline: none;
cursor: pointer;
font-size: 1rem;
}
.tabs_button.active {
background: #dddddd;
}
.tabs_button:not(:last-of-type) {
border-bottom: 1px solid #cccccc;
}
body {
margin: 0;
}
<div class="wrapper">
<nav class="tabs_sidebar">
<button class="tabs_button" onclick="openPage(event, 'Home')" id="defaultOpen">Home</button>
<button class="tabs_button" onclick="openPage(event, 'News')">News</button>
<button class="tabs_button" onclick="openPage(event, 'Contact')">Contact</button>
</nav>
<section class="tabs_content--active">
<div id="Home" class="tabs_content">
<h2>Tab #1</h2>
<p>Home</p>
</div>
<div id="News" class="tabs_content">
<h2>Tab #2</h2>
<p>News</p>
</div>
<div id="Contact" class="tabs_content">
<h2>Tab #3</h2>
<p>Contact</p>
</div>
</section>
</div>
Apart from that, in my estimation, you're doing too much by JavaScript. Instead of applying inline styles, you should only apply and remove classes which, in turn, apply styles. You end up writing less code and it's easier to manage & debug.
Another thing I would personally change is replace className usage with classList, which has a more powerful and more expressive syntax.
This doesn't mean it's not doable the way you set out to do it.
The main problem when mixing stylesheet CSS with inline styles is that inline styles can only be overwritten with !important, which leads to a whole new level of problems. By only applying classes you keep the specificity battle inside your stylesheet, which is where it should take place.
Here's how I'd write it:
function openPage(e) {
const target = e.target.dataset['target'];
if (target) {
[...document.querySelectorAll('.tabs_sidebar > button')].forEach(el => {
el.classList[el.dataset['target'] === target ? 'add' : 'remove']('active');
document.getElementById(el.dataset['target'])
.classList[el.dataset['target'] === target ? 'add' : 'remove']('active');
})
}
}
document.querySelector('.tabs_sidebar').addEventListener('click', openPage);
.wrapper {
min-height: 100vh;
display: grid;
grid-template-columns: 150px 1fr;
border: 1px solid #cccccc;
padding: 15px;
grid-template-areas: 'nav main'
}
.tabs_sidebar {
grid-area: nav;
display: flex;
flex-direction: column;
background: #cccccc;
}
section {
display: flex;
flex-direction: row;
}
.tabs_content--active > div {
width: 100%;
grid-area: main;
background: #f6e3e3;
padding-left: 15px;
font-size: 1rem;
display: none;
}
.tabs_content--active > div.active {
display: block;
}
.tabs_sidebar > button {
display: block;
padding: 10px;
background: #eeeeee;
border: none;
width: 100%;
outline: none;
cursor: pointer;
font-size: 1rem;
}
.tabs_sidebar > button.active {
background: #dddddd;
}
.tabs_sidebar > button:not(:last-of-type) {
border-bottom: 1px solid #cccccc;
}
body {
margin: 0;
}
* {
box-sizing: border-box;
}
<div class="wrapper">
<nav class="tabs_sidebar">
<button data-target="Home" class="active">Home</button>
<button data-target="News">News</button>
<button data-target="Contact">Contact</button>
</nav>
<section class="tabs_content--active">
<div id="Home" class="active">
<h2>Tab #1</h2>
<p>Home</p>
</div>
<div id="News">
<h2>Tab #2</h2>
<p>News</p>
</div>
<div id="Contact">
<h2>Tab #3</h2>
<p>Contact</p>
</div>
</section>
</div>
Note I've also made changes to markup (HTML) and CSS, not just JavaScript.
As requested in comments, here's an explanation on what the JavaScript code does:
Instead of placing a click handler on each individual button, I've placed one on their parent. It has the advantage of working on future buttons as well, should your page be dynamic. If you bind on each button, once the buttons change you have to figure out which ones are new and only bind the handler on those (you don't want to bind the handler twice on the same element).
When a click is performed inside that button container, you first have to determine which button was clicked (or if the click was outside of any existing buttons). See how data attributes work here. If the click was performed on a button (if (target)), instead of having two loops (one through the buttons and one through the tabs), I only used one loop (through the buttons).
Instead of doing a classic for loop I chose to cast the NodeList returned by querySelectorAll to an array which I can then iterate using .forEach().
Note: To be totally fair, I suspect the for loop (what you used) is more performant but the difference is quite small and I tend to go for the shorter syntax:
[...document.querySelectorAll('.tabs_sidebar > button')].forEach(el => {
// do stuff with each looped element. In this case, a button
})
Now that we have the element, let's do something with it: Add or remove the class active based on whether or not the current button's target matches the one that was clicked (which we already have saved in target const).
I personally prefer classList to className. It works on the classes array, rather than on the resulting classes string. Has add, remove and replace methods and you don't end up having to worry about adding spaces manually (or trimming them). I'm using
el.classList[el.dataset['target'] === target ? 'add' : 'remove']('active');
instead of
if (el.dataset['target'] === target) {
el.classList.add('active');
} else {
el.classList.remove('active');
}
I used the array notation to call add and remove methods on classList. (i.e: in Javascript you can write foo.bar(arg) as foo['bar'](arg) - all methods are still object properties). The advantage is that inside the array notation you can switch the method being called, based on a ternary condition, which is exactly what I'm doing above.
Using the same technique, I'm also adding/removing the active class from the corresponding tab, which I'm selecting by id.
document.getElementById(el.dataset['target'])
.classList[el.dataset['target'] === target ? 'add' : 'remove']('active');
The only thing that could slightly be improved about my code is saving the result of the ternary condition instead of doing it twice. Like this:
if (target) {
[...document.querySelectorAll('.tabs_sidebar > button')].forEach(el => {
const isActive = el.dataset['target'] === target;
el.classList[isActive ? 'add' : 'remove']('active');
document.getElementById(el.dataset['target'])
.classList[isActive ? 'add' : 'remove']('active');
})
}
Hope that makes sense.
Related
I'm taking a Javascript Basics course and I'm stuck on getting the list to sort. Specifically with Step 8. How do I get it to recognize that they select either ascending or descending and make it populate? Right now it disappears and I see no console errors. Thanks for any help!
Here is what I have so far. You can ignore the controlling your code section in the html. I took that js section out to hopefully make it easier to follow:
/* FETCH */
// Step 1: Declare a global empty array variable to store a list of temples
let templeList = [];
// Step 2: Declare a function named output that accepts a list of temples as an array argument and does the following for each temple:
function output(temples) {
templeList = temples;
let div = document.getElementById('temples');
temples.forEach(temple => {
// - Creates an HTML <article> element
let article = document.createElement('article');
// - Creates an HTML <h3> element and add the temple's templeName property to it
// - Appends the <h3> element, the two <h4> elements, and the <img> element to the <article> element as children
let h3 = document.createElement('h3');
h3.innerHTML = temple.templeName;
article.appendChild(h3);
// - Creates an HTML <h4> element and add the temple's location property to it
// - Appends the <h3> element, the two <h4> elements, and the <img> element to the <article> element as children
let h4First = document.createElement('h4');
h4First.innerHTML = temple.location;
article.appendChild(h4First);
// - Creates an HTML <h4> element and add the temple's dedicated property to it
// - Appends the <h3> element, the two <h4> elements, and the <img> element to the <article> element as children
let h4Second = document.createElement('h4');
h4Second.innerHTML = temple.dedicated;
article.appendChild(h4Second);
// - Creates an HTML <img> element and add the temple's imageUrl property to the src attribute and the temple's templeName property to the alt attribute
let image = document.createElement('img');
image.setAttribute('src', temple.imageUrl);
article.appendChild(image);
// - Appends the <article> element to the HTML element with an ID of temples
div.appendChild(article);
});
};
// Step 3: Create another function called getTemples. Make it an async function.
async function getTemples() {
// Step 4: In the function, using the built-in fetch method, call this absolute URL: 'https://byui-cse.github.io/cse121b-course/week05/temples.json'. Create a variable to hold the response from your fetch. You should have the program wait on this line until it finishes.
let responseFromURL = await fetch('https://byui-cse.github.io/cse121b-course/week05/temples.json');
// Step 5: Convert your fetch response into a Javascript object ( hint: .json() ). Store this in the templeList variable you declared earlier (Step 1). Make sure the the execution of the code waits here as well until it finishes.
let templeArray = await responseFromURL.json();
output(templeArray);
};
// Step 6: Finally, call the output function and pass it the list of temples. Execute your getTemples function to make sure it works correctly.
getTemples(templeList);
// Step 7: Declare a function named reset that clears all of the <article> elements from the HTML element with an ID of temples
function reset() {
return document.getElementById('temples').innerHTML = '';
};
// Step 8: Declare a function named sortBy that does the following:
function sortBy() {
// - Calls the reset function
reset();
// - Sorts the global temple list by the currently selected value of the HTML element with an ID of sortBy
let asc = document.getElementById('templeNameAscending');
let dsc = document.getElementById('templeNameDescending');
let sort = document.getElementById('sortBy');
if (asc) {
let sorted = templeList.sort(function(a,b) {return a-b});
return output(sorted);
}else if (dsc) {
let sorted = templeList.sort(function(a,b) {return a-b});
return output(sorted);
};
};
// Step 9: Add a change event listener to the HTML element with an ID of sortBy that calls the sortBy function
document.getElementById('sortBy').addEventListener('change', sortBy);
/* STRETCH */
// Consider adding a "Filter by" feature that allows users to filter the list of temples
// This will require changes to both the HTML and the JavaScript files
/* HTML Selectors */
article {
margin: 10px;
}
body {
font-family: 'Kalam', cursive;
}
div {
margin: 10px;
text-align: center;
}
footer {
background-color: gray;
color: white;
padding: 5px;
text-align: center;
}
header {
margin: auto;
text-align: center;
}
img {
width: 80%;
}
label {
display: inline-block;
min-width: 120px;
text-align: right;
}
main {
text-align: center;
}
nav {
background-color: black;
color: white;
}
nav ul {
display: flex;
flex-direction: column;
justify-content: center;
align-content: space-around;
margin: 0;
padding: 0;
}
nav ul li:first-child {
display: block;
}
nav ul li {
display: none;
list-style: none;
margin: 10px;
}
nav ul li a {
color: white;
display: block;
padding: 10px;
text-decoration: none;
}
nav ul li a:hover {
background-color: #efefef;
color: black;
}
section {
/* display: flex;
flex-direction: column;
align-items: center; */
}
/* Class Selectors */
.active {
background-color: white;
color: black;
}
.open li {
display: block;
}
/* ID Selectors */
#favorite-foods, #hobbies, #places-lived {
margin: 0;
padding: 0;
list-style: none;
display: inline-block;
text-align: left;
vertical-align: top;
}
#temples {
display: grid;
grid-template-columns: 1fr;
}
/* Media Queries */
#media only screen and (min-width: 32.5em) {
nav ul {
flex-direction: row;
}
nav ul li:first-child {
display: none;
}
nav ul li {
display: inline;
margin: 0 10px;
}
#temples {
grid-template-columns: 1fr 1fr;
}
}
#media only screen and (min-width: 64em) {
#temples {
grid-template-columns: 1fr 1fr 1fr;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>CSE 121b: Week 05 | Sample Solution</title>
<link href="https://fonts.googleapis.com/css?family=Kalam&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<nav>
<ul id="menu">
<li><a id="toggleMenu">≡</a></li>
<li>Home</li>
<li>Lesson 2</li>
<li>Lesson 3</li>
<li>Lesson 4</li>
<li>Lesson 5</li>
</ul>
</nav>
<header>
<h1>Controlling Your Code</h1>
</header>
<main>
<section>
<h2>
Today is <span id="message2"></span>.
</h2>
<h3>
<span id="message1"></span>
</h3>
</section>
<hr>
<section>
<h2>Temples in Utah</h2>
<p>
Sort by:
<select id="sortBy">
<option value="templeNameAscending">Temple Name Ascending</option>
<option value="templeNameDescending">Temple Name Descending</option>
</select>
</p>
<div id="temples">
</div>
</section>
</main>
<footer>
©<span id="year"></span> | Controlling Your Code | Lesson 5
</footer>
<script src="scripts/main.js"></script>
<script src="scripts/task5.js"></script>
</body>
</html>
Here it is. First of all, you are tracking changes on input field and checking if it's equal to templeNameAscending or templeNameDescending, and based on it sort the results.
Main thing is this part of code:
if (e.target.value === 'templeNameAscending') {
let sorted = templeList.sort(function(a,b) {return a.templeName>b.templeName ? 1 : -1});
return output(sorted);
}else if (e.target.value === 'templeNameDescending') {
let sorted = templeList.sort(function(a,b) {return b.templeName>a.templeName ? 1 : -1});
return output(sorted);
};
You made a mistake by trying to select those fields by id, and you never assigned id to them.
If you're confused with events, take a look at event objects
Here is the working snippet:
/* FETCH */
// Step 1: Declare a global empty array variable to store a list of temples
let templeList = [];
// Step 2: Declare a function named output that accepts a list of temples as an array argument and does the following for each temple:
function output(temples) {
templeList = temples;
let div = document.getElementById('temples');
temples.forEach(temple => {
// - Creates an HTML <article> element
let article = document.createElement('article');
// - Creates an HTML <h3> element and add the temple's templeName property to it
// - Appends the <h3> element, the two <h4> elements, and the <img> element to the <article> element as children
let h3 = document.createElement('h3');
h3.innerHTML = temple.templeName;
article.appendChild(h3);
// - Creates an HTML <h4> element and add the temple's location property to it
// - Appends the <h3> element, the two <h4> elements, and the <img> element to the <article> element as children
let h4First = document.createElement('h4');
h4First.innerHTML = temple.location;
article.appendChild(h4First);
// - Creates an HTML <h4> element and add the temple's dedicated property to it
// - Appends the <h3> element, the two <h4> elements, and the <img> element to the <article> element as children
let h4Second = document.createElement('h4');
h4Second.innerHTML = temple.dedicated;
article.appendChild(h4Second);
// - Creates an HTML <img> element and add the temple's imageUrl property to the src attribute and the temple's templeName property to the alt attribute
let image = document.createElement('img');
image.setAttribute('src', temple.imageUrl);
article.appendChild(image);
// - Appends the <article> element to the HTML element with an ID of temples
div.appendChild(article);
});
};
// Step 3: Create another function called getTemples. Make it an async function.
async function getTemples() {
// Step 4: In the function, using the built-in fetch method, call this absolute URL: 'https://byui-cse.github.io/cse121b-course/week05/temples.json'. Create a variable to hold the response from your fetch. You should have the program wait on this line until it finishes.
let responseFromURL = await fetch('https://byui-cse.github.io/cse121b-course/week05/temples.json');
// Step 5: Convert your fetch response into a Javascript object ( hint: .json() ). Store this in the templeList variable you declared earlier (Step 1). Make sure the the execution of the code waits here as well until it finishes.
let templeArray = await responseFromURL.json();
output(templeArray);
};
// Step 6: Finally, call the output function and pass it the list of temples. Execute your getTemples function to make sure it works correctly.
getTemples(templeList);
// Step 7: Declare a function named reset that clears all of the <article> elements from the HTML element with an ID of temples
function reset() {
return document.getElementById('temples').innerHTML = '';
};
// Step 8: Declare a function named sortBy that does the following:
function sortBy(e) {
// - Calls the reset function
reset();
// - Sorts the global temple list by the currently selected value of the HTML element with an ID of sortBy
let sort = document.getElementById('sortBy');
if (e.target.value === 'templeNameAscending') {
let sorted = templeList.sort(function(a,b) {return a.templeName>b.templeName ? 1 : -1});
return output(sorted);
}else if (e.target.value === 'templeNameDescending') {
let sorted = templeList.sort(function(a,b) {return b.templeName>a.templeName ? 1 : -1});
return output(sorted);
};
};
// Step 9: Add a change event listener to the HTML element with an ID of sortBy that calls the sortBy function
document.getElementById('sortBy').addEventListener('change', sortBy);
/* STRETCH */
// Consider adding a "Filter by" feature that allows users to filter the list of temples
// This will require changes to both the HTML and the JavaScript files
/* HTML Selectors */
article {
margin: 10px;
}
body {
font-family: 'Kalam', cursive;
}
div {
margin: 10px;
text-align: center;
}
footer {
background-color: gray;
color: white;
padding: 5px;
text-align: center;
}
header {
margin: auto;
text-align: center;
}
img {
width: 80%;
}
label {
display: inline-block;
min-width: 120px;
text-align: right;
}
main {
text-align: center;
}
nav {
background-color: black;
color: white;
}
nav ul {
display: flex;
flex-direction: column;
justify-content: center;
align-content: space-around;
margin: 0;
padding: 0;
}
nav ul li:first-child {
display: block;
}
nav ul li {
display: none;
list-style: none;
margin: 10px;
}
nav ul li a {
color: white;
display: block;
padding: 10px;
text-decoration: none;
}
nav ul li a:hover {
background-color: #efefef;
color: black;
}
section {
/* display: flex;
flex-direction: column;
align-items: center; */
}
/* Class Selectors */
.active {
background-color: white;
color: black;
}
.open li {
display: block;
}
/* ID Selectors */
#favorite-foods, #hobbies, #places-lived {
margin: 0;
padding: 0;
list-style: none;
display: inline-block;
text-align: left;
vertical-align: top;
}
#temples {
display: grid;
grid-template-columns: 1fr;
}
/* Media Queries */
#media only screen and (min-width: 32.5em) {
nav ul {
flex-direction: row;
}
nav ul li:first-child {
display: none;
}
nav ul li {
display: inline;
margin: 0 10px;
}
#temples {
grid-template-columns: 1fr 1fr;
}
}
#media only screen and (min-width: 64em) {
#temples {
grid-template-columns: 1fr 1fr 1fr;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>CSE 121b: Week 05 | Sample Solution</title>
<link href="https://fonts.googleapis.com/css?family=Kalam&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles/main.css">
</head>
<body>
<nav>
<ul id="menu">
<li><a id="toggleMenu">≡</a></li>
<li>Home</li>
<li>Lesson 2</li>
<li>Lesson 3</li>
<li>Lesson 4</li>
<li>Lesson 5</li>
</ul>
</nav>
<header>
<h1>Controlling Your Code</h1>
</header>
<main>
<section>
<h2>
Today is <span id="message2"></span>.
</h2>
<h3>
<span id="message1"></span>
</h3>
</section>
<hr>
<section>
<h2>Temples in Utah</h2>
<p>
Sort by:
<select id="sortBy">
<option value="templeNameAscending">Temple Name Ascending</option>
<option value="templeNameDescending">Temple Name Descending</option>
</select>
</p>
<div id="temples">
</div>
</section>
</main>
<footer>
©<span id="year"></span> | Controlling Your Code | Lesson 5
</footer>
<script src="scripts/main.js"></script>
<script src="scripts/task5.js"></script>
</body>
</html>
The goal is to do this:
The problem is that at the end of the row 2 I need to add two "harcoded" things:
The "# more" button (only when is needed)
The "face-plus" button (always)
This is the "ellipsis" effect I am asking about.
I have tried the following:
// HTML
<div class="container">
<div class="children">
Hola1
</div>
<div class="children">
Hola2
</div>
<div class="children">
Hola3
</div>
<div class="children">
Hola4
</div>
<div class="children">
Hola5
</div>
<div class="children">
Hola6
</div>
</div>
// css
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
width: 150px;
border: 1px solid black;
padding: 5px;
height: 80px; // this makes the trick of having 2 rows
overflow: hidden; // this makes the trick of having 2 rows
}
.children {
border: 1px solid black;
margin: 5px;
padding: 5px;
}
and I get this result:
but I am not being able to add the "fixed" elements at the end of the line 2 because I have more hidden elements on the list.
Some key points:
width of elements is dynamic
I want a maximum of 2 rows.
at the end of the row 2 I need to have the "# more" button (only when needed) and the face icon.
Thanks in advance
To avoid having to do fiddly bits of arithmetic you could clear the container, and add the elements one at a time, seeing if the one you have just added (plus its associated 'more' element and icon) go outside the bottom of the container.
If it has gone outside the bottom then go back one, so the more statement is correct.
I do not know whether you intended to put the more statement and icon into the DOM (or whether perhaps through a pseudo element having left space for them). This snippet has them in the DOM so you can click on the more element and do whatever you want to do then.
And you will have to run this code on each load and resize.
const container = document.querySelector('.container');
container.style.visibility = 'hidden'; //just in case there's a little flash as we add the elements
const cbottom = container.getBoundingClientRect().bottom;
const children = document.querySelectorAll('.children');
const num = children.length;
container.innerHTML = '<div class="moreEl"><span class="remainder">xxx</span> MORE</div><img src="youricon.jpg" style="width: 20px; aspect-ratio: 1 / 1;">';
const moreEl = container.querySelector('.moreEl');
const remainder = moreEl.querySelector('span');
// now add each child one at a time, with either the 'nn more' plus icon or just the icon in front of it until the child is outside the container when step back one
let i = 0;
for (i; i < children.length; i++) {
remainder.innerHTML = num - i - 1;
container.insertBefore(children[i], moreEl);
if ((children[i].getBoundingClientRect().top > cbottom) || (moreEl.getBoundingClientRect().top >= cbottom)) {
// can't get this element in, let alone with any associated more element, so go back one
children[i].parentElement.removeChild(children[i]);
remainder.innerHTML = num - i;
break;
}
}
if (i >= (num - 1)) {
moreEl.parentElement.removeChild(moreEl);
} else {
for (let j = i; j < num; j++) {
container.append(children[j]);
}
}
container.style.visibility = 'visible';
* {
box-sizing: border-box;
}
.container {
visibility: hidden;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
width: 150px;
border: 1px solid black;
padding: 5px;
height: 80px;
/* this makes the trick of having 2 rows */
overflow: hidden;
/* this makes the trick of having 2 rows */
}
.children {
border: 1px solid black;
margin: 5px;
padding: 5px;
}
.moreEl {
padding: 10px;
}
<div class="container">
<div class="children">
Hola1
</div>
<div class="children">
Hola2
</div>
<div class="children">
Hola3
</div>
<div class="children">
Hola4
</div>
<div class="children">
Hola5
</div>
<div class="children">
Hola6
</div>
<div class="children">
Hola7
</div>
<div class="children">
Hola8
</div>
<div class="children">
Hola9
</div>
<div class="children">
Hola10
</div>
</div>
Note: this snippet has more 'Hola' elements so it's easier to test out different container widths.
There are 5 boxes, which can be changed from 'white'<->'yellow' colors by mouse events (mouseover, mouseout and click). There is also a blue area with text displaying the level of the clicked box.
After clicking into the third box, I got 'hard level' text in blue area and 3 boxes color in yellow.
What I need is to return it to the default level ('easy level' and first box in yellow only) by clicking the reset button.
I have been trying do this like this , but it isn't working:
resetBtn = document.querySelector('#update');
and eventlistener:
resetBtn.addEventListener('click', highlightStars(`#star1`), true)
Here is an example:
window.addEventListener('DOMContentLoaded', changeStars, false);
const resetBtn = document.querySelector('#update');
/* Change level of the game depending on user choice */
function changeStars() {
/* Displaying level text inside blue box */
const updateAltText = currentLevelIndex => {
let levelText = document.querySelector('#level-text');
/* 'currentLevelIndex + 1' replaces event 'currentElement' */
levelText.textContent = document.querySelector(`#star${currentLevelIndex + 1}`).alt;
}
/* Captcha level number - default is 1 */
const getNumber = str => Number(str.match(/\d+/)[0]) || 1;
/* Star index is always one number lower than level number (indexing rules) */
const getStarIndex = event => getNumber(event.target.id) - 1;
let stars = document.querySelectorAll('.star');
const handleStarClick = event => {
/* FIRST - blocking possibility to change star behaviour by mouse events */
gameLevel.removeEventListener('mouseover', highlightStars);
gameLevel.removeEventListener('mouseout', highlightStars);
/* SECOND - making all needed star with yellow color */
const stars = document.querySelectorAll('.star');
for (let i = 0; i <= getStarIndex(event); i++) {
stars[i].classList.add('yellow');
}
};
const highlightStars = event => {
const starIndex = getStarIndex(event);
updateAltText(starIndex);
for (let i = 1; i <= starIndex; i++) {
const star = document.querySelector(`#star${i + 1}`);
star.classList.toggle('yellow');
}
};
// resetBtn.addEventListener('click', highlightStars(`#star1`), true);
resetBtn.addEventListener('click', updateAltText(0), true);
const gameLevel = document.querySelector('.game-level');
gameLevel.addEventListener("mouseover", highlightStars);
gameLevel.addEventListener("mouseout", highlightStars);
gameLevel.addEventListener('click', handleStarClick, {once: true});
}
.stars {
display: flex;
margin: 10px auto;
width: 500px;
}
input[type='image'] {
width: 60px;
height: 60px;
border: thin solid black;
}
.yellow {
background-color: yellow;
}
.game-level {
display: flex;
width: 300px;
height: 100%;
}
.level-block {
display: flex;
width: 200px;
margin-left: 10px;
justify-content: center;
align-items: center;
border: 1px solid hsl(217, 86%, 50%);
border-radius: 25px;
background-color: hsl(212, 29%, 80%);
}
.level-block > span {
font-size: 18px;
}
.reset {
width: 80px;
height: 80px;
}
<div class="stars">
<div class="game-level">
<input type="image" class="star yellow" id="star1" src="" width="60" alt="easy level">
<input type="image" class="star" id="star2" src="" width="60" alt="normal level">
<input type="image" class="star" id="star3" src="" width="60" alt="hard level">
<input type="image" class="star" id="star4" src="" width="60" alt="very hard level">
<input type="image" class="star" id="star5" src="" width="60" alt="impossible level">
</div>
<div class="level-block">
<span id="level-text">Easy level</span>
</div>
</div>
<input type="button" class="reset" id="update" value="RESET">
The following demo uses JavaScript for click events only, all mouse events (ie hover) are pure CSS. The reset behavior simply removes .active class on all buttons then adds .active class to the first button. Instead of the first button title being displayed after a reset -- the reset button title: "Game Reset" is displayed, it might be a little confusing for users if there's no confirmation of a reset. Other behavior is included in demo that is logical and consistent such as toggling, hovering to a temporary state and clicking for a persistent state etc. Details are commented in demo.
// Reference the form
const stars = document.forms.stars;
/*
Register the form to the click event -- when a click occurs anywhere on or within the form, callback function twinkle() is
called
*/
stars.onclick = twinkle;
/**
//A -- twinkle passes a reference to the Event Object... (e)
//B1 - Two Event Object properties are used to reference:
The tag the was clicked by user: event.target
The tag registered to the event: event.currentTarget
//B2 - The HTMLFormElement property: .elements collects all form
controls into a Live HTML Collection (aka NodeList)
//C -- ui.star is a Collection of form controls with [name=star]
The brackets [] and spread operator ... converts the
NodeList into an Array
//D -- Reference the message tag. If the clicked tag was the reset
button -- for...of loop iterates through each [name=star]
and removes the class .active from all [name=star]
//E1 - Next add .active class to the default button
//E2 - Set the legend.message text to the value of clicked button
[title] attribute...
~~~~~~~
//F -- ...But if a button.star was clicked, a check to verify if
clicked tag has the .active class -- then a for...of
loop identical to the one described in line D is used to
remove any .active class.
//G -- After there are no .active, the Boolean declared in line F
determines whether the clicked tag gets the .active class
and its [title] attribute displayed or not
*/
function twinkle(e) {
const active = e.target;
const ui = e.currentTarget.elements;
const starZ = [...ui.star];
const msg = document.querySelector(".message");
if (active.matches("#clear")) {
for (let star of starZ) {
star.classList.remove("active");
}
ui.star1.classList.add('active');
msg.textContent = active.title;
} else if (active.matches(".star")) {
let status = active.classList.contains("active");
for (let star of starZ) {
star.classList.remove("active");
}
if (!status) {
active.classList.add("active");
msg.textContent = active.title;
} else {
active.classList.remove("active");
msg.textContent = "";
}
}
return false;
}
:root {
font: 400 small-caps 2.5vw/1 Arial
}
.levels {
display: table;
width: 96%;
height: auto;
border: 1px solid hsl(217, 86%, 50%);
border-radius:4px;
}
.message {
display: table-caption;
width: 40vw;
height: 6vh;
margin: 0 auto 2vh;
padding: 0.5vh 0;
border: 1px solid hsl(217, 86%, 50%);
border-radius: 1.5rem;
background-color: hsla(212, 29%, 80%, 25%);
text-align: center;
font-size: 1.5rem;
color: #0078D7;
}
#clear {
float: right;
transform: rotate(45deg);
padding: 0;
border: none;
background: none;
font-size: 3.5rem;
cursor: pointer;
}
#clear:focus {
outline: 0;
}
/*
Flex is applied to the button.star'S parent tag so the order
property can be utilized.
*/
.flex {
display: flex;
justify-content: space-evenly;
align-items: center;
width: 70vw;
}
.star {
display: table-cell;
position: relative;
width: 16vw;
height: 24vh;
border: thin solid black;
background: #DDD;
font-size: 3.75rem;
text-align: center;
vertical-align: middle;
cursor: pointer;
}
/*
GSC (General Sibling Combinator: ~ ) provides highlighting across
multiple buttons.
Exp. 5 buttons: [-] [-] [X] ~ [>] ~ [>]
*/
.star.active,
.star:hover,
.star.active ~ .star,
.star:hover ~ .star {
background: gold;
}
/*
HTML layout has button.star in reverse order. Applying order to
each button rectifies the order by APPEARING in order while the
HTML structure remains reversed.
*/
#star1 {
order: 1;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
#star2 {
order: 2;
}
#star3 {
order: 3;
}
#star4 {
order: 4;
}
#star5 {
order: 5;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
#star1:hover,
#star1.active {
color: #5BC0DE;
}
#star2:hover,
#star2.active {
color: #FF1C8D;
}
#star3:hover,
#star3.active {
color: #00D800;
}
#star4:hover,
#star4.active {
color: #0000D5;
}
#star5:hover,
#star5.active {
color: #D50000;
}
<form id="stars" action="">
<fieldset name="levels" class="levels">
<legend class="message">Novice</legend>
<button id="clear" type="reset" title="Game Reset">🔄</button>
<section class="flex">
<button id="star5" name='star' class="star" title="Master">🟐</button>
<button id="star4" name='star' class="star" title="Expert">🟌</button>
<button id="star3" name='star' class="star" title="Advanced">🟊</button>
<button id="star2" name='star' class="star" title="Intermediate">🟆</button>
<button id="star1" name='star' class="star active" title="Novice">🟂</button>
</section>
</fieldset>
</form>
var NavLinks = document.querySelectorAll('.nav-link');
var circuses = document.querySelectorAll('.circle');
for (var i = 0; i < NavLinks.length; i++) {
var navLink = NavLinks[i];
navLink.addEventListener('click', function () {
for (var i = 0; i < circuses.length; i++) {
var circle = circuses[i];
circle.style.display='none';
}
var theLastChild = navLink.lastChild;
theLastChild.style.display='block';
}
);
}
.nav-container{
height: 10px;
background: white;
padding: 30px 0px 40px 0px;
margin-left: 18%;
margin-right: 18%;
}
.nav-body ul{
text-align: right;
}
.nav-body ul li{
display: inline- block;
float: left;
margin-right: 30px;
}
#logo{
margin-right: 0px;
}
.nav-body ul li{
line-height: 0.6;
}
#logo{
margin-top: -10px;
}
#logo-light-blue{
color: #5dc5ef;
font-weight: 900;
}
#logo-dark-blue{
color: #1885c8;
font-weight: 900;
}
.circle {
display: none;
width: 8px;
height: 8px;
background: #5dc5ef;
/* -moz-border-radius: 50px;
-webkit-border-radius: 50px; */
border-radius: 4px;
margin: auto;
margin-top: 7px;
}
<header class="nav-container">
<nav class="nav-body">
<ul>
<li class="nav-link">צור קשר
<div class="circle"></div></li>
<li class="nav-link">המלצות ומאמרים
<div class="circle"></div></li>
<li class="nav-link">שאלות נפוצות
<div class="circle"></div></li>
<li class="nav-link">אודות ד"ר שי מרון אלדר
<div class="circle"></div></li>
<li class="nav-link">אודות ההליכים
<div class="circle"></div></li>
<li class="nav-link">ראשי
<div class="circle"></div></li>
<li id="logo"> <h3> <span id="logo-light-blue"> ד"ר </span><span id="logo-dark-blue"> שי מרון אלדר </span></h3><br>
<h6> פתרונות כירורגיים להשמנת יתר וניתוחים זעיר פולשניים</h6></li>
</ul>
</nav>
</header>
I need to make a blue circle under that category menu, which I pressed. But now blue circle added only to last menu category. Doesn't matter which one was pressed.
I looking for the last child of that menu category which was pressed. But it shows me every time last child of all menu categories.
What is wrong?
>
You have errors in HTML. Span tags need to be closed.
<li id="logo">
<h3>
<span id="logo-light-blue"> ד"ר </span>
<span id="logo-dark-blue"> שי מרון אלדר </span>
</h3>
<br>
<h6> פתרונות כירורגיים להשמנת יתר וניתוחים זעיר פולשניים</h6>
</li>
And Id attributes should be unique to the element, you are repeating the circle as an Id all over the place.
<div id="circle"></div></li>
It this doesn't solve it, try explaining the question better since even in the demo you have put result is all over the place. Are we missing some CSS or a style lib?
EDIT: I think I know what you wanna, is it this? Have a look at fiddle:
fiddle here
Do you need circle removed from other elements once you click your element?
If you need the circle to be only on 1 element, it needs to be removed from others.
Here is a fiddle showing that:
fiddle with only 1 circle
Difference is in:
var NavLinks = document.querySelectorAll('.nav-link');
for (var i = 0; i < NavLinks.length; i++) {
var navLink = NavLinks[i];
navLink.addEventListener('click', function (event) {
var allNavs = document.querySelectorAll('.nav-link div');
for (var it = 0; it < allNavs.length; it++){
console.log(allNavs[it]);
allNavs[it].classList.add('invisible');
allNavs[it].classList.remove('circleVisible');
}
console.log(allNavs);
var targetElement = event.target || event.srcElement;
var circleDiv = targetElement.parentNode.querySelectorAll('div');
console.log(circleDiv[0]);
circleDiv[0].classList.add('circleVisible');
circleDiv[0].classList.remove('invisible');
console.log(circleDiv[0]);
}
);
}
I have left console.logs, so you see how it works, remove them when running the code for real :)
The first big problem I see is you have nested for loops but are using the same iterator variable of i. If you are going to next the loops, you need the inner loop to have a different variable. In situations like this, I will often use ii just because it's easy.
Furthermore, you seem to be doing this in a roundabout way. I'm not entirely sure what you need, but if it is as it appears, then this solution is simpler.
CSS
.circle {
display: none;
... other attributes
}
.active-menu-item > .circle {
display: block;
}
JavaScript
var NavLinks = document.querySelectorAll('.nav-link');
for (var i = 0; i < NavLinks.length; i++) {
var navLink = NavLinks[i];
navLink.addEventListener('click', function () {
for (var ii = 0; ii < NavLinks.length; ii++) {
NavLinks[ii].classList.remove("active-menu-item");
}
navLink.classList.add("active-menu-item");
});
}
I am implementing a 'blades' experience in a page. When I append an additional Blade into the Container...the previous blades 'pop' down.
Q: How do I append a new element into view without effecting previous elements?
MY FIDDLE:
I created a JSFiddle...but the service is not currently available...I will append it shortly.
https://jsfiddle.net/PrisonerZ3RO/oynae1hd/4/#
MY CSS:
<style>
/** DASHBOARD CONTAINER **/
.dashboard-container { border-right: solid 1px #000; margin-top: 5px; margin-bottom: 5px; overflow-x: scroll; white-space: nowrap; width: 100%; }
.dashboard-container .widget { clear: both; display: inline-block; vertical-align: top; }
/** FORM CONTAINER **/
.form-container { border: 1px solid #ccc; border-radius: 3px; height: 500px; margin-bottom: 5px; padding: 5px; width: 500px; }
/** BLADE CONTAINER **/
.blade-container .blade { border: 1px solid #ccc; border-radius: 3px; display: inline-block; height: 506px; margin-right: 2px; padding: 2px; width: 200px; }
</style>
MY HTML:
<script id="tmplBlade" type="text/template">
<div class="blade">
Blade
</div>
</script>
<div class="dashboard-container">
<div class="widget">
<div class="form-container">
Form Controls go here
<input id="btnAppend" type="button" value="Append Blade" />
</div>
</div>
<div class="widget">
<div class="blade-container">
</div>
</div>
</div>
MY JAVASCRIPT:
<script type="text/javascript">
$(document).ready(function () {
function PageController()
{
var that = this,
dictionary = {
elements: { btnAppend: null, bladeContainer: null },
selectors: { btnAppend: '#btnAppend', bladeContainer: '.blade-container', tmplBlade: '#tmplBlade' }
};
var initialize = function () {
// Elements
dictionary.elements.btnAppend = $(dictionary.selectors.btnAppend);
dictionary.elements.bladeContainer = $(dictionary.selectors.bladeContainer);
// Events
dictionary.elements.btnAppend.on('click', that.on.click.btnAppend);
};
this.on = {
click: {
btnAppend: function (e) {
var html = $(dictionary.selectors.tmplBlade).html().trim();
var $element = $(html);
$element.hide();
dictionary.elements.bladeContainer.prepend($element);
// Slide-in
$element.show('slide', { direction: 'left' });
}
}
};
initialize();
}
var pageController = new PageController();
});
</script>
I've come across this problem before. The only way I've found to get around it is to do the following:
1) Create a .hidden class width margin-left: -200px
2) Add a CSS transition on margin-left to the .blade class
3) Apply the .hidden class to a new blade
4) Show the new blade
5) Remove the .hidden class from the new blade
Please see the following fork of your fiddle for a working solution: https://jsfiddle.net/yxL4embt/
How do I append a new element into view without effecting previous elements?
I'm not sure if I entirely get what you're asking since you'll always be affecting the other elements by moving them over when you append a new element. You can, however, prevent the pop-down effect you're seeing. The .ui-effects-wrapperadded by jQuery UI is display: block, so add the following to your CSS:
.blade-container .ui-effects-wrapper {
display: inline-block !important;
}
Then make sure your other blades are always aligned to the top of your container:
.blade-container .blade {
...
...
vertical-align: top;
}
This will bump all the blades over (right) and allow a new blade to slide in from the left.