adding more button for list in responsive navigation - javascript

I have a navigation of lets say 12 items, and when resolution gets smaller items drop in a new line. I need to make that when an item doesn't fit on a navigation anymore it should put a "MORE" dropdown button on the right side of nav. and put that item that doesn't fit in a dropdown.
If you don't understand me there is image below.
But the problem is that navigation items aren't always the same width because navigation items are generated from REST api.
I tryed to make jQuery script for calculating items width and adding them to navigation.
Here is the script I created, I made it in a hurry so it's really bad.
I need to help on how to properly calculate items witdh and navigation width and calculating when to add items to navigation or remove items from navigation.
Here is image if you don't get it: http://img.hr/aagV
/*
* Here we check how many items can we put on the navigation bar
* If item doesn't fit we clone it on the more dropdown button
*/
function removeMany() {
var i = $items.length - 1;
if (itemsWidth > navWidth) {
while (itemsWidth > navWidth) {
$($items[i]).removeClass('first-level-item').addClass('second-level-item');
dropdownItems.push($items[i]);
$($items[i]).removeClass('showed');
$items.pop();
i--;
getItemsWidth();
}
$nav.append($navMore);
dropdownItems.reverse().forEach(function (element, index, array) {
$('ul.second-level').append(element);
});
getItems();
}
}
//If window is resized to bigger resolution we need to put back items on the navbar
function addMany() {
var i = dropdownItems.length - 1;
if (dropdownItems.length != 0) {
do {
$('ul.first-level').append(dropdownItems.reverse()[i]);
$items.push(dropdownItems[i]);
dropdownItems.pop();
i--;
getItemsWidth();
} while (itemsWidth < navWidth);
$navMore.remove();
$items.each(function (i) {
$(this).addClass('first-level-item showed').removeClass('second-level-item');
});
if (!(dropdownItems != 0)) {
return;
} else {
$nav.append($navMore);
}
}
}
body {
margin: 0;
padding: 0;
border: 0; }
ul, li {
margin: 0;
padding: 0;
list-style: none; }
ul.second-level li {
display: block !important; }
ul.second-level li > a {
color: black; }
a {
color: #fff;
text-decoration: none;
text-transform: uppercase; }
.second-level-item a {
color: #333 !important; }
.navigation {
width: 960px;
max-width: 100%;
background: #211;
color: #aaa;
margin: 0 auto; }
.first-level .first-level-item {
display: inline-block;
padding: 10px; }
.first-level .item-more {
display: inline-block; }
.first-level .item-more .second-level-item {
display: inline-block; }
.second-level {
position: absolute;
top: 100%;
right: 0;
width: 200px;
background: #fff;
padding: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4); }
.has-second-level {
position: relative; }
.has-second-level .second-level {
display: none; }
.has-second-level:hover {
background: #fff;
color: #000; }
.has-second-level:hover .second-level {
display: block; }
/*# sourceMappingURL=style.css.map */
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DropDown</title>
<link rel="stylesheet" href="css/reset.css"/>
<link rel="stylesheet" href="css/style.css"/>
</head>
<body>
<nav class="navigation">
<ul class="first-level">
<li class="first-level-item showed">Introduction to Irish Culture</li>
<li class="first-level-item showed">Cellular and Molecular Neurobiology</li>
<li class="first-level-item showed">Guitar foundations</li>
<li class="first-level-item showed">Startup Inovation</li>
<li class="first-level-item showed">Astrophysics</li>
<li class="first-level-item item-more has-second-level">
<span> More </span>
<ul class="second-level">
</ul>
</li>
</ul>
</nav>
<script src="https://code.jquery.com/jquery-2.1.1.js"></script>
</body>
</html>

If you have fixed-width list-items, then it is simple to collect extra list-items and push them into a separate list. Here is a simple example. Explanation is in the code comments.
View the snippet in full-screen and try changing the window width.
Also a Fiddle: http://jsfiddle.net/abhitalks/860LzgLL/
Full Screen: http://jsfiddle.net/abhitalks/860LzgLL/embedded/result/
Snippet:
var elemWidth, fitCount, fixedWidth = 120,
$menu = $("ul#menu"), $collectedSet;
// Assuming that the list-items are of fixed-width.
collect();
$(window).resize(collect);
function collect() {
// Get the container width
elemWidth = $menu.width();
// Calculate how many list-items can be accomodated in that width
fitCount = Math.floor(elemWidth / fixedWidth) - 1;
// Create a new set of list-items more than the fit count
$collectedSet = $menu.children(":gt(" + fitCount + ")");
// Empty the collection submenu and add the cloned collection set
$("#submenu").empty().append($collectedSet.clone());
}
* { box-sizing: border-box; margin: 0; padding: 0; }
div { position: relative; background-color: #ccc; height: 32px; overflow: visible; }
ul#menu, ol { height: 32px; max-width: 80%; overflow: hidden; }
ul#menu > li, ol > li { display: block; float: left; height: 32px; width: 120px; padding: 4px 8px; }
ol { position: absolute; right: 0; top: 0; overflow: visible; }
ol > li { min-width: 120px; }
ol ul { position: absolute; top: 120%; right: 10%; }
ol li ul > li { list-style: none; background-color: #eee; border: 1px solid gray; padding: 4px 8px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<ul id="menu">
<li>Option One</li><li>Option Two</li><li>Option Three</li>
<li>Option Four</li><li>Option Five</li><li>Option Six</li>
</ul>
<ol><li>Collected<ul id="submenu"></ul></li></ol>
</div>
Update:
This is regarding your query on differing / variable widths of list-items. There would be a minor change.
Also a Fiddle: http://jsfiddle.net/abhitalks/tkbmcupt/1/
Full Screen: http://jsfiddle.net/abhitalks/tkbmcupt/1/embedded/result/
Snippet:
var elemWidth, fitCount, varWidth = 0, ctr, $menu = $("ul#menu"), $collectedSet;
// Get static values here first
ctr = $menu.children().length; // number of children will not change
$menu.children().each(function() {
varWidth += $(this).outerWidth(); // widths will not change, so just a total
});
collect(); // fire first collection on page load
$(window).resize(collect); // fire collection on window resize
function collect() {
elemWidth = $menu.width(); // width of menu
// Calculate fitCount on the total width this time
fitCount = Math.floor((elemWidth / varWidth) * ctr) - 1;
// Reset display and width on all list-items
$menu.children().css({"display": "block", "width": "auto"});
// Make a set of collected list-items based on fitCount
$collectedSet = $menu.children(":gt(" + fitCount + ")");
// Empty the more menu and add the collected items
$("#submenu").empty().append($collectedSet.clone());
// Set display to none and width to 0 on collection,
// because they are not visible anyway.
$collectedSet.css({"display": "none", "width": "0"});
}
* { box-sizing: border-box; margin: 0; padding: 0; }
div { position: relative; background-color: #ccc; height: 32px; overflow: visible; }
ul#menu, ol { height: 32px; max-width: 80%; overflow: hidden; }
ul#menu > li, ol > li { display: block; float: left; height: 32px; white-space: nowrap; padding: 4px 8px; }
ol { position: absolute; right: 0; top: 0; overflow: visible; }
ol > li { min-width: 120px; }
ol ul { position: absolute; top: 120%; right: 10%; }
ol li ul > li { list-style: none; background-color: #eee; border: 1px solid gray; padding: 4px 8px;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<ul id="menu">
<li>Option One</li><li>Option Two</li><li>Option Three</li>
<li>Option Four</li><li>Option Five</li><li>Option Six</li>
</ul>
<ol><li>Collected<ul id="submenu"></ul></li></ol>
</div>

Can and SHOULD be optimised (as it is quite inefficient from what i've tested), but that's up to you.
$(document).ready(function(){
var moreW = $(".more").outerWidth(), //width of your "more" element
totalW = -moreW, //cumulated width of list elements
totalN = $('.nav li').length - 1, //number of elements minus the "more" element
dw = document.documentElement.clientWidth;
$('.nav li').each(function(){
totalW += $(this).outerWidth();
});
function moveToDropdown(){
dw = document.documentElement.clientWidth;
//moves elements into the list
while(totalW > (dw - moreW)){
var temp = $(".nav li:nth-last-child(2)"); //element to be moved
totalW = totalW - temp.outerWidth();
$(".dropdown").append(temp.clone());
temp.remove();
}
//moves elements out of the list
var newList = $('.dropdown li').length; //check if we have elements
if(newList > 0){
var element = $('.dropdown li:last-child'), //element to be moved
elementW = $('.dropdown li:last-child').outerWidth(); //width of element to be moved
if(totalW + elementW < dw - moreW){
while(totalW + elementW < dw - moreW ){
var element = $('.dropdown li:last-child'),
elementW = $('.dropdown li:last-child').outerWidth();
totalW = totalW + elementW;
$(".nav > li:last-child").before(element.clone());
element.remove();
}
}
}
}
moveToDropdown();
$(window).resize(moveToDropdown)
});
.clearfix:after{
display:block;
content:'';
clear:both;
}
body,html{
width:100%;
height:100%;
margin:0;
padding:0;
}
ul{
list-style:none;
width:100%;
padding:0;
margin:0;
}
ul li{
float:left;
padding:5px;
}
.nav > li {
position:relative;
}
.nav ul{
position:absolute;
top:25px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul class="nav clearfix">
<li>Item</li>
<li>Item</li>
<li>Item</li>
<li>Item</li>
<li>Item</li>
<li>Item</li>
<li>Item</li>
<li class="more">
more
<ul class="dropdown">
<!-- we'll add elements here -->
</ul>
</li>
</ul>

This question is too old, but i want to post my answer too. Maybe this is more cleaner and easier way. I have created a pen: https://codepen.io/sergi95/pen/bmNoML
<div id="mainMenu" class="main-menu">
<ul id="autoNav" class="main-nav">
<li>
home
</li>
<li>
about us
</li>
<li>
portfolio
</li>
<li>
team
</li>
<li>
blog
</li>
<li>
contact
</li>
<li id="autoNavMore" class="auto-nav-more">
more
<ul id="autoNavMoreList" class="auto-nav-more-list">
<li>
policy
</li>
</ul>
</li>
</ul>
const $mainMenu = $("#mainMenu");
const $autoNav = $("#autoNav");
const $autoNavMore = $("#autoNavMore");
const $autoNavMoreList = $("#autoNavMoreList");
autoNavMore = () => {
let childNumber = 2;
if($(window).width() >= 320) {
// GET MENU AND NAV WIDTH
const $menuWidth = $mainMenu.width();
const $autoNavWidth = $autoNav.width();
if($autoNavWidth > $menuWidth) {
// CODE FIRES WHEN WINDOW SIZE GOES DOWN
$autoNav.children(`li:nth-last-child(${childNumber})`).prependTo($autoNavMoreList);
autoNavMore();
} else {
// CODE FIRES WHEN WINDOW SIZE GOES UP
const $autoNavMoreFirst = $autoNavMoreList.children('li:first-child').width();
// CHECK IF ITEM HAS ENOUGH SPACE TO PLACE IN MENU
if(($autoNavWidth + $autoNavMoreFirst) < $menuWidth) {
$autoNavMoreList.children('li:first-child').insertBefore($autoNavMore);
}
}
if($autoNavMoreList.children().length > 0) {
$autoNavMore.show();
childNumber = 2;
} else {
$autoNavMore.hide();
childNumber = 1;
}
}
}
// INIT
autoNavMore();
$(window).resize(autoNavMore);
.main-menu {
max-width: 800px;
}
.main-nav {
display: inline-flex;
padding: 0;
list-style: none;
}
.main-nav li a {
padding: 10px;
text-transform: capitalize;
white-space: nowrap;
font-size: 30px;
font-family: sans-serif;
text-decoration: none;
}
.more-btn {
color: red;
}
.auto-nav-more {
position: relative;
}
.auto-nav-more-list {
position: absolute;
right: 0;
opacity: 0;
visibility: hidden;
transition: 0.2s;
text-align: right;
padding: 0;
list-style: none;
background: grey;
border-radius: 4px;
}
.auto-nav-more:hover .auto-nav-more-list {
opacity: 1;
visibility: visible;
}

The script that Abhitalks made did not work properly for different element sizes. I modified the code a little bit do that it does:
$(function() {
function makeMenuFit() {
//Get data
var menuSize = menu.width();
//Determine how many items that fit
var menuTotalWidth = 0;
var itemThatFit = 0;
for(var i = 0; i < menuItems.length; i++) {
menuTotalWidth += menuItems[i];
if(menuTotalWidth <= menuSize) {
itemThatFit++;
continue;
}
break;
}
menu.children().css({"display": "block", "width": "auto"});
var collectedSet = menu.children(":gt(" + (itemThatFit - 1) + ")");
$("#submenu").empty().append(collectedSet.clone());
collectedSet.css({"display": "none", "width": "0"});
}
var menu = $(".tabletNavigation > ul");
var menuItems = [];
menu.children().each(function() {
menuItems.push($(this).outerWidth());
});
$(window).resize(makeMenuFit);
makeMenuFit();
});

Related

Adding class dynamically based on which element is being scrolled past

I have a nav which contains letters and sections which are associated to letters in the nav.
When a user scrolls to a section, I want to addClass active to that letter. For example:
User scrolls to section with the id of a, the anchor with the data-letter with a will be active.
Currently, on scroll, all my letters in the nav become active and this is because it's always thinking it's on section A.
Demo:
$(function() {
$(window).scroll(function() {
// step 1: get id of section
var visible_section = $('section:visible'), id = visible_section.attr('id');
console.log(id);
// step 2: add class where id and data-letter match
$("nav a").removeClass("active");
$("nav a[data-letter='"+id+"']").addClass("active");
});
});
.nav {
background: grey;
padding: 30px 15px;
position: fixed;
top: 0;
width: 100%;
}
.nav a {
padding: 0 15px;
text-decoration: none;
}
.nav a:hover {
background: black;
color: white;
}
.nav a.active {
background: black;
color: white;
}
.sections {
margin-top: 100px;
}
section {
padding: 200px 0;
color: red;
display: flex;
align-items: center;
justify-content: center;
font-size: 50px;
}
section:nth-child(odd) {
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<main>
<nav class="nav">
A
B
C
D
</nav>
<div class="sections">
<section id="a">A</section>
<section id="b">B</section>
<section id="c">C</section>
<section id="d">D</section>
</div>
</main>
FYI: https://stackoverflow.com/a/5354536/4571790
function isVisible(elm) {
var rect = elm.getBoundingClientRect();
var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
return !(rect.bottom < 0 || rect.top - viewHeight >= 0);
}
$(function() {
$(window).scroll(function() {
// step 1: get id of section
var visible_section = $('section:visible'), id="";
// this code will find which section is the first visible
$(".sections").find("section").each((i,a)=>id==""?(isVisible(a)?id=$(a).attr("id"):id):id);
$("#result").html(id +" is visible now");
//console.log(id);
// step 2: add class where id and data-letter match
$("nav a").removeClass("active");
$("nav a[data-letter='"+id+"']").addClass("active");
});
});
.nav {
background: grey;
padding: 30px 15px;
position: fixed;
top: 0;
width: 100%;
}
.nav a {
padding: 0 15px;
text-decoration: none;
}
.nav a:hover {
background: black;
color: white;
}
.nav a.active {
background: black;
color: white;
}
.sections {
margin-top: 100px;
}
section {
padding: 200px 0;
color: red;
display: flex;
align-items: center;
justify-content: center;
font-size: 50px;
}
section:nth-child(odd) {
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<main>
<nav class="nav">
A
B
C
D
<span id="result"></span>
</nav>
<div class="sections">
<section id="a">A</section>
<section id="b">B</section>
<section id="c">C</section>
<section id="d">D</section>
</div>
</main>
You should differentiate the nav tabs by setting their data-letter as you described to a, b, c and d:
<nav class="nav">
A
B
C
D
</nav>
All your nav become active not because it always thinks it's on a, but because all data-letter are set to a.
You can change ratio from 0.1 to 1:
$(function () {
let ratio = 0.6; // From 0.1 to 1
$(window).scroll(function () {
let sections = $('.sections section');
let scrollTop = $(window).scrollTop();
let screen_height = $(window).height();
$.each(sections, function () {
let top = $(this).offset().top;
let calc = (top - scrollTop) / screen_height;
if (calc >= (ratio * -1) && calc <= ratio) {
let id = $(this).attr('id');
$("nav a").removeClass("active");
$("nav a[data-letter='" + id + "']").addClass("active");
//console.log(`${id} is Active`);
}
});
});
});
.nav {
background: grey;
padding: 30px 15px;
position: fixed;
top: 0;
width: 100%;
}
.nav a {
padding: 0 15px;
text-decoration: none;
}
.nav a:hover {
background: black;
color: white;
}
.nav a.active {
background: black;
color: white;
}
.sections {
margin-top: 100px;
}
section {
padding: 200px 0;
color: red;
display: flex;
align-items: center;
justify-content: center;
font-size: 50px;
}
section:nth-child(odd) {
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<main>
<nav class="nav">
A
B
C
D
</nav>
<div class="sections">
<section id="a">A</section>
<section id="b">B</section>
<section id="c">C</section>
<section id="d">D</section>
</div>
</main>
Your confusion seems to stem from the fact that you have an inaccurate understanding of what :visible selector in jQuery selects. From documentation: "Elements are considered visible if they consume space in the document. Visible elements have a width or height that is greater than zero." By this definition, all of the sections are "visible".
To achieve your use case you need to calculate coordinates of individual sections and compare them with scroll position in the document. The following appears to do what you are looking for (try it out here):
$(function () {
$(window).scroll(function () {
// determine element that is fully in the viewport
const fullyVisibleSection = Array.from(document.querySelectorAll('section')).find((section) => {
const topY = section.offsetTop;
const bottomY = topY + section.offsetHeight;
return (topY >= window.scrollY) && (bottomY <= (window.scrollY + window.innerHeight));
});
// only update classes if the section is fully visible
if (fullyVisibleSection) {
$("nav a").removeClass("active");
// add class where id and data-letter match
$("nav a[data-letter='" + fullyVisibleSection.id + "']").addClass("active");
}
});
});

Vanilla JavaScript solution to make a fluid menu tab active state?

I've been looking at this Codepen, and trying to find a vanilla JS way to do this (my company doesn't use jQuery).
So far I've made the line the correct width when you click on a menu item, but I can't figure out how to make it stretch like in the Codepen. I added a custom attribute of index to keep track of numbers, and also applied a class to easily target the element. I wasn't sure if there's also a way to just have one. Feel free to change what I've already made.
EDIT: I updated the code below to make it work going left, but not right. Also it only works if the links are next to each other. Anyone?
My codepen: https://codepen.io/ahoward-mm/pen/jOmgxQJ?editors=0010 (desktop only).
var navList = document.querySelector(".navigation__list");
var navItems = navList.getElementsByClassName("navigation__item");
var navLine = document.querySelector(".navigation__line");
for (var i = 0; i < navItems.length; i++) {
navItems[i].classList.add(`navigation__item--${i + 1}`);
navItems[i].setAttribute("index", `${i + 1}`);
var prevItem = 0;
var currentItem = 1;
navItems[i].addEventListener("click", function() {
var current = document.getElementsByClassName("active");
if (current.length > 0) {
current[0].className = current[0].className.replace(" active", "");
}
this.className += " active";
prevItem = currentItem;
currentItem = this.getAttribute("index");
navLine.style.width = `${
document
.querySelector(`.navigation__item--${currentItem}`)
.querySelector(".navigation__link")
.getBoundingClientRect().width +
document
.querySelector(`.navigation__item--${prevItem}`)
.getBoundingClientRect().width
}px`;
navLine.style.left = `${
this.querySelector(".navigation__link").offsetLeft
}px`;
setTimeout(function() {
navLine.style.width = `${
document
.querySelector(`.navigation__item--${currentItem}`)
.querySelector(".navigation__link")
.getBoundingClientRect().width
}px`;
}, 700);
});
}
body {
color: #444;
display: flex;
min-height: 100vh;
flex-direction: column;
}
.navigation {
display: block;
position: sticky;
top: -0.5px;
background-color: #edece8;
margin: 60px 0 0;
text-align: center;
}
.navigation__list {
list-style: none;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 0;
position: relative;
}
.navigation__link {
color: inherit;
line-height: inherit;
word-wrap: break-word;
text-decoration: none;
background-color: transparent;
display: block;
text-transform: uppercase;
letter-spacing: 1.5px;
font-size: 0.875rem;
font-weight: bold;
margin: 15px (20px * 2) 0 (20px * 2);
position: relative;
}
.navigation__line {
height: 2px;
position: absolute;
bottom: 0;
margin: 10px 0 0 0;
background: red;
transition: all 1s;
}
.navigation__item {
list-style: none;
display: flex;
}
<nav class="navigation">
<ul class="navigation__list">
<li class="navigation__item">
<a class="navigation__link" href="#">Lorem ipsum</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Dolor</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Consectetur adipiscing</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Donec ut</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Placerat dignissim</a>
</li>
<div class="navigation__line"></div>
</ul>
</nav>
Here you go.
Idk what your JavaScript code did, so I just decided to convert the jQuery code from codepen to vanilla JavaScript from scratch. And it works.
(It was a good 2-2.5 hrs exercise.)
So, what I did is converted jQuery to its Vanilla JavaScript Equivalent.
For Ex: $() => document.querySelector(), elem.addClass() => elem.classList.add(), elem.find() => elem.querySelectorAll(), elem.css({prop: val}) => elem.style.prop = val; etc.
var nav = document.querySelector(".navigation");
var navLine = nav.querySelector(".navigation__line");
var pos = 0;
var wid = 0;
function setUnderline() {
var active = nav.querySelectorAll(".active");
if (active.length) {
pos = active[0].getBoundingClientRect().left;
wid = active[0].getBoundingClientRect().width;
navLine.style.left = active[0].offsetLeft + "px";
navLine.style.width = wid + "px";
}
}
setUnderline()
window.onresize = function() {
setUnderline()
};
nav.querySelectorAll("ul li a").forEach((elem) => {
elem.onclick = function(e) {
e.preventDefault();
if (!this.parentElement.classList.contains("active") &&
!nav.classList.contains("animate")
) {
nav.classList.add("animate");
var _this = this;
nav
.querySelectorAll("ul li")
.forEach((e) => e.classList.remove("active"));
try {
var position = _this.parentElement.getBoundingClientRect();
var width = position.width;
if (position.x >= pos) {
navLine.style.width = position.x - pos + width + "px";
setTimeout(() => {
navLine.style.left = _this.parentElement.offsetLeft + "px";
navLine.style.width = width + "px";
navLine.style.transitionDuration = "150ms";
setTimeout(() => nav.classList.remove("animate"), 150);
_this.parentElement.classList.add("active");
}, 300);
} else {
navLine.style.width = pos - position.left + wid + "px";
navLine.style.left = _this.parentElement.offsetLeft + "px";
setTimeout(() => {
navLine.style.width = width + "px";
navLine.style.transitionDuration = "150ms";
setTimeout(() => nav.classList.remove("animate"), 150);
_this.parentElement.classList.add("active");
}, 300);
}
} catch (e) {}
pos = position.left;
wid = width;
}
}
});
body {
color: #444;
display: flex;
min-height: 100vh;
flex-direction: column;
}
.navigation {
display: block;
position: sticky;
top: -0.5px;
background-color: #edece8;
margin: 60px 0 0;
text-align: center;
}
ul li:not(:last-child) {
margin-right: 30px;
}
li.active {
opacity: 1;
}
.navigation__list {
list-style: none;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 0;
position: relative;
}
.navigation__link {
color: inherit;
line-height: inherit;
word-wrap: break-word;
text-decoration: none;
background-color: transparent;
display: block;
text-transform: uppercase;
letter-spacing: 1.5px;
font-size: 0.875rem;
font-weight: bold;
margin: 15px (20px * 2) 0 (20px * 2);
position: relative;
}
.navigation__line {
height: 2px;
position: absolute;
bottom: 0;
margin: 10px 0 0 0;
background: red;
transition: all 0.3s;
left: 0;
}
.navigation__item {
list-style: none;
display: flex;
}
<link rel="stylesheet" href="style.css">
<body>
<nav class="navigation">
<ul class="navigation__list">
<li class="navigation__item active">
<a class="navigation__link" href="#">Lorem ipsum</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Dolor</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Consectetur adipiscing</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Donec ut</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Placerat dignissim</a>
</li>
<div class="navigation__line"></div>
</ul>
</nav>
<script src="./script.js"></script>
</body>
So, your code works fine for when you're switching to a link that's before the current one. The problem is that when you change to a link that's ahead, you need to adjust the left property only after you adjust the width.
I made multiple changes in the code for clarity and correctness. Like using classList to add/remove classes and dataset for custom HTML attributes.
However, the only functional change is to check if the previous item is ahead of the current one, and if it is, only apply the left adjustment later, after the width adjustment.
Edit: I just realized that there was second issue with the code. The second aspect that wasn't working as desired was when moving multiple links ahead or behind, the line wouldn't expand long enough. I updated the snippet to account for that by calculating the distance from the link that's earlier to the link that's later, plus the width of the later link.
var navList = document.querySelector(".navigation__list");
var navItems = navList.querySelectorAll(".navigation__item");
var navLine = document.querySelector(".navigation__line");
var prevItem = 0;
var currentItem = 1;
navItems.forEach((navItem, i) => {
navItem.classList.add(`navigation__item--${i + 1}`);
navItem.dataset.index = i + 1;
navItem.addEventListener("click", function () {
var current = document.querySelector(".active");
if (current) {
current.classList.remove("active");
}
this.classList.add("active");
prevItem = currentItem;
currentItem = this.dataset.index;
var movingAhead = currentItem > prevItem;
var aheadElem = movingAhead ? document.querySelector(`.navigation__item--${currentItem} .navigation__link`) : document.querySelector(`.navigation__item--${prevItem} .navigation__link`);
var behindElem = movingAhead ? document.querySelector(`.navigation__item--${prevItem} .navigation__link`) : document.querySelector(`.navigation__item--${currentItem} .navigation__link`);
navLine.style.width = `${(aheadElem.offsetLeft - behindElem.offsetLeft) + aheadElem.getBoundingClientRect().width
}px`;
if (!movingAhead) {
navLine.style.left = `${this.querySelector(".navigation__link").offsetLeft}px`;
}
setTimeout(function () {
var currentLink = document.querySelector(`.navigation__item--${currentItem} .navigation__link`);
navLine.style.width = `${
currentLink.getBoundingClientRect().width
}px`;
if (movingAhead) {
navLine.style.left = `${currentLink.offsetLeft}px`;
}
}, 700);
});
})
body {
color: #444;
display: flex;
min-height: 100vh;
flex-direction: column;
}
.navigation {
display: block;
position: sticky;
top: -0.5px;
background-color: #edece8;
margin: 60px 0 0;
text-align: center;
}
.navigation__list {
list-style: none;
margin: 0;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 0;
position: relative;
}
.navigation__link {
color: inherit;
line-height: inherit;
word-wrap: break-word;
text-decoration: none;
background-color: transparent;
display: block;
text-transform: uppercase;
letter-spacing: 1.5px;
font-size: 0.875rem;
font-weight: bold;
margin: 15px (20px * 2) 0 (20px * 2);
position: relative;
}
.navigation__line {
height: 2px;
position: absolute;
bottom: 0;
margin: 10px 0 0 0;
background: red;
// width: calc(100% - 80px);
transition: all 0.8s;
}
.navigation__item {
list-style: none;
display: flex;
}
<nav class="navigation">
<ul class="navigation__list">
<li class="navigation__item">
<a class="navigation__link" href="#">Lorem ipsum</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Dolor</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Consectetur adipiscing</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Donec ut</a>
</li>
<li class="navigation__item">
<a class="navigation__link" href="#">Placerat dignissim</a>
</li>
<div class="navigation__line"></div>
</ul>
</nav>

Simple jQuery left/right slide toggle animation for mobile menu

I'm trying to create a menu that slides in and out from the right hand side of the screen for a mobile version of a site.
I have a 'ul' that starts off screen on page load due to its large margin. The plan is to have a button that will toggle that margin back and forth with '.animate' in order to hide and reveal the 'ul'.
The first chunk of code below works but won't repeat. So, on 'click', the menu appears, hides and then appears once more before it stops responding. This confused me so I tried a different route and went with an 'if' statement but now it just keeps sliding left despite the class definitely changing (i've checked it in the console).
Now i'm stumped! Can anyone help?
// MOBILE MENU
$(function() {
// create identical menu buttons with different classes
var $active = $("<div class='mm-active'><hr><hr><hr></div>");
var $inactive = $("<div class='mm-inactive'><hr><hr><hr></div>");
// append 'inactive' menu button to menu div
$(".mobile-menu").prepend($inactive);
$($inactive).click(function() {
$($inactive).hide();
$(this).next("ul").animate({'margin-left': '-='+90}, 1000);
$(".mobile-menu").prepend($active);
});
$($active).click(function() {
$(this).nextAll("ul").animate({'margin-left': '+='+90}, 1000);
$($active).remove();
$($inactive).show();
});
});
//And here with the 'if' statement...
$(function() {
// create identical menu buttons with different classes
var $mm_btn = $("<div><hr><hr><hr></div>");
var $classname = ($mm_btn).attr("class");
// append mobile menu button to menu div
$(".mobile-menu").prepend($mm_btn);
$($mm_btn).click(function() {
$($mm_btn).toggleClass('active');
if($classname === 'active') {
$(this).next("ul").animate({'margin-left': '+='+90}, 1000);
} else {
$(this).next("ul").animate({'margin-left': '-='+90}, 1000);
}
});
});
.mobile-menu {
position: absolute;
top: 5px;
right: 0;
width: 25px;
margin: 0 25px 0 0;
padding: 5px 0 8px 5px;
z-index: 1;
}
.mobile-menu hr {
border: 0;
height: 2px;
background: black;
}
.mobile-menu ul {
position: relative;
z-index: -1;
display: inline-block;
text-align: right;
margin-left: 50px;
margin-top: -50px;
padding: 50px 25px 5px 5px;
list-style: none;
}
.mobile-menu ul li {
padding: 3px;
}
<div class="mobile-menu">
<ul>
<li class="projects">projects</li>
<li>about</li>
<li>contact</li>
</ul>
</div>
In first case, when you are removing element all events are also removed: .remove()
In second case: $classname was set to empty string on page load and is never changed this is why only else is executed.
// MOBILE MENU
$(function() {
// create identical menu buttons with different classes
var $active = $("<div class='mm-active'><hr><hr><hr></div>");
// append 'inactive' menu button to menu div
$(".mobile-menu").prepend($active);
$($active).click(function() {
if ($active.hasClass('mm-active')) {
$(this).nextAll("ul").animate({
'margin-left': '-=' + 90
}, 1000);
} else {
$(this).nextAll("ul").animate({
'margin-left': '+=' + 90
}, 1000);
}
$active.toggleClass('mm-active');
});
});
body {
overflow: hidden;
}
.mobile-menu {
position: absolute;
top: 35px;
right: 0;
width: 25px;
margin: 0 25px 0 0;
padding: 5px 0 8px 5px;
z-index: 1;
}
.mobile-menu hr {
border: 0;
height: 2px;
background: black;
}
.mobile-menu ul {
position: relative;
z-index: -1;
display: inline-block;
text-align: right;
margin-left: 50px;
margin-top: -50px;
padding: 50px 25px 5px 5px;
list-style: none;
}
.mobile-menu ul li {
padding: 3px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="mobile-menu">
<ul>
<a href="projects.html">
<li class="projects">projects</li>
</a>
<a href="about.html">
<li>about</li>
</a>
<a href="contact.html">
<li>contact</li>
</a>
</ul>
</div>

Vertically Stretch List Items

I am building a phonegap application. I have the following:
<ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
<li>Three <br>a Half</li>
</ul>
How can I make the <li> elements stretch vertically and fill the whole height of the page given that this needs to be dynamic so that it adapts to other viewports. The text inside the <li> elements needs to be vertically centred and supports multiple lines.
Is there any clean way of doing this?
I suggest using the display: table family of CSS3 rules. They'll be dynamic and maintain full height spacing if done correctly:
body, html {
height: 100%;
margin: 0;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
display: table;
width: 100%;
height: 100%;
}
ul li {
display: table-row;
}
ul li a { /* assuming an anchor child. Can be anything */
display: table-cell;
vertical-align: middle;
padding: 1em 3em;
}
Demo: http://jsfiddle.net/6z3q35x0/1/
There are two parts to your question: the first is to force the <ul> element to stretch to fill the viewport, and the second is to vertically and horizontally center the <li> content. However, the centering requires modifications to your markup. We can wrap all the content in <li> using <div> elements.
For centering, we can use CSS3 flexbox for that. This would be a JS-free solution, although it enjoys less cross-browser support. For viewport size, we can use the vw and vh units respectively.
ul {
display: flex;
flex-direction: column;
list-style: none;
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
li {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
}
li div {
}
/* For stylistics only */
li:nth-child(odd) {
background-color: #ddd;
}
<link href="http://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.2/normalize.min.css" rel="stylesheet"/>
<ul>
<li><div>One</div></li>
<li><div>Two</div></li>
<li><div>Three</div></li>
<li><div>Three <br>a Half</div></li>
</ul>
However, there might be situations where using CSS flexbox and viewport units are not ideal — iOS7, for example, has a well-documented rendering bug that does not calculate vh properly. In this case, we might have to rely on JS instead. The height of each <li> is simply divided by the number of <li>s present in the container.
var calcHeight = function() {
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
var li = document.querySelectorAll("ul li");
for(var i=0; i<li.length; i++) {
li[i].style.height = (h/li.length)+'px';
}
}
calcHeight();
window.onresize = calcHeight();
ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
li {
display: block;
width: 100%;
position: relative;
}
li div {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
li:nth-child(odd) {
background-color: #ddd;
}
<link href="http://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.2/normalize.min.css" rel="stylesheet"/>
<ul>
<li><div>One</div></li>
<li><div>Two</div></li>
<li><div>Three</div></li>
<li><div>Three <br>a Half</div></li>
</ul>
Here is a JavaScript solution, which will give the heights according to the viewport's height.
Forcing ul to take the entire viewport's height:
Demo on Fiddle
var li = document.getElementsByTagName('li');
function doMath() {
for (i = 0; i < li.length; i++) {
li[i].style.height = window.innerHeight / li.length + 'px';
}
}
doMath();
window.onresize = doMath;
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
background: rosybrown;
}
span {
display: block;
position: relative;
top: 50%;
text-align: center;
transform: translateY(-50%);
}
li:nth-of-type(2n) {
background: plum;
}
body {
margin: 0;
}
<ul>
<li><span>One</span></li>
<li><span>Two</span></li>
<li><span>Three</span></li>
<li><span>Three<br />a Half</span></li>
<li><span>Four</span></li>
<li><span>Five<br />a Half</span></li>
<li><span>Six</span></li>
</ul>
Forcing lis to take the entire viewport's height:
Demo on Fiddle
var li = document.getElementsByTagName('li');
function doMath() {
for (i = 0; i < li.length; i++) {
li[i].style.height = window.innerHeight + 'px';
}
}
doMath();
window.onresize = doMath;
ul {
padding: 0;
margin: 0;
list-style: none;
}
li {
background: rosybrown;
}
span {
display: block;
position: relative;
top: 50%;
text-align: center;
transform: translateY(-50%);
}
li:nth-of-type(2n) {
background: plum;
}
body {
margin: 0;
}
<ul>
<li><span>One</span></li>
<li><span>Two</span></li>
<li><span>Three</span></li>
<li><span>Three<br />a Half</span></li>
<li><span>Four</span></li>
<li><span>Five<br />a Half</span></li>
<li><span>Six</span></li>
</ul>
You need to add height 100% to body and html, try this
<style>
body, html {
height: 100%;
}
ul {
height: 100%;
display: table;
}
li {
height: 25%;
display: table-row;
}
li div{
height: 25%;
display: table-cell;
vertical-align: middle;
}
</style>
Jsfiddle link: http://jsfiddle.net/d80xw55e/1/
Since you're allowing Javscript, this is pretty easy, I'm just having trouble getting the bullet point right. Hopefully you won't be using that though?
function liH(){
var lis = document.getElementsByTagName("li");
var spans = document.getElementsByTagName("span");
var liHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) / lis.length;
for(var i = 0; i < lis.length; ++i){
var spanH = spans[i].getBoundingClientRect().height;
lis[i].style.paddingTop = ((liHeight - spanH) / 2) + "px";
lis[i].style.paddingBottom = lis[i].style.paddingTop;
//spans[i].style.top = -(spanH / 4) + "px"
}
}
liH();
window.onresize = liH;
li {background:#eee;margin-bottom:5px;}
span {position:relative;}
<ul>
<li><span>elem1</span></li>
<li><span>elem2</span></li>
<li><span>elem<br />3</span></li>
</ul>
The reason the view is not exact is because of the padding on the top that won't remove, and because of the 5px margin which I put to distinguish the elements.

How to Use Images as Navigation with innerfade Slideshow?

I am very new to JavaScript and only have the most basic understanding of how it works, so please bear with me. :) I'm using the jquery.innerfade.js script to create a slideshow with fade transitions for a website I'm developing, and I have added navigation buttons (which are set as background-images) that navigate between the “slides”. The navigation buttons have three states: default/off, hover, and on (each state is a separate image). I created a separate JavaScript document to set the buttons to “on” when they are clicked. The “hover” state is achieved through the CSS.
Both the slideshow and the navigation buttons work well. There is just one thing I want to add: I would like the appropriate navigation button to display as “on” while the related “slide” is “playing”.
Here's the HTML:
<div id="mainFeature">
<ul id="theFeature">
<li id="the1feature"><img src="_images/carousel/promo1.jpg" /></li>
<li id="the2feature"><img src="_images/carousel/promo2.jpg" /></li>
<li id="the3feature"><img src="_images/carousel/promo3.jpg" /></li>
</ul>
<div id="promonav-con">
<div id="primarypromonav">
<ul class="links">
<li id="the1title" class="promotop"><a rel="1" href="#promo1" class="promo1" id="promo1" onMouseDown="promo1on()"><strong>Botox Cosmetic</strong></a></li>
<li id="the2title" class="promotop"><a rel="2" href="#promo2" class="promo2" id="promo2" onMouseDown="promo2on()"><strong>Promo 2</strong></a></li>
<li id="the3title" class="promotop"><a rel="3" href="#promo3" class="promo3" id="promo3" onMouseDown="promo3on()"><strong>Promo 3</strong></a></li>
</ul>
</div>
</div>
And here is the jquery.innerfade.js, with my changes:
(function($) {
$.fn.innerfade = function(options) {
return this.each(function() {
$.innerfade(this, options);
});
};
$.innerfade = function(container, options) {
var settings = {
'speed': 'normal',
'timeout': 2000,
'containerheight': 'auto',
'runningclass': 'innerfade',
'children': null
};
if (options)
$.extend(settings, options);
if (settings.children === null)
var elements = $(container).children();
else
var elements = $(container).children(settings.children);
if (elements.length > 1) {
$(container).css('position', 'relative').css('height', settings.containerheight).addClass(settings.runningclass);
for (var i = 0; i < elements.length; i++) {
$(elements[i]).css('z-index', String(elements.length-i)).css('position', 'absolute').hide();
};
this.ifchanger = setTimeout(function() {
$.innerfade.next(elements, settings, 1, 0);
}, settings.timeout);
$(elements[0]).show();
}
};
$.innerfade.next = function(elements, settings, current, last) {
$(elements[last]).fadeOut(settings.speed);
$(elements[current]).fadeIn(settings.speed, function() {
removeFilter($(this)[0]);
});
if ((current + 1) < elements.length) {
current = current + 1;
last = current - 1;
} else {
current = 0;
last = elements.length - 1;
}
this.ifchanger = setTimeout((function() {
$.innerfade.next(elements, settings, current, last);
}), settings.timeout);
};
})(jQuery);
// **** remove Opacity-Filter in ie ****
function removeFilter(element) {
if(element.style.removeAttribute){
element.style.removeAttribute('filter');
}
}
jQuery(document).ready(function() {
jQuery('ul#theFeature').innerfade({
speed: 1000,
timeout: 7000,
containerheight: '291px'
});
// jQuery('#mainFeature .links').children('li').children('a').attr('href', 'javascript:void(0);');
jQuery('#mainFeature .links').children('li').children('a').click(function() {
clearTimeout(jQuery.innerfade.ifchanger);
for(i=1;i<5;i++) {
jQuery('#the'+i+'feature').css("display", "none");
//jQuery('#the'+i+'title').children('a').css("background-color","#226478");
}
// if(the_widths[(jQuery(this).attr('rel')-1)]==960) {
// jQuery("#vic").hide();
// } else {
// jQuery("#vic").show();
// }
// jQuery('#the'+(jQuery(this).attr('rel'))+'title').css("background-color", "#286a7f");
jQuery('#the'+(jQuery(this).attr('rel'))+'feature').css("display", "block");
clearTimeout(jQuery.innerfade.ifchanger);
});
});
And the separate JavaScript that I created:
function promo1on() {document.getElementById("promo1").className="promo1on"; document.getElementById("promo2").className="promo2"; document.getElementById("promo2").className="promo2"; }
function promo2on() {document.getElementById("promo2").className="promo2on"; document.getElementById("promo1").className="promo1"; document.getElementById("promo3").className="promo3"; }
function promo3on() {document.getElementById("promo3").className="promo3on"; document.getElementById("promo1").className="promo1"; document.getElementById("promo2").className="promo2"; }
And, finally, the CSS:
#mainFeature {float: left; width: 672px; height: 290px; margin: 0 0 9px 0; list-style: none;}
#mainFeature li {list-style: none;}
#mainFeature #theFeature {margin: 0; padding: 0; position: relative;}
#mainFeature #theFeature li {position: absolute; top: 0; left: 0;}
#promonav-con {width: 463px; height: 26px; padding: 0; margin: 0; position: absolute; z-index: 900; top: 407px; left: 283px;}
#primarypromonav {padding: 0; margin: 0;}
#mainFeature .links {padding: 0; margin: 0; list-style: none; position: relative; font-family: arial, verdana, sans-serif; width: 463px; height: 26px;}
#mainFeature .links li.promotop {list-style: none; display: block; float: left; display: inline; margin: 0; padding: 0;}
#mainFeature .links li a {display: block; float: left; display: inline; height: 26px; text-decoration: none; margin: 0; padding: 0; cursor: pointer;}
#mainFeature .links li a strong {margin-left: -9999px;}
#mainFeature .links li a.promo1 {background: url(../_images/carouselnav/promo1.gif); width: 155px;}
#mainFeature .links li:hover a.promo1 {background: url(../_images/carouselnav/promo1_hover.gif); width: 155px;}
#mainFeature .links li a.promo1:hover {background: url(../_images/carouselnav/promo1_hover.gif); width: 155px;}
.promo1on {background: url(../_images/carouselnav/promo1_on.gif); width: 155px;}
#mainFeature .links li a.promo2 {background: url(../_images/carouselnav/promo2.gif); width: 153px;}
#mainFeature .links li:hover a.promo2 {background: url(../_images/carouselnav/promo2_hover.gif); width: 153px;}
#mainFeature .links li a.promo2:hover {background: url(../_images/carouselnav/promo2_hover.gif); width: 153px;}
.promo2on {background: url(../_images/carouselnav/promo2_on.gif); width: 153px;}
#mainFeature .links li a.promo3 {background: url(../_images/carouselnav/promo3.gif); width: 155px;}
#mainFeature .links li:hover a.promo3 {background: url(../_images/carouselnav/promo3_hover.gif); width: 155px;}
#mainFeature .links li a.promo3:hover {background: url(../_images/carouselnav/promo3_hover.gif); width: 155px;}
.promo3on {background: url(../_images/carouselnav/promo3_on.gif); width: 155px;}
Hopefully this makes sense! Again, I'm very new to JavaScript/JQuery, so I apologize if this is a mess. I'm very grateful for any suggestions. Thanks!
The JavasScript that I created does what I want it to do, i.e. it causes the navigation buttons to change states appropriately, displaying different images for "default/off", "hover", and "on". What I can't figure out how to do is create a "link" between jquery.innerfade.js (which I didn't create and, sadly, don't understand very well) and the JavaScript that I wrote. Ideally, as long as the first promo image ("_images/carousel/promo1.jpg") is displaying via jquery.innerfade.js, the first promo navigation button ("function promo1on()") would display in the "on" state.
To give an idea of the result that I want, take a look at the Martha Stewart site:
http://www.marthastewart.com/
I am trying to recreate a slideshow like that, only using JavaScript and CSS instead of Flash. Hope that makes sense! Thanks!!!
Katie

Categories