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

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>

Related

How to make each current element in loop centered in container

As the container slides to the left, I want each tab to be centered for a brief moment in the container.
Before the animation begins, I want the very first tab to be centered in the container on page load before the animation begins. I know I need to divide each element's offsetWidth by 2 so that it is calculated to be centered but I'm not sure where/how to do that.
Link to JS Fiddle: https://jsfiddle.net/nb5a92x3/
window.onload = function() {
const tabsContainer = document.querySelector('.tabsContainer');
const tabs = tabsContainer.querySelectorAll('.tab');
function animate(target) {
let siblings = [];
let elm = target;
let keyframes = [];
let offset;
// Push the widths of each previous element to an array
while (elm = elm.previousElementSibling) {
siblings.push(elm.offsetWidth);
}
// Add the sum of all previous elements
offset = siblings.reduce((prev, current) => prev + current, 0);
// Animate the container to the left
setTimeout(function() {
tabsContainer.style.left = `calc(50% - ${offset}px)`;
}, i * 800);
}
// Run the code
for (i = 0; i < tabs.length; i++) {
animate(tabs[i]);
}
}
body {
background: #ccc;
}
section {
text-align: center;
width: 375px;
background: white;
margin: 0 auto;
padding: 16px;
position: relative;
display: flex;
overflow: hidden;
}
.tabsContainer {
display: flex;
position: relative;
margin: 0;
padding: 0;
list-style: none;
gap: 0 16px;
transition: left 0.2s ease;
.tab {
border-radius: 25px;
background: gray;
text-align: center;
height: 34px;
display: inline-flex;
align-items: center;
padding: 0 16px;
}
}
<section>
<ul class="tabsContainer">
<li class="tab">TAB ONE</li>
<li class="tab">ANOTHER TAB HERE</li>
<li class="tab">A REALLY LONG TAB HERE</li>
<li class="tab">LAST TAB</li>
</ul>
</section>
If you set in CSS your container to margin-left: 50%
than all you need is x = EL_curr.offsetLeft + EL_curr.offsetWidth / 2
and apply that x value to the slider-container CSS translateX (which is far more performant than animating the CSS left property, since transform can be hardware accelerated).
// Utility functions:
const EL = (sel, par) => (par || document).querySelector(sel);
const ELS = (sel, par) => (par || document).querySelectorAll(sel);
// Tabs animator:
const tabsAnimator = (EL_slider) => {
const ELS_items = ELS(".tab", EL_slider);
const tot = ELS_items.length;
let curr = 0;
let tOut;
const anim = () => {
const EL_curr = ELS_items[curr];
const x = EL_curr.offsetLeft + EL_curr.offsetWidth / 2;
EL_slider.style.transform = `translateX(-${x}px)`;
curr += 1; // Increment counter
if (curr > tot - 1) curr = 0; // Reset counter
tOut = setTimeout(anim, 1500);
};
anim(); // Init!
};
ELS(".tabsContainer").forEach(tabsAnimator);
* {
margin: 0;
box-sizing: border-box;
}
.tabsWrapper {
width: 375px;
margin: 20px auto;
padding: 16px 0;
position: relative;
overflow: hidden;
background: #ccc;
border-radius: 3em;
}
.tabsContainer {
position: relative;
padding: 0;
display: inline-flex;
list-style: none;
gap: 0 15px;
transition: transform 0.3s;
margin-left: 50%;
}
.tabsContainer .tab {
border-radius: 25px;
background: gold;
white-space: nowrap;
padding: 5px 15px;
}
<section class="tabsWrapper">
<ul class="tabsContainer">
<li class="tab">5 OFF FRIDAY</li>
<li class="tab">10 OFFSUMMER</li>
<li class="tab">SAVE MORE</li>
<li class="tab">HALF OFF EVERYTHING</li>
</ul>
</section>
<section class="tabsWrapper">
<ul class="tabsContainer">
<li class="tab">JavaScript</li>
<li class="tab">HTML</li>
<li class="tab">CSS</li>
</ul>
</section>
don't use horizontal paddings on the wrapper or container slider.
don't just use section as a selector. It's too common. Use rather a specific class like .tabsWrapper on the wrapper parent.
Additionally, if you'd like to pause the animation on "mouseenter", use clearTimeout(tOut); — and just anim() in the "mouseleave" Event.

