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.
Related
I have this navbar and everytime I click an option in the navbar the absolute positioned indicator gets the position of the option on the left and the width with the help of getBoundingClientRect() and it is moved to the target.
The problem is when I resize the window the indicator changes it's position and moves away.To stay in the same place when I resize the window I applied an eventListener to the window and everytime is resized I get the new values of left and width with getBoundingClientRect().
It works but I wonder if that is a bad way to do it because of the calculations that happen everytime the window is resized and if that is the case what is a better way to do this.
Here is the code:
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.getBoundingClientRect().left}px`;
navigator.style.width = `${firstOption.getBoundingClientRect().width}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.getBoundingClientRect().left}px`;
navigator.style.width = `${e.target.getBoundingClientRect().width}px`;
};
});
window.addEventListener('resize', function() {
let navOptionActive = nav.querySelector('.nav-option-active');
navigator.style.left = `${navOptionActive.getBoundingClientRect().left}px`;
navigator.style.width = `${navOptionActive.getBoundingClientRect().width}px`;
});
* {
margin: 0;
padding: 0;
}
nav {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin: 100px auto;
padding: 7vh 30vw;
width: auto;
background:#eeeeee;
}
.nav-option {
padding: 0 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
You can make your <nav> element tightly wrap the buttons, then position the underline relative to the <nav>. A new wrapper <div> around the <nav> takes care of the margins and gray background. Instead of getBoundingClientRect() you then need to use offsetLeft and offsetWidth.
Note that this doesn't handle the changes in response to your #media query. For that, you could add a resize listener that specifically only handles changes across the 1200px threshold. Alternatively, you could reparent the underline to be a child of the actual nav button while it's not animating. Neither solution is great, but both would get the job done.
const navigator = document.querySelector('.navigator');
const firstOption = document.querySelector('.first-option');
const navOptions = document.querySelectorAll('.nav-option');
const nav = document.querySelector('nav');
navigator.style.left = `${firstOption.offsetLeft}px`;
navigator.style.width = `${firstOption.offsetWidth}px`;
nav.addEventListener('click', function(e) {
if(e.target.classList.contains('nav-option')) {
navOptions.forEach(option => option.classList.remove('nav-option-active'));
e.target.classList.add('nav-option-active');
navigator.style.left = `${e.target.offsetLeft}px`;
navigator.style.width = `${e.target.offsetWidth}px`;
};
});
* {
margin: 0;
padding: 0;
}
.nav-wrapper {
margin: 100px 0;
display: flex;
justify-content: center;
background: #eeeeee;
}
nav {
position: relative;
display: flex;
}
.nav-option {
padding: 7vh 15px;
font-size: 22px;
cursor: pointer;
}
.navigator {
position: absolute;
left: 0;
bottom: 0;
height: 5px;
background: orangered;
transition: .4s ease all;
}
#media (max-width: 1200px) {
.nav-option {
font-size: 18px;
padding: 10px;
}
}
<div class="nav-wrapper">
<nav>
<div class="navigator"></div>
<div class="nav-option first-option nav-option-active">HOME</div>
<div class="nav-option">INFO</div>
<div class="nav-option">CONTACT</div>
<div class="nav-option">ABOUT</div>
<div class="nav-option">MENU</div>
</nav>
</div>
If you have to use getBoundingClientRect (which honestly has nothing wrong with it), you can throttle the call, so that only the last resize after sufficient time has passed will execute. There are zillion ways of doing this, I will leave one example:
window.onresize = (function(id = null, delay = 600, oEvent = null){
return function fire(event){
return (new Promise(function(res,rej){
if (id !== null){
oEvent = event;
rej("busy");
return;
}
id = setTimeout(function(){
res(oEvent || event);
},delay);
})).then(function(event){
id = null;
console.log(event, "do getBoundingClientRect call");
}).catch(function(){void(0);});
};
}());
Replace console.log with what you want to do.
Your other option is to switch to intersection observer, if you can restructure your rendering logic. That will require some work
I have a problem with my code, I'm trying to make my menu that contains list of items scrollable on button click (left and right buttons). The thing is after i click on right button, it works but it does not let me click it again....if i do it does nothing.So it goes once right and once left only. I want to be able to keep pressing it untill i reach the last item in the menu and vice versa.
My html code for the menu:
<div class="menu-wrapper">
<ul class="menu">
<li class="item active">Hair</li>
<li class="item">Massage</li>
<li class="item">Nails</li>
<li class="item">Facial</li>
<li class="item">Tattoo</li>
<li class="item">Institue</li>
<li class="item">Masking</li>
<li class="item">Doudou</li>
<li class="item">Facial</li>
<li class="item">Tattoo</li>
<li class="item">Institue</li>
<li class="item">Masking</li>
<li class="item">Doudou</li>
</ul>
<div class="paddles">
<button class="left-paddle paddle hidden">
<
</button>
<button class="right-paddle paddle">
>
</button>
</div>
</div>
My Css code is:
.menu-wrapper {
position: relative;
border-radius: 10px;
height: 50px;
margin: 1em auto;
overflow-x: hidden;
overflow-y: hidden;
}
.menu {
height: 50px;
background: white;
box-sizing: border-box;
padding-left: 0;
white-space: nowrap;
overflow-x: hidden;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
}
.item {
display: inline-block;
width: 155px;
height: auto;
text-align: center;
box-sizing: border-box;
padding: 10px;}
.item.active{color: white; background-color: #6f51ed;}
.item:hover{
cursor: pointer;
}
.paddle {
position: absolute;
top: 0;
bottom: 0;
width: 2em;
}
.left-paddle {
left: 0;
color:#6f51ed;
background-color: transparent;
border-color: transparent;
outline: none;
font-size: x-large;
padding-bottom: 5px;
}
.right-paddle {
right: 0;
color:#6f51ed;
background-color: transparent;
border-color: transparent;
outline: none;
font-size: x-large;
padding-bottom: 5px;
}
.hidden {
display: none;
}
My Jquery/Js code is:
<script>
var scrollDuration = 450;
// paddles
var leftPaddle = document.getElementsByClassName('left-paddle');
var rightPaddle = document.getElementsByClassName('right-paddle');
// get items dimensions
var itemsLength = $('.item').length;
var itemSize = $('.item').outerWidth(true);
// get some relevant size for the paddle triggering point
var paddleMargin = 5;
// get wrapper width
var getMenuWrapperSize = function() {
return $('.menu-wrapper').outerWidth();
}
var menuWrapperSize = getMenuWrapperSize();
// the wrapper is responsive
$(window).on('resize', function() {
menuWrapperSize = getMenuWrapperSize();
});
// size of the visible part of the menu is equal as the wrapper size
var menuVisibleSize = menuWrapperSize;
// get total width of all menu items
var getMenuSize = function() {
return itemsLength * itemSize;
};
var menuSize = getMenuSize();
// get how much of menu is invisible
var menuInvisibleSize = menuSize - menuWrapperSize;
// get how much have we scrolled to the left
var getMenuPosition = function() {
return $('.menu').scrollLeft();
};
// finally, what happens when we are actually scrolling the menu
$('.menu').on('scroll', function() {
// get how much of menu is invisible
menuInvisibleSize = menuSize - menuWrapperSize;
// get how much have we scrolled so far
var menuPosition = getMenuPosition();
var menuEndOffset = menuInvisibleSize - paddleMargin;
// show & hide the paddles
// depending on scroll position
if (menuPosition <= paddleMargin) {
$(leftPaddle).addClass('hidden');
$(rightPaddle).removeClass('hidden');
} else if (menuPosition < menuEndOffset) {
// show both paddles in the middle
$(leftPaddle).removeClass('hidden');
$(rightPaddle).removeClass('hidden');
} else if (menuPosition >= menuEndOffset) {
$(leftPaddle).removeClass('hidden');
$(rightPaddle).addClass('hidden');
}
});
// scroll to left
$(rightPaddle).on('click', function() {
$('.menu').animate( { scrollLeft: itemSize}, scrollDuration);
});
// scroll to right
$(leftPaddle).on('click', function() {
$('.menu').animate( { scrollLeft: -itemSize }, scrollDuration);
});
</script>
Because you are scrolling to the same position, you need to add or subtract the itemSize to the current scroll position of .menu.
// scroll to left
$(rightPaddle).on('click', function() {
$('.menu').animate({
scrollLeft: $('.menu').scrollLeft() + itemSize
}, scrollDuration);
});
// scroll to right
$(leftPaddle).on('click', function() {
$('.menu').animate({
scrollLeft: $('.menu').scrollLeft() - itemSize
}, scrollDuration);
});
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>
I tried to build a carousel with only Vanilla JavaScript but I encounter some issues along the way. The carousel should display 5 items on the page from a total of 10, and when the "next" button is clicked should slide 3 elements. The problem is that will continue to translateX after the last element in the slider was reached.
const carousel = document.querySelector("[data-target='carousel']");
const card = carousel.querySelector("[data-target='card']");
const leftButton = document.querySelector("[data-action='slideLeft']");
const rightButton = document.querySelector("[data-action='slideRight']");
const carouselWidth = carousel.offsetWidth;
const cardStyle = card.currentStyle || window.getComputedStyle(card);
const cardMarginRight = Number(cardStyle.marginRight.match(/\d+/g)[0]);
const cardCount = carousel.querySelectorAll("[data-target='card']").length;
let offset = 0;
const maxX = -((cardCount / 3) * carouselWidth + cardMarginRight * (cardCount / 3) - carouselWidth - cardMarginRight);
// click events
leftButton.addEventListener("click", function () {
if (offset !== 0) {
offset += carouselWidth + cardMarginRight;
carousel.style.transform = `translateX(${offset}px)`;
}
});
rightButton.addEventListener("click", function () {
if (offset !== maxX) {
offset -= carouselWidth + cardMarginRight;
carousel.style.transform = `translateX(${offset}px)`;
}
});
.wrapper {
height: 200px;
width: 100%;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.button-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
}
.carousel {
margin: 0;
padding: 0;
list-style: none;
width: 100%;
display: flex;
position: absolute;
left: 0;
transition: all 1s ease;
}
.card {
background: black;
min-width: 344px;
height: 200px;
margin-right: 24px;
display: inline-block;
color: white;
font-size: 45px;
display: grid;
place-items: center;
}
<div class="wrapper">
<ul class="carousel" data-target="carousel">
<li class="card" data-target="card">1</li>
<li class="card" data-target="card">2</li>
<li class="card" data-target="card">3</li>
<li class="card" data-target="card">4</li>
<li class="card" data-target="card">5</li>
<li class="card" data-target="card">6</li>
<li class="card" data-target="card">7</li>
<li class="card" data-target="card">8</li>
<li class="card" data-target="card">9</li>
<li class="card" data-target="card">10</li>
</ul>
</div>
<div class="button-wrapper">
<button data-action="slideLeft">left</button>
<button data-action="slideRight">right</button>
</div>
The problem is in this line, you should not set it to equal because if that's not equal it continues sliding
if (offset !== maxX)
const carousel = document.querySelector("[data-target='carousel']");
const card = carousel.querySelector("[data-target='card']");
const leftButton = document.querySelector("[data-action='slideLeft']");
const rightButton = document.querySelector("[data-action='slideRight']");
const carouselWidth = carousel.offsetWidth;
const cardStyle = card.currentStyle || window.getComputedStyle(card);
const cardMarginRight = Number(cardStyle.marginRight.match(/\d+/g)[0]);
const cardCount = carousel.querySelectorAll("[data-target='card']").length;
let offset = 0;
const maxX = -((cardCount / 3) * carouselWidth + cardMarginRight * (cardCount / 3) - carouselWidth - cardMarginRight);
// click events
leftButton.addEventListener("click", function () {
if (offset !== 0) {
offset += carouselWidth + cardMarginRight;
carousel.style.transform = `translateX(${offset}px)`;
}
});
rightButton.addEventListener("click", function () {
if (offset+500 > maxX) {
offset -= carouselWidth + cardMarginRight;
carousel.style.transform = `translateX(${offset}px)`;
}
});
.wrapper {
height: 200px;
width: 100%;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.button-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
}
.carousel {
margin: 0;
padding: 0;
list-style: none;
width: 100%;
display: flex;
position: absolute;
left: 0;
transition: all 1s ease;
}
.card {
background: black;
min-width: 344px;
height: 200px;
margin-right: 24px;
display: inline-block;
color: white;
font-size: 45px;
display: grid;
place-items: center;
}
<div class="wrapper">
<ul class="carousel" data-target="carousel">
<li class="card" data-target="card">1</li>
<li class="card" data-target="card">2</li>
<li class="card" data-target="card">3</li>
<li class="card" data-target="card">4</li>
<li class="card" data-target="card">5</li>
<li class="card" data-target="card">6</li>
<li class="card" data-target="card">7</li>
<li class="card" data-target="card">8</li>
<li class="card" data-target="card">9</li>
<li class="card" data-target="card">10</li>
</ul>
</div>
<div class="button-wrapper">
<button data-action="slideLeft">left</button>
<button data-action="slideRight">right</button>
</div>
I think the problem here is the more or less fixed width of each card which leads to different results on different screen sizes.
min-width: 344px;
Ive changed to (just for example)
min-width: 19%; <!-- you need 5 cards on screen -->
and in your JavaScript something like this:
// get card size
const cardWidth = card.offsetWidth;
//and later on e.g for button to the right do offset 3 cards
offset -= (cardWidth + cardMarginRight) * 3;
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();
});