Bootstrap Avoid navbar-toggle button to close opened sub menus on mobile - javascript

I have a code based on Bootstrap 3.3.7
There are different menu items and sub menus on my menu, I want to have all sub menu items to be open on mobile, Means no need to click on any menu items to display it's sub menus, So I wrote a JS code to open all sub menus on mobile:
function opensubmenus() {
if ($(window).width() < 768) {
$("#top-navbar-collapse li").addClass('open');
} else {
$("#top-navbar-collapse li").removeClass('open');
}
}
$(window).resize(opensubmenus);
opensubmenus();
But the problem is when I click on a navbar-toggle button, It closes all submenus, But I need to keep them open on mobile devices all the time
You can check my online sample on this site: https://dedidata.com
Here I have shown a screenshot: https://pasteboard.co/IfSMCIu.jpg
I don't like to disable navbar-toggle button completely, I need it to toggle the whole navbar, But I don't like it close the submenus, My JS code opens the submenus, But navbar-toggle closes those submenus

This snippet will be applied to all dropdowns, you can modify it to get what dropdowns you need.
I will explain what it does:
const targets = document.getElementsByClassName('dropdown-toggle');
for(let i = 0; i < targets.length; i++) {
targets[i].addEventListener('click', () => {
targets[i].hasAttribute('data-toggle') &&
targets[i].removeAttribute('data-toggle');
// Managing locally the open and close
targets[i].parentElement.classList.toggle('open');
});
}
First line:
const targets = document.getElementsByClassName('dropdown-toggle');
we get all the elements with class name dropdown-toggle (this is used in boostrap for dropdowns menus)
For each element, we attach a click listener to be able to manage "manually" dropdowns if the dropdown menu is clicked by the user.
This is managed by line: targets[i].parentElement.classList.toggle('open');
And the important one to avoid auto closing menus is to remove the attribute data-toggle.
targets[i].hasAttribute('data-toggle') &&
targets[i].removeAttribute('data-toggle');
If you only one apply a solution like this for mobiles you can use is.js to check when you are in mobile (android/ios)
UPDATE
This update will open the menus automatically:
const menuItems = document.getElementsByClassName('navbar-toggle');
for (let i = 0; i < menuItems.length; i++) {
menuItems[i].hasAttribute('data-toggle') && menuItems[i].addEventListener('click', () => {
const elements = document.getElementsByClassName('dropdown-toggle');
for (let i = 0; i < elements.length; i++) {
elements[i].hasAttribute('data-toggle') && elements[i].removeAttribute('data-toggle');
elements[i].parentElement.classList.add('open');
}
});
}

The following code expands the sub menus when a click occurs on navbar-toggle
and it change the aria-expanded to the correct value based on the open/close state of sub menu
function opensubmenus() {
if ($(window).width() < 768) {
$("#top-navbar-collapse li").addClass('open');
$("#top-navbar-collapse li a").attr('aria-expanded','true');
}else{
$("#top-navbar-collapse li").removeClass('open');
$("#top-navbar-collapse li a").attr('aria-expanded','false');
}
}
$('#top-menu .navbar-toggle').click(function(){
setTimeout(opensubmenus, 100);
});
$(window).resize(opensubmenus);
opensubmenus();
Thanks to #abelito for his hint

Straight code after tinkering for 10 minutes. I definitely don't recommend this as a final answer, but this will get you on the track towards one way of accomplishing it:
function delayedSubmenuOpen() { setTimeout(openAllSubmenus, 100); }
function openAllSubmenus() {
var eles = document.getElementsByClassName("dropdown-toggle");
for (i = 0; i < eles.length; i++) {
eles.item(i).parentElement.className += " open";
}
}
var navigationHamburger = document.getElementsByClassName("navbar-toggle").item(0);
navigationHamburger.addEventListener("click", delayedSubmenuOpen);
I would definitely replace all of these with cross-browser compliant jQuery calls, as I did it in straight javascript and only tested in my browser on Chrome.
I'd also look into just editing the CSS instead rather than relying on javascript to do this -- maybe on page load, make a renamed copy of the ".open" class and add it to all elements with the classname "menu-item-has-children" -- this way it can't be toggled off by the javascript. Sounds like you may have tried this but definitely worth looking into rather than depending on some hokey JS.

