Related
Problem: I call playerPlaceShip (from a DOM interaction module) inside a game loop module. This lets the user hover the board, click to place the ship, and finally in the event handler call board.placeShip() (from the gameBoard module). This works in isolation, but when I add another playerPlaceShip call to place an additional ship, it executes immediately before the first ship can be placed by clicking.
Desired outcome: A way to wait until the click event from the first function call completes before the next function call begins.
What I've tried: Hours of unsuccessfully trying to write and use promises. Hours of reading about promises. Spent a lot of time unsuccessfully trying to rethink how the code is structured. It seems like the click event should be driving the next action, but I don't see how to do that without writing more and more function calls inside the click event handler, which would seem to take control of the game flow away from the game loop module and put it in the DOM interaction module.
Full modules on GitHub: https://github.com/Pete-Fowler/battleship/tree/player-place-ships/src/modules
Code excerpts:
// In game loop module after creating ships, players, and board objects:
// Render Board
renderBoard(p1Board, p1Box);
renderBoard(p2Board, p2Box);
// Player place ships - lets user hover and click board to place
playerPlaceShip(p1Board, p1Carrier);
playerPlaceShip(p1Board, p1Battleship); // this gets called too soon before click event from the first call completes
// In DOM module:
const clickToPlace = (e, board, ship) => {
let { x, y } = e.target.dataset;
x = parseInt(x, 10);
y = parseInt(y, 10)
board.place(ship, x, y, axis);
renderShadow(e, 'place', ship.length);
removeListeners();
}
// Main function for player to place ship
const playerPlaceShip = (board, ship) => {
const squares = document.querySelectorAll('#p1 .board .square');
narrative.textContent = `Lead your ${ship.type} into battle. Press X to steer.`;
squares.forEach(square => {
square.addEventListener('mouseover', (e) => renderShadow(e, 'fill', ship.length));
square.addEventListener('mouseout', (e) => renderShadow(e, 'clear', ship.length));
square.addEventListener('click', (e) => clickToPlace(e, board, ship));
});
window.addEventListener('keydown', (e) => {
if(e.key === 'x') {
switchAxis();
squares.forEach(square => square.classList.remove('hovered'));
renderShadow(lastCoords, 'fill', ship.length);
}
});
}
Thanks!
I wasn't able to checkout the branch, got a strange error: invalid path 'src/images/background.jpg:Zone.Identifier', maybe because of the colon : after jpg. So I downloaded the zip.
Otherwise I would have done a pull request, that would be easier for you to merge.
I added logic so that the ship is always inside the board, and created a custom event to trigger after place ship. There are comments, see if this will help you move on.
game.js
import gameBoard from "./gameBoard";
import player from "./player";
import makeShip from "./ship";
import { p1Box, p2Box, playerPlaceShip, placeShipEventName, AIPlaceShip, renderBoard, UIAttack } from "./DOM";
const startingShipCount = 5;
// SETUP
// Make game boards
const p1Board = gameBoard();
p1Board.init();
const p2Board = gameBoard();
p2Board.init();
// Make players
const p1 = player("Gustav", p1Board, "human");
const p2 = player("Terminator", p2Board, "AI");
// Make p1 ships
const p1Ptb = makeShip("patrolBoat");
const p1Sub = makeShip("sub");
const p1Destroyer = makeShip("destroyer");
const p1Battleship = makeShip("battleship");
const p1Carrier = makeShip("carrier");
// Make AI ships
const p2Ptb = makeShip("patrolBoat");
const p2Sub = makeShip("sub");
const p2Destroyer = makeShip("destroyer");
const p2Battleship = makeShip("battleship");
const p2Carrier = makeShip("carrier");
// Render Board
renderBoard(p1Board, p1Box);
renderBoard(p2Board, p2Box);
// AI place ships
p2Board.place(p2Ptb, 0, 1, "y");
p2Board.place(p2Sub, 2, 6, "y");
p2Board.place(p2Destroyer, 4, 2, "y");
p2Board.place(p2Battleship, 6, 6, "y");
p2Board.place(p2Carrier, 8, 4, "y");
renderBoard(p1Board, p1Box);
renderBoard(p2Board, p2Box);
//################################################
//###################### HANDLE placeShipPhase
//################################################
let countShipsPlaced = 0;
const handlePlaceShipPhase = () => {
countShipsPlaced++;
if (countShipsPlaced == startingShipCount) {
startGame();
} else {
playerPlaceShip(p1Board, p1Carrier);
}
};
//######################################################
//####### LISTENING to the custom event Place Ship
//######################################################
window.addEventListener(placeShipEventName, handlePlaceShipPhase);
// Player places ships
playerPlaceShip(p1Board, p1Carrier);
const startGame = () => {
alert("Game started, battle!");
};
// MAIN GAME LOOP - will need loop
// Player attack
// UIAttack(p2Board);
// AI attack
// Gameover - after exit loop
// The game loop should set up a new game by creating Players and Gameboards.
// For now just populate each Gameboard with predetermined coordinates. You can
// implement a system for allowing players to place their ships later.
// The game loop should step through the game turn by turn using only methods
// from other objects. If at any point you are tempted to write a new function
// inside the game loop, step back and figure out which class or module that
// function should belong to.
// Create conditions so that the game ends once one players ships have all
// been sunk. This function is appropriate for the Game module.
DOM.js
/* eslint-disable no-unused-expressions */
const p1Box = document.querySelector("#p1");
const p2Box = document.querySelector("#p2");
const narrative = document.querySelector("#narrative");
let axis = "y"; // used to render shadow in playerPlaceShip
let selectedSquares = [];
let lastCoords;
const boardSize = 10;
//save the current ship to be used in the "x" key event listender
let currentShip;
//moved outside of the placeship otherwise will add duplicated events
window.addEventListener("keydown", (e) => {
if (e.key.toLocaleLowerCase() === "x") {
const squares = document.querySelectorAll("#p1 .board .square");
switchAxis();
squares.forEach((square) => square.classList.remove("hovered"));
renderShadow(lastCoords, "fill", currentShip.length);
}
});
//#############################################
//##### CREATING the custom event Place Ship
//#############################################
const placeShipEventName = "playerplaceship";
const placeShipEvent = new Event(placeShipEventName);
// Helper functions for playerPlaceShip
const switchAxis = () => {
axis === "x" ? (axis = "y") : (axis = "x");
};
const renderShadow = (e, fill, length) => {
let { x, y } = e.target.dataset;
x = parseInt(x, 10);
y = parseInt(y, 10);
selectedSquares = [];
let count = countOfSquaresOutOfBoard(x, y, length);
//#### LOGIC TO RENDER SHIP ALWAYS INSIDE BOARD
for (let i = -count; i < length - count; i++) {
setSelectedSquares(x, y, i);
}
for (const el of selectedSquares) {
fill === "fill" ? el.classList.add("hovered") : el.classList.remove("hovered");
if (fill === "place") {
el.classList.add("placed");
}
}
lastCoords = e;
};
const removeListeners = () => {
const squares = document.querySelectorAll("#p1 .board .square");
squares.forEach((square) => {
square.replaceWith(square.cloneNode());
});
};
const clickToPlace = (shipSquare, board, ship) => {
let { x, y } = shipSquare.dataset;
x = parseInt(x, 10);
y = parseInt(y, 10);
board.place(ship, x, y, axis);
renderShadow(lastCoords, "place", ship.length);
removeListeners();
//#######################################################
//############# TRIGGERING the custom event place ship
//#########################################################
window.dispatchEvent(placeShipEvent);
console.log(board.getMap());
};
// Main function for player to place ship
const playerPlaceShip = (board, ship) => {
currentShip = ship;
const squares = document.querySelectorAll("#p1 .board .square");
narrative.textContent = `Lead your ${ship.type} into battle. Press X to steer.`;
squares.forEach((square) => {
square.addEventListener("mouseover", (e) => renderShadow(e, "fill", ship.length));
square.addEventListener("mouseout", (e) => renderShadow(e, "clear", ship.length));
square.addEventListener("click", (e) => clickToPlace(selectedSquares[0], board, ship));
});
};
const countOfSquaresOutOfBoard = (x, y, length) => {
let count = 0;
if (axis === "x") {
count = x + length - boardSize;
}
if (axis === "y") {
count = y + length - boardSize;
}
return count < 0 ? 0 : count;
};
const setSelectedSquares = (x, y, i) => {
if (axis === "x") {
selectedSquares.push(document.querySelector(`#p1 .square[data-x="${x + i}"][data-y="${y}"]`));
} else {
selectedSquares.push(document.querySelector(`#p1 .square[data-x="${x}"][data-y="${y + i}"]`));
}
};
// Lets AI place ship
const AIPlaceShip = (board) => {};
const renderBoard = (board, box) => {
// Clear old content prior to re-render if needed
let grid = document.querySelector(`#${box.id} .board`);
if (grid) {
grid.textContent = "";
} else {
grid = document.createElement("div");
grid.className = "board";
}
// Individual squares on board
for (let i = 0; i <= 9; i += 1) {
for (let j = 9; j >= 0; j -= 1) {
const square = document.createElement("div");
square.className = "square";
square.dataset.x = i;
square.dataset.y = j;
grid.append(square);
}
}
box.append(grid);
};
// Player attack phase - sends x, y from clicked square to board.incoming()
const attackCallback = (e, board) => {
const { x, y } = e.target.dataset;
board.incoming(x, y);
const squares = document.querySelectorAll("#p2 .square");
squares.forEach((el) => {
el.removeEventListener("click", attackCallback);
el.classList.remove("hoverable");
});
console.log(board.getMap());
};
// Player attack phase - adds click event listener and hover effect
const UIAttack = (board) => {
const squares = document.querySelectorAll("#p2 .square");
squares.forEach((el) => {
el.addEventListener("click", (e) => attackCallback(e, board));
el.classList.add("hoverable");
});
narrative.textContent = "Click to fire on the enemy fleet";
};
export { p1Box, p2Box, placeShipEventName, playerPlaceShip, AIPlaceShip, renderBoard, UIAttack };
I am using jointjs to create an interactive flowcharting application, is there a way to prevent elements from being dragged over the top of one another?
You can revert the position of an element when the user finishes dragging and overlap is found.
paper.on({
'element:pointerdown': (elementView, evt) => {
// store the position before the user starts dragging
evt.data = { startPosition: elementView.model.position() };
},
'element:pointerup': (elementView, evt) => {
const { model: element } = elementView;
const { model: graph } = paper;
const elementsUnder = graph.findModelsInArea(element.getBBox()).filter(el => el !== element);
if (elementsUnder.length > 0) {
// an overlap found, revert the position
const { x, y } = evt.data.startPosition;
element.position(x, y);
}
}
});
I have a problem with my script, and that is that I want to play an audio when I click on a .bbp button, but this button is inside a hidden div that is then cloned.
Only when the cloned div becomes visible in the DOM, I want to play an audio when I click on .bbp, but it does not work for me.
SEE DEMO LIVE (Codepen) - The Snippet does not run on Stackoverflow
Note that if you comment #products, the audio assigned to .bbp yes will play, otherwise it will NOT play, since the audio
script can not identify if #products is visible in the DOM or not.
So, first I need to know that .bbp is visible, and I can not find how I can do it.
Any idea...?
Thanks in advance!
//-----------------
HTML & CSS
#products {display:none}
#derecha {display:none}
<div class="comprar">Clone 1</div> <!--Clone the div from "products" to "derecha"-->
<div class="bbp">X</div> <!--Delete the cloned div placed into "derecha"-->
SCRIP (Play Audio)
let audioHolderComprar = {};
$('.comprar').click(()=>{
let tempIdentifier = Date.now();
audioHolderComprar[tempIdentifier] = new Audio('comprar.mp3');
audioHolderComprar[tempIdentifier].play();
setTimeout(() => {
delete audioHolderComprar[tempIdentifier];
}, audioHolderComprar[tempIdentifier].duration + 1200);
});
//------------------
let audioHolderBorrar = {};
$('.bbp').click(()=>{
let tempIdentifier = Date.now();
audioHolderBorrar[tempIdentifier] = new Audio('borrar.mp3');
audioHolderBorrar[tempIdentifier].play();
setTimeout(() => {
delete audioHolderBorrar[tempIdentifier];
}, audioHolderBorrar[tempIdentifier].duration + 1200);
});
As I've mentioned in my comment, you have two places where you handle the click event for .bpp - these interfere with each other.
Also you're mixing the places where you should add html and javascript code. Though it works, it's a little bit messy.
Replace all of the content in your HTML pane on the left by this:
<div id="container">
<div id="productos">
<!-- =============== -->
<div id="cont-p1" class="cont-p">
<div id="producto-1">
<div class="img-prod"><img src="https://upload.wikimedia.org/wikipedia/commons/3/39/Lichtenstein_img_processing_test.png"></div>cont-p1 cloned!<br><br>Input Value = 1</div>
<input class="add-prod" type="num" value="1">
<div class="bbp">X</div></div>
</div> <!-- // productos -->
<div class="derecha" id="derecha"></div> <!-- // div derecha -->
<div id="comp-p1" data-clone="cont-p1" class="comp-clone comprar">Clone 1</div>
<div class="cont-num" id="clicks">0</div>
<div class="cont-num" id="clicksdos">0</div>
<div id="cont-resultado">
<input name="total" id="total">
</div>
<div id="cont-note">How to play the audio on the button to close the cloned div <span>.bbp</span><br>( <span class="red">X</span> ),<br>if the audio script can not know that it has been cloned...?
<br><br>
Note the CSS (line 3) that the div container of the all div´s that must be cloned is in <span>display=none</span>, but if you comment this line it can reproduce the audio onclick in the X button</div>
</div> <!-- // container -->
and all of the following goes into the JS pane to the right:
/*
https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js
*/
let audioHolderComprar = {};
$('.comprar').click(()=>{
let tempIdentifier = Date.now();
audioHolderComprar[tempIdentifier] = new Audio('https://notificationsounds.com/soundfiles/8b16ebc056e613024c057be590b542eb/file-sounds-1113-unconvinced.mp3');
audioHolderComprar[tempIdentifier].play();
// removing after play process gets over so if won't consume memory
setTimeout(() => {
delete audioHolderComprar[tempIdentifier];
}, audioHolderComprar[tempIdentifier].duration + 1200 /* you can remove threshold value if you wants to */);
});
//------------------
let audioHolderBorrar = {};
let clicks = 0;
let clicksdos = 0;
const safeInt = (key) => {
let value = parseInt(getValue(key));
return (isNaN(value) || value < 0) ? 0 : value;
}
// This loads our clicks from the LocalStorage
const loadClicks = () => {
clicks = safeInt('clicks');
clicksdos = safeInt('clicksdos');
}
const loadHTML = () => {
return getValue('html', '');
}
const loadFromStorage = () => {
let html = loadHTML();
if (html !== '') {
loadClicks();
}
displayClicks();
document.querySelector(".derecha").innerHTML = html;
}
// Display the clicks on the screen
const displayClicks = () => {
clicks = (clicks === NaN) ? 0 : clicks;
clicksdos = (clicksdos === NaN) ? 0 : clicksdos;
document.querySelector('#clicks').innerHTML = clicks;
document.querySelector('#clicksdos').innerHTML = clicksdos;
// Hide / Show Result
let display = (clicks > 0) ? 'block' : 'none';
document.querySelector('#cont-resultado').style.display = display;
document.querySelector('.derecha').style.display = display;
//document.querySelector('#aviso-producto-agregado').style.display = "block";
}
const adjustClicks = (value) => {
clicks += value;
clicksdos += value;
storeValue('clicks', clicks);
storeValue('clicksdos', clicksdos);
displayClicks();
}
const addClick = () => adjustClicks(1);
const removeClick = () => adjustClicks(-1);
// Manage localStorage
const storeValue = (key, value) => (localStorage) ? localStorage.setItem(key, value) : '';
const getValue = (key, defaultValue) => (localStorage) ? localStorage.getItem(key) : defaultValue;
const storeHTML = () => storeValue("html", document.getElementsByClassName("derecha")[0].innerHTML);
// Add a node to the Derecha
const addToDerecha = (nodeId) => {
let node = document.querySelector(`#${nodeId}`);
document.querySelector('.derecha').appendChild(node.cloneNode(true));
storeHTML();
displaySuma();
};
// Monitor ALL click events
document.addEventListener('click', (event) => {
let target = event.target;
// Add
if (target.matches('.comp-clone')) {
addClick();
addToDerecha(event.target.dataset.clone);
}
// Remove
if (target.matches('.bbp')) {
let tempIdentifier = Date.now();
audioHolderBorrar[tempIdentifier] = new Audio('https://notificationsounds.com/soundfiles/99c5e07b4d5de9d18c350cdf64c5aa3d/file-sounds-1110-stairs.mp3');
audioHolderBorrar[tempIdentifier].play();
// removing after play process gets over so if won't consume memory
setTimeout(() => {
delete audioHolderBorrar[tempIdentifier];
}, audioHolderBorrar[tempIdentifier].duration + 1200 /* you can remove threshold value if you wants to */);
getParent('.derecha', target).removeChild(target.parentNode);
removeClick();
storeHTML();
displaySuma();
}
});
// This is just a helper function.
const getParent = (match, node) => (node.matches(match)) ? node : getParent(match, node.parentNode);
// New Script for sum inputs
//const displaySuma = () => document.getElementById("total").value = suma();
const displaySuma=()=>document.getElementById("total").value=suma().toLocaleString("es-ES");
const suma = function() {
return Array.from(document.querySelectorAll(".derecha div .add-prod"))
.reduce((a, v) => a + parseFloat(v.value), 0);
}
// Code to run when the document loads.
document.addEventListener('DOMContentLoaded', () => {
if (localStorage) {
loadFromStorage();
}
displaySuma();
});
</script>
<script>
// Displays the new product alert added when the scroll is detected in the div #derecha
var displaced = document.getElementById('derecha')
if (displaced.scrollHeight > displaced.offsetHeight) {
document.getElementById("notice-product-added").style.display = "block";
};
// LocalStorage for the div #notice-product-added
const showMsgCart=localStorage.getItem('showMsgCarrito');if(showMsgCart==='false'){$('#notice-product-added').hide();}$('#notice-product-added').on('click',function(){$('#notice-product-added').fadeOut('slow');localStorage.setItem('showMsgCarrito','false');});
After that you should hear the closing sound.
I will attempt to keep this brief, but I am not 100% sure of the correct method of achieving what I am aiming for. I have been thrown in the deep end with React with not much training, so I have most likely been going about most of this component incorrectly, a point in the right direction will definitely help, I don't really expect for someone to completely redo my component for me as it's quite long.
I have a navigation bar SubNav, that finds the currently active item based upon the url/path, this will then move an underline element that inherits the width of the active element. To do this, I find the position of the active item and position accordingly. The same goes for when a user hovers over another navigation item, or when the window resizes it adjusts the position accordingly.
I also have it when at lower resolutions, when the nav gets cut off to have arrows appear to scroll left/right on the navigation to view all navigation items.
Also, if on a lower resolution and the currently active navigation item is off screen, the navigation will scroll to that item and then position the underline correctly.
This, currently works as I have it in my component, this issue is, I don't believe I have done this correctly, I am using a lodash function delay to delay at certain points (I guess to get the correct position of certain navigation items, as it isn't correct at the time of the functions call), which I feel is not the way to go. This is all based on how fast the page loads etc and will not be the same for each user.
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
Without using the delay, the values coming back from my state are incorrect as they haven't been set yet.
My question is, how do I go about this correctly? I know my code below is a bit of a read but I have provided a CODESANBOX to play about with.
I have 3 main functions, that all sort of rely on one another:
getPostion()
This function finds the active navigation item, checks if it's within the viewport, if it is not, then it changes the left position of the navigation so it's the leftmost navigation item on the screen, and via setSizes(getSizes()) moves the underline directly underneath.
getSizes()
This is called as an argument within setSizes to update the sizes state, which returns the left and right boundaries of all navigation items
getUnderlineStyle()
This is called as an argument within setUnderLineStyle within the getSizes() function to update the position of the underline object in relation to the position of active navigation item grabbed from the sizes state, but I have to pass the sizesObj as an argument in setSizes as the state has not been set. I think this is where my confusion began, I think I was under the impression, that when I set the state, I could then access it. So, I started using delay to combat.
Below is my whole Component, but can be seen working in CODESANBOX
import React, { useEffect, useState, useRef } from "react";
import _ from "lodash";
import { Link, Route } from "react-router-dom";
import "../../scss/partials/_subnav.scss";
const SubNav = props => {
const subNavLinks = [
{
section: "Link One",
path: "link1"
},
{
section: "Link Two",
path: "link2"
},
{
section: "Link Three",
path: "link3"
},
{
section: "Link Four",
path: "link4"
},
{
section: "Link Five",
path: "link5"
},
{
section: "Link Six",
path: "link6"
},
{
section: "Link Seven",
path: "link7"
},
{
section: "Link Eight",
path: "link8"
}
];
const currentPath =
props.location.pathname === "/"
? "link1"
: props.location.pathname.replace(/\//g, "");
const [useArrows, setUseArrows] = useState(false);
const [rightArrow, updateRightArrow] = useState(false);
const [leftArrow, updateLeftArrow] = useState(false);
const [sizes, setSizes] = useState({});
const [underLineStyle, setUnderLineStyle] = useState({});
const [arrowStyle, setArrowStyle] = useState({});
const [activePath, setActivePath] = useState(currentPath);
const subNavRef = useRef("");
const subNavListRef = useRef("");
const arrowRightRef = useRef("");
const arrowLeftRef = useRef("");
let elsRef = Array.from({ length: subNavLinks.length }, () => useRef(null));
useEffect(
() => {
const reposition = getPosition();
subNavArrows(window.innerWidth);
if (!reposition) {
setSizes(getSizes());
}
window.addEventListener(
"resize",
_.debounce(() => subNavArrows(window.innerWidth))
);
window.addEventListener("resize", () => setSizes(getSizes()));
},
[props]
);
const getPosition = () => {
const activeItem = findActiveItem();
const itemHidden = findItemInView(activeItem);
if (itemHidden) {
const activeItemBounds = elsRef[
activeItem
].current.getBoundingClientRect();
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth =
arrowLeftRef.current !== "" && arrowLeftRef.current !== null
? arrowLeftRef.current.getBoundingClientRect().width
: arrowRightRef.current !== "" && arrowRightRef.current !== null
? arrowRightRef.current.getBoundingClientRect().width
: 30;
const activeItemPos =
activeItemBounds.left * -1 + arrowWidth + currentPos;
const styling = {
left: `${activeItemPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
400,
setArrowStyle(styling)
);
return true;
}
return false;
};
const findActiveItem = () => {
let activeItem;
subNavLinks.map((i, index) => {
const pathname = i.path;
if (pathname === currentPath) {
activeItem = index;
return true;
}
return false;
});
return activeItem;
};
const getSizes = () => {
const rootBounds = subNavRef.current.getBoundingClientRect();
const sizesObj = {};
Object.keys(elsRef).forEach(key => {
const item = subNavLinks[key].path;
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const left = bounds.left - rootBounds.left;
const right = rootBounds.right - bounds.right;
sizesObj[item] = { left, right };
});
setUnderLineStyle(getUnderlineStyle(sizesObj));
return sizesObj;
};
const getUnderlineStyle = (sizesObj, active) => {
sizesObj = sizesObj.length === 0 ? sizes : sizesObj;
active = active ? active : currentPath;
if (active == null || Object.keys(sizesObj).length === 0) {
return { left: "0", right: "100%" };
}
const size = sizesObj[active];
const styling = {
left: `${size.left}px`,
right: `${size.right}px`,
transition: `left 300ms, right 300ms`
};
return styling;
};
const subNavArrows = windowWidth => {
let totalSize = sizeOfList();
_.delay(
() => {
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
300,
setUseArrows(totalSize > windowWidth)
);
};
const sizeOfList = () => {
let totalSize = 0;
Object.keys(elsRef).forEach(key => {
const el = elsRef[key];
const bounds = el.current.getBoundingClientRect();
const width = bounds.width;
totalSize = totalSize + width;
});
return totalSize;
};
const onHover = active => {
setUnderLineStyle(getUnderlineStyle(sizes, active));
setActivePath(active);
};
const onHoverEnd = () => {
setUnderLineStyle(getUnderlineStyle(sizes, currentPath));
setActivePath(currentPath);
};
const scrollRight = () => {
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowRightRef.current.getBoundingClientRect().width;
const subNavOffsetWidth = subNavRef.current.clientWidth;
let nextElPos;
for (let i = 0; i < elsRef.length; i++) {
const bounds = elsRef[i].current.getBoundingClientRect();
if (bounds.right > subNavOffsetWidth) {
nextElPos = bounds.left * -1 + arrowWidth + currentPos;
break;
}
}
const styling = {
left: `${nextElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const scrollLeft = () => {
const windowWidth = window.innerWidth;
// const lastItemInView = findLastItemInView();
const firstItemInView = findFirstItemInView();
let totalWidth = 0;
const hiddenEls = elsRef
.slice(0)
.reverse()
.filter((el, index) => {
const actualPos = elsRef.length - 1 - index;
if (actualPos >= firstItemInView) return false;
const elWidth = el.current.getBoundingClientRect().width;
const combinedWidth = elWidth + totalWidth;
if (combinedWidth > windowWidth) return false;
totalWidth = combinedWidth;
return true;
});
const targetEl = hiddenEls[hiddenEls.length - 1];
const currentPos = subNavListRef.current.getBoundingClientRect().left;
const arrowWidth = arrowLeftRef.current.getBoundingClientRect().width;
const isFirstEl =
targetEl.current.getBoundingClientRect().left * -1 + currentPos === 0;
const targetElPos = isFirstEl
? targetEl.current.getBoundingClientRect().left * -1 + currentPos
: targetEl.current.getBoundingClientRect().left * -1 +
arrowWidth +
currentPos;
const styling = {
left: `${targetElPos}px`
};
_.delay(
() => {
setSizes(getSizes()),
updateRightArrow(findItemInView(elsRef.length - 1)),
updateLeftArrow(findItemInView(0));
},
500,
setArrowStyle(styling)
);
};
const findItemInView = pos => {
const rect = elsRef[pos].current.getBoundingClientRect();
return !(
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
};
const findLastItemInView = () => {
let lastItem;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
lastItem = i;
}
}
return lastItem;
};
const findFirstItemInView = () => {
let firstItemInView;
for (let i = 0; i < elsRef.length; i++) {
const isInView = !findItemInView(i);
if (isInView) {
firstItemInView = i;
break;
}
}
return firstItemInView;
};
return (
<div
className={"SubNav" + (useArrows ? " SubNav--scroll" : "")}
ref={subNavRef}
>
<div className="SubNav-content">
<div className="SubNav-menu">
<nav className="SubNav-nav" role="navigation">
<ul ref={subNavListRef} style={arrowStyle}>
{subNavLinks.map((el, i) => (
<Route
key={i}
path="/:section?"
render={() => (
<li
ref={elsRef[i]}
onMouseEnter={() => onHover(el.path)}
onMouseLeave={() => onHoverEnd()}
>
<Link
className={
activePath === el.path
? "SubNav-item SubNav-itemActive"
: "SubNav-item"
}
to={"/" + el.path}
>
{el.section}
</Link>
</li>
)}
/>
))}
</ul>
</nav>
</div>
<div
key={"SubNav-underline"}
className="SubNav-underline"
style={underLineStyle}
/>
</div>
{leftArrow ? (
<div
className="SubNav-arrowLeft"
ref={arrowLeftRef}
onClick={scrollLeft}
/>
) : null}
{rightArrow ? (
<div
className="SubNav-arrowRight"
ref={arrowRightRef}
onClick={scrollRight}
/>
) : null}
</div>
);
};
export default SubNav;
You can make use of useLayoutEffect hook to determine whether the values have been updated and take an action. Since you want to determine whether all the values has been updated, you need to compare old and new values in useEffect. You can refer to the below post to know how to write a usePrevious custom hook
How to compare oldValues and newValues on React Hooks useEffect?
const oldData = usePrevious({ rightArrow, leftArrow, sizes});
useLayoutEffect(() => {
const {rightArrow: oldRightArrow, leftArrow: oldLeftArrow, sizes: oldSizes } = oldData;
if(oldRightArrow !== rightArrow && oldLeftArrow !== leftArrow and oldSizes !== sizes) {
setArrowStyle(styling)
}
}, [rightArrow, leftArrow, sizes])
I think the reason of your delay is necessary here since you calculate based on rectangles of the first and the last element which are affected when you click on button and do animation of scrolling 500ms. So as a result your calculation needs to wait for animation to be done. change the number of animation and delay you will see the relation.
the style I meant.
#include transition(all 500ms ease);
In short, I think what you are using is the right way as long as you have animations related to the calculation.
setState takes an optional second argument which is a callback that executes after the state has been updated and the component has been re-rendered.
Another option is the componentDidUpdate lifecycle method.
I am using Animate Plus for animating accordion. I have multiple definition lists (dl) and I would like to target them all, not just the first one.
Here is the relevant piece of code:
const accordion = {
element: document.querySelector("dl"),
translate: 0
}
const buttons = Array.from(
accordion.element.getElementsByTagName("button"),
element => ({
element,
translate: 0
})
)
I was trying to convert to an array and use the spread operator […document.querySelectorAll("dl")] but without success.
How can I use querySelectorAll in this specific context to target all dl tags?
Example of my accordion can be found here:
https://codepen.io/anon/pen/QYwQqV
Here is an updated CodePen using Array.from(document.querySelectorAll('dl')) to target every dl in the document:
https://codepen.io/jo_va/pen/OdVpbJ
Since there are now multiple accordions, I first build an array of accordions and I modified all almost functions to take an accordion and a list of buttons as first parameters.
At the end, you just have to iterate over the accordions and execute the same logic you had, but parameterized for the current accordion and its buttons.
const accordions = Array.from(document.querySelectorAll("dl")).map(dl => ({
element: dl,
translate: 0
}))
const getButtons = accordion => Array.from(
accordion.element.getElementsByTagName("button"),
element => ({
element,
translate: 0
})
)
const timing = {
easing: "out-quartic",
duration: 400
}
const clear = element =>
Object.values(element.attributes).forEach(({ name }) =>
element.removeAttribute(name)
)
const hide = async (accordion, buttons, collapsing) => {
const objects = buttons.filter(({ translate }) => translate)
const direction = "reverse"
rotate(collapsing.previousElementSibling.lastElementChild, direction)
slide(accordion, objects)
await fold(collapsing, direction)
clear(collapsing)
}
const show = (accordion, buttons, expanding) => {
const button = expanding.previousElementSibling.lastElementChild
const index = buttons.findIndex(({ element }) => element == button)
const objects = buttons.slice(index + 1)
const { height } = expanding.getBoundingClientRect()
expanding.className = "open"
rotate(button)
slide(accordion, objects, height)
fold(expanding)
}
const slide = (accordion, array, to = 0) => {
center(accordion, to)
animate({
...timing,
elements: array.map(({ element }) => element.parentElement),
transform(index) {
const object = array[index]
const from = object.translate
object.translate = to
return [`translateY(${from}px)`, to]
}
})
}
const center = (accordion, height) => {
const from = accordion.translate
const to = Math.round(-height / 2)
accordion.translate = to
animate({
...timing,
elements: accordion.element,
transform: [`translateY(${from}px)`, to]
})
}
const fold = async (content, direction = "normal") =>
await animate({
...timing,
direction,
elements: content,
opacity: [0, 1],
transform: ["scaleY(0)", 1]
})
const rotate = ({ lastElementChild: elements }, direction = "normal") =>
animate({
elements,
direction,
easing: "out-cubic",
duration: 600,
transform: ["rotate(0turn)", 0.5]
})
const toggle = (accordion, buttons) => async ({ target }) => {
const collapsing = accordion.element.querySelector(".open")
const expanding = target.parentElement.nextElementSibling
if (collapsing) await hide(accordion, buttons, collapsing)
if (collapsing != expanding) show(accordion, buttons, expanding)
}
accordions.forEach(accordion => {
const buttons = getButtons(accordion)
buttons.forEach(
({ element }) => element.addEventListener("click", toggle(accordion, buttons))
)
})
import animate from "https://cdn.jsdelivr.net/npm/animateplus#2/animateplus.js"
I hope that I correctly understood your problem and that helps you.
Cheers.
How about this:
// collect all dl elements
const accordion = {
definitionLists: Array.from(document.querySelectorAll("dl")),
translate: 0
}
// get all button elements inside of each dl in a single array
const buttons = accordion.definitionLists.reduce((acc, dl) => {
let currentDlButtons = Array.from(dl.getElementsByTagName("button"),
element => ({
element,
translate: 0
}));
return acc.concat(currentDlButtons);
}, []);