I am unable to view the updated state with mouse moves when using setState() post mounting. What is wrong with the below approach? The state is not updated post mounting the RefDemo Component .
The requirementis to have a slider with 3 thumbs which should all start from 0 and can be dragged individually to different positions.
import React, { Component } from "react";
import "./Slider.css";
class RefsDemo extends Component {
constructor(props) {
super(props);
this.slider1 = React.createRef();
this.slider2 = React.createRef();
this.slider3 = React.createRef();
this.value1 = React.createRef();
this.value2 = React.createRef();
this.value3 = React.createRef();
this.bar = React.createRef();
this.state = {
lastOffset1: 0,
posCurrentX1: 0,
lastOffset2: 0,
posCurrentX2: 0,
lastOffset3: 0,
posCurrentX3: 0,
name: ""
};
this.range = 100;
//console.log(this.state);
this.onmouseDown1 = this.onmouseDown1.bind(this);
this.onmouseDown2 = this.onmouseDown2.bind(this);
this.onmouseDown3 = this.onmouseDown3.bind(this);
}
componentDidMount() {
//console.log(this.slider1);
//var {lastOffset1, posCurrentX1, lastOffset2, posCurrentX2, lastOffset3, posCurrentX3} = this.state
this.barLength = getComputedStyle(this.bar.current).width;
this.sliderLength = getComputedStyle(this.slider1.current).width;
this.travelLength = Math.round(
this.barLength.substring(0, this.barLength.length - 2)
);
console.log(this.state);
this.slider1.current.addEventListener("mousedown", this.onmouseDown1);
this.slider2.current.addEventListener("mousedown", this.onmouseDown2);
this.slider3.current.addEventListener("mousedown", this.onmouseDown3);
}
onType = (event) => {
this.setState({
...this.state,
name: event.target.value
});
};
onmouseDown1 = (event) => {
event.preventDefault();
const posInitialX = event.clientX;
this.slider1.current.addEventListener("mouseup", (event) => {
this.slider1.current.onMouseMove = null;
this.slider1.current.onMouseUp = null;
//this.lastOffset1 = this.posCurrentX1;
this.setState({
...this.state,
lastOffset1: this.state.posCurrentX1
});
});
this.slider1.current.addEventListener("mousemove", (event) => {
event.preventDefault();
//this.posCurrentX1 = this.lastOffset1 + event.clientX - posInitialX;
this.setState({
...this.state,
posCurrentX1: this.state.lastOffset1 + event.clientX - posInitialX
});
//console.log(window.innerWidth);
// console.log(document.body.style.width);
if (
this.state.posCurrentX1 <= this.travelLength &&
this.state.posCurrentX1 >= 0
) {
// console.log('posCurrentX: ', posCurrentX, 'barLength: ', travelLength);
this.slider1.current.style.left = `${this.state.posCurrentX1}px`;
this.value1.current.innerHTML = Math.round(
(this.range / this.travelLength) * this.state.posCurrentX1
);
}
});
};
onmouseDown2 = (event) => {
event.preventDefault();
const posInitialX = event.clientX;
document.addEventListener("mouseup", (event) => {
this.slider2.current.onMouseMove = null;
this.slider2.current.onMouseUp = null;
//this.lastOffset2 = this.posCurrentX2;
this.setState({
...this.state,
lastOffset2: this.state.posCurrentX2
});
});
document.addEventListener("mousemove", (event) => {
event.preventDefault();
//this.posCurrentX2 = this.lastOffset2 + event.clientX - posInitialX;
this.setState({
...this.state,
posCurrentX2: this.state.lastOffset2 + event.clientX - posInitialX
});
// console.log(window.innerWidth);
// console.log(document.body.style.width);
if (
this.state.posCurrentX2 <= this.travelLength &&
this.state.posCurrentX2 >= 0
) {
// console.log('posCurrentX: ', posCurrentX, 'barLength: ', travelLength);
this.slider2.current.style.left = `${this.state.posCurrentX2}px`;
this.value2.current.innerHTML = Math.round(
(this.range / this.travelLength) * this.state.posCurrentX2
);
}
});
};
onmouseDown3 = (event) => {
event.preventDefault();
const posInitialX = event.clientX;
document.addEventListener("mouseup", (event) => {
this.slider3.current.onMouseMove = null;
this.slider3.current.onMouseUp = null;
//this.lastOffset3 = this.posCurrentX3;
this.setState({
...this.state,
lastOffset3: this.state.posCurrentX3
});
});
document.addEventListener("mousemove", (event) => {
event.preventDefault();
//this.posCurrentX3 = this.lastOffset3 + event.clientX - posInitialX;
this.setState({
...this.state,
posCurrentX3: this.state.lastOffset3 + event.clientX - posInitialX
});
// console.log(window.innerWidth);
// console.log(document.body.style.width);
if (
this.state.posCurrentX3 <= this.travelLength &&
this.state.posCurrentX3 >= 0
) {
// console.log('posCurrentX: ', posCurrentX, 'barLength: ', travelLength);
this.slider3.current.style.left = `${this.state.posCurrentX3}px`;
this.value3.current.innerHTML = Math.round(
(this.range / this.travelLength) * this.state.posCurrentX3
);
}
});
};
componentDidUpdate() {
this.slider1.current.addEventListener("mousedown", this.onmouseDown1);
this.slider2.current.addEventListener("mousedown", this.onmouseDown2);
this.slider3.current.addEventListener("mousedown", this.onmouseDown3);
console.log(this.state);
}
// componentWillUnmount() {
// document.removeEventListener("mousedown", this.onmousedown1);
// document.removeEventListener("mousedown", this.onmousedown2);
// document.removeEventListener("mousedown", this.onmousedown3);
// }
render() {
return (
<>
<h3>Hello All</h3>
<input type="text" name={this.state.name} onChange={this.onType} />
<p>{this.state.name}</p>
<div className="box">
<div className="container">
<div className="bar" id="bar" ref={this.bar}>
<div
className="slider"
id="slider-1"
ref={this.slider1}
onMouseDown={this.onmouseDown1}
></div>
<div
className="slider"
id="slider-2"
ref={this.slider2}
onMouseDown={this.onmouseDown2}
></div>
<div
className="slider"
id="slider-3"
ref={this.slider3}
onMouseDown={this.onmouseDown3}
></div>
</div>
</div>
<div className="values">
<div className="value" id="value-1">
3
</div>
<div className="value" id="value-2">
4
</div>
<div className="value" id="value-3">
5
</div>
</div>
</div>
</>
);
}
}
export default RefsDemo;
Slider.css
.box {
width: 60%;
display: flex;
margin: 20px auto;
}
.container {
flex: 1 1 auto;
background-color: #eee;
padding: 50px 30px;
border-radius: 6px;
margin-right: 20px;
}
.bar {
height: 6px;
border-radius: 100px;
background-color: #aaa;
position: relative;
}
.slider {
position: absolute;
top: 50%;
left: 0;
height: 30px;
width: 30px;
border-radius: 50%;
background-color: #fff;
transform: translate(-50%, -50%);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
cursor: pointer;
}
#slider-1 {
background-color: #6cc639;
}
#slider-2 {
background-color: #3e4cda;
}
#slider-3 {
background-color: #d33f43;
}
.values {
width: 60px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
font-family: Roboto, Helvetica, sans-serif;
font-size: 24px;
}
#value-1 {
color: #6cc639;
}
#value-2 {
color: #3e4cda;
}
#value-3 {
color: #d33f43;
}
The three pointers are stuck at same place and I am unable to record mouse movements while dragging the pointers one by one.
I have tried adding the listeners as below in Slider.js file.However it still doesn't work.
Related
I'm working on a Tic Tac Toe project and having trouble with a certain task.
Build the functions that allow players to add marks to a specific spot on the board, and then tie it to the DOM, letting players click on the gameboard to place their marker. Don’t forget the logic that keeps players from playing in spots that are already taken!
Specifically the "Don't forget the logic that keeps players from playing in spots that are already taken!
I'm trying to see how to prevent the players from updating their marker from "X" to "O" and vice versa. I'd have to implement some sort of way to check if the cell is empty then don't update the array/board any more, but I'm not sure how to go about it.
const Gameboard = (() => {
"use strict"
let gameboard = new Array(9).fill("");
const setCell = (index, value) => {
console.log({ index, value });
gameboard[index] = value;
};
const resetBoard = () => {
gameboard = ["", "", "", "", "", "", "", "", ""];
}
const getBoard = () => [...gameboard];
return {
getBoard,
resetBoard,
setCell
}
})();
const displayController = (() => {
"use strict"
const board = document.querySelector(".board")
const renderBoard = (arr) => {
for (let i = 0; i < arr.length; i++) {
let cell = document.createElement("div")
cell.id = i;
cell.classList = "cell";
cell.setAttribute("cell-data", i)
cell.textContent = arr[i]
board.append(cell)
}
}
const clearBoard = () => {
while (board.hasChildNodes()) {
board.removeChild(board.firstChild);
}
}
return {
board,
renderBoard,
clearBoard
}
})();
const gameController = (() => {
"use strict"
const playerFunction = (player, marker) => {
return {
player,
marker,
}
}
let player1 = playerFunction("Test1", "X")
let player2 = playerFunction("Test2", "O")
let isPlayerOneTurn = true
const playerTurn = () => {
if (isPlayerOneTurn) {
isPlayerOneTurn = false
return player1.marker
} else {
isPlayerOneTurn = true
return player2.marker
}
}
const boardEl = displayController.board;
displayController.renderBoard(Gameboard.getBoard());
boardEl.addEventListener("click", (e) => {
if (e.target.classList.contains("cell")) {
const index = e.target.getAttribute("cell-data");
Gameboard.setCell(index, playerTurn());
displayController.clearBoard();
displayController.renderBoard(Gameboard.getBoard());
}
});
})();
body {
margin-top: 10rem;
font-family: 'Dongle', sans-serif;
}
header {
display: flex;
justify-content: center;
}
h1 {
color: rgba(0, 0, 0);
font-size: 70px;
font-weight: 500;
margin-bottom: auto;
}
.board {
display: grid;
grid-template-columns: repeat(3, 100px);
justify-content: center;
}
.cell {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: 70px;
border: solid 1px;
width: 100px;
user-select: none;
height: 100px;
}
.cell:hover {
background-color: rgb(175, 175, 175, .5);
}
.cell:nth-child(1) {
border-top: none;
border-left: none;
}
.cell:nth-child(2) {
border-top: none;
}
.cell:nth-child(3),
.cell:nth-child(4),
.cell:nth-child(6) {
border-top: none;
border-right: none;
border-left: none;
}
.cell:nth-child(5) {
border-top: none;
}
.cell:nth-child(7) {
border: none;
}
.cell:nth-child(8) {
border-top: none;
border-bottom: none;
}
.cell:nth-child(9) {
border: none;
}
<div class="board"></div>
With the code you already have in place, you can read a cell's value with Gameboard.getBoard()[index]. If that is still the empty string (and thus not "X" or "O"), then it is OK to play at that index.
So you could add this statement:
if (Gameboard.getBoard()[index]) return; // Exit the handler without action
Here is your updated snippet:
const Gameboard = (() => {
"use strict"
let gameboard = new Array(9).fill("");
const setCell = (index, value) => {
gameboard[index] = value;
};
const resetBoard = () => {
gameboard = ["", "", "", "", "", "", "", "", ""];
}
const getBoard = () => [...gameboard];
return {
getBoard,
resetBoard,
setCell
}
})();
const displayController = (() => {
"use strict"
const board = document.querySelector(".board")
const renderBoard = (arr) => {
for (let i = 0; i < arr.length; i++) {
let cell = document.createElement("div")
cell.id = i;
cell.classList = "cell";
cell.setAttribute("cell-data", i)
cell.textContent = arr[i]
board.append(cell)
}
}
const clearBoard = () => {
while (board.hasChildNodes()) {
board.removeChild(board.firstChild);
}
}
return {
board,
renderBoard,
clearBoard
}
})();
const gameController = (() => {
"use strict"
const playerFunction = (player, marker) => {
return {
player,
marker,
}
}
let player1 = playerFunction("Test1", "X")
let player2 = playerFunction("Test2", "O")
let isPlayerOneTurn = true
const playerTurn = () => {
if (isPlayerOneTurn) {
isPlayerOneTurn = false
return player1.marker
} else {
isPlayerOneTurn = true
return player2.marker
}
}
const boardEl = displayController.board;
displayController.renderBoard(Gameboard.getBoard());
boardEl.addEventListener("click", (e) => {
if (!e.target.classList.contains("cell")) return;
const index = e.target.getAttribute("cell-data");
if (Gameboard.getBoard()[index]) return;
Gameboard.setCell(index, playerTurn());
displayController.clearBoard();
displayController.renderBoard(Gameboard.getBoard());
});
})();
body {
margin-top: 10rem;
font-family: 'Dongle', sans-serif;
}
header {
display: flex;
justify-content: center;
}
h1 {
color: rgba(0, 0, 0);
font-size: 70px;
font-weight: 500;
margin-bottom: auto;
}
.board {
display: grid;
grid-template-columns: repeat(3, 100px);
justify-content: center;
}
.cell {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: 70px;
border: solid 1px;
width: 100px;
user-select: none;
height: 100px;
}
.cell:hover {
background-color: rgb(175, 175, 175, .5);
}
.cell:nth-child(1) {
border-top: none;
border-left: none;
}
.cell:nth-child(2) {
border-top: none;
}
.cell:nth-child(3),
.cell:nth-child(4),
.cell:nth-child(6) {
border-top: none;
border-right: none;
border-left: none;
}
.cell:nth-child(5) {
border-top: none;
}
.cell:nth-child(7) {
border: none;
}
.cell:nth-child(8) {
border-top: none;
border-bottom: none;
}
.cell:nth-child(9) {
border: none;
}
<div class="board"></div>
Not your question, but don't forget to add logic that detects a win or a draw and act accordingly.
Here a small demo. There are a few block; hovering on each block appears a tooltip(orange rect). It doesn't work correctly.
Tooltip should be displayed from left or right side. To get sizes of tooltip need to display it. Coords to display tooltip can be calculated only after tooltip is displayed
Codesandbox https://codesandbox.io/s/react-ref-65jj6?file=/src/index.js:88-231
const { useState, useEffect, useCallback } = React;
function App() {
return (
<div>
<HoveredBlock index={1} />
<HoveredBlock index={2} blockStyle={{ marginLeft: "5%" }} />
<HoveredBlock index={3} blockStyle={{ marginLeft: "50%" }} />
</div>
);
}
function calcCoords(blockRect, hoverRect) {
const docWidth = document.documentElement.clientWidth;
const isLeft = blockRect.right + hoverRect.width > docWidth;
const coords = {};
if (!isLeft) {
coords.x = blockRect.right;
coords.y = blockRect.top;
coords.type = "right";
} else {
coords.x = blockRect.left - 5 - hoverRect.width;
coords.y = blockRect.top;
coords.type = "left";
}
return coords;
}
function HoveredBlock({ index, blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback(
(node) => {
if (showHover && node) {
setHoverRect(node.getBoundingClientRect());
}
},
[showHover]
);
useEffect(() => {
if (showHover && hoverRect) {
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [hoverRect]);
const isHidden = !showHover || !coords ? 'hidden' : '';
return (
<div>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
>
{index}
</div>
<div
ref={hoverRef}
className={'hover-block' + isHidden}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
/>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 82%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
display: none;
}
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
I solved it. I changed the way how to hide element: visibility:hidded instead of display:none
export default function HoveredBlock({ blockStyle }) {
const [blockRect, setBlockRect] = useState();
const [hoverRect, setHoverRect] = useState();
const [showHover, setShowHover] = useState(false);
const [coords, setCoords] = useState();
const blockRef = useCallback((node) => {
if (node) {
setBlockRect(node.getBoundingClientRect());
}
}, []);
const hoverRef = useCallback((node) => {
if (node) {
setHoverRect(node.getBoundingClientRect());
}
}, []);
useEffect(() => {
if (showHover) {
console.log({ blockRect, hoverRect });
const coords = calcCoords(blockRect, hoverRect);
setCoords(coords);
}
}, [showHover, blockRect, hoverRect]);
return (
<>
<div
ref={blockRef}
className="block"
style={blockStyle}
onMouseEnter={() => setShowHover(true)}
onMouseLeave={() => setShowHover(false)}
/>
<div
ref={hoverRef}
className={cx("hover-block", {
hidden: !showHover || !coords
})}
style={{
left: coords && coords.x,
top: coords && coords.y
}}
></div>
</>
);
}
.block {
width: 100px;
height: 100px;
background-color: aquamarine;
margin-left: 20%;
}
.hover-block {
position: fixed;
width: 100px;
height: 100px;
background-color: coral;
}
.hidden {
visibility: hidden;
}
I have literally tried for a few hours to replicate a clickable ticker, much like they have at the very top of this site: https://www.thebay.com/
I'm confused about what triggers useEffect and long story short, I can't come up with a solution that keeps the ticker moving AND also gives the option of clicking forward/backwards via arrows. Clicking the arrow should not permanently pause the ticker.
function Ticker() {
const [tickerDisplay, setTickerDisplay] = useState('Free In-store Pickup')
const [tickerIndex, setTickerIndex] = useState(0)
const [arrowClicked, setArrowClicked] = useState(false)
const notices = [
'Easy Returns within 30 Days of Purchase',
'Free Shipping on $99+ Orders',
'Free In-store Pickup',
]
const handleClick = (side) => {
setArrowClicked(true)
switch (side) {
case 'left':
setTickerIndex(
tickerIndex === 0 ? notices.length - 1 : tickerIndex - 1
)
break
case 'right':
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
break
default:
console.log('something went wrong')
break
}
}
useEffect(() => {
if (arrowClicked) {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
setArrowClicked(false)
return
}
setTimeout(() => {
setTickerDisplay(notices[tickerIndex])
setTickerIndex(
tickerIndex === notices.length - 1 ? 0 : tickerIndex + 1
)
console.log('This will run every 6 seconds!')
}, 6000)
}, [tickerIndex, notices, tickerDisplay, arrowClicked])
return (
<IconContext.Provider value={{ className: 'ticker-icons-provider' }}>
<div className='ticker'>
<FaChevronLeft onClick={() => handleClick('left')} />
<div className='ticker_msg-wrapper'>{tickerDisplay}</div>
<FaChevronRight onClick={() => handleClick('right')} />
</div>
</IconContext.Provider>
)
}
export default Ticker
What is the best way to code this component?
This is not a work of art and probably some things could've been done better.
Hope that suits you.
const { useRef, useState, useEffect } = React;
const getItems = () => Promise.resolve(['All of our questions are now open', 'Answers extended: 72 hours after questions open', 'Post a question or get an answer', 'Free badges on 20k points'])
const Ticker = ({onPrevious, onNext, items, currentIndex}) => {
const ref = useRef(null);
const [size, setSize] = useState({
width: 0,
widthPx: '0px',
height: 0,
heightPx: '0px'
})
useEffect(() => {
if(ref && ref.current) {
const {width, height} = ref.current.getBoundingClientRect();
setSize({
width,
widthPx: `${width}px`,
height,
height: `${height}px`
})
}
}, [ref]);
const calculateStyleForItem = (index) => {
return {
width: size.width,
transform: `translateX(${0}px)`
}
}
const calculateStyleForContainer = () => {
return {
width: `${size.width * (items.length + 1)}px`,
transform: `translateX(${-currentIndex * size.width + 2 * size.width}px)`
}
}
return <div ref={ref} className="ticker">
<div style={{width: size.widthPx, height: size.heightPx}} className="ticker__foreground">
<div onClick={onPrevious} className="arrow">{'<'}</div>
<div onClick={onNext} className="arrow">{'>'}</div>
</div>
<div>
<div style={calculateStyleForContainer()} className="ticker__values">
{items.map((value, index) => <div key={index} style={calculateStyleForItem(index)}className="ticker__value">{value}</div>)}
</div>
</div>
</div>
}
const App = () => {
const [items, setItems] = useState([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [clicked, setClicked] = useState(false);
useEffect(() => {
let isUnmounted = false;
getItems()
.then(items => {
if(isUnmounted) {
return
}
setItems(items);
})
return () => {
isUnmounted = true;
}
}, [])
useEffect(() => {
if(!items.length) {
return () => {
}
}
let handle = null;
const loop = () => {
if(!clicked) {
onNext(null);
}
setClicked(false);
handle = setTimeout(loop, 2000);
}
handle = setTimeout(loop, 2000);
return () => {
clearTimeout(handle);
}
}, [items, clicked])
const onPrevious = () => {
setClicked(true);
setCurrentIndex(index => (index - 1) > -1 ? index - 1 : items.length - 1)
}
const onNext = (programmatically) => {
if(programmatically) {
setClicked(programmatically);
}
setCurrentIndex(index => (index + 1) % items.length)
}
return <div>
{items.length ? <Ticker onPrevious={onPrevious} onNext={onNext} currentIndex={currentIndex} items={items}/> : 'Loading'}
</div>
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
html, body {
box-sizing: border-box;
margin: 0;
}
.ticker {
display: flex;
justify-content: center;
align-items: center;
background: black;
font-size: 1rem;
color: white;
font-weight: bold;
padding: 1rem;
overflow: hidden;
}
.ticker__foreground {
position: absolute;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
.ticker__values {
transition: all .3s ease-in;
}
.ticker__value {
text-align: center;
display: inline-block;
vertical-align: middle;
float: none;
}
.arrow {
font-size: 1.5rem;
cursor: pointer;
padding: 1rem;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone#6/babel.min.js"></script>
<div id="root"></div>
I am trying to create star rating where the functionality has to be following:
In read mode, the stars are shown as per average (should support 100%
i.e 5 or 96% i.e 4.6) in write mode, the user can only rate 1, 1.5, 2,
2.5 etc not 2.6
The read mode is working as expected but is having problem with write mode.
The problem in write mode is I cannot update the rating with non-decimal value from 1 to 5 and also half value like 1.5, 2.5, 3.5 etc. On hovering how do i decide if my mouse pointer is in the full star or half of star? Can anyone look at this, please?
I have created a sandbox for showing the demo
Here it is
https://codesandbox.io/s/9l6kmnw7vw
The code is as follow
UPDATED CODE
// #flow
import React from "react";
import styled, { css } from "styled-components";
const StyledIcon = styled.i`
display: inline-block;
width: 12px;
overflow: hidden;
direction: ${props => props.direction && props.direction};
${props => props.css && css(...props.css)};
`;
const StyledRating = styled.div`
unicode-bidi: bidi-override;
font-size: 25px;
height: 25px;
width: 125px;
margin: 0 auto;
position: relative;
padding: 0;
text-shadow: 0px 1px 0 #a2a2a2;
color: grey;
`;
const TopStyledRating = styled.div`
padding: 0;
position: absolute;
z-index: 1;
display: block;
top: 0;
left: 0;
overflow: hidden;
${props => props.css && css(...props.css)};
width: ${props => props.width && props.width};
`;
const BottomStyledRating = styled.div`
padding: 0;
display: block;
z-index: 0;
`;
class Rating extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rating: this.props.rating || null,
// eslint-disable-next-line
temp_rating: null
};
}
handleMouseover(rating) {
console.log("rating", rating);
this.setState(prev => ({
rating,
// eslint-disable-next-line
temp_rating: prev.rating
}));
}
handleMouseout() {
this.setState(prev => ({
rating: prev.temp_rating
}));
}
rate(rating) {
this.setState({
rating,
// eslint-disable-next-line
temp_rating: rating
});
}
calculateWidth = value => {
const { total } = this.props;
const { rating } = this.state;
return Math.floor((rating / total) * 100).toFixed(2) + "%";
};
render() {
const { disabled, isReadonly } = this.props;
const { rating } = this.state;
const topStars = [];
const bottomStars = [];
const writableStars = [];
console.log("rating", rating);
// eslint-disable-next-line
if (isReadonly) {
for (let i = 0; i < 5; i++) {
topStars.push(<span>★</span>);
}
for (let i = 0; i < 5; i++) {
bottomStars.push(<span>★</span>);
}
} else {
// eslint-disable-next-line
for (let i = 0; i < 10; i++) {
let klass = "star_border";
if (rating >= i && rating !== null) {
klass = "star";
}
writableStars.push(
<StyledIcon
direction={i % 2 === 0 ? "ltr" : "rtl"}
className="material-icons"
css={this.props.css}
onMouseOver={() => !disabled && this.handleMouseover(i)}
onFocus={() => !disabled && this.handleMouseover(i)}
onClick={() => !disabled && this.rate(i)}
onMouseOut={() => !disabled && this.handleMouseout()}
onBlur={() => !disabled && this.handleMouseout()}
>
{klass}
</StyledIcon>
);
}
}
return (
<React.Fragment>
{isReadonly ? (
<StyledRating>
<TopStyledRating
css={this.props.css}
width={this.calculateWidth(this.props.rating)}
>
{topStars}
</TopStyledRating>
<BottomStyledRating>{bottomStars}</BottomStyledRating>
</StyledRating>
) : (
<React.Fragment>
{rating}
{writableStars}
</React.Fragment>
)}
</React.Fragment>
);
}
}
Rating.defaultProps = {
css: "",
disabled: false
};
export default Rating;
Now the writable stars is separately done to show the stars status when hovering and clicking but when I am supplying rating as 5 it is filling the third stars instead of 5th.
I think your current problem seems to be with where your mouse event is set, as you are handling it on the individual stars, they disappear, and trigger a mouseout event, causing this constant switch in visibility.
I would rather set the detection of the rating on the outer div, and then track where the mouse is in relation to the div, and set the width of the writable stars according to that.
I tried to make a sample from scratch, that shows how you could handle the changes from the outer div. I am sure the formula I used can be simplified still, but okay, this was just to demonstrate how it can work.
const { Component } = React;
const getRating = x => (parseInt(x / 20) * 20 + (x % 20 >= 13 ? 20 : x % 20 >= 7 ? 10 : 0));
class Rating extends Component {
constructor() {
super();
this.state = {
appliedRating: '86%'
};
this.setParentElement = this.setParentElement.bind( this );
this.handleMouseOver = this.handleMouseOver.bind( this );
this.applyRating = this.applyRating.bind( this );
this.reset = this.reset.bind( this );
this.stopReset = this.stopReset.bind( this );
}
stopReset() {
clearTimeout( this.resetTimeout );
}
setParentElement(e) {
this.parentElement = e;
}
handleMouseOver(e) {
this.stopReset();
if (e.currentTarget !== this.parentElement) {
return;
}
const targetRating = getRating(e.clientX - this.parentElement.offsetLeft);
if (this.state.setRating !== targetRating) {
this.setState({
setRating: targetRating
});
}
}
applyRating(e) {
this.setState({
currentRating: this.state.setRating
});
}
reset(e) {
this.resetTimeout = setTimeout(() => this.setState( { setRating: null } ), 50 );
}
renderStars( width, ...classes ) {
return (
<div
onMouseEnter={this.stopReset}
className={ ['flex-rating', ...classes].join(' ')}
style={{width}}>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
<span onMouseEnter={this.stopReset} className="star">★</span>
</div>
);
}
renderFixed() {
return this.renderStars('100%', 'fixed');
}
renderReadOnlyRating() {
const { appliedRating } = this.state;
return this.renderStars( appliedRating, 'readonly' );
}
renderWriteRating() {
let { setRating, currentRating } = this.state;
if (setRating === 0) {
setRating = '0%';
}
if (currentRating === undefined) {
currentRating = '100%';
}
return this.renderStars( setRating || currentRating, 'writable' );
}
render() {
return (
<div>
<div
ref={ this.setParentElement }
className="rating"
onMouseMove={ this.handleMouseOver }
onMouseOut={ this.reset }
onClick={ this.applyRating }>
{ this.renderFixed() }
{ this.renderReadOnlyRating() }
{ this.renderWriteRating() }
</div>
<div>Current rating: { ( ( this.state.currentRating || 0 ) / 20) }</div>
</div>
);
}
}
ReactDOM.render( <Rating />, document.getElementById('container') );
body { margin: 50px; }
.rating {
font-family: 'Courier new';
font-size: 16px;
position: relative;
display: inline-block;
width: 100px;
height: 25px;
align-items: flex-start;
justify-content: center;
align-content: center;
background-color: white;
}
.flex-rating {
position: absolute;
top: 0;
left: 0;
display: flex;
height: 100%;
overflow: hidden;
cursor: pointer;
}
.fixed {
color: black;
font-size: 1.1em;
font-weight: bold;
}
.readonly {
color: silver;
font-weight: bold;
}
.writable {
color: blue;
background-color: rgba(100, 100, 100, .5);
}
.star {
text-align: center;
width: 20px;
max-width: 20px;
min-width: 20px;
}
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>
I created an image carousel, where every few seconds, the old image slides left out of the window, while a new image slides in from the right. However, after switching tabs and going back, the carousel no longer animates, and the old image is displayed below the carousel rather than sliding out of the screen.
Javascript code
var carouselImages = [
'https://rm-goldenmedia.imgix.net/f417b6c79aef3008c247ce1f76ed0175.jpg?auto=format',
'https://rm-goldenmedia.imgix.net/49e0f836260e386a655dae0914985502.jpg?auto=format',
'https://rm-goldenmedia.imgix.net/4fcda316beec7d5f1b5dbe3f96fd2293.jpg?auto=format',
'https://rm-goldenmedia.imgix.net/963f4f742ee1b59456827747db253e64.jpg?auto=format'
];
var App = React.createClass({
render: function () {
return (
<div className="main">
<Carousel images={carouselImages} />
</div>
);
}
});
var Carousel = React.createClass({
propTypes: {
images: React.PropTypes.arrayOf([
React.PropTypes.string
]).isRequired,
showThumbnails: React.PropTypes.bool,
slideshowActive: React.PropTypes.bool,
slideshowDelay: React.PropTypes.number
},
getDefaultProps: function () {
return {
defaultSelectedIndex: 0,
showThumbnails: true,
slideshowActive: true,
slideshowDelay: 4000
};
},
getInitialState: function () {
return {
animationDirection: 'previous',
selectedIndex: this.props.defaultSelectedIndex
};
},
componentDidMount: function() {
if (this.props.slideshowActive) {
this.progressSlideshow();
}
},
render: function () {
var Animation = React.addons.CSSTransitionGroup;
return (
<div {...this.getProps()}>
<div className="carousel--image">
<Animation transitionName={'animation--' + this.state.animationDirection}>
{this.renderCurrentImage()}
</Animation>
</div>
{this.renderThumbs()}
</div>
);
},
renderCurrentImage: function () {
var selected = this.state.selectedIndex;
var props = {
key: selected,
src: this.props.images[selected]
};
return (
<img {...props} />
);
},
renderThumbs: function () {
var thumbnails = null;
if (this.props.showThumbnails) {
thumbnails = (
<div className="carousel--thumbs">
{this.props.images.map(this.renderThumb)}
</div>
);
}
return thumbnails;
},
renderThumb: function (src, index) {
var selected = (index === this.state.selectedIndex) ? ' carousel--selected' : '';
var props = {
className: 'carousel--thumb' + selected,
key: index,
onClick: this.handleThumbClick.bind(null, index),
src: src
}
return <img {...props} />;
},
getProps: function () {
var props = {
className:'carousel',
onKeyDown: this.handleKeyDown,
tabIndex:'0'
};
if (this.props.slideshowActive) {
props.onMouseEnter = this.handleMouseEnter;
props.onMouseLeave = this.handleMouseLeave;
}
return props;
},
handleKeyDown: function (event) {
var left = 37;
var up = 38;
var right = 39;
var down = 40;
var key = event.which;
if (key === down || key === left) {
this.goInDirection('previous');
} else if (key === up || key === right) {
this.goInDirection('next');
}
},
handleMouseEnter: function () {
clearTimeout(this.timeout);
},
handleMouseLeave: function () {
this.progressSlideshow();
},
handleThumbClick: function (index) {
this.goToIndex(index);
},
progressSlideshow: function () {
this.setState({animationDirection: 'next'});
this.timeout = setTimeout(function () {
this.goInDirection('next');
this.progressSlideshow();
}.bind(this), this.props.slideshowDelay);
},
goToIndex: function (index) {
var direction = (this.state.selectedIndex > index ) ? 'previous' : 'next';
this.setState({
animationDirection: direction,
selectedIndex: index
});
},
goInDirection: function (direction) {
var totalImages = this.props.images.length;
var modifier = (direction === 'next') ? 1 : -1;
var newIndex = this.state.selectedIndex + modifier;
if (newIndex === totalImages) {
newIndex = 0;
} else if (newIndex === -1) {
newIndex = totalImages - 1;
}
this.setState({
animationDirection: direction,
selectedIndex: newIndex
});
}
});
React.render(<App />, document.getElementById('app'));
CSS code
body {
color: white;
padding: 0;
background: black;
}
.main {
font: 16px / 1 Arial;
position: relative;
}
.carousel {
display: inline-block;
overflow: hidden;
outline: 0;
}
.carousel--thumbs {
position: absolute;
top: 370px;
margin-top: 10px;
text-align: center;
display: flex;
z-index: 3;
overflow: hidden;
}
.carousel--thumb {
border: 1px solid transparent;
cursor: pointer;
display: inline-block;
height: 93px;
width: 247px;
transition: transform .2s ease-in-out;
}
.carousel--thumb:hover {
transform: translate(0, -3px);
}
.carousel--selected {
border-color: #fff;
}
.animation--next-enter {
transform: translate(100%);
}
.animation--next-enter-active {
transform: translate(0, 0);
transition: transform 1500ms ease-in-out;
}
.animation--next-leave {
left: 0;
position: absolute;
top: 0;
transform: translate(0, 0);
}
.animation--next-leave-active {
transform: translate(-100%, 0);
transition: transform 1500ms ease-in-out;
}
.animation--previous-enter {
transform: translate(-100%, 0);
}
.animation--previous-enter-active {
transform: translate(0, 0);
transition: transform 1500ms ease-in-out;
}
.animation--previous-leave {
left: 0;
position: absolute;
top: 0;
transform: translate(0, 0);
}
.animation--previous-leave-active {
transform: translate(100%, 0);
transition: transform 1500ms ease-in-out;
}
HTML code
<!DOCTYPE html>
<html>
<head>
<script data-require="react-with-addons#0.14.3" data-semver="0.14.3" src="https://unpkg.com/react#0.14.3/dist/react-with-addons.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="app"></div>
<script src="script.js"></script>
</body>
</html>