Add this to you css
#media only screen and (max-width: 768px) {
.megamenu .dropdown ul.dropdown-menu {
display: block;
position: static;
float: none;
width: auto;
margin-top: 0;
background-color: transparent;
border: 0;
-webkit-box-shadow: none;
box-shadow: none;
}
.navbar-fixed-top .navbar-collapse {
background-color: rgba(0, 0, 0, 0.7)!important;
}
}
And change the function to look like this
function opensubmenus() {
if (jQuery(window).width() < 768) {
jQuery("#top-navbar-collapse").addClass('in');
} else {
jQuery("#top-navbar-collapse").removeClass('in');
}
}
jQuery(window).resize(opensubmenus);
opensubmenus();

Related

html img tag within button tag not picking up javascript applied to button tag

I have a dynamic formset where the user can click a button to remove forms from the formset before clicking submit. I've achieved this by applying some javascript to the button tag. This is a django app.
My issue arises when I add a trash can icon as the button's image. The image itself is not clickable and dynamic (i.e. removing the form when clicked), but the button area around the image is. I would like the entire thing to be clickable.
When trying to click the trash icon itself, nothing happens, and the console logs this error:
Uncaught TypeError: Cannot set properties of null (setting 'checked') at HTMLButtonElement.removeIngredient
When tracing this error, I encounter the following in the chrome console sources tab:
This image corresponds with the javascript below
HTML
<button class="remove-button" id="remove-ingredient" type="submit"><img src="{% static 'inventory/trash-icon.png' %}"></button>
CSS
.remove-button {
border: none;
width: 100%;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
}
.remove-button img {
height: 20px;
width: 20px;
}
.remove-button:hover {
cursor: pointer;
}
Javascript
function removeBtnListener () {
const removeIngredientBtns = document.getElementsByClassName('remove-button')
for (let i = 0; i < removeIngredientBtns.length; i++) {
removeIngredientBtns[i].addEventListener('click', removeIngredient)
}
}
function removeIngredient (event) {
if (event) {
event.preventDefault()
}
let ingredientFormToBeRemoved = event.path[1]
let indexString = ingredientFormToBeRemoved.id
let array = indexString.split('-')
let indexNum = array[2]
let deleteCheckbox = document.getElementById(`id_ingredientquantity_set-${indexNum}-DELETE`)
deleteCheckbox.checked = true
ingredientFormToBeRemoved.setAttribute('class', 'hidden')
}
Here is an image of the formset. By clicking the area around the trash can icon (red outline), the form is removed. By clicking the trash can icon itself (green arrow), nothing happens.
Does anyone know how to make this work so the entire element removes the form properly when clicked?

toggle / display image through an event listener

I have a list of featured products which I get through an API call, with the title and the icon displayed in the list. All the products also have images (which I also get through the same API call)
I want the image to not display when the icon is not active, but to display when the icon is active. Not sure how I get to display that specific image when the icon to that product is active.
(kinda new into coding, so sorry if this is a weird question)
export function featuredProducts(products)
const featuredProductsContainer = document.querySelector(".featured-products_list");
featuredProductsContainer.innerHTML = "";
for (let i = 0; i < products.length; i++) {
console.log(products[i]);
if (products[i].featured) {
featuredProductsContainer.innerHTML +=
`<li class="featured-products">
<p>${products[i].title}<i class="far fa-flag" data-name="${products[i].title}"></i></p></li>
<img src="http://localhost:1337${products[i].image.url}" class="${products[i].title}"
height="300" width="300" style="display: none;">`;
}
}
const flag = document.querySelectorAll(".featured-products i");
flag.forEach(function(icon) {
icon.addEventListener("click", clicked);
});
function clicked(event) {
event.target.classList.toggle("fas"); //active
event.target.classList.toggle("far"); //unactive
}
}
TL;DR You'll want to add css to hide/show the images (As #barmar answered above).
I will propose a slightly different approach, which is toggling the classes on the images directly, to avoid a more complex rearrangement of the markup and gigantic css selectors.
But first, to make it easier, you should place the img tags inside their li, not beside them.
So, first, let's move the closing li tag to the end, after the img. Note I'm also removing the inline style of style="display: none;".
for (let i = 0; i < products.length; i++) {
console.log(products[i]);
if (products[i].featured) {
featuredProductsContainer.innerHTML +=
`<li class="featured-products">
<p>${products[i].title}<i class="far fa-flag" data-name="${products[i].title}"></i></p>
<img src="http://localhost:1337${products[i].image.url}" class="${products[i].title}"
height="300" width="300"></li>`;
}
}
Then, in your click handler, let's do something different:
function clicked(event) {
// remove all active classes
const $imgs = document.querySelectorAll('.fas')
$imgs.forEach(i => i.classList.toggle('fas'))
// add active class to targeting img
const $img = event.target.closest('li.featured-products').querySelector('img')
$img.classList.toggle("fas")
$img.classList.toggle("far");
}
Lastly, modified from from #barmar
.featured-products img.fas {
display: block;
}
.featured-products img.far {
display: none;
}
You can do this with CSS. Since your event listener toggles the far and fas classes, use CSS selectors that match an img inside those containers.
.featured-products.fas img {
display: block;
}
.featured-products.far img {
display: none;
}
There are many ways to go about this, a lot depends on what triggers the active state of the icon.
if it's any kind of input and you can keep the data in the same container then all you need to do Is add an "active" css class to the parent. This is the most performant way as you keep reads, writes and any reflows to a minimum.
Just add a general rule in in your css for the active class:
.active img { visibility: visible; }
If the images are in a separate element, you can add a dataset custom property to the icon in your html. With a value you can use in Javascript.
I. e.
<img id="icon" dataset-foo="imgContainer">
and in JS
var imgContainer = document.getElementById(icon.dataset.foo)
imgContainer.classList.add("active")
You can wrap it in a function and maybe save any references in an object. This way it's easy to keep track of any data and have very readable code.