solution for use button with HREF scrolling or modify <a> link to a button

i have 5 button i wanna use them to scroll to section
for the first i make button, i customize button with css , and after when i try to make a scroll , i got problem, dosent work
i try to use a link , the problem is setting button on css dont work very good , the text is on TOP left
i try to use now on css scroll slow with JS ? or HTML with #link
thanks
HTML ( many version of boutton i make for u ONLY FORT TESTING )
<section id="jour">
<div class="container20">
Dimanche
<button onclick="window.location.href='#Lundi'"class="btn1">Lundi</br><b>13</b></button>
<button onclick="window.location.href='#Mardi'"class="btn1">Mardi</br><b>12</b></button>
<button onclick="scrollTo.location.href='#Mercredi'"class="btn1">Mercredi</br><b>13</b></button>
<button onclick="scrollTo.location.href='#Jeudi'"class="btn1">Jeudi</br><b>14</b></button>
<button class="btn1">Dimanche</br><b>12</b></button>
</div>
</section>
CSS :
.container20 {
display: flex;
justify-content: center;
}
.btn1 {
display: block;
height: 80px;
width: 80px;
color : #262552;
font-family: "Montserrat", sans-serif;
font-weight: 900;
font-size: 12px;
letter-spacing: 0.6px;
padding: 1px 1px;
border-radius: 3px;
transition: 0.3s;
margin: 30px 1px ;
background: #9FEDD7;
border: 3px solid #f7ee6f;
}
#jour .btn1:hover {
background-color:#f7ee6f;
border: 2px solid #f7ee6f;
color : #262552 ;
}
// Cache selectors
var lastId,
topMenu = $("#top-menu"),
topMenuHeight = topMenu.outerHeight()+15,
// All list items
menuItems = topMenu.find("a"),
// Anchors corresponding to menu items
scrollItems = menuItems.map(function(){
var item = $($(this).attr("href"));
if (item.length) { return item; }
});
// Bind click handler to menu items
// so we can get a fancy scroll animation
menuItems.click(function(e){
var href = $(this).attr("href"),
offsetTop = href === "#" ? 0 : $(href).offset().top-topMenuHeight+1;
$('html, body').stop().animate({
scrollTop: offsetTop
}, 300);
e.preventDefault();
});
// Bind to scroll
$(window).scroll(function(){
// Get container scroll position
var fromTop = $(this).scrollTop()+topMenuHeight;
// Get id of current scroll item
var cur = scrollItems.map(function(){
if ($(this).offset().top < fromTop)
return this;
});
// Get the id of the current element
cur = cur[cur.length-1];
var id = cur && cur.length ? cur[0].id : "";
if (lastId !== id) {
lastId = id;
// Set/remove active class
menuItems
.parent().removeClass("active")
.end().filter("[href='#"+id+"']").parent().addClass("active");
}
});
body {
height: 6000px;
font-family: Helvetica, Arial;
}
#top-menu {
position: fixed;
z-index: 1;
background: white;
left: 0;
right: 0;
top: 0;
}
#top-menu li {
float: left;
}
#top-menu a {
display: block;
padding: 5px 25px 7px 25px;
width: 4em;
text-align: center;
-webkit-transition: .5s all ease-out;
-moz-transition: .5s all ease-out;
transition: .5s all ease-out;
border-top: 3px solid white;
color: #aaa;
text-decoration: none;
}
#top-menu a:hover {
color: #000;
}
#top-menu li.active a {
border-top: 3px solid #333;
color: #333;
}
#foo {
position: absolute;
top: 400px;
}
#bar {
position: absolute;
top: 800px;
}
#baz {
position: absolute;
top: 1200px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js"></script>
<ul id="top-menu">
<li class="active">
Top
</li>
<li>
Foo
</li>
<li>
Bar
</li>
<li>
Baz
</li>
</ul>
<a id="foo">Foo</a>
<a id="bar">Bar</a>
<a id="baz">Baz</a>

