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
Related
I am resizing and positioning a box using the mousemove event. Those. i change transform translate and width (height) with pageX (pageY). But due to the fact that the mouse event mousemove does not always have time to be processed (for example, if you move the mouse quickly) or does not have time to read conditions, the block goes out of bounds.
Question: what do I need to do in this case so that the block does not go beyond the boundaries?
This is how it looks roughly. Those. in this example, the second_block is outside the first_block (500px), i.e. it does not have time to read the condition. How should this issue be resolved? Also for convenience https://jsfiddle.net/ManuOP/t1r4szdx/3/
<div id="first_block" class="first_block">
<div id="auxiliary_block">
<div id="second_block" class="second_block"></div>
<input id="point" class="point" name="name_point" type="button">
</div>
</div>
<script src="1.block_in_center_question.js"></script>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
div {
display: flex;
flex-direction: column;
}
div.first_block {
height: 300px;
width: 500px;
background: green;
}
div#auxiliary_block {
position: absolute;
}
div.second_block {
height: 200px;
width: 300px;
background: orange;
}
input.point {
position: absolute;
cursor: pointer;
height: 14px;
width: 14px;
border: none;
background: black;
right: -7px;
top: 50%;
}
"use strict";
let second_block = document.getElementById('second_block');
let point = document.getElementById('point');
function change_second_block() {
if(second_block.clientWidth < 500) {
second_block.style.width = `${start_x + event.pageX}px`;
}
}
point.addEventListener('mousedown', (event) => {
window.start_x = second_block.clientWidth - event.pageX;
document.addEventListener('mousemove', change_second_block);
});
You could just test the new width and if it's too large then constrain it to be no more than the maximum.
This snippet does this for the x direction and forces it to remain at or below 500px.
"use strict";
let second_block = document.getElementById('second_block');
let point = document.getElementById('point');
function change_second_block() {
if (second_block.clientWidth < 500) {
second_block.style.width = (start_x + event.pageX) < 500 ? `${start_x + event.pageX}px` : '500px';
}
}
point.addEventListener('mousedown', (event) => {
window.start_x = second_block.clientWidth - event.pageX;
document.addEventListener('mousemove', change_second_block);
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
div {
display: flex;
flex-direction: column;
}
div.first_block {
height: 300px;
width: 500px;
background: green;
}
div#auxiliary_block {
position: absolute;
}
div.second_block {
height: 200px;
width: 300px;
background: orange;
}
input.point {
position: absolute;
cursor: pointer;
height: 14px;
width: 14px;
border: none;
background: black;
right: -7px;
top: 50%;
}
<div id="first_block" class="first_block">
<div id="auxiliary_block">
<div id="second_block" class="second_block"></div>
<input id="point" class="point" name="name_point" type="button">
</div>
</div>
You can work around this issue if the size of the change in the item is above a certain limit, or by checking the limit and stopping the update. I prevented the overflow caused by rapid mouse movement by updating its code as follows:
function change_second_block()
{
console.log("Event.PageX: " + event.pageX);
if(event.pageX < 500 )
{
if(second_block.clientWidth < 500)
{
second_block.style.width = `${start_x + event.pageX}px`;
}
}
}
References
Javascript mouse event not captured properly when mouse moved very fast
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'm trying to put go-to-top button in the bottom right angle of the screen. It should appear on scroll function, a disappear when I go back to top.
The button exists, but when I scroll down, it stays with "home page", so as I scroll more, it is not visible anymore. How to fix the problem? You can see my codes down here. Thanks a lot in advance!
window.onscroll = function(){goTop()};
let goTop = function() {
var rocket = document.querySelector(".go-to-top");
var scrollExt = document.body.scrollTop;
if(document.body.scrollTop > 500 || document.documentElement.scrollTop > 500){
rocket.style.display = "block";
} else{
rocket.style.position = "none";
}
};
let rocketClick = function() {
document.documentElement.scrollTop = 0;
}
.go-to-top{
display: none;
z-index: 10;
position: fixed;
bottom: 40px;
right: 40px;
width: 40px;
height: 40px;
transform: rotate(-45deg);
color: black;
padding: 10px;
text-decoration: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.go-to-top i{
font-size: 50px;
}
.go-to-top:hover{
cursor: pointer;
text-decoration: none;
}
<!--the rest of markup-->
<div class="rocket">
<a href="#" class="go-to-top">
<i class="fas fa-rocket"></i>
</a>
</div>
<script src="script.js"></script>
<!--closing markup-->
rocket.style.position:none would give your element the css style of position:none. none is not a valid value for the position property.
You can see the position values here -> CSS position
By the look of your code you would need to use display instead of position.
Also, you make a variable scrollExt and you do not use it. Plus, you make a rocketClick function but you do not call it on your element.
window.onscroll = function() {
goTop();
};
const goTop = function() {
const rocket = document.querySelector('.go-to-top');
const scrollExt = document.body.scrollTop;
if (scrollExt > 500 || document.documentElement.scrollTop > 500) {
rocket.style.display = 'block';
} else {
rocket.style.display = 'none';
}
};
const rocketClick = function() {
document.documentElement.scrollTop = 0;
};
main {
height:1500px;
background:red;
}
.go-to-top{
display: none;
z-index: 10;
position: fixed;
bottom: 40px;
right: 40px;
width: 40px;
height: 40px;
transform: rotate(-45deg);
color: black;
padding: 10px;
text-decoration: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.go-to-top i{
font-size: 50px;
}
.go-to-top:hover{
cursor: pointer;
text-decoration: none;
}
<main></main>
<div class="rocket" onclick=rocketClick()>
<a href="#" class="go-to-top">
rocket icon
</a>
</div>
Suggestions :
Keep using let and const. Do not use var. Also, use const for your functions. You do not change the content of the functions anywhere so you can use const instead of let.
Add an animation(transition) to the scrollTop.
In the last few weeks Google show a carousel for Stack Overflow result, It's awesome and move smooth but weird for me, there is no JavaScript DOM changing and even CSS that cause to horizontal scroll, I cannot find it out.
Even I read about CSS Horizontal Scroll but it is so different and it is just for Google Chrome in other browsers it doesn't exist.
After some searches and experiments I found out this weird carousel is actually a long horizontal division with display: none scroll bar, but how with grab and moving mouse pointer, the division scroll moving? is that a native chrome trick? or just use JavaScript to calculate the motion of horizontal scroll?
Actually, I have found the answer to my question, but I decided to post it here now. Google website developers use many sexy tricks to implement the UI and this carousel is one of them, definitely, this is not DOM manipulation, it's a simple scrolled division. Because of this, the division moves very smoothly and even with dragging move properly.
A scroll has some native behavior in some devices like Apple devices or other touch devices, even Microsoft new laptops have some features about scrolling by touch. but if we use a few JavaScript codes to handle dragging it will be nice, see following code:
HINT: You can use your native device horizontal scrolling features like the two-finger horizontal scroll on MacBook trackpad OR using click and drag to move the carousel horizontally
var slider = document.querySelector('.items');
var isDown = false;
var startX;
var scrollLeft;
slider.addEventListener('mousedown', function (e) {
isDown = true;
slider.classList.add('active');
startX = e.pageX - slider.offsetLeft;
scrollLeft = slider.scrollLeft;
});
slider.addEventListener('mouseleave', function () {
isDown = false;
slider.classList.remove('active');
});
slider.addEventListener('mouseup', function () {
isDown = false;
slider.classList.remove('active');
});
slider.addEventListener('mousemove', function (e) {
if (!isDown) return;
e.preventDefault();
var x = e.pageX - slider.offsetLeft;
var walk = (x - startX) * 3; //scroll-fast
slider.scrollLeft = scrollLeft - walk;
});
#import url(https://fonts.googleapis.com/css?family=Rubik);
body,
html {
color: #fff;
text-align: center;
background: #efefef;
font-family: Helvetica, sans-serif;
margin: 0;
}
.grid-container {
background: #efefef;
font-family: 'Rubik', sans-serif;
}
#supports (display: grid) {
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas: "header header header" "title title footer" "main main main";
}
#media screen and (max-width: 500px) {
.grid-container {
grid-template-columns: 1fr;
grid-template-rows: 0.3fr 1fr auto 1fr;
grid-template-areas: "header" "title" "main" "footer";
}
}
.grid-item {
color: #fff;
background: skyblue;
padding: 3.5em 1em;
font-size: 1em;
font-weight: 700;
}
.header {
background-color: #092a37;
grid-area: header;
padding: 1em;
}
.title {
color: #555;
background-color: #f4fbfd;
grid-area: title;
}
.main {
color: #959595;
background-color: white;
grid-area: main;
padding: 0;
overflow-x: scroll;
overflow-y: hidden;
}
.footer {
background-color: #5bbce4;
grid-area: footer;
padding: 0.6em;
}
.items {
position: relative;
width: 100%;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
transition: all 0.2s;
transform: scale(0.98);
will-change: transform;
user-select: none;
cursor: pointer;
}
.items.active {
background: rgba(255, 255, 255, 0.3);
cursor: grabbing;
cursor: -webkit-grabbing;
transform: scale(1);
}
.item {
display: inline-block;
background: skyblue;
min-height: 100px;
min-width: 400px;
margin: 2em 1em;
}
#media screen and (max-width: 500px) {
.item {
min-height: 100px;
min-width: 200px;
}
}
}
a {
display: block;
color: #c9e9f6;
text-decoration: underline;
margin: 1em auto;
}
a:hover {
cursor: pointer;
}
p {
text-align: left;
text-indent: 20px;
font-weight: 100;
}
i {
color: skyblue;
}
<div class="grid-container">
<main class="grid-item main">
<div class="items">
<div class="item item1"></div>
<div class="item item2"></div>
<div class="item item3"></div>
<div class="item item4"></div>
<div class="item item5"></div>
<div class="item item6"></div>
<div class="item item7"></div>
<div class="item item8"></div>
<div class="item item9"></div>
<div class="item item10"></div>
</div>
</main>
</div>
For ReactJs developer I created a good hook for supporting horizontal scroll in desktop:
import { useEffect } from 'react';
import type { MutableRefObject } from 'react';
const useHorizontalScroll = (
scrollWrapperRef: MutableRefObject<HTMLElement>,
scrollSpeed = 1
): void => {
useEffect(() => {
const horizWrapper = scrollWrapperRef.current;
let isDown = false;
let startX: number;
let scrollLeft: number;
horizWrapper?.addEventListener('mousedown', (e: any) => {
isDown = true;
startX = e.pageX - horizWrapper?.offsetLeft;
scrollLeft = horizWrapper?.scrollLeft;
});
horizWrapper?.addEventListener('mouseleave', () => {
isDown = false;
});
horizWrapper?.addEventListener('mouseup', () => {
isDown = false;
});
horizWrapper?.addEventListener('mousemove', (e: any) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - horizWrapper?.offsetLeft;
const walk = (x - startX) * scrollSpeed;
horizWrapper.scrollLeft = scrollLeft - walk;
});
}, [scrollSpeed, scrollWrapperRef]);
};
export default useHorizontalScroll;
I've tried to look for a solution for this but have failed miserably. It's my first ever time using JS (I'm trying to learn) so the possibility of my just not understanding the answers in the search results properly is quite high - sorry about that.
I am wanting a JS carousel, generated from an array, with Prev/Next buttons (ideally responsive etc but that'll come at a later stage), preferably with captions underneath. I can get the carousel to work but I end up getting a text link when I click on either Prev or Next. And I've no idea how to add the caption array underneath (I've taken out the JS for the captions for now because it was messing everything else up even further).
Relevant HTML:
<body onload="changePilt()">
<span id="prev" class="arrow">❮</span>
<div class="karussell" id="karussell">
<img class="karu" name="esislaid">
</div>
<span id="next" class="arrow">❯</span>
<div class="caption">
<h3 name="esikiri"></h3>
</div>
</body>
CSS, just in case:
.karussell {
position: relative;
width: 100%;
max-height: 600px;
overflow: hidden;
}
.arrow {
cursor: pointer;
position: absolute;
top: 40%;
width: auto;
color: #00A7E0;
margin-top: -22px;
padding: 16px;
font-weight: bold;
font-size: 18px;
transition: 0.6s ease;
border-radius: 0 3px 3px 0;
}
#next {
right: 0;
border-radius: 3px 0 0 3px;
}
#prev {
left: 0;
}
.arrow:hover {
background-color: rgba(0,0,0,0.8);
}
.caption {
text-align: center;
color: #00A7E0;
padding: 2px 16px;
}
.karu {
max-width: 75%;
}
#media (max-width:767px){.karu{max-width: 95%;}}
And finally, the dreaded JS:
var i = 0;
var s = 0;
var esileht = [];
var aeg = 5000;
//Image List
esileht[0] = 'img/tooted/raamat/graafvanalinn2016.jpg';
esileht[1] = 'img/tooted/kaart/kaart_taskus_esipool.jpg';
esileht[2] = 'img/tooted/kaart/graafkaart_esikylg.jpg';
//Change Image
function changePilt (){
document.esislaid.src = esileht[i];
if(i < esileht.length -1){
i++;
} else {
i = 0;
}
setTimeout("changePilt()", aeg);
}
document.onload = function() {
}
// Left and Right arrows
//J2rgmine
function jargmine(){
s = s + 1;
s = s % esileht.length;
return esileht [s];
}
//Eelmine
function eelmine(){
if (s === 0) {
s = esileht.length;
}
s = s -1;
return esileht[s];
}
document.getElementById('prev').addEventListener('click', function (e){
document.getElementById('karussell').innerHTML = eelmine();
}
);
document.getElementById('next').addEventListener('click', function (e) {
document.getElementById('karussell').innerHTML = jargmine();
}
);
I'm sure the solution is dreadfully obvious, I just cannot seem to be able to figure it out...
instead of innerHTML change src attribute of image
document.querySelector('#karussell img').src = eelmine();
And
document.querySelector('#karussell img').src = jargmine();