jQuery Accordion misbehaving when expanding and contracting

I'm having trouble figuring out a bug in my accordion. The arrow icons are misbehaving when clicked on. If you go here, you will see some categories hidden by accordions. When you click on one and then close it, it behaves properly. But if you click on one, then click on another below before closing the first one, you'll notice that the arrow for the one above has returned to its "closed" position without closing it.
Here is the HTML that makes up each accordion:
<dl class="accordion">
<dt><strong>Accordion Title:</strong> Details</dt>
<dd>Hidden details</dd>
</dl>
The CSS for the arrows:
.accordion dt:after {
content: "\f125";
color: #ED4F30;
font-family: "Ionicons";
padding-left: 10px;
}
and
.accordion dt.accordion-active:after {
content: "\f123";
color: #ED4F30;
font-family: "Ionicons";
padding-left: 10px;
}
And finally the jQuery that I'm using to expand/collapse:
function ($) {
//Hide all panels
var allPanels = $('.accordion > dd').hide();
//Show first panel
//$('.accordion > dd:first-of-type').show();
//Add active class to first panel
//$('.accordion > dt:first-of-type').addClass('accordion-active');
//Handle click function
jQuery('.accordion > dt').on('click', function () {
var $this = $(this) ,
$target = $this.next();
jQuery('.accordion > dt.accordion-active').not($this.toggleClass('accordion-active')).removeClass('accordion-active');
$this.siblings('dd').not($target.addClass('active').slideToggle()).slideUp();
return false;
});
}
Any ideas what I'm missing?
It looks like you are overcomplicating things a little. You don't need to use the "not()" method to filter anything out. You are only toggling between 2 states (add/remove class, show/hide element) so you only need to use 2 jQuery methods, which were already in your code.
jQuery('.accordion > dt').on('click', function () {
var $this = $(this),
$target = $this.next();
$this.toggleClass('accordion-active');
$target.slideToggle();
return false;
});
Here's a JSFiddle based on the code you provided: http://jsfiddle.net/e5pe5/
Let me know if this is the intended functionality of your accordion.

Checking if Element hasClass then prepend and Element

