Replicate CSS :focus selector for multiple elements using javascript - javascript

I have this website: https://www.australianathleticscalendar.com.au/
I want to make it so you can have selected an 'Events' filter and 'State' filter at once (I.e. 100m and QLD at once), and the bubbles in each filter will have styling (I.e. like :focus). With each filter, select one category. When you change categories within a filter, the previous one selected must also be unstyled (I.e. can't select 100m and 200m at once. Moving from 100m to 200m would remove styling from 100m and add it to 200m).
The problem is with css is that you can only :focus one element at a time.
How can I achieve this using javascript?
I've pulled the relevant code into a codepen here.
This is the functions which draw the two filters:
function drawCategories() {
var template = Handlebars.compile(document.getElementById("menu-template").innerHTML);
console.log('draw ', this.products);
document.getElementById('menu-container').innerHTML = template(this.events);
}
function drawStates() {
var template = Handlebars.compile(document.getElementById("menu-template-2").innerHTML);
console.log('draw ', this.products);
document.getElementById('menu-container-states').innerHTML = template(this.states);
}
function showCategory(event) {
this.title = event;
drawProducts();
}
function showState(state) {
this.titleStates = state;
drawProducts();
}
And this is the HTML for the two filters:
<!-- Events filter -->
<div class="container">
<script id="menu-template" type="text/x-handlebars-template">
<ul class="navbar-nav">
{{#each this as |event|}}
<li class="nav-item"></li>
<a class="nav-link" href="#" onclick="showCategory('{{event.name}}');">{{event.name}}</a>
{{/each}}
<a class="navbar-brand hover-color" href="#" onclick="showAllEvents();">All Events</a>
</ul>
<ul class="navbar-nav ml-auto"></ul>
<li class="nav-item">
</li>
</ul>
</div>
</script>
</div>
<!-- States filter -->
<div class="container">
<script id="menu-template-2" type="text/x-handlebars-template">
<ul class="navbar-nav">
{{#each this as |state|}}
<li class="nav-item"></li>
<a class="nav-link nav-link-states" href="#" onclick="showState('{{state.name}}');">{{state.name}}</a>
{{/each}}
<a class="navbar-brand hover-color-states" href="#" onclick="showAllStates();">All States</a>
</ul>
<ul class="navbar-nav ml-auto"></ul>
<li class="nav-item">
</li>
</ul>
</div>
</script>
</div>

As often there are many ways to achieve this. I think the easiest might be to add an extra argument to the calls made in the click handlers -- passing this -- so the handler knows exactly which DOM element needs to get styling:
<a onclick="showCategory(this, '{{event.name}}');">
<a onclick="showAllEvents(this);">
<a onclick="showState(this, '{{state.name}}');">
<a onclick="showAllStates(this);">
Then in your code define a function that will apply a CSS class to a given DOM element, but also makes sure that it will be the only element having that class. Then use this utility function in the click handlers, which must now also take that extra argument:
// Utility function that ensures only one element has the given CSS class:
function selectElem(elem, className) {
let prevSelected = document.querySelector("." + className);
if (prevSelected) prevSelected.classList.remove(className);
elem.classList.add(className);
}
function showCategory(elem, event) { // <-- extra parameter, here and below...
selectElem(elem, "selectedEvent"); // <-- add this call, here and below...
this.title = event;
drawProducts();
}
function showAllEvents(elem) {
selectElem(elem, "selectedEvent");
this.title = "All Events";
drawProducts();
}
function showState(elem, state) {
selectElem(elem, "selectedState"); // <-- different style here and below
this.titleStates = state;
drawProducts();
}
function showAllStates(elem) {
selectElem(elem, "selectedState");
this.titleStates = "All States";
drawProducts();
}
Now it is up to you to define the style of two new CSS classes: selectedEvent and selectedState. You can give them the same definition with a comma:
.selectedState, .selectedEvent {
# Your styling ...
}

Related

Vanilla JS TypeError: Cannot read property

I have a header with a logo and navigation that has a menu toggle link that displays on smaller viewports. I am try to achieve this with Vanilla JS as I want to move away from jQuery.
When I click on the menu toggle link, I am presented with this error: Uncaught TypeError: Cannot read property 'className' of null at HTMLLIElement.<anonymous> (ignition.js:6) (anonymous) # ignition.js:6
I'm currently learning Vanilla JS and I am having a bit of trouble understanding what this means and where the error maybe.
const navToggle = document.querySelector(".nav-toggle");
navToggle.addEventListener("click", (event) => {
const navItem = event.currentTarget.querySelector(".nav-item");
if (navItem.className.includes("active")) {
navItem.className.remove("active");
event.target.find("a").innerHTML("<i class='fas fa-bars'></i>");
} else {
navItem.className.add("active");
event.target.find("a").innerHTML("<i class='fas fa-times'></i>");
}
});
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css">
<nav class="navbar">
<div class="container">
<ul class="nav-menu">
<li class="nav-logo">🚀 Ignition <small>Beta</small></li>
<li class="nav-item">Home</li>
<li class="nav-item">Documentation</li>
<li class="nav-item">Theme</li>
<li class="nav-item">Blog</li>
<li class="nav-toggle"><i class="fas fa-bars"></i></li>
</ul>
</div>
</nav>
Maybe here's the code to achieve what you want: When click the 'nav-toggle' icon:
Toggle active for all nav-item
Toggle icon of nav-toggle
const navToggle = document.querySelector(".nav-toggle");
navToggle.addEventListener("click", (event) => {
// toggle icon
const curIcon = document.querySelector('.nav-toggle a i')
curIcon.classList.toggle('fa-bars')
curIcon.classList.toggle('fa-times')
// toggle active of nav-items
const navItems = document.getElementsByClassName('nav-item')
for (const navItem of navItems) {
navItem.classList.toggle('active')
}
});
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css">
<nav class="navbar">
<div class="container">
<ul class="nav-menu">
<li class="nav-logo">🚀 Ignition <small>Beta</small></li>
<li class="nav-item">Home</li>
<li class="nav-item">Documentation</li>
<li class="nav-item">Theme</li>
<li class="nav-item">Blog</li>
<li class="nav-toggle"><i class="fas fa-bars"></i></li>
</ul>
</div>
</nav>
navItem is null because you're calling querySelector(".nav-item") on the current target which is the <li class="nav-toggle">...</li> element. That element does not contain any children with class nav-item.
This is because when you are clicking on the specific nav-toggle element and attempting to use the querySelector from it, it's going to return null since no nav-item elements exist below it.
It seems like what you would want to do would be to find all of the child elements in your entire menu and apply the necessary logic similar to something like this:
const navToggle = document.querySelector(".nav-toggle");
navToggle.addEventListener("click", (event) => {
// You'll likely want a reference to your "toggle" element if you need
// to access it within the loop
const toggle = event.currentTarget;
// Get the top level menu
const menu = document.querySelector(".nav-menu");
// Get your menu items and iterate through them
const menuItems = menu.querySelectorAll(".nav-item")
// Iterate through your individual items here
menuItems.forEach(navItem => {
// Add your logic here
});
});

jQuery each run function on every other element

I'm working on a basic dropdown element in HTML and jQuery and I'm trying to get better at understanding JavaScript and jQuery so this questions is a bit about code refactoring as well.
So here is what I've gotten so far:
HTML
<li class="nav-item">
<a class="nav-link" href="#">Foo</a>
<div class="subnav">
...
</div>
</li>
JavaScript
const navLink = $('.nav-link');
navLink.each(function () {
let $this = $(this);
$this.click(function (e) {
let hasSubnav = $this.parent().find('.subnav');
if(hasSubnav.length !== 0) {
e.preventDefault();
$this.toggleClass('dropdown-active');
}
hasSubnav.stop(true, true).slideToggle(200);
})
});
This solutions works fine. So what I want to do next is to check if another element in my loop is active, close is accordingly and then open the one I just clicked.
I thought about just putting a default click function before the each function like this:
navLink.click(function () {
$('.subnav').slideUp();
});
navLink.each(function () {
let $this = $(this);
$this.click(function (e) {
let hasSubnav = $this.parent().find('.subnav');
if(hasSubnav.length !== 0) {
e.preventDefault();
$this.toggleClass('dropdown-active');
}
hasSubnav.stop(true, true).slideDown(200);
})
});
But this does not seem to work. So my question is, is there a pretty way to achieve this maybe even inside of the each function? I've red about .not(this) in this post, which will maybe work (haven't tried it yet) but I thought that this would be duplicated code and that there might be a better way to get this to work.
Your code is now looping through every single nav-link and adding a click handler to them one by one. It is possible to remove the each loop, since you can just add a click handler to all nav-links at once.
All you have to do is add a click handler to the nav-link and then remove the active class and slide up all open dropdowns before executing your logic. See working code example below for reference:
// Collapse all initially
$(".subnav").slideUp();
// Add click handler to all nav-links
const navLink = $('.nav-link');
navLink.click(function(e) {
// Remove active classes on other elements & slide up
const otherLinks = navLink.not(this);
otherLinks.removeClass('dropdown-active');
otherLinks.parent().find('.subnav').slideUp();
// Slide down the subnav of selected element
let hasSubnav = $(this).parent().find('.subnav');
if (hasSubnav.length !== 0) {
e.preventDefault();
$(this).addClass('dropdown-active');
}
hasSubnav.stop(true, true).slideToggle(200);
})
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<li class="nav-item">
<a class="nav-link" href="#">Foo</a>
<div class="subnav">
Link1
Link2
Link3
</div>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Foo</a>
<div class="subnav">
Link1
Link2
Link3
</div>
</li>

Add extra pair of UL using JavaScript/JQuery

I have done coding the first part HTML then JavaScript/JQuery. Now I want to surround the final common list with a UL need to be done using JavaScript/JQuery. So the final common list will be surrounded by two UL instead of one. Eg
Final Outcome
<ul id="CommonLister">
<ul> <!--Need to add this-->
<li class="columnItem">John</li>
<li class="columnItem">Mark</li>
</ul><!--Need to add this-->
</ul>
Current Code
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<ul id="listOne">
<li class="columnItem">John</li><!--will be removed and put under CommonLister-->
<li class="columnItem">James</li>
<li class="columnItem">Mary</li><!--will be removed and put under CommonLister-->
</ul>
<ul id="listTwo">
<li class="columnItem">John</li><!--will be removed and put under CommonLister-->
<li class="columnItem">Mark</li>
<li class="columnItem">Mary</li><!--will be removed and put under CommonLister-->
</ul>
<ul id="CommonLister">
<li class="columnItem">John</li>
<li class="columnItem">Mark</li>
</ul>
</div>
$(function() {
$('#run-code').on('click', function(e) {
e.preventDefault();
//What were you doing? nope.
var currentItems = {}; //Blank object
var $mergeColumn = $('#CommonLister'); //Common list reference
$('.columnItem').each(function(i, el) {
var $el = $(el); //Notation I use to differentiate between the regular HTML Element and jQuery element
if (!currentItems.hasOwnProperty($el.html())) {
//Has this name come up before? if not, create it.
currentItems[$el.html()] = []; //Make it equal to a brand spanking new array
}
currentItems[$el.html()].push(el);
//Add the item to the array
});
$.each(currentItems, function(name, data) {
//Loop through each name. We don't actually use the name variable because we don't care what someone's name is
if (data.length > 1) {
//Do we have more than 1 element in our array? time to move some stuff
$.each(data, function(i, el) {
var $el = $(el); //See note above
if (i == 0) {
//If this is the first element, let's just go ahead and move it to the merge column ul
$el.appendTo($mergeColumn);
} else {
$el.remove(); //Otherwise, we've already got this element so delete this one.
} //end if/else
}); //end $.each(data)
} //end if data.length >1
}); //end $.each(currentItems)
}); //end $.on()
}); //end $(
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="run-code" class="btn btn-success">Click Me</button>
<h4>List 1</h4>
<ul id="listOne">
<li class="columnItem">John</li>
<!--will be removed and put under CommonLister-->
<li class="columnItem">James</li>
<li class="columnItem">Mary</li>
<!--will be removed and put under CommonLister-->
</ul>
<h4>List 2</h4>
<ul id="listTwo">
<li class="columnItem">John</li>
<!--will be removed and put under CommonLister-->
<li class="columnItem">Mark</li>
<li class="columnItem">Mary</li>
<!--will be removed and put under CommonLister-->
</ul>
<h4>Common List</h4>
<ul id="CommonLister">
<!--Extra ul will be added here-->
</ul>
It's invalid nesting a ul directly in a ul like this but if you have to, you could use jquery wrapAll:
$( "li" ).wrapAll( "<ul></ul>" );
See fiddle: https://jsfiddle.net/9xLt6d9f/
I agree with charlietfl that it seems strange to do it this way. However, to answer your question, the best way to force this improperly formatted HTML code would be hardcode it into your original file. Try the following code for the end of your file:
<h4>Common List</h4>
<ul id="CommonLister">
<ul id="CommonListerSub">
<!--Extra ul will be added here-->
</ul>
</ul>
Then, simply change one line of your code:
var $mergeColumn = $('#CommonListerSub'); //Common list reference
This will force it to list the list items under the nested ul tags.
I hope this is an acceptable solution. If for some reason it doesn't work, please comment as to what additional limitations you have, and perhaps share the link of the page that is giving you the required template or format specifications.

Passing text to a javascript function

I'm just starting in on javascript and working on a problem that our instructor gave us. We have an html site with four buttons, each of the buttons has a color, and when you hit the button it changes the background/text color. Sample HTML and javascript below.
HTML
<div id="wrapper">
<ul id="switcher">
<li id="grayButton"></li>
<li id="whiteButton"></li>
<li id="blueButton"></li>
<li id="yellowButton"></li>
</ul>
</div>
CSS
document.getElementById("yellowButton").onclick = turnYellow;
function turnYellow (){
document.getElementById("wrapper").style.backgroundColor = "yellow";
document.getElementById("wrapper").style.color = "orange";
}
I got that to work fine, but I was trying to refactor so that my function was more generic:
document.getElementById("grayButton").onclick = changeColor("gray", "white");
function changeColor(backColor, frontColor) {
document.getElementById("wrapper").style.backgroundColor = backColor;
document.getElementById("wrapper").style.color = frontColor;
}
and I can't figure out why that doesn't work. Any thoughts?
You need to call your function in an anonymous function, adding the parameters () caused the function to be invoked immediately:
document.getElementById("grayButton").onclick = function() {
changeColor("gray", "white");
}
"element.onlick" event requires a function object (callable/callback) not a return value. Thus it should be:
document.getElementById("grayButton").onclick = function(){
changeColor("gray", "white");
}
Also, HTML5 data attributes seems like a great idea:
<html>
<body>
<div id="wrapper">
<ul id="switcher">
<li data-bg-color="red" data-color="white" onclick="changeColor(this)">
red/white
</li>
<li data-bg-color="gray" data-color="black" onclick="changeColor(this)">
gray/black
</li>
</ul>
</div>
<script>
function changeColor(el) {
var wrapper = document.getElementById("wrapper");
wrapper.style.color = el.getAttribute('data-color');
wrapper.style.backgroundColor = el.getAttribute('data-bg-color');
}
</script>
</body>
</html>
HTML5 Data Attributes
This is my solution My example
HTML
<body onload="start()">
<div id="wrapper">
<ul>
<li data-params="yellow,orange">Hit me</li>
<li data-params="green,darkgreen">Hit me</li>
<li data-params="aqua,blue">Hit me</li>
<li data-params="chocolate,brow">Hit me</li>
</ul>
</div>
</body>
JavaScript
function start(){
//Get necesary elements
var list = document.getElementsByTagName("li");
//Adding event
for(var i in list){
list[i].addEventListener("click", changeStyle);
}
}
function changeStyle(){
var params = this.dataset.params.split(","),
wrapper = document.getElementById("wrapper");
wrapper.style.backgroundColor = params[0];
wrapper.style.color = params[1];
}
You simply called the function changeColor and assigned its return value which is undefined to the event handler.
I am pretty sure it even worked and changed the text color and background color for that element but not like what you expected i.e. synchronously vs asynchronously.
You just need to leave a reference to the event handler like this changeColor without the parentheses like what you did in the first example and the event handler would take care of everything for you when the event occurs/fires and it detects it.

switching two ids with ajax callback

I was kindly helped by Jonathan over here simple javascript question-linking button state to image swap?
The problem is that this makes the "active" class the same class for both list items.
Each list item needs to toggle its own active and its own inactive class (each is a button with its own css styling and background image).
Can you please help me modify the script so that I can do that?
Here is Jonathans provided code:
<li class="transcript">
<a id="transcriptionhorbutton" class="inactive"
href="javascript:void()"
onclick="getDataReturnText('/lessons/transcriptions/ajaxcalls/L1horizontal.txt', callback);make_active(this);"></a>
</li>
<li class="transcript">
<a id="transcriptionvertbutton" class="inactive"
href="javascript:void()"
onclick="getDataReturnText('/lessons/transcriptions/ajaxcalls/L1vertical.txt', callback);make_active(this);"></a>
</li>
<script>
var buttons = [ document.getElementById("transcriptionvertbutton"),
document.getElementById("transcriptionhorbutton")];
function make_active(el) {
deactivate_buttons();
el.setAttribute("class","active");
}
function deactivate_buttons() {
buttons[0].setAttribute("class","inactive");
buttons[1].setAttribute("class","inactive");
}
</script>
I understand that the problem is here:
function make_active(el) {
deactivate_buttons();
el.setAttribute("class","active");
}
but I don't know enough to separate that into two different classes.
Just add an extra parameter to the function:
function make_active(el, classname) {
deactivate_buttons();
el.setAttribute("class",classname);
}
Then change your calls just a bit. Here is the completed code. Note I changed all calls of setAttribute to .className instead. This was just so you don't run into any trouble with IE6:
<li class="transcript">
<a id="transcriptionhorbutton" class="inactive"
href="javascript:void()"
onclick="getDataReturnText('/lessons/transcriptions/ajaxcalls/L1horizontal.txt', callback);make_active(this,'active_class_1');"></a>
</li>
<li class="transcript">
<a id="transcriptionvertbutton" class="inactive"
href="javascript:void()"
onclick="getDataReturnText('/lessons/transcriptions/ajaxcalls/L1vertical.txt', callback);make_active(this,'active_class_2');"></a>
</li>
<script>
var buttons = [ document.getElementById("transcriptionvertbutton"),
document.getElementById("transcriptionhorbutton")];
function make_active(el, classname) {
deactivate_buttons();
el.className = classname;
}
function deactivate_buttons() {
buttons[0].className = "inactive_class_1";
buttons[1].className = "inactive_class_2";
}
</script>

Categories

Resources