Animating react list dragging items - javascript

The snippet below contains an array of 10 items. I'm able to drag and drop the list items and even able to achieve some basic animations when grabbing the list item:
const App = () => {
const [myArray, setMyArray] = React.useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const [draggedIndex, setDraggedIndex] = React.useState(-1);
const onDragStart = (e, index) => {
setDraggedIndex(index);
const emptyDiv = document.createElement('div');
emptyDiv.style.width = '0px';
emptyDiv.style.height = '0px';
e.dataTransfer.setDragImage(emptyDiv, 0, 0);
e.currentTarget.className = 'draggable';
};
const onMouseDown = (e) => {
e.currentTarget.className = 'draggable';
};
const onMouseUp = (e) => {
e.currentTarget.className = 'listItem';
};
const onDragOver = (e, index) => {
e.preventDefault();
if (draggedIndex === -1 || draggedIndex === index) {
return;
}
let items = myArray.filter((item, i) => i !== draggedIndex);
items.splice(index, 0, myArray[draggedIndex]);
setMyArray(items);
setDraggedIndex(index);
};
const onDragEnd = (e) => {
setDraggedIndex(-1);
e.target.className = 'listItem';
};
return (
<div className="App">
{myArray.map((x, i) => (
<div
className="listItem"
draggable
key={x}
onDragStart={(e) => onDragStart(e, i)}
onDragOver={(e) => onDragOver(e, i)}
onDragEnd={onDragEnd}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
>
<h3>hello - {x}</h3>
</div>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
.App {
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
}
.listItem {
border: 2px solid black;
margin: 5px;
width: 400px;
cursor: grab;
transform: scale(100%);
transition: transform 0.3s ease-in-out;
}
.draggable {
border: 2px solid green;
margin: 5px;
width: 400px;
cursor: grab;
transform: scale(108%);
transition: transform 0.3s ease-in-out;
}
.listItem:-moz-drag-over {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Can I get help with CSS animations to make the list item movements smoother during dragging so it looks less choppy? The goal is to achieve the following effect - when I drag an item it would smoothly reposition itself up/down, and the item that is being dragged over would smoothly move in the opposite direction.
EDIT 1:
Please check the code snippet, run it, and try to drag the list items to understand my requirements.
Basically, what I want is to animate the transition of the dragged item depending on which direction (up or down) the item is being dragged. In theory, while dragging an item up, it could apply a class something like .dragged-up and that class would have animation/transition that would create an illusion that that item moving from the lower to the higher position.
The same principle could be applied to the items above and below the item being dragged. For example, If the item that is being dragged over moves from the top to the bottom, a different class could be applied, something like .listItem-down, and that class could contain an opposite animation. Also, I suspect it would need to have a lower z-index so it would appear below the dragged item.
Not sure if it's the most efficient approach and if it's possible to do it that way at all. So far, while trying to implement something like this, I've been getting issues of items overlapping and as a result, the event function was being executed on the wrong div, causing some undesired effects.
Some help and a working snippet would be much appreciated!

This answer is inspired by this solution, and attempts to make a greatly simplified port of its main ideas to functional React components that works for the draggable elements in the use case.
In the posted example, order of items in the array is updated on every event of dragging over. To create a transition when the reorder happens, the difference before and after the change for each item can be detected, and used as the starting and ending points for the animation.
The following approach assigns a keyed ref for each item to keep track of the updates, and check for the changes in their rendered position with getBoundingClientRect in a useLayoutEffect, so that further actions can be taken before the browser repaints the screen.
In order to calculate the differences, the position of items in the last render prevPos is stored separately as another ref, so that it persists between renders. In this simplified example, only top position is checked and calculated for a difference, to create an offset for translateY to happen.
Then to arrange for the transition, requestAnimationFrame is called two times, with the first frame rendering the items in the offset positions (starting point, with offset in translateY), and the second their in new natural positions (ending point, with 0 in translateY).
While at this point useLayoutEffect already handle the animations as expected, the fact that onDragOver triggers and updates the state very often could easily cause errors in the motion display.
I tried to implement some basic debouncing for the update of the state array and introduced another useEffect to handle the debounced update, but it seems that the effects might be still occasionally unstable.
While lots of improvements could still be done, here is the experimental example:
const App = () => {
const [myArray, setMyArray] = React.useState([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
]);
const [draggedKey, setDraggedKey] = React.useState(null);
const [pendingNewKey, setPendingNewKey] = React.useState(null);
const elRef = React.useRef({});
const keyInAnimation = React.useRef(null);
const prevPos = React.useRef({});
// 👇 Attempt to debounce update of array
React.useEffect(() => {
if (
pendingNewKey === null ||
draggedKey === null ||
draggedKey === pendingNewKey ||
keyInAnimation.current === draggedKey
)
return;
const updateArray = () => {
setMyArray((prev) => {
const prevIndex = prev.findIndex((x) => x === draggedKey);
const newIndex = prev.findIndex((x) => x === pendingNewKey);
const newArray = [...prev];
newArray[prevIndex] = pendingNewKey;
newArray[newIndex] = draggedKey;
return newArray;
});
};
const debouncedUpdate = setTimeout(updateArray, 100);
return () => clearTimeout(debouncedUpdate);
}, [pendingNewKey, draggedKey]);
React.useLayoutEffect(() => {
Object.entries(elRef.current).forEach(([key, el]) => {
if (!el) return;
// 👇 Get difference in position to calculate an offset for transition
const { top } = el.getBoundingClientRect();
if (!prevPos.current[key] && prevPos.current[key] !== 0)
prevPos.current[key] = top;
const diffTop = Math.floor(prevPos.current[key] - top);
if (diffTop === 0 || Math.abs(diffTop) < 30) return;
prevPos.current[key] = top;
el.style.transform = `translateY(${diffTop}px)`;
el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
// 👇 First frame renders offset positions, second the transition ends
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (!el) return;
el.style.transform = `translateY(0px)`;
el.style.transition =
'scale 0.3s ease-in-out, transform 100ms ease-out';
});
});
});
}, [myArray.toString()]);
const onDragStart = (e, key) => {
keyInAnimation.current = key;
setDraggedKey(key);
const emptyDiv = document.createElement('div');
emptyDiv.style.width = '0px';
emptyDiv.style.height = '0px';
e.dataTransfer.setDragImage(emptyDiv, 0, 0);
e.currentTarget.className = 'draggable';
};
const onMouseDown = (e) => {
e.currentTarget.className = 'draggable';
};
const onMouseUp = (e) => {
e.currentTarget.className = 'listItem';
};
const onDragOver = (e, key) => {
e.preventDefault();
if (draggedKey === null) return;
if (draggedKey === key) {
keyInAnimation.current = key;
setPendingNewKey(null);
return;
}
if (keyInAnimation.current === key) {
return;
}
keyInAnimation.current = key;
setPendingNewKey(key);
// 👇 Attempt to reduce motion error but could be unnecessary
Object.values(elRef.current).forEach((el) => {
if (!el) return;
el.style.transform = `translateY(0px)`;
el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
});
};
const onDragEnd = (e) => {
setDraggedKey(null);
setPendingNewKey(null);
keyInAnimation.current = null;
e.target.className = 'listItem';
};
return (
<div className="App">
{myArray.map((x) => (
<div
className="listItem"
draggable
key={x}
onDragStart={(e) => onDragStart(e, x)}
onDragOver={(e) => onDragOver(e, x)}
onDragEnd={onDragEnd}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
ref={(el) => (elRef.current[x] = el)}
>
<h3>hello - {x}</h3>
</div>
))}
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
.App {
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
isolation: isolate;
gap: 15px;
}
.listItem {
border: 2px solid black;
margin: 5px;
width: 400px;
cursor: grab;
z-index: 1;
transition: scale 0.3s ease-in-out;
background-color: white;
}
.draggable {
border: 2px solid hotpink;
margin: 5px;
width: 400px;
cursor: grab;
scale: 108%;
z-index: 10;
transition: scale 0.3s ease-in-out;
background-color: white;
}
.listItem:-moz-drag-over {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>

Related

How to set animation-fill-mode of in parameters of .animate function vanilla JS?

I am struggling to find documentation or examples of the method in vanilla JavaScript that allows me to set the animation-fill-mode. I am using the Element.animate(animation, timing) function to achieve this.
I have attempted adding animation-fill-mode to the entries of the timing object, but it is not a valid parameter according to my tests. I have also attempted to use Animation.onfinish and Animation.pause() in tandem to pause it when it completes, but that also does not work. Here is all the code that this uses:
const quotemove = [
{ transform: "translate(0vw) rotate(0deg)" },
{ transform: "translate(80vw) rotate(180deg)" }
]
const quotetiming = {
duration: 1000,
iterations: 1,
// this is where i attempted to add fill mode
}
const quoteholders = document.getElementsByClassName("quote")
for(let i = 0; i < quoteholders.length; i++) {
let quoteholder = quoteholders.item(i)
const quotemark = quoteholder.querySelector(".quotationmark")
quoteholder.addEventListener("mouseover", () => {
let animation = quotemark.animate(quotemove, quotetiming)
})
}
I should also mention that I intend on adding another animation to the mouseout event so that it stays in one position while you hover, and another when not.
If it is not possible to set the fill mode to forwards and in the future implement the above request, then is there another similar approach I should consider? I appreciate it.
Your quotetiming KeyframeEffect object would be the right place.
Not sure what you did wrong, what you need is to set the fill property:
const quotemove = [
{ transform: "translate(0vw) rotate(0deg)" },
{ transform: "translate(80vw) rotate(180deg)" }
]
const quotetiming = {
duration: 1000,
iterations: 1,
fill: "forwards"
}
const quoteholders = document.getElementsByClassName("quote")
for(let i = 0; i < quoteholders.length; i++) {
let quoteholder = quoteholders.item(i)
const quotemark = quoteholder.querySelector(".quotationmark")
quoteholder.addEventListener("mouseover", () => {
let animation = quotemark.animate(quotemove, quotetiming)
})
}
.quote {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: lightgray;
}
blockquote {
background: ivory;
transform-origin: center;
display: inline-block;
}
<div class="quote">
<blockquote class="quotationmark">Hover the page</blockquote>
</div>

Box styling seemingly not applied when adding new elements with button

Refer to link. Link is to the project "Scroll-Animation" I am cloning in local browser to practice front-end.
I have two buttons, +, -. The '+' adds new boxes, the '-' removes boxes from the body. I implemented the '+' but it seems the styling for boxes is not working. That is, it seems to add the "div" elements I created to body but the div's are not style like I have them in .css file for .box { ... } Any help with how to fix this is greatly appreciated. Thanks!
style.css
...
.box {
height: 200px;
width: 400px;
border-radius: 10px;
background-color: steelblue;
margin: 10px;
color: white;
display: grid;
place-items: center;
box-shadow: 2px 4px 5px rgba(0,0,0,.3);
transform: translateX(-400%);
transition: transform .5s ease;
}
.box:nth-of-type(even) {
transform: translateX(400%);
}
.box.show {
transform: translateX(0);
}
...
app.js
const boxes = document.querySelectorAll('.box');
const btns = document.querySelectorAll('.btn');
window.addEventListener('scroll', slideBox);
slideBox();
// NOTE: Need to fix.
btns.forEach(btn => {
btn.addEventListener('click', (e) => {
const cList = e.currentTarget.classList;
if (cList.contains('add')) {
console.log('Work');
var h2 = document.createElement('h2');
h2.innerHTML = 'Content';
var newBox = document.createElement("div");
var attr = document.createAttribute("class");
attr.value = "box";
newBox.setAttributeNode(attr);
newBox.appendChild(h2);
document.body.appendChild(newBox);
}
})
})
// NOTE: This function works!!!
function slideBox() {
const pageBot = window.innerHeight / 5 * 4;
const pageTop = window.innerHeight / 5 * 1;
boxes.forEach(box => {
const boxTop = box.getBoundingClientRect().top;
const boxBot = box.getBoundingClientRect().bottom;
if (boxTop < pageBot && boxBot > pageTop) {
box.classList.add('show');
} else {
box.classList.remove('show');
}
})
}
index.html
<body>
<h1>Scroll to see the animation</h1>
<div class="button-container">
<button class="btn add">+</button>
<button class="btn minus">-</button>
</div>
<!-- <div class="box"><h2>Content</h2></div>
<div class="box"><h2>Content</h2></div>
...
<div class="box"><h2>Content</h2></div> -->
<script src="app.js"></script>
</body>
I think the problem is after you add a new box, but afterward you don't query all boxes again in slideBox. You can check the fix here
The full modification in Javascript with some comments
const btns = document.querySelectorAll('.btn');
window.addEventListener('scroll', slideBox)
slideBox();
btns.forEach(btn => {
btn.addEventListener('click', (e) => {
const cList = e.currentTarget.classList;
if (cList.contains('add')) {
console.log('Work');
var h2 = document.createElement('h2');
h2.innerHTML = 'Content';
var newBox = document.createElement("div");
var attr = document.createAttribute("class");
attr.value = "box";
newBox.setAttributeNode(attr);
newBox.appendChild(h2);
document.body.appendChild(newBox);
}
})
})
function slideBox() {
//query all boxes every time you scroll because of new boxes
const boxes = document.querySelectorAll('.box')
const triggerBottom = window.innerHeight / 5 * 4
boxes.forEach(box => {
const boxTop = box.getBoundingClientRect().top
if (boxTop < triggerBottom) {
box.classList.add('show')
} else {
box.classList.remove('show')
}
})
}

Fast scroll to change style

Is it possible to scroll to .project and make the background red without to scroll slow and near the class .project?
Basically I want the user to be able to scroll and get the red color displayed even if he or she scrolls quickly, but when the user is above or under projectPosition.top, the background should be the standard color (black).
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY == projectPosition.top) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>
Thanks in advance.
Instead of listen for the scroll event you could use the Intersection Observer API which can monitor elements that come in and out of view. Every time an observed element either enters or leaves the view, a callback function is fired in which you can check if an element has entered or left the view, and handle accordingly.
It's also highly performant and saves you from some top and height calculations.
Check it out in the example below.
If you have any questions about it, please let me know.
Threshold
To trigger the callback whenever an element is fully into view, not partially, set the threshold option value to [1]. The default is [0], meaning that it is triggered whenever the element is in view by a minimum of 1px. [1] states that 100% of the element has to be in view to trigger. The value can range from 0 to 1 and can contain multiple trigger points. For example
const options = {
threshold: [0, 0.5, 1]
};
Which means, the start, halfway, and fully in to view.
const project = document.querySelector('.project');
const observerCallback = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('red');
} else {
entry.target.classList.remove('red');
}
});
};
const options = {
threshold: [1]
}
const observer = new IntersectionObserver(observerCallback, options);
observer.observe(project);
.top,
.bottom{
height: 700px;
width: 100%;
}
.project {
background: black;
height: 200px;
width: 100%;
}
.project.red {
background: red;
}
<div class="top"></div>
<div class="project"></div>
<div class="bottom"></div>
To make it 'fast' you better will have to use the >= operator than ==:
var project = document.getElementsByClassName('project')[0];
var projectPosition = project.getBoundingClientRect();
document.addEventListener('scroll', () => {
var scrollY = window.scrollY;
if (scrollY >= projectPosition.top && scrollY <= projectPosition.top + projectPosition.height) {
project.style.background = "red";
project.style.height = "100vh";
} else {
project.style.background = "black";
project.style.height = "200px";
}
});
.top {
height: 700px;
}
.project {
background: black;
height: 200px;
width: 100%;
}
<div class="top"></div>
<div class="project"></div>
<div class="top"></div>

Loop through array using wheel event a increments

I want to change the colour of a <main> element using the wheel event. If the event.deltaY is negative i.e scrolling up I want to loop through the array backwards, so, if index = 2 it would go blue, purple, black, white and then to the end, so blue. And if the event.deltaY is positive i.e scrolling down, I want to loop through the array forwards, if index = 3, it would go blue, orange, white, black, etc. It should keep looping infinitely and either way whenever there is a scroll. Currently, I'm having trouble setting the index to loop at either end. Any help would be greatly appreciated.
const container = document.querySelector('main')
const classes = ['white', 'black', 'purple', 'blue', 'orange']
let index = 0
const throttle = (func, limit) => {
let inThrottle
return function() {
const args = arguments
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
function changeColour(event) {
if (event.deltaY < 0) {
++index
if (index === index.length - 1) {
index = 0
}
console.log(index);
container.classList = classes[index]
}
if (event.deltaY > 0) {
--index
console.log(index);
if (index === 0) {
index = index.length - 1
}
container.classList = classes[index]
}
}
container.addEventListener('wheel', throttle(changeColour, 1250))
main {
height: 50px;
width: 50px;
border: 1px solid;
}
main.white {
background-color: #FFFFFF;
}
main.black {
background-color: #101010;
}
main.purple {
background-color: #8E002E;
}
main.blue {
background-color: #002091;
}
main.orange {
background-color: #C05026;
}
<main>
</main>
Issues
index = index.length - 1 index = classes.length -1
container.classList = classes[index] container.classList.add(classes[index])
There is no statement to remove any of the other classes.
Event listeners and on-event properties do not use the parentheses of a function because it will be misinterpreted as a direct call to the function to run. Callback functions are the opposite in that they wait to be called on. If an extra parameter is needed by the callback function you'll need to wrap the callback function with an anonymous function, here's the proper syntax:
on-event property: document.querySelector('main').onwheel = scrollClass;
event listener: document.querySelector('main').addEventListener('wheel', scrollClass)
on-event property passing extra parameters:
document.querySelector('main').onwheel = function(event) {
const colors = ['gold', 'black', 'tomato', 'cyan', 'fuchsia'];
scrollClass(event, colors);
}
event listener passing extra parameters:
document.querySelector('main').addEventListener('wheel', function(event) {
const colors = ['gold', 'black', 'tomato', 'cyan', 'fuchsia'];
scrollClass(event, colors);
}
Details commented in demo
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Wheel Event Demo</title>
<style>
:root {
font: 400 16px/1.2 Consolas;
}
html,
body {
width: 100%;
height: 100%;
}
body {
overflow-y: scroll;
overflow-x: hidden;
padding: 5px 0 20px;
}
main {
height: 50vh;
width: 50vw;
margin: 10px auto;
border: 1px solid #000;
}
main.gold {
background-color: #FFCC33;
transition: 0.5s;
}
main.black {
background-color: #101010;
transition: 0.5s;
}
main.tomato {
background-color: #FF6347;
transition: 0.5s;
}
main.cyan {
background-color: #E0FFFF;
transition: 0.5s;
}
main.fuchsia {
background-color: #FD3F92;
transition: 0.5s;
}
</style>
</head>
<body>
<main></main>
<script>
// Reference the main tag
const container = document.querySelector('main');
// Declare index
let index = 0;
/** scrollClass(event, array)
//# Params: event [Event Object]: Default object needed for event properties
// array [Array].......: An array of strings representing classNames
//A Pass Event Object and array of classes
//B Reference the tag bound to the wheel event (ie 'main')
//C1 if the change of wheel deltaY (vertical) is less than 0 -- increment index
//C2 else if the deltaY is greater than 0 - decrement index
//C3 else it will just be index (no change)
//D1 if index is greater than the last index of array -- reset index to 0
//D2 else if index is less than zero -- reset index yo the last index of array
//D3 else index is unchanged.
//E Remove all classes of the currentTarget ('main')
//F Find the string located at the calculated index of the array and add it as a
className to currentTarget
*/
function scrollClass(event, array) { //A
const node = event.currentTarget; //B
index = event.deltaY < 0 ? ++index : event.deltaY > 0 ? --index : index; //C1-3
index = index > array.length - 1 ? 0 : index < 0 ? array.length - 1 : index; //D1-3
node.className = ''; //E
node.classList.add(array[index]); //F
}
/*
Register wheel event to main
When an event handler (ie scrollClass()) has more than the Event Object to pass
as a parameter, you need to wrap the event handler in an anonymous function.
Also, it's less error prone if the parameter is declared within the
anonymous function.
*/
container.addEventListener('wheel', function(event) {
const colors = ['gold', 'black', 'tomato', 'cyan', 'fuchsia'];
scrollClass(event, colors);
});
</script>
</body>
</html>
Modified code to include the corrections
const container = document.querySelector('main')
const classes = ['white', 'black', 'purple', 'blue', 'orange']
let index = 0
const throttle = (func, limit) => {
let inThrottle
return function() {
const args = arguments
const context = this
if (!inThrottle) {
func.apply(context, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
function changeColour(event) {
if (event.deltaY < 0) {
++index
if (index >= classes.length) { // 👈 Here
index = 0
}
console.log(index);
container.classList = classes[index]
}
if (event.deltaY > 0) {
--index
console.log(index);
if (index <= -1) { // 👈 Here
index = classes.length - 1 // 👈 Here
}
container.classList = classes[index]
}
}
container.addEventListener('wheel', throttle(changeColour, 1250))
main {
height: 50px;
width: 50px;
border: 1px solid;
}
main.white {
background-color: #FFFFFF;
}
main.black {
background-color: #101010;
}
main.purple {
background-color: #8E002E;
}
main.blue {
background-color: #002091;
}
main.orange {
background-color: #C05026;
}
<main>
</main>

Smooth Scrolling, Cross-Browser, Without jQuery

Below you will find a script I created for smooth scrolling when clicking local links. It is done via transform (no jQuery). As seen, I have implemented it using both inline CSS as well as external style sheets. I recommend the inline version, as it might be difficult to guess the index of the relevant style sheet.
The problem however, is that the actual movement of the scrollbar, happens after the transform is applied. Thus, if you click a link before scrolling transition is done, the code misbehaves.
Any thoughts on a solution to this?
EDIT:
I know there are jQuery solutions and third party polyfill libraries out there. My goal, however, was to recreate the jQuery functionality in plain vanilla JavaScript.
My Script:
// Get style rule's declaration block
function getStyleDeclaration(styleSheet, selectorText) {
const rules = styleSheet.cssRules;
return Array.from(rules).find(r => r.selectorText === selectorText).style;
// for (let i = 0; i < rules.length; i += 1) {
// if (rules[i].selectorText === selectorText) return rules[i].style;
// }
}
// Get specific style sheet, based on its title
// Many style sheets do not have a title however
// Which is main reason it is preferred to work with
// inline styles instead
function getStyleSheet(title) {
const styleSheets = document.styleSheets;
return Array.from(styleSheets).find(s => s.title === title);
// for (let i = 0; i < styleSheets.length; i += 1) {
// if (styleSheets[i].title === title) return styleSheets[i];
// }
}
function scrollToElement_ExternalStyleSheet(anchor, target) {
anchor.addEventListener("click", e => {
e.preventDefault();
const time = 1000;
// Distance from viewport to topof target
const distance = -target.getBoundingClientRect().top;
// Modify external style sheet
const transStyle = getStyleDeclaration(document.styleSheets[1], ".trans");
transStyle.transform = "translate(0, " + distance + "px)";
transStyle.transition = "transform " + time + "ms ease";
const root = document.documentElement; // <html> element
root.classList.add("trans");
window.setTimeout(() => {
root.classList.remove("trans");
root.scrollTo(0, -distance + window.pageYOffset);
}, time);
});
}
function scrollToElement_InlineStyle(anchor, target) {
const root = document.documentElement;
anchor.addEventListener('click', e => {
e.preventDefault();
const time = 900;
const distance = -target.getBoundingClientRect().top;
root.style.transform = 'translate(0, ' + distance + 'px)';
root.style.transition = 'transform ' + time + 'ms ease';
window.setTimeout(() => {
root.scrollTo(0, -distance + window.pageYOffset);
root.style.transform = null; // Revert to default
root.style.transition = null;
}, time);
});
}
function applySmoothScroll() {
const anchors = document.querySelectorAll("a");
const localAnchors = Array.from(anchors).filter(
a => a.getAttribute("href").indexOf("#") != -1
);
localAnchors.forEach(a => {
const targetString = a.getAttribute("href");
const target = document.querySelector(targetString);
// scrollToElement_ExternalStyleSheet(a, target);
scrollToElement_InlineStyle(a, target);
});
}
applySmoothScroll();
.box {
padding-bottom: 300px;
padding-top: 0.5rem;
background-color: orange;
text-align: center;
font-size: 200%;
}
.box:nth-child(even) {
background-color: lightblue;
color: white;
}
a {
color: black;
}
body {
margin: 0;
}
.trans {
transform: translate(0, -100px);
transition: transform 900ms ease;
}
<div id="s0" class="box">Click Me!</div>
<div id="s1" class="box">Click Me!</div>
<div id="s2" class="box">Click Me!</div>

Categories

Resources