What I am trying to achieve here is when a user clicks an element it becomes hidden, once this happens I want to prepend inside the containing element another Element to make all these items visible again.
var checkIfleft = $('#left .module'),checkIfright = $('#right .module');
if(checkIfleft.hasClass('hidden')) {
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if(checkIfright.hasClass('hidden')) {
right.prepend('<span class="resetRight">Reset Right</span>');
}
I tried multiple ways, and honestly I believe .length ==1 would be my best bet, because I only want one element to be prepended. I believe the above JS I have will prepend a new element each time a new item is hidden if it worked.
Other Try:
var checkIfleft = $('#left .module').hasClass('hidden'),
checkIfright = $('#right .module').hasClass('hidden');
if(checkIfleft.length== 1) {
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if(checkIfright.length== 1) {
right.prepend('<span class="resetRight">Reset Right</span>');
}
else if(checkIfleft.length==0){
$('.resetLeft').remove()
} else if (checkIfright.length==0){
$('.resetRight').remove()
}
Basically if one element inside the container is hidden I want a reset button to appear, if not remove that reset button...
hasClass() only works on the first item in the collection so it isn't doing what you want. It won't tell you if any item has that class.
You can do something like this instead where you count how many hidden items there are and if there are 1 or more and there isn't already a reset button, then you add the reset button. If there are no hidden items and there is a reset button, you remove it:
function checkResetButtons() {
var resetLeft = $('#left .resetLeft').length === 0;
var resetRight = $('#left .resetRight').length === 0;
var leftHidden = $('#left .module .hidden').length !== 0;
var rightHidden = $('#right .module .hidden').length !== 0;
if (leftHidden && !resetLeft) {
// make sure a button is added if needed and not already present
$('#left').prepend('<span class="resetLeft">Reset Left</span>');
} else if (!leftHidden) {
// make sure button is removed if no hidden items
// if no button exists, this just does nothing
$('#left .resetLeft').remove();
}
if (rightHidden && !resetRight) {
$('#right').prepend('<span class="resetRight">Reset Right</span>');
} else if (!rightHidden) {
$('#right .resetRight').remove();
}
}
// event handlers for the reset buttons
// uses delegated event handling so it will work even though the reset buttons
// are deleted and recreated
$("#left").on("click", ".resetLeft", function() {
$("#left .hidden").removeClass("hidden");
$("#left .resetLeft").remove();
});
$("#right").on("click", ".resetRight", function() {
$("#right .hidden").removeClass("hidden");
$("#right .resetRight").remove();
});
FYI, if we could change the HTML to use more common classes, the separate code for left and right could be combined into one piece of common code.
Add the reset button when hiding the .module, if it's not already there :
$('#left .module').on('click', function() {
$(this).addClass('hidden');
var parent = $(this).closest('#left');
if ( ! parent.find('.resetLeft') ) {
var res = $('<span />', {'class': 'resetLeft', text : 'Reset Left'});
parent.append(res);
res.one('click', function() {
$(this).closest('#left').find('.module').show();
$(this).remove();
});
}
});
repeat for right side !
I've recently experimented with using CSS to do some of this stuff and I feel that it works quite well if you're not trying to animate it. Here is a jsfiddle where I can hide a module and show the reset button in one go by adding/removing a 'hideLeft' or 'hideRight' class to the common parent of the two modules.
It works by hiding both reset button divs at first. Then it uses .hideLeft #left { display:none;} and .hideLeft #right .resetLeft { display: block; } to hide the left module and display the reset button when .hideLeft has been added to whichever element both elements descend from. I was inspired by modernizr a while back and thought it was a neat alternative way to do things. Let me know what you think, if you find it helpful, and if you have any questions :)

How can I tell if a page is in print view?

I'm using a simple toggle of the CSS display attribute from none to block in a FAQ page. However, I want the page to show everything when printing. What happens now is if you go to the page and go to print mode, it'll open all the closed items since I added this code to my print.css stylesheet...
.faq
{
display:block;
}
However, if you open an item, close it again, and then go to print mode, that item will remain closed.
My JS code looks like this...
` var divNum = new Array("faq1", "faq2", "faq3", "faq4", "faq5", "faq6", "faq7", "faq8", "faq9", "faq10", "faq11", "faq12", "faq13");
function openClose(theID) {
for (var i = 0; i < divNum.length; i++) {
if (divNum[i] == theID) {
if (document.getElementById(divNum[i]).style.display == "block") { document.getElementById(divNum[i]).style.display = "none" }
else { document.getElementById(divNum[i]).style.display = "block" }
}
}
}`
and the HTML looks like this
<a class="faq" onClick="openClose('faq1')">Question?</a><br />
<p id="faq1" class="faq">Answer</p>
What can I do to make sure everything is opened when I go into print mode?
Instead of manipulating the display status of an element directly with your JS code, I would have classes defined in CSS then simply toggle the classes using JS.
If your classes are defined for #media screen only then you won't have to worry about what the current display status is of any of the FAQ entries.
EDIT: For example, in your CSS file:
#media screen .faq.open {
display: block;
}
#media screen .faq {
display: none;
}
Then your JS would look like:
function openClose(theID) {
for (var i = 0; i < divNum.length; i++) {
if (divNum[i] == theID) {
if (document.getElementById(divNum[i]).className.match(new RegExp('(\\s|^)open(\\s|$)'))) { document.getElementById(divNum[i]).className = ele.className.replace(new RegExp('(\\s|^)open(\\s|$)'), ' '); }
else { document.getElementById(divNum[i]).className += " open" }
}
}
}
Note, I haven't tested this so there may be some syntax errors. Also, most of my projects already include jQuery so the method I typically use is much cleaner code. I have not used jQuery here because you didn't use it in your code samples.

Categories

Resources