Translating jQuery to Vanilla JS issue

I was watching a tutorial that used jQuery and wanted to turn it into JS, but my code is broken - was hoping someone could help me with this:
Tutorial JS:
$(function() {
var btn = $('button');
var progressBar = $('.progressbar');
btn.click(function() {
progressBar.find('li.active').next().addClass('active');
})
})
Taken from URL:http://www.kodhus.com/kodity/codify/kod/mGXAtb
Here is my failed attempt at rewriting the jQuery using JavaScript DOM:
var btn1 = document.getElementsByTagName('BUTTON');
var progBar = document.getElementsByClassName('progressbar');
function clickMe1() {
var elm = progBar.querySelectorAll("li");
var emlClass = elm.querySelector(".active");
return emlClass.nextElementSibling.addClass('active');
}
btn1.addEventListener("click", clickMe1, false);
where did I go wrong?
Working fiddle.
Your code will work after several changes check the notes below :
You've missed addClass() there it's a jQuery function, for vanilla JS use .classList.add() instead:
return emlClass.nextElementSibling.classList.add("active");
querySelectorAll(); will return a list of nodes you have to loop through them and add class, use :
var emlClass = progBar.querySelectorAll("li.active");
Instead of :
var elm = progBar.querySelectorAll("li");
var emlClass = elm.querySelector(".active");
Then loop and add active class:
for(var i=0;i<emlClass.length;i++){
emlClass[i].nextElementSibling.classList.add("active");
}
getElementsByTagName() and getElementsByClassName() will also returns a list of nodes with given name, you have to specify which one you want to pick (selecting the first in my example) :
var btn1 = document.getElementsByTagName('BUTTON')[0];
var progBar = document.getElementsByClassName('progressbar')[0];
Hope this helps.
var btn1 = document.getElementsByTagName('BUTTON')[0];
var progBar = document.getElementsByClassName('progressbar')[0];
function clickMe1() {
var emlClass = progBar.querySelectorAll("li.active");
for(var i=0;i<emlClass.length;i++){
emlClass[i].nextElementSibling.classList.add("active");
}
}
btn1.addEventListener("click", clickMe1, false);
.container {
width: 100%;
}
.progressbar {
counter-reset: step;
margin: 0;
margin-top: 50px;
padding: 0;
}
.progressbar li {
list-style-type: none;
float: left;
width: 33.33%;
position: relative;
text-align: center;
}
.progressbar li:before {
content: counter(step);
counter-increment: step;
width: 30px;
height: 30px;
line-height: 30px;
border: 2px solid #ddd;
display: block;
text-align: center;
margin: 0 auto 10px auto;
border-radius: 50%;
background-color: white;
}
.progressbar li:after {
content: '';
position: absolute;
width: 100%;
height: 2px;
background-color: #ddd;
top: 15px;
left: -50%;
z-index: -1;
}
.progressbar li:first-child:after {
content: none;
}
.progressbar li.active {
color: green;
}
.progressbar li.active:before {
border-color: green;
}
.progressbar li.active + li:after {
background-color: green;
}
button {
position: relative;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 2px;
left: 50%;
margin-top: 30px;
transform: translate(-50%);
cursor: pointer;
outline: none;
}
button:hover {
opacity: 0.8;
}
<div class="container">
<ul class="progressbar">
<li class="active">Step 1</li>
<li>Step 2</li>
<li>Step 3</li>
</ul>
</div>
<button>Next step</button>
.querySelectorAll("li") will return an array (or an array-like object) with one or more <li> tags. So you need to either:
loop through every <li> in that list and do the rest,
or just take the first item from that list if you don't want to worry about there being more than one li in the page,
or use .querySelector (not .querySelectorAll) to just take the first <li> for you.
MDN

