I have an element with the name ".offer" class right above the footer inside the page. When I approach this element in the scroll movement, I want the sticky header to close.
I wrote a function in scroll event for this but I am curious about something.
When I create a function like the example below, are the variables in the function re-created in memory for each scroll move? Or what's the truth for performance in a simple page scroll event?
const stickyNearOfferClose = () => {
let scrollPos;
let header = document.querySelector("header");
const documentOffset = document.querySelector(".offer").offsetTop;
header.classList.toggle("sticky", window.scrollY);
scrollPos = window.scrollY;
if (scrollPos >= documentOffset) {
header.classList.add("hide");
} else {
header.classList.remove("hide");
}
};
window.addEventListener("scroll", function () {
stickyNearOfferClose();
});
In addition to the above question
When I want to use the above function for one or more scroll actions,
Should we use it define variables inside a function or within an object for both in terms of performance and usability?
I shared two different examples below. Which one do you think should be?
const Obj = {
scrollPos : null,
elHeader: document.querySelector("header"),
documentOffset: document.querySelector(".offer").offsetTop,
// otherfunction (){},
stickyNearOfferClose() {
Obj.elHeader.classList.toggle("sticky", window.scrollY);
if (Obj.scrollPos >= Obj.documentOffset) {
Obj.elHeader.classList.add("hide");
} else {
Obj.elHeader.classList.remove("hide");
}
},
// init(){}
};
window.addEventListener("scroll", function () {
Obj.scrollPos = window.scrollY;
requestAnimationFrame(Obj.stickyNearOfferClose);
});
// OR
const Obj = {
// variables
stickyNearOfferClose() {
let scrollPos;
const elHeader = document.querySelector("header");
const elOffer = document.querySelector(".offer");
const documentOffset = elOffer.offsetTop;
let stickyClose = () => {
elHeader.classList.toggle("sticky", window.scrollY);
if (scrollPos >= documentOffset) {
elHeader.classList.add("hide");
} else {
elHeader.classList.remove("hide");
}
};
window.addEventListener("scroll", () => {
scrollPos = window.scrollY;
requestAnimationFrame(stickyClose);
});
},
spyScrolling() {
let scrollPos;
const sections = document.querySelectorAll(".hero");
let scrollActiveUrl = () => {
for (let s in sections) {
if (
sections.hasOwnProperty(s) &&
sections[s].offsetTop <= scrollPos + 150
) {
const id = sections[s].id;
document.querySelector(".active").classList.remove("active");
document.querySelector(`a[href*=${id}]`).classList.add("active");
}
}
};
window.addEventListener("scroll", () => {
scrollPos = document.documentElement.scrollTop || document.body.scrollTop;
requestAnimationFrame(scrollActiveUrl);
});
}
init(){
this.stickyNearOfferClose();
this.spyScrolling()
}
};
Yes, the variables in the function will be recreated on every scroll events. But in your case, you can put some variables outside of the function to save a bit.
As per MDN, you shouldn't call expensive DOM manipulations directly in the function. Instead, it is recommended to throttle the event using requestAnimationFrame(), setTimeout(), or a CustomEvent. You can learn more at here.
Following is the improved example of your code:
let scrollPos;
const elHeader = document.querySelector("header");
const elOffer = document.querySelector(".offer");
// Note: I'm assuming that `documentOffset` don't need to be updated on every scroll events.
// Update it as `scrollPos` if it need to be updated on every scroll events.
const documentOffset = elOffter.offsetTop;;
const stickyNearOfferClose = () => {
// Note: Please move the following line in the scroll event
// if it's also needed to be called on every scroll events.
elHeader.classList.toggle("sticky", window.scrollY);
if (scrollPos >= documentOffset) {
elHeader.classList.add("hide");
} else {
elHeader.classList.remove("hide");
}
};
window.addEventListener("scroll", function () {
scrollPos = window.scrollY;
requestAnimationFrame(stickyNearOfferClose);
});
Related
I need to add positon: fixed to .box only if the user scrolls the page.
The important point here is that the element with the class .box get generated only after the user scrolls the page.
This is what I came up with:
window.addEventListener('scroll', () => {
const myDiv = document.querySelector('.box')
if (myDiv) {
if (myDiv.style.position !== 'fixed') {
myDiv.style.position = 'fixed'
}
}
})
The problem with my code is that the scroll event is now going to fire a million events all the time and kill performance.
What would be the right way to achieve the above without firing scroll event again and again.
After the box has been inserted into the DOM, you can query the box and add the fixed position style to it.
If you're doing this often then here is a more general function that will (given a selector, e.g. .box) wait for an element to be inserted into the DOM:
function waitForElement(selector) {
let element = null;
let handled = false;
let nextFrame;
return (callback) => {
function tick() {
element = document.querySelector(selector);
if (element === null) {
nextFrame = requestAnimationFrame(tick);
}
if (element !== null && !handled) {
handled = true;
callback(element);
}
}
cancelAnimationFrame(nextFrame);
requestAnimationFrame(tick);
};
}
Usage:
const waitForBox = waitForElement(".box");
waitForBox((box) => {
box.style.position = "fixed";
});
Demo:
// Demo section, ignore this
const plane = document.querySelector(".plane");
const box = document.createElement("div");
box.className = "box";
box.style.width = "100px";
box.style.height = "100px";
box.style.background = "crimson";
function waitForElement(selector) {
let element = null;
let handled = false;
let nextFrame;
return (callback) => {
function tick() {
element = document.querySelector(selector);
if (element === null) {
nextFrame = requestAnimationFrame(tick);
}
if (element !== null && !handled) {
handled = true;
callback(element);
}
}
cancelAnimationFrame(nextFrame);
requestAnimationFrame(tick);
};
}
// A function to call inside scroll callback that will
// wait for the .box element to be found in the DOM
const waitForBox = waitForElement(".box");
let boxAppended = false;
document.addEventListener("scroll", () => {
// Appending the box to the DOM, ignore this
if (!boxAppended) {
boxAppended = true;
plane.appendChild(box);
}
// Callback will be fired once when the .box
// element is found in the DOM
waitForBox((box) => {
box.style.position = "fixed";
});
});
body {
padding: 0;
height: 100vh;
overflow: auto;
}
.plane {
height: 200vh;
}
<div class="plane"></div>
You can for example use requestAnimationFrame() or setTimeout() to prevent the scroll event from firing too many times.
Here is an example of how to do this with requestAnimationFrame() from the Mozilla scroll event documentation:
let lastKnownScrollPosition = 0;
let ticking = false;
function doSomething(scrollPos) {
// Do something with the scroll position
}
document.addEventListener('scroll', function(e) {
lastKnownScrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
doSomething(lastKnownScrollPosition);
ticking = false;
});
ticking = true;
}
});
Let me preface by saying I am relatively green in respect to JavaScript. However, I am trying to create a JavaScript function that will listen for a wheel event, determine the direction of the scroll and scroll the next part of the page into view. Similar to a swipe gesture. I have it working in Firefox as intended, user scrolls down the page moves down, up the page moves up. However, in Chrome and Edge, the element.scrollIntoView() function seems to be called, but nothing happens.
I am using Blazor with JSInterop to get the page to scroll.
Here is the gist of the file:
// scrolldetection.js //
window.scrolldetection {
scrollSetup: function (elementId) {
autoscroller(elementId);
}
}
function autoscroller(elementId) {
// Set up vars
var idList = document.getElementById(elementId).children.id;
var idListIndex = // Gets current index based on current location on page
var curDirection;
document.addEventListener("wheel", function () {
var currentPos = window.scrollY;
if (!curDirection) {
// Check for what direction user scrolls
idListIndex = // Function that determines new index
scrollScreen(idList, idListIndex);
}
var timerId = setTimeout(function () {
curDirection = undefined;
clearTimeout(timerId);
}, 700);
}
}
function scrollScreen(idList, curIndex) {
console.log("list index: " + curIndex);
element = document.getElementById(idList[curIndex]);
console.log(element.id);
element.scrollIntoView({ behavior: 'smooth' });
}
I have another function that calls scrollIntoView() on the same elements, via a button press. Works just fine.
Here is that function:
// anchorlink.js // This one works as intended //
window.anchorlink = {
scrollIntoView: function (elementId) {
var elem = document.getElementById(elementId);
if (elem) {
elem.scrollIntoView({ behavior: 'smooth' });
}
}
}
The console.log() calls within the scrollScreen() function show up properly in the console in each browser. Leading me to believe that element.scrollIntoView() is being called, but something is going wrong to make it not function appropriately. If there is any other information you need, I will be happy to provide, thanks.
After working on this some more, I decided the concept I was using wasn't as user friendly as I had hoped.
I decided to let the user scroll freely along the page and when they stop scrolling, then determine the closest DOM element and scroll to it. Works on Edge, Chrome and Firefox.
window.contentcenter = {
contentCenter: function (elementId) {
var centeringFunction = debounce(function () { autocenter(elementId) }, 200);
document.addEventListener("scroll", centeringFunction);
}
}
function autocenter(elementId) {
var currentElement = detectCurrentElement(elementId);
currentElement.scrollIntoView({ behavior: "smooth" });
}
function detectCurrentElement(elementId) {
var element = document.getElementById(elementId);
var currentPos = window.scrollY;
var contentIdList = getContentIdList(elementId);
var currentElement = closestContent(currentPos, element, contentIdList);
return currentElement;
}
function closestContent(pos, element, contentIdList) {
var contentId = Math.round(pos / (element.offsetHeight / contentIdList.length));
var currentElement = document.getElementById(contentIdList[contentId]);
return currentElement;
}
function getContentIdList(elementId) {
var idList = []
var childElements = document.getElementById(elementId).children;
for (var i = 0; i < childElements.length; i++) {
idList.push(childElements[i].id);
}
return idList;
}
function debounce(func, timeout) {
var timer;
return function () {
var context = this, args = arguments;
var later = function () {
timer = null;
func.apply(context, args);
};
clearTimeout(timer);
timer = setTimeout(later, timeout)
};
}
I have written a detect swipe left or right script; it has one unintended consequence. Links that reside within the swipeable container are no longer clickable.
I was wondering if anyone had any solutions they could point me to or any thoughts to experiment with my code.
I have tried:
Adding/toggling with event.preventDefault() within the Pointer Events
Looking at other get gesture type scripts, they include a distance threshold. If it is does not meet the threshold, I remove all event listeners. Adding this creates an unexpected result of working, only if you click the button twice.
Find the clickable elements within the container such as a span a , add an incremental click event listener then change the window location ref. Kinda works, but when you swipe near the span; the selection takes over and swipe actions stop. Experimenting with css touch-action: none or user-select:none have not improved the experience. It feels like I am breaking the default behaviour and reinventing the wheel.
link to a demo here on JS Bin
JS code here
window.addEventListener('load', () => {
let test = new getSwipeX({
elementId: 'container'
});
// previous attempt - adding a click on top of the browser default
let grabBtn = document.getElementsByClassName('button')[0];
grabBtn.addEventListener('click', (e) => {
window.location.href = 'www.google.co.uk'
});
})
function getSwipeX({elementId}) {
this.e = document.getElementsByClassName(elementId)[0];
this.initialPosition = 0;
this.lastPosition = 0;
this.threshold = 200;
this.diffInPosition = null;
this.diffVsThreshold = null;
this.gestureState = 0;
this.getTouchStart = (event) => {
event.stopPropagation();
if (window.PointerEvent) {
this.e.setPointerCapture(event.pointerId);
}
return this.initalTouchPos = this.getGesturePoint(event);
}
this.getTouchMove = (event) => {
event.stopPropagation();
return this.lastPosition = this.getGesturePoint(event);
}
this.getTouchEnd = (event) => {
event.stopPropagation();
if (window.PointerEvent) {
this.e.releasePointerCapture(event.pointerId);
}
this.doSomething();
this.initialPosition = 0;
}
this.getGesturePoint = (event) => {
this.point = event.pageX
return this.point;
}
this.whatGestureDirection = (event) => {
this.diffInPosition = this.initalTouchPos - this.lastPosition;
this.diffVsThreshold = Math.abs(this.diffInPosition) > this.threshold;
(Math.sign(this.diffInPosition) > 0) ? this.gestureState = 'L' : (Math.sign(this.diffInPosition) < 0) ? this.gestureState = 'R' : this.gestureState = 'N';
return [this.diffInPosition, this.diffVsThreshold, this.gestureState];
}
this.doSomething = (event) => {
let [gestureDelta,gestureThreshold,gestureDirection] = this.whatGestureDirection();
// USE THIS TO DEBUG
console.log(gestureDelta,gestureThreshold,gestureDirection);
if (gestureThreshold) {
(gestureDirection == 'L') ? console.log('Left') : console.log('Right');
} else {
this.e.removeEventListener('pointerdown', this.getTouchStart, true);
this.e.removeEventListener('pointermove', this.getTouchMove, true);
this.e.removeEventListener('pointerup', this.getTouchEnd, true);
this.e.removeEventListener('pointercancel', this.getTouchEnd, true);
}
}
if (window.PointerEvent) {
this.e.addEventListener('pointerdown', this.getTouchStart, true);
this.e.addEventListener('pointermove', this.getTouchMove, true);
this.e.addEventListener('pointerup', this.getTouchEnd, true);
this.e.addEventListener('pointercancel', this.getTouchEnd, true);
}
}
I have an element that is scrollable. I also have a function that scrolls to a specific position. I would like to call a function when the scrollTo is finished.
Plunkr example
var x = document.querySelector('.container');
$scope.scrollTo = function() {
x.scrollTo({
top: 300 ,
behavior: 'smooth'
});
};
// do something when scrollTo is finished
By checking the position of the element I am scrolling to and comparing that to the current scroll position of the container you can see when the scrolling action is finished.
function isScrollToFinished() {
const checkIfScrollToIsFinished = setInterval(() => {
if (positionOfItem === scrollContainer.scrollTop) {
// do something
clearInterval(checkIfScrollToIsFinished);
}
}, 25);
}
The interval checks if the position of the scroll container is equal to the position of the element I'm scrolling to. Then do a action and clear the interval.
I suggest checking that the scroll movement is gone.
Less parameter, no risk, and works for extremes positions.
let position = null
const checkIfScrollIsStatic = setInterval(() => {
if (position === window.scrollY) {
clearInterval(checkIfScrollIsStatic)
// do something
}
position = window.scrollY
}, 50)
I modified #DoiDor's answer to ensure that the position is rounded and include a timeout fallback just in case. Otherwise the position may never be reached exactly, and the promise would never resolve.
async function scrollToPosition(container, position) {
position = Math.round(position);
if (container.scrollTop === position) {
return;
}
let resolveFn;
let scrollListener;
let timeoutId;
const promise = new Promise(resolve => {
resolveFn = resolve;
});
const finished = () => {
container.removeEventListener('scroll', scrollListener);
resolveFn();
};
scrollListener = () => {
clearTimeout(timeoutId);
// scroll is finished when either the position has been reached, or 100ms have elapsed since the last scroll event
if (container.scrollTop === position) {
finished();
} else {
timeoutId = setTimeout(finished, 100);
}
};
container.addEventListener('scroll', scrollListener);
container.scrollTo({
top: position,
behavior: 'smooth',
});
return promise;
}
I'm using a solution similar to this:
function scrollElementTo(el, position) {
return new Promise((resolve) => {
const scrollListener = (evt) => {
if (typeof evt === 'undefined') {
return;
}
const target = evt.currentTarget;
if (target.scrollTop === position) {
target.removeEventListener('scroll', scrollListener);
resolve();
}
};
el.addEventListener('scroll', scrollListener);
// scroll to desired position (NB: this implementation works for
// vertical scroll, but can easily be adjusted to horizontal
// scroll as well)
el.scrollTo(0, position);
});
}
const scrollContainer = document.querySelector('#theScrollContainer');
// desired y coords for scroll
const yDesiredScroll = 234;
scrollElementTo(scrollContainer, yDesiredScroll).then(() => {
// do something here
});
There is an application that listens to scrolling the screen by the user.
So that's why if the scrolling is done with the mouse wheel, the data comes an order of magnitude slower than if you scroll the page with a space.
Server code is not laid out, there is no point.
Below is the application code.
#HostListener('window:scroll', ['$event']) checkScroll() {
if (!this.getdata) {
const componentPosition = this.el.nativeElement.offsetTop;
const componentHeight = this.el.nativeElement.getBoundingClientRect().height;
const scrollPosition = window.pageYOffset;
const windowheight = window.outerHeight;
const needposition = componentPosition + componentHeight - windowheight - 500;
if (scrollPosition >= needposition) {
this.getdata = true;
this.getMoreNew();
}
}
}
getMoreNew() {
if (!this.nomoredata) {
const city = this.city;
const find = this.find;
const last = this.persones[this.persones.length - 1]['orderbyname'];
this.httpClient.post<Array<any>>('/assets/api/index.php', {action: 'person', type: this.type, last, limit: this.limit, city, find })
.subscribe(
data => {
if (data.length === 0) {
this.nomoredata = true;
this.getdata = false;
} else {
this.persones = this.persones.concat(data);
this.getdata = false;
}
}
);
} else {
this.getdata = false;
}
}
See screenshot from devtools:
I am not entirely sure what the question is, but I think it's that your scroll is too slow and you want to have a way to help offload the computation for the event? You could try using a debounce to ignore the scrolling until the user is done.