I would like to create a sticky element that disappears when a certain div (bottles) get to bottom of the screen.
I have this code that works:
function update() {
const bottles = document.getElementById("bottles");
const rect = bottles.getBoundingClientRect();
const distance = window.innerHeight - rect.bottom; //value is 0 when bottom of bottles is at the bottom of screen
if (distance < 0) {
Array.from(document.getElementsByClassName("sticky-element")).forEach((el) => el.classList.remove("hidden"));
} else {
Array.from(document.getElementsByClassName("sticky-element")).forEach((el) => el.classList.add("hidden"));
}
}
document.addEventListener('scroll', update);
update();
However this is not the best code and it's going to be part of AB testing so the distances would have to be different. Therefore I tried to do something like this but it doesn't work:
function update(distance) {
if (distance < 0) {
Array.from(document.getElementsByClassName("sticky-element")).forEach((el) => el.classList.remove("hidden"));
} else {
Array.from(document.getElementsByClassName("sticky-element")).forEach((el) => el.classList.add("hidden"));
}
}
In case of the option A:
function elementDistance() {
const bottles = document.getElementById("bottles");
const rect = bottles.getBoundingClientRect();
const distance = window.innerHeight - rect.bottom; //value is 0 when bottom of bottles is at the bottom of screen
return distance;
}
document.addEventListener('scroll', update);
document.addEventListener('scroll', elementDistance);
update(elementDistance);
In case of the option B:
function elementDistance() {
const distance = some other values
return distance;
}
document.addEventListener('scroll', update);
document.addEventListener('scroll', elementDistance);
update(elementDistance);
Related
Is there a way to allow native scrolling easily without heavy JS modifications when you reach the border of a div via custom drag and drop via touchmove listener?
When you drag the text in the div here you'll see the div inside is scrolling automatically
I provided an example with touchmove listeners but this one does not scroll, when you reach a border with your mouse
Is there an easy way to include a scrolling behavior to the 2nd example?
const element = document.body.querySelector('#draggable');
const isInContainer = (x,y) => {
const elements = document.elementsFromPoint(x, y)
return elements.find(el => el && el.classList && el.classList.contains('container')) || false;
}
const onMouseMove = (e) => {
if(isInContainer(e.pageX, e.pageY)){
element.style.top = e.pageY + 'px';
element.style.left = e.pageX + 'px';
}
}
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp)
}
element.addEventListener('mousedown', (e) => {
e.preventDefault();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp)
});
In case someone has a native better solution I'm willing to accept that one... for the time being this would be my current way to solve the issue.
Note: I made a custom interval for scrolling and don't use the mousemove event so users don't have to move the mouse to trigger it. moving outside will start the interval moving inside will clear it.
// the container that should scroll
const scrollBody = document.getElementById('scrollContainer');
// parameter to check which directions should scroll
const scrollPositions = {
left: false,
right: false,
up: false,
down: false,
}
// how far should be scrolled
const nextScrollDistance = {
x: 0,
y: 0
}
// scroll interval
let scrollInterval= null;
const startScrolling = (scrollBody) => {
if (scrollInterval !== null) {
return true;
}
const intervalCallback = () => {
if (scrollInterval !== null && nextScrollDistance.x === 0 && nextScrollDistance.y === 0) {
window.clearInterval(scrollInterval);
scrollInterval = null;
} else {
scrollBody.scrollLeft += nextScrollDistance.x;
scrollBody.scrollTop += nextScrollDistance.y;
}
}
scrollInterval = window.setInterval(intervalCallback, 50);
}
const onMouseMove = (e) => {
if(isInContainer(e.pageX, e.pageY)){
element.style.top = e.pageY + 'px';
element.style.left = e.pageX + 'px';
}
const rects = scrollBody.getBoundingClientRect();
// check directions
// max x that can be scrolled
const maxX = scrollBody.scrollWidth - scrollBody.clientWidth;
// max y that can be scrolled
const maxY = scrollBody.scrollHeight - scrollBody.clientHeight;
// check all directions if it's even possible to scroll
const canScrollTop = Math.round(scrollBody.scrollTop) > 0;
const canScrollBottom = Math.round(scrollBody.scrollTop) < maxY;
const canScrollLeft = Math.round(scrollBody.scrollLeft) > 0;
const canScrollRight = Math.round(scrollBody.scrollLeft) < maxX;
// current x and y coordinates of the mouse
const x = e.pageX;
const y = e.pageY;
// dynamic value to decrease the speed.. otherwise it might scroll too fast
const minifier = 2;
// the modifiers for scrollTop and scrollLeft
nextScrollDistance.y = 0;
nextScrollDistance.x = 0;
if (canScrollBottom && y > rects.bottom) {
// distance between the right border and the mouse
const distance = Math.abs(y - rects.bottom);
// the next time it scrolls -> scroll distance / minifier
nextScrollDistance.y = Math.round(distance / minifier)
scrollPositions.down = true;
} else {
scrollPositions.down = false;
}
// all other directions...
if (canScrollTop && y < rects.top) {
const distance = Math.abs(y - rects.top);
nextScrollDistance.y = Math.round(distance / minifier) * -1;
scrollPositions.up = true;
} else {
scrollPositions.up = false;
}
if (canScrollRight && x > rects.right) {
const distance = Math.abs(x - rects.right);
nextScrollDistance.x = Math.round(distance / minifier)
scrollPositions.right = true;
} else {
scrollPositions.right = false;
}
if (canScrollLeft && x < rects.left) {
const distance = Math.abs(x - rects.left);
nextScrollDistance.x = Math.round(distance / minifier) * -1;
scrollPositions.left = true;
} else {
scrollPositions.left = false;
}
// in case one of those are set.. trigger scrolling
if (nextScrollDistance.x || nextScrollDistance.y) {
startScrolling();
}
}
Need to apply the animateScript() function to different images/divs on mouseover. i think i just need to change my document.querySelector but im not sure how to do so so that it applies to one of the 4 different images.
let tID; //we will use this variable to clear the setInterval()
//let x = event.clientX, y = event.clientY, elementMouseIsOver = document.elementFromPoint(x, y);
function stopAnimate() {
clearInterval(tID);
} //end of stopAnimate()
function animateScript() {
let position = 80; //start position for the image slicer
let height = 0;
const interval = 190; //180 ms of interval for the setInterval()
const diff = 80; //diff as a variable for position offset
const next_row = 110;
let elements = document.querySelectorAll(':hover');
tID = setInterval(() => {
document.querySelector(".avatar").style.backgroundPosition = `-${position}px -${height}px`;
//we use the ES6 template literal to insert the variable "position"
if (height < 220)
{
if (position < 720) {
position = position + diff;
console.log(position);
}
else if (position == 720){
//console.log("height");
position = 80;
height = next_row + height;
console.log("height: ", height);
}
}
else {
if (position < 400) {
position = position + diff;
console.log(position);
}
else if (position == 480){
position = 80;
height = 0;
console.log("height: ", height);
}
}
//reset the position to 256px, once position exceeds 1536px
}, interval); //end of setInterval
} //end of animateScript()
You're hard-coding the .avatar selector, which will always get the first element with that class, so you're correct that that will need to change.
One easy solution would be to pass the hovered element into your function.
const handleMouseEnter = (e) => {
const el = e.target;
animateScript(el);
}
element.addEventListner("mouseenter", handleMouseEnter);
function animateScript(element) {
[...]
tID = setInterval(() => {
element.style.backgroundPosition = `whatever style`;
[...]
}, interval);
}
I have several div with class 'comp'. Each comp has images inside. What I want to achieve is to implement lazy loading for images insdie each comp. So, that when a specific comp element enters in the viewport I want the images to be loaded.
I want to use eventlistener and debounce as a polyfill approach as I tried the intersection observer DOM API but it has a limitation i.e. it is not supported by some recent Safari and IE versions.
Any help in this regards will be really appreciated.
Code:
const options = {
threshold: 0
};
const io = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
loadImage(entry.target);
entry.target.classList.add('loaded');
io.unobserve(entry.target);
}
});
}, options);
const targetElements = document.querySelectorAll(".comp");
for (let element of targetElements) {
if (document.body.className.match(/\bintersection-observer-supported\b/)) {
io.observe(element);
} else {
var domRect = element.getBoundingClientRect();
}
}
function loadImage(imageElement) {
setTimeout(() => {
console.dir(imageElement.querySelector('img'));
imageElement.querySelector('img.target-image').src = imageElement.querySelector('img.target-image').dataset.src;
}, 500);
}
//Polyfill
function elementInViewport(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;
while(el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}
return (
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
);
}
I'm trying to render a certain number of images on a screen for my app in React Native .53.3. I pass the number of images I want (they are identical images) through the navigator and then in turn pass that prop onto a javascript function that generates top/left coordinates for the images randomly.
Here is where it all goes wrong: The screen is unreachable. The button on the screen proceeding it doesn't work -- it won't even register a press -- and so the screen with the images never renders.
If I insert a return statement before the while-loop, the script functions fine and I'm able to even navigate to the image screen. So I think this problem is somehow related to the loop. I'm a bit of a JS newbie but I think my code all looks nice and correct...
Here is the script in question:
import { Dimensions } from 'react-native';
export function barGeneration(n) {
var barCoords = [];
var noZone = [{
x: Dimensions.get('window').width / 2,
y: Dimensions.get('window').height / 2
}];
while (barCoords.length < n) {
var left = ((Dimensions.get('window').height) - 101) * Math.random();
var top = (Dimensions.get('window').width - 101) * Math.random();
var validX = true;
var validY = true;
for (var i = 0; i < noZone.length; i++) {
if (left > noZone[i].x - 101) {
validX = false;
}
else if (left < noZone[i].x + 101) {
validX = false;
}
if (top > noZone[i].y - 101) {
validY = false;
}
else if (top > noZone[i].y + 101) {
validY = false;
}
}
if (validX && validY) {
barCoords.push({
top: top,
left: left,
rot: Math.random()*360 + 'deg'
});
noZone.push({
x: left,
y: top,
});
validX = false;
validY = false;
}
}
return barCoords;
}
And here is where the script is called:
componentWillMount() {
// assigning constant coordinate values to junk bars
var barCoords = barGeneration(this.props.navigation.state.params.n);
this.setState({barCoords: barCoords});
Navigation from the non-functioning button:
<Button
raised
backgroundColor='#CCC'
color='black'
iconRight={{name: 'arrow-right', type: 'feather'}}
onPress={() => this.props.navigation.navigate('PracticeTrial2', {n:2, count:0, scores:[]});}
title='Start Practice: Memory for 2 items'
/>
I have written a function that when invoked, will scroll the page downwards in a smooth fashion.
The problem is that the amount scrolled is not precise. It can be off by one pixel or so depending on the distance scrolled and the duration of the scroll.
Is there a better way to do this such that the scroll can move the exact number of desired pixels?
const EASING = BezierEasing(0, .1, .1, 1); // Example values.
const DURATION = 1000; // ms.
document.addEventListener('DOMContentLoaded',
() => {
document.querySelector('#foo')
.addEventListener('click', onClick, false);
});
function onClick(e) {
scroll(400); // px.
e.preventDefault();
return false;
}
function scroll(distance) {
var start, travelled, html;
start = performance.now();
travelled = 0;
html = document.querySelector('html');
(function move(now) {
var fractionDone, dy, toMove;
fractionDone = (now-start) / DURATION;
if((1 - fractionDone) <= 0) {
return; // Done!
}
if(window.scrollY + window.innerHeight
=== html.offsetHeight) {
return; // At bottom of page.
}
dy = ((EASING.get(fractionDone)) * distance);
toMove = Math.floor((dy - travelled)); // `scrollBy` only accepts integers.
if(toMove > 0) {
window.scrollBy(0, toMove);
travelled += toMove;
}
requestAnimationFrame(move);
}(start));
}
<!DOCTYPE html>
<html>
<head>
<script src="https://rawgit.com/gre/bezier-easing/master/build.js"></script>
</head>
<body>
<a href id="foo">Click Me!</a>
<script>
/* print some numbers to
the DOM to help visualize
the scrolling */
var body = document.querySelector('body');
for(var x = 0; x < 50; x++) {
var div = document.createElement("div");
var text = document.createTextNode(x);
div.appendChild(text);
body.appendChild(div);
}
</script>
</body>
</html>
could you do something like this to account for the last iteration?
basically if toMove gets rounded down to 0, but distance hasnt been travelled yet, force it to do scroll one more?
if(toMove > 0 || (toMove == 0 && travelled != distance) {
window.scrollBy(0, (toMove ? toMove : 1));
travelled += toMove;
}
I decided to modify the done logic to move any remaining distance:
// ...
if((1 - fractionDone) <= 0) {
window.scrollBy(0, (distance - travelled)); // Scroll any remaining distance.
return; // Done!
}
// ...
Which, I think, solves the issue.