Move to specific div based on button click

I was trying to move the divs (here it's question number) based on the prev and next button. So that the selected question is always visible on screen.
Here is the demo : http://jsfiddle.net/arunslb123/trxe4n3u/12/
Screen :
click and question number and click prev or next button to understand my issue.
My code :
$("#next")
.click(function () {
$(".c.current-question")
.each(function () {
var divIdx = $(this)
.attr('id');
var scrollTo = $('#' + divIdx)
.position()
.left;
$("#scrollquestion")
.animate({
'scrollLeft': scrollTo
}, 800);
});
});
$("#prev")
.click(function () {
$(".c.current-question")
.each(function () {
var divIdx = $(this)
.attr('id');
var scrollTo = $('#' + divIdx)
.position()
.left;
$("#scrollquestion")
.animate({
'scrollLeft': -scrollTo
}, 800);
});
});
Using scrollLeft is a bit tricky. I did a small redo of your use-case based on positioning and then moving it based on left of the container. The tricky part is to reliably calculate the negative position when scrolled to the extreme right. Also, need to take into account the widths and margins.
Check the below snippet:
var $wrap = $("#numWrap"), $strip = $("#strip"),
$leftArrow = $(".wrapper > .arrows").first(),
wrapWidth = $wrap.width() + $leftArrow.width(),
margin = 10;
fill(20); select($(".numberItem").first());
$strip.on("click", ".numberItem", function() { select($(this)); });
function select($elem) {
$(".numberItem").removeClass("selected");
$elem.addClass("visited").addClass("selected");
focus($elem[0]);
}
function focus(elem) {
var stripPos = $strip.position(),
numPos = $(elem).offset(),
elemWidth = $(elem).width() + margin,
numRight = numPos.left + elemWidth;
if (numRight > wrapWidth) {
$strip.css({"left": stripPos.left - elemWidth});
}
if (numPos.left < (margin + $leftArrow.width())) {
$strip.css({"left": stripPos.left + elemWidth});
}
}
$(".wrapper").on("click", "a.arrow", function() {
var stripPos = $strip.position();
if (this.id == "lft") {
$strip.css({"left": stripPos.left + (wrapWidth / 2)});
} else {
$strip.css({"left": stripPos.left - (wrapWidth / 2)});
}
});
$(".controls").on("click", "a.arrow", function() {
var $sel = $(".selected"), numPos, $sel, elemWidth;
$elem = $sel.length > 0 ? $sel.first() : $(".numberItem").first();
if (this.id == "lft") {
$sel = $elem.prev().length > 0 ? $elem.prev() : $elem;
select($sel);
} else {
$sel = $elem.next().length > 0 ? $elem.next() : $elem;
select($sel);
}
numPos = $sel.offset(); elemWidth = $sel.width() + margin;
numRight = numPos.left + elemWidth;
if (numPos.left > wrapWidth) {
$strip.css({"left": -($sel.text()) * $sel.width() });
}
if (numRight < 0) {
$strip.css({"left": +($sel.text()) * $sel.width() });
}
});
function fill(num){
for (var i = 1; i <= num; i++) {
var $d = $("<a href='#' class='numberItem'>" + i + "</a>");
$strip.append($d);
}
}
* { box-sizing: border-box; padding: 0; margin: 0; font-family: sans-serif; }
div.wrapper {
background-color: #ddd; width: 100vw; height: 64px;
clear: both; overflow: hidden; margin-top: 16px;
}
div.arrows {
float: left; width: 10%; min-width: 24px; height: 64px; line-height: 64px;
text-align: center; vertical-align: middle; overflow: hidden;
}
div.numWrap {
float: left; height: 64px; line-height: 64px;
width: 80%; vertical-align: middle;
overflow: hidden; position: relative;
}
div.strip {
position: absolute; left: 0px;
width: auto; white-space: nowrap;
transition: left 1s;
}
a.numberItem {
display: inline-block; text-align: center; margin: 0px 8px;
background-color: #fff; border-radius: 50%; width: 48px; height: 48px;
font-size: 1.2em; line-height: 48px; text-decoration: none;
}
a.numberItem.visited { background-color: #fff; color: #000; border: 2px solid #01aebc; }
a.numberItem.selected { background-color: #01aebc; color: #fff; }
div.controls { clear: both; }
div.controls > div.arrows { width: auto; margin: 0 12px; }
a, a:focus, a:active, a:link, a:visited {
display: inline-block;
text-decoration: none; font-weight: 600;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<div class="arrows">
<a id="lft" class="arrow" href="#">〈</a>
</div>
<div id="numWrap" class="numWrap">
<div id="strip" class="strip"></div>
</div>
<div class="arrows">
<a id="rgt" class="arrow" href="#">〉</a>
</div>
</div>
<div class="controls">
<div class="arrows">
<a id="lft" class="arrow" href="#">〈 Previous</a>
</div>
<div class="arrows">
<a id="rgt" class="arrow" href="#">Next 〉</a>
</div>
<div>
Explanation:
Using absolute positioning on the number container, which is nested to get 100% width.
Markup:
<div class="wrapper">
<div class="arrows"><a id="lft" class="arrow" href="#">〈</a></div>
<div id="numWrap" class="numWrap">
<div id="strip" class="strip"></div> <!-- nesting here -->
</div>
<div class="arrows"><a id="rgt" class="arrow" href="#">〉</a></div>
</div>
CSS:
div.wrapper {
background-color: #ddd; width: 100vw; height: 64px;
clear: both; overflow: hidden; margin-top: 16px;
}
div.arrows {
float: left; width: 10%; min-width: 24px; height: 64px; line-height: 64px;
text-align: center; vertical-align: middle; overflow: hidden;
}
div.numWrap {
float: left; height: 64px; line-height: 64px;
width: 80%; vertical-align: middle;
overflow: hidden; position: relative; /* relatively positioned */
}
div.strip {
position: absolute; left: 0px; /* absolutely positioned */
width: auto; white-space: nowrap;
transition: left 1s; /* instead of jquery animate */
}
With this structure, we can now use left to control the scrolling.
For partially obscured numbers, try to gently focus-in (nudge into view) a number which is partially obscured. This can be done by checking the position relative to parent and adding the width/margin to it and also accounting for width of the left arrow (it might peep thru).
Javascript:
function focus(elem) {
var stripPos = $strip.position(),
numPos = $(elem).offset(),
elemWidth = $(elem).width() + margin,
numRight = numPos.left + elemWidth;
// if it is towards right side, nudge it back inside
if (numRight > wrapWidth) {
$strip.css({"left": stripPos.left - elemWidth});
}
// if it is towards left side, nudge it back inside
if (numPos.left < (margin + $leftArrow.width())) {
$strip.css({"left": stripPos.left + elemWidth});
}
}
Once the user has scrolled the list too far and then tries to click on previous / next buttons to select a question, then we need to move the entire container upto the selected number. We can easily do this by multiplying the question number with element width and then changing the left in positive (if towards right) or in negative (if towards left).
Javascript:
// if left of element is more than the width of parent
if (numPos.left > wrapWidth) {
$strip.css({"left": -($sel.text()) * $sel.width() });
}
// if right of element is less than 0 i.e. starting position
if (numRight < 0) {
$strip.css({"left": +($sel.text()) * $sel.width() });
}
Here is a fiddle to play with: http://jsfiddle.net/abhitalks/aw166qhx/
You will need to further adapt it to your use-case, but you get the idea.

adding more button for list in responsive navigation

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();
});

Categories