I am currently in the process of replicating some of the core functionalities that exists in Trello. One of the functionalities i am looking into is the drag-and-drop.
I have managed to implement the functionality, albeit with an issue. When you drag a card from one column to another column, there is some form of animation (see the linked YouTube video for reference). To describe it in words, the animation portray a movement back to the starting position of the dragged card, although it is not the case.
https://www.youtube.com/watch?v=wSoaaKLj3r0
My current code:
/* Column.js */
import React, {useState, useRef} from 'react';
import './Column.css';
function Column({data}) {
const [list, setList] = useState(data);
const [dragging, setDragging] = useState(false);
const dragItem = useRef();
const dragNode = useRef();
const handleDragStart = (event, params) => {
console.log('drag starting', params)
dragItem.current = params;
dragNode.current = event.target;
dragNode.current.addEventListener('dragend', handleDragEnd)
setTimeout(() => {
setDragging(true);
}, 0)
}
const handleDragEnter = (event, params) => {
console.log('entering drag', params);
const currentItem = dragItem.current;
if(event.target !== dragNode.current) {
console.log('TARGET IS NOT THE SAME')
setList(oldList => {
let newList = JSON.parse(JSON.stringify(oldList));
newList[params.groupIndex].items.splice(params.itemIndex, 0, newList[currentItem.groupIndex].items.splice(currentItem.itemIndex,1)[0])
dragItem.current = params
return newList;
})
}
}
const handleDragEnd = () => {
console.log('Ending Drag')
setDragging(false);
dragNode.current.removeEventListener('dragend', handleDragEnd)
dragItem.current = null;
dragNode.current = null;
}
const getStyles = (params) => {
const currentItem = dragItem.current;
if (currentItem.groupIndex === params.groupIndex && currentItem.itemIndex === params.itemIndex) {
return 'current drag-and-drop-item';
}
return 'drag-and-drop-item'
};
return (
<section className="drag-and-drop-container">
<div className="drag-and-drop">
{list.map((group, groupIndex) => (
<div
key={group.title}
className="drag-and-drop-group"
onDragEnter={dragging && !group.items.length
? (event) => handleDragEnter(event, {groupIndex, itemIndex:0})
: null
}
>
<div className="drag-and-drop-group-title">{group.title}</div>
{group.items.map((item, itemIndex) => (
<div
draggable
onDragStart={(dragEvent) => {handleDragStart(dragEvent, {groupIndex, itemIndex})}}
onDragEnter={dragging
? (event) => {handleDragEnter(event, {groupIndex, itemIndex})}
: null
}
key={item}
className={dragging
? getStyles({groupIndex, itemIndex})
: "drag-and-drop-item"
}
>
{item}
</div>
))}
</div>
))}
</div>
</section>
)
}
export default Column;
/* Column.css */
#import url(./../../index.css);
.drag-and-drop{
display: grid;
gap: 0.5rem;
width: 100%;
height: 100%;
grid-template-columns: repeat(auto-fill, 300px);
background-color: var(--primary-color);
padding: 0.5rem;
align-items: start;
}
.drag-and-drop-group{
background-color: var(--secondary-color-80);
padding: 0.5rem;
}
.drag-and-drop-group-title{
margin-bottom: 0.5rem;
font-size: 1.2rem;
font-family: 'Poppins';
}
.drag-and-drop-item {
background-color: #FFFFFF;
color: var(--primary-color);
min-height: 150px;
}
.drag-and-drop-item:not(:last-of-type){
margin-bottom: 0.5rem;
}
.current{
background-color: var(--primary-color);
}
Findings:
The problem occurs exclusively on my MacBook, and therefore not on my Windows computer.
I have tried with Event.preventDefault();, however without luck.
Question:
What can I do to prevent the animation on Mac OS?
Related
I have converted my tensorflow model into a .tflite file, not sure if it is the correct format, but I tried uploading it onto firebase but it was not compatible with reactjs as it only gives the code snippets in Kotlin, Java and swift, whereas reactjs does not use any of the 3 as it is a Javascript library. I followed this tutorial where I used a cocoSsd model from tensorflow to detect my images, I am writing this to ask, what am I supposed to do in order for this reactjs app to use my custom trained model instead of the model provided by tensorflow, cocoSsd.
I assume I have to host my model on a public web server and import it as an API?
Would appreciate if someone educate me on this, first time implementing a custom model onto a webapp, hence I am quite lost, thank you!
My code snippet is below
import React, { useRef, useState } from "react";
import styled from "styled-components";
import "#tensorflow/tfjs-backend-cpu";
//import "#tensorflow/tfjs-backend-webgl";
import * as cocoSsd from "#tensorflow-models/coco-ssd";
const ObjectDetectorContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const DetectorContainer = styled.div`
min-width: 200px;
height: 700px;
border: 3px solid #fff;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
`;
const TargetImg = styled.img`
height: 100%;
`;
const HiddenFileInput = styled.input`
display: none;
`;
const SelectButton = styled.button`
padding: 7px 10px;
border: 2px solid transparent;
background-color: #fff;
color: #0a0f22;
font-size: 16px;
font-weight: 500;
outline: none;
margin-top: 2em;
cursor: pointer;
transition: all 260ms ease-in-out;
&:hover {
background-color: transparent;
border: 2px solid #fff;
color: #fff;
}
`;
const TargetBox = styled.div`
position: absolute;
left: ${({ x }) => x + "px"};
top: ${({ y }) => y + "px"};
width: ${({ width }) => width + "px"};
height: ${({ height }) => height + "px"};
border: 4px solid #1ac71a;
background-color: transparent;
z-index: 20;
&::before {
content: "${({ classType, score }) => `${classType} ${score.toFixed(1)}%`}";
color: #1ac71a;
font-weight: 500;
font-size: 17px;
position: absolute;
top: -1.5em;
left: -5px;
}
`;
export function ObjectDetector(props) {
const fileInputRef = useRef();
const imageRef = useRef();
const [imgData, setImgData] = useState(null);
const [predictions, setPredictions] = useState([]);
const [isLoading, setLoading] = useState(false);
const isEmptyPredictions = !predictions || predictions.length === 0;
const openFilePicker = () => {
if (fileInputRef.current) fileInputRef.current.click();
};
const normalizePredictions = (predictions, imgSize) => {
if (!predictions || !imgSize || !imageRef) return predictions || [];
return predictions.map((prediction) => {
const { bbox } = prediction;
const oldX = bbox[0];
const oldY = bbox[1];
const oldWidth = bbox[2];
const oldHeight = bbox[3];
const imgWidth = imageRef.current.width;
const imgHeight = imageRef.current.height;
const x = (oldX * imgWidth) / imgSize.width;
const y = (oldY * imgHeight) / imgSize.height;
const width = (oldWidth * imgWidth) / imgSize.width;
const height = (oldHeight * imgHeight) / imgSize.height;
return { ...prediction, bbox: [x, y, width, height] };
});
};
const detectObjectsOnImage = async (imageElement, imgSize) => {
const model = await cocoSsd.load({});
const predictions = await model.detect(imageElement, 6);
const normalizedPredictions = normalizePredictions(predictions, imgSize);
setPredictions(normalizedPredictions);
console.log("Predictions: ", predictions);
};
const readImage = (file) => {
return new Promise((rs, rj) => {
const fileReader = new FileReader();
fileReader.onload = () => rs(fileReader.result);
fileReader.onerror = () => rj(fileReader.error);
fileReader.readAsDataURL(file);
});
};
const onSelectImage = async (e) => {
setPredictions([]);
setLoading(true);
const file = e.target.files[0];
const imgData = await readImage(file);
setImgData(imgData);
const imageElement = document.createElement("img");
imageElement.src = imgData;
imageElement.onload = async () => {
const imgSize = {
width: imageElement.width,
height: imageElement.height,
};
await detectObjectsOnImage(imageElement, imgSize);
setLoading(false);
};
};
return (
<ObjectDetectorContainer>
<DetectorContainer>
{imgData && <TargetImg src={imgData} ref={imageRef} />}
{!isEmptyPredictions &&
predictions.map((prediction, idx) => (
<TargetBox
key={idx}
x={prediction.bbox[0]}
y={prediction.bbox[1]}
width={prediction.bbox[2]}
height={prediction.bbox[3]}
classType={prediction.class}
score={prediction.score * 100}
/>
))}
</DetectorContainer>
<HiddenFileInput
type="file"
ref={fileInputRef}
onChange={onSelectImage}
/>
<SelectButton onClick={openFilePicker}>
{isLoading ? "Recognizing..." : "Select Image"}
</SelectButton>
</ObjectDetectorContainer>
);
}
This webapp is basically asking for an image input, taking the image and throwing it to the model and displaying what the model detected.
Let me know if I'm too vague or if anything else is needed, thank you!
I'm trying to make a toast message component that works like this.
It comes in from outside the right screen, disappears out of the screen after a certain period of time, and the component get deleted after the animation.
It works perfectly when I make a single component. However, when I create multiple toast components, some of them just get removed before the animation starts.
I tried many ways for 3days, but nothing worked... 😭 I would appreciate it if you could check which part is wrong. Help me plz
Toast Contatiner
import React, { useState, useCallback } from 'react';
import ToastItem from './Toast';
import styled from 'styled-components';
type ToastItemInfo = {
content: string | React.ReactNode;
id: string;
};
const ToastContainer = () => {
const [toastItems, setToastItems] = useState<ToastItemInfo[]>([]);
const handleClick = () => {
setToastItems((toastItems) => [
{
content: 'Toasted !',
id: String(Math.random()).substring(2, 8),
},
...toastItems,
]);
};
const removeToast = useCallback((id: string) => {
setToastItems((toastItems) => toastItems.filter((toast) => toast.id !== id));
}, []);
const hasItems = (toastItems: ToastItemInfo[]) => {
return toastItems.length > 0;
};
const mapItems = (toastItems: ToastItemInfo[]) => {
return toastItems.map((toast) => (
<ToastItem key={toast.id} id={toast.id} removeToast={removeToast}>
{toast.content}
</ToastItem>
));
};
return (
<div>
<Button onClick={handleClick}>Toast 🍞</Button>
{hasItems(toastItems) && <Container>{mapItems(toastItems)}</Container>}
</div>
);
};
const Container = styled.div`
position: fixed;
display: flex;
flex-direction: column;
gap: 8px;
top: 0;
right: 0;
margin: 16px;
`;
// .slideout {
// animation: 0.5s linear animate;
// }
// #keyframes animate {
// from {
// transform: translateX(0);
// }
// to {
// transform: translateX(120%);
// }
// }
const Button = styled.button`
width: 132px;
height: 40px;
background: #434253;
color: white;
font-size: 1rem;
border: 2px solid white;
cursor: pointer;
`;
export default ToastContainer;
ToastItem
import React, { memo, useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import ProgressBar from './ProgressBar';
interface ToastProps {
id: string;
children: string | React.ReactNode;
removeToast: (id: string) => void;
}
const autoCloseDelay = 3000;
const ToastItem: React.FC<ToastProps> = (props) => {
const { id, removeToast, children } = props;
const [toClose, setToClose] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const removeFn = () => {
removeToast(id);
};
const { current } = ref;
const timeout = setTimeout(() => {
current?.addEventListener('animationend', () => {
removeToast(id);
});
setToClose(true);
}, autoCloseDelay);
return () => {
clearTimeout(timeout);
current?.removeEventListener('animationend', removeFn);
};
}, [id, removeToast]);
return (
<Toast id={id} ref={ref} onClick={() => removeToast(id)} toClose={toClose}>
{children}
<ProgressBar autoCloseDelay={autoCloseDelay} />
</Toast>
);
};
const slideIn = keyframes`
from{
transform: translateX(120%);
}
to{
transform: translateX(0);
}
`;
const slideOut = keyframes`
from{
transform: translateX(0);
}
to{
transform: translateX(120%);
}
`;
const Toast = styled.div<{ toClose: boolean }>`
box-sizing: border-box;
position: relative;
padding: 0 16px;
width: 250px;
height: 58px;
line-height: 58px;
background: #fabb4d;
color: #5f2700;
border-radius: 2px;
cursor: pointer;
animation: 0.5s ease 0s ${(props) => (props.toClose ? slideOut : slideIn)};
&:hover {
transform: scale(1.05);
}
`;
export default memo(ToastItem);
enter image description here
I can't get the data from the API to display it on the screen when I try to retrieve it I get a 400 error I don't know how to retrieve the data I put the right routes and so far nothing works.
import Card from "../../components/Card";
import styled from "styled-components";
import { Loader } from "../../utils/styles/Atoms";
import { useFetch, useTheme } from "../../utils/hooks";
import freelancers from "../Freelances";
const CardsContainer = styled.div`
display: grid;
gap: 24px;
grid-template-rows: 350px 350px;
grid-template-columns: repeat(2, 1fr);
align-items: center;
justify-items: center;
`;
const LoaderWrapper = styled.div`
display: flex;
justify-content: center;
`;
export function getFreelance(id) {
const freelance = [];
console.log(freelance);
return freelance.find((freelancer) => freelancer.id === id);
}
function Freelance() {
const { theme } = useTheme();
const freelanceData = getFreelance();
const { data, isLoading, error } = useFetch(
`http://localhost:8000/profile/?id=${freelanceData}`
);
console.log(data);
const freelanceProfil = data?.freelanceProfil;
if (error) {
return <span>Oups il y a eu un problème</span>;
}
return (
<div>
{isLoading ? (
<LoaderWrapper>
<Loader theme={theme} data-testid="loader" />
</LoaderWrapper>
) : (
<CardsContainer>
{getFreelance((profile, index) => (
<Card
key={`${profile.name}-${index}`}
label={profile.job}
title={profile.name}
picture={profile.picture}
/>
))}
</CardsContainer>
)}
</div>
);
}
export default Freelance;
/*The different routes to make API calls*/
/*
Node Js API
ul
li /freelances
li /profile/?id={id}
li /survey
li /results/?a1={answer1}&a2={answer2}&a3={answer3}...
*/
/*The model to get the unique user using his Id*/
const freelancesData = require("../models/freelances");
function getFreelance(id) {
return freelancesData.find((freelancer) => freelancer.id === id);
}
module.exports = getFreelance;
I think you need to use the ID instead of the whole object to call the API. I suggest do it like this:
const freelanceData = getFreelance();
const freelanceId = freelanceData.id; // get the ID from data
const { data, isLoading, error } = useFetch(
`http://localhost:8000/profile/?id=${freelanceId}`
);
I used react.js Hooks with useState and useEffect, when I scroll-down and the screen comes down Header hides after 250 pixels. Now I want to know how to display Header using the react Hooks when I scroll up.
const Navbar = () => {
const [show, setShow] = useState(false)
const controlNavbar = () => {
if (window.scrollY > 250 ) {
setShow(true)
}else{
setShow(false)
}
}
useEffect(() => {
window.addEventListener('scroll', controlNavbar)
return () => {
window.removeEventListener('scroll', controlNavbar)
}
}, [])
and header:
<header className={`active ${show && 'hidden'}`}></header>
css:
.active{
height: 4rem;
width: 100%;
position: fixed;
top: 0px;
transition: 0.3s linear;
display: flex;
justify-content:stretch;
align-items: center;
background-color: #FFFFFF;
border-bottom: 1px solid rgba(0, 0, 0, .1);
z-index: 40;
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, .08);
/* padding: 0 7%; */
}
.hidden{
height: 4rem;
width: 100%;
z-index: 40;
border-bottom: 1px solid rgba(0, 0, 0, .1);
box-shadow: 0 2px 5px -1px rgba(0, 0, 0, .08);
position: fixed;
top: -80px;
transition: 0.3s linear;
}
Instead of using a static value (250), you need to perform dynamic checking with the previous scroll. This is my complete solution (using nextJS):
import React, { useState, useEffect } from 'react';
const Navbar = () => {
const [show, setShow] = useState(true);
const [lastScrollY, setLastScrollY] = useState(0);
const controlNavbar = () => {
if (typeof window !== 'undefined') {
if (window.scrollY > lastScrollY) { // if scroll down hide the navbar
setShow(false);
} else { // if scroll up show the navbar
setShow(true);
}
// remember current page location to use in the next move
setLastScrollY(window.scrollY);
}
};
useEffect(() => {
if (typeof window !== 'undefined') {
window.addEventListener('scroll', controlNavbar);
// cleanup function
return () => {
window.removeEventListener('scroll', controlNavbar);
};
}
}, [lastScrollY]);
return (
<nav className={`active ${show && 'hidden'}`}>
....
</nav>
);
};
export default Navbar;
Simplest answer using tailwindCSS
import React, {useState, useEffect} from 'react'
const Navbar = () => {
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [visible, setVisible] = useState(true)
const handleScroll = () => {
const currentScrollPos = window.scrollY
if(currentScrollPos > prevScrollPos){
setVisible(false)
}else{
setVisible(true)
}
setPrevScrollPos(currentScrollPos)
}
useEffect( () => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll)
})
return (
<div className={`bg-slate-700 h-14 sticky ${visible ? 'top-0' : ''} `}>
Some Company Name
</div>
)
}
export default Navbar
I have come up with better approach which is much optimized.
useEffect(() => {
let previousScrollPosition = 0;
let currentScrollPosition = 0;
window.addEventListener('scroll', function (e) {
// Get the new Value
currentScrollPosition = window.pageYOffset;
//Subtract the two and conclude
if (previousScrollPosition - currentScrollPosition < 0) {
setShow(false);
} else if (previousScrollPosition - currentScrollPosition > 0) {
setShow(true);
}
// Update the previous value
previousScrollPosition = currentScrollPosition;
});
}, []);
From a couple of searches here I stumbled upon a scroll funtion on raw js but you can implement it in your case and tweak it how you want it, because currently your function will only use the false if scrollY < 250...
On your controlNav function create a variable that will track the location/point of your previous scroll then compare it to the current value of the scroll.. it will look like this:
const controlNavbar = () => {
if (window.scrollY >= this.lastScroll ) {
setShow(true)
}else{
setShow(false)
}
this.lastScroll = window.scrollY;
}
NB This will only work for scroll up and scroll down with no minimum value..
reference to the raw js
I am trying to achieve this: https://codesandbox.io/s/framer-motion-2-drag-to-reorder-fc4rt?file=/src/use-position-reorder.js
Basically you can drag a card, place it somewhere in the container and reorder the card based on where you drag/put it from the original place.
The problem is that in my code, you can drag the card but not place it. I tried to change the code so many times but I can't find a way out of it. Below you will find the code where I render the component FavouriteTreeNodeComponent which is a LI(List) nested in FavouriteTreeNodesList a UL(Unordered List) :
// react
import { useRef, useEffect, useCallback, useState } from 'react';
// animation spring
import { animated, Transition, useSpring } from 'react-spring';
import { AnimateSharedLayout } from 'framer-motion';
// styled components
import styled from 'styled-components';
// framer motion
import { motion } from 'framer-motion';
import { usePositionReorder } from './ReorderingModalNodes/use-position-reorder';
// icons
import { MdClose } from 'react-icons/md';
// components
import TemplateItem from './TemplateItem';
import SearchBar from './SearchBar';
import DurationIntervalComponent from './DurationIntervalComponent';
import ContextItem from './ContextItem';
import FavouriteTreeNodeComponent from './FavouriteTreeNodeComponent';
const TimeEntryModal = ({ showModal, setShowModal }) => {
// STATES
const [selected, setSelected] = useState('');
const [reportingTemplates, setReportingTemplates] = useState([]);
const [reportingContexts, setReportingContexts] = useState([]);
// USEREF
const modalRef = useRef();
// ANIMATION MODAL
const animation = useSpring({
config: {
duration: 250,
},
opacity: showModal ? 1 : 0,
transform: showModal ? `translateY(0%)` : `translateY(-100%)`,
});
// CLOSING MODAL FROM BACKGROUND
const closeModal = (e) => {
if (modalRef.current === e.target) {
setShowModal(false);
}
};
// CLOSING MODAL WITH ESCAPE BUTTON
const keyPress = useCallback(
(e) => {
if (e.key === 'Escape' && showModal) {
setShowModal(false);
console.log('I pressed');
}
},
[setShowModal, showModal]
);
useEffect(() => {
document.addEventListener('keydown', keyPress);
return () => document.removeEventListener('keydown', keyPress);
}, [keyPress]);
const variants = {
hidden: {
y: '-100vh',
},
visible: { y: 0, transition: { ease: 'easeInOut', duration: 1 } },
exit: {
y: '-100vh',
},
};
// DATA
const colorsData = [
'#86d8bda1',
'#eb472a83',
'#8fcfe3ad',
'#db8ddfb0',
'#e8e46fc0',
'#e1aa66b2',
];
const List = [
'3000',
'4000',
'5000',
'6000',
'7000',
'8000',
];
// FAVOURITES LIST ANIMATION
const [updatedList, updatePosition, updateOrder] =
usePositionReorder(List);
// FETCHES
useEffect(() => {
getTemplates();
}, []);
const url = 'http://localhost:8080/graphql';
const getTemplates = async () => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `query {
reportingTemplates {
name
_id
colorScheme
reportingContexts {
name
_id
}
}
}`,
}),
});
const data = await res.json();
console.log(data);
const dataWithColors = data.data.reportingTemplates.map((obj, i) => ({
...obj,
color: colorsData[i],
}));
setReportingTemplates(dataWithColors);
setSelected(dataWithColors[0].color);
setReportingContexts(dataWithColors[0].reportingContexts);
console.log('data', dataWithColors);
} catch (err) {
console.log('ERROR', err);
}
};
console.log('data state', reportingTemplates);
return (
<Background ref={modalRef} onClick={closeModal}>
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
>
<ModalWrapper>
<ModalContent>
<Nav>
<h3>New Entry Tirsdag 2. Sep </h3>
</Nav>
<ModalColumns>
<ColumnOne>
<TemplatesContainer>
<AnimateSharedLayout>
<Templates>
{reportingTemplates.map((template, index) => (
<TemplateItem
key={template.name}
color={template.color}
title={template.name}
isSelected={selected === template.color}
onClick={() => {
setReportingContexts(
reportingTemplates[index].reportingContexts
);
setSelected(template.color);
}}
/>
))}
</Templates>
</AnimateSharedLayout>
</TemplatesContainer>
<InformationBlock>
<h4>Information</h4>
{reportingContexts.map((context) => (
<ContextItem
key={context.name}
text={context.name}
// isSelected={selected === context.color}
// onClick={() => setSelected(context.color)}
/>
))}
</InformationBlock>
<DurationIntervalComponent />
</ColumnOne>
<ColumnTwo>
<SearchBar />
<Recent>
<h4>Recent</h4>
</Recent>
<Favourites>
<h4>Favourites</h4>
<FavouriteTreeNodesList>
{updatedList.map((treeNode, index) => (
<FavouriteTreeNodeComponent
key={index}
index={index}
text={treeNode}
updateOrder={updateOrder}
updatePosition={updatePosition}
/>
))}
</FavouriteTreeNodesList>
</Favourites>
</ColumnTwo>
</ModalColumns>
</ModalContent>
<CloseModalButton
aria-label="Close Modal"
onClick={() => setShowModal((prev) => !prev)}
/>
<Footer>
<DeleteBlock>
<p> Delete </p>
</DeleteBlock>
<CancelNSaveBlock>
<Cancel>
<p> Cancel </p>
</Cancel>
<SaveNClose>
<p> Save and close </p>
</SaveNClose>
<SaveNAdd>
<p> Save and add another </p>
</SaveNAdd>
</CancelNSaveBlock>
</Footer>
</ModalWrapper>
</motion.div>
</Background>
);
};
// STYLES
const Background = styled.div`
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
position: fixed;
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
`;
const ModalWrapper = styled.div`
width: 80vw;
height: 80vh;
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.2);
background: #fff;
color: #000;
display: flex;
flex-direction: column;
align-items: stretch;
border-radius: 10px;
`;
const CloseModalButton = styled(MdClose)`
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
padding: 0;
z-index: 10;
`;
// MODAL CONTENT
const ModalContent = styled.div`
padding: 20px;
position: relative;
`;
// NAV
const Nav = styled.div``;
const ModalColumns = styled.div`
padding: 10px 0px;
display: flex;
`;
// COLUMN ONE
const ColumnOne = styled.div`
width: 50%;
padding: 20px;
`;
const TemplatesContainer = styled.div``;
const Templates = styled.ul`
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: initial;
justify-content: center;
`;
const InformationBlock = styled.div`
padding-bottom: 20px;
`;
// COLUMN TWO
const ColumnTwo = styled.div`
width: 50%;
padding: 20px;
`;
const Recent = styled.div`
background-color: #f6f6f9;
border: 1px solid #c5c5c5;
border-radius: 20px;
`;
const Favourites = styled.div`
list-style: none;
`;
const FavouriteTreeNodesList = styled.ul`
position: relative;
width: 100%;
`;
// FOOTER
const Footer = styled.div`
display: flex;
position: fixed;
padding: 0 20px;
bottom: 10px;
/* text-align: center; */
width: 100%;
`;
const DeleteBlock = styled.div`
color: #fb423a;
`;
const CancelNSaveBlock = styled.div`
display: flex;
margin-left: auto;
`;
// const [{name:"cancel", color func: ()=>sdasd }, "sada", "asdasd"]
const Cancel = styled.div`
color: #02b396;
margin-right: 40px;
`;
const SaveNClose = styled.div`
font-weight: bolder;
color: #02b396;
margin-right: 40px;
`;
const SaveNAdd = styled.div`
font-weight: bolder;
color: #02b396;
`;
export default TimeEntryModal;
After that the list FavouriteTreeNodeComponent is rendered in the code below:
import { useState } from 'react';
// styled components
import styled from 'styled-components';
// framer motion
import { motion } from 'framer-motion';
import { useMeasurePosition } from './ReorderingModalNodes/use-measure-position';
const FavouriteTreeNodeComponent = ({
text,
updateOrder,
updatePosition,
index,
}) => {
const [isdragged, setIsDragged] = useState(false);
const itemRef = useMeasurePosition((pos) => updatePosition(index, pos));
return (
<ListContainer>
<motion.div
style={{
zIndex: isdragged ? 2 : 1,
height: text.length * 10,
padding: "5px",
}}
dragConstraints={{
top: 0,
bottom: 0,
}}
dragElastic={1}
layout
ref={itemRef}
onDragStart={() => setIsDragged(true)}
onDragEnd={() => setIsDragged(false)}
animate={{
scale: isdragged ? 1.05 : 1,
}}
onViewportBoxUpdate={(_, delta) => {
isdragged && updateOrder(index, delta.y.translate);
}}
drag="y">{text}
</motion.div>
</ListContainer>
);
};
// STYLES
const ListContainer = styled.li`
height: auto;
gap: 9px 0px;
position: relative;
padding: 10px 0px;
list-style: none;
div {
background-color: white;
border-radius: 5px;
border: 1px solid #c5c5c5;
}
`;
export default FavouriteTreeNodeComponent;
The above code is connected to the file: useMeasurePosition same as the original project:
import { useEffect, useRef } from 'react';
export function useMeasurePosition(update) {
// We'll use a `ref` to access the DOM element that the `motion.li` produces.
// This will allow us to measure its height and position, which will be useful to
// decide when a dragging element should switch places with its siblings.
const ref = useRef(null);
// Update the measured position of the item so we can calculate when we should rearrange.
useEffect(() => {
update({
height: ref.current.offsetHeight,
top: ref.current.offsetTop,
});
});
return ref;
}
and to the file: usePositionReorder:
import { useState, useRef } from 'react';
import { clamp, distance } from 'popmotion';
import { arrayMoveImmutable as move} from 'array-move';
export function usePositionReorder(initialState) {
const [order, setOrder] = useState(initialState);
// We need to collect an array of height and position data for all of this component's
// `Item` children, so we can later us that in calculations to decide when a dragging
// `Item` should swap places with its siblings.
const positions = useRef([]).current;
const updatePosition = (i, offset) => (positions[i] = offset);
// Find the ideal index for a dragging item based on its position in the array, and its
// current drag offset. If it's different to its current index, we swap this item with that
// sibling.
const updateOrder = (i, dragOffset) => {
const targetIndex = findIndex(i, dragOffset, positions);
if (targetIndex !== i) setOrder(move(order, i, targetIndex));
};
return [order, updatePosition, updateOrder];
}
const buffer = 30;
export const findIndex = (i, yOffset, positions) => {
let target = i;
const { top, height } = positions[i];
const bottom = top + height;
// If moving down
if (yOffset > 0) {
const nextItem = positions[i + 1];
if (nextItem === undefined) return i;
const swapOffset =
distance(bottom, nextItem.top + nextItem.height / 2) + buffer;
if (yOffset > swapOffset) target = i + 1;
// If moving up
} else if (yOffset < 0) {
const prevItem = positions[i - 1];
if (prevItem === undefined) return i;
const prevBottom = prevItem.top + prevItem.height;
const swapOffset = distance(top, prevBottom - prevItem.height / 2) + buffer;
if (yOffset < -swapOffset) target = i - 1;
}
return clamp(0, positions.length, target);
};