minesweeper game logic problem (fetching another components props from a component) - javascript

I'm new to React and I'm trying to grasp the broad strokes by building a simple minesweeper game. I'm able to render a 10x10 board (Grid), and reveal squares (Square). However, I've hit a wall with the step in which multiple squares are revealed with a single click.
The way I've written it (I think) requires a square component to know if the surrounding square components have a mine. How should I go about this? Or am I writing it in a way that isn't really consistent with React's intended use?
Code:
----------
Grid.js
----------
import React from 'react';
import Square from './Square';
export default function Grid(props) {
const grid = new Array(10);
for (let i = 0; i < 10; i++) {
grid[i] = new Array(10).fill(null);
}
return (
<div>
{
grid.map((row, x) => {
return (
<div key={x}>
{
row.map((col, y) => {
const randInt = Math.floor(Math.random() * 5);
return (
<Square
row={x}
col={y}
id={`square-${x}-${y}`}
key={`square-${x}-${y}`}
hasMine={randInt === 0}
/>
)
})
}
</div>
)
})
}
</div>
)
}
---------
Square.js
---------
export default function Square(props) {
function uncover(e) {
if (!props.hasMine && e.target.className !== 'white-square') {
e.target.className = 'white-square';
let right = document.getElementById(`square-${props.row}-${props.col + 1}`);
let bottomRight = document.getElementById(`square-${props.row + 1}-${props.col + 1}`);
let bottom = document.getElementById(`square-${props.row + 1}-${props.col}`);
let bottomLeft = document.getElementById(`square-${props.row + 1}-${props.col - 1}`);
let left = document.getElementById(`square-${props.row}-${props.col - 1}`);
let topLeft = document.getElementById(`square-${props.row - 1}-${props.col - 1}`);
let top = document.getElementById(`square-${props.row - 1}-${props.col}`);
let topRight = document.getElementById(`square-${props.row - 1}-${props.col + 1}`);
if (right) {
right.click();
}
if (bottomRight) {
bottomRight.click();
}
if (bottom) {
bottom.click();
}
if (bottomLeft) {
bottomLeft.click()
}
if (left) {
left.click()
}
if (topLeft) {
topLeft.click()
}
if (top) {
top.click()
}
if (topRight) {
topRight.click()
}
}
}
return (
<div
className={props.hasMine ? 'red-square' : 'grey-square'}
id={props.id}
onClick={uncover}
/>
)
}

Related

this is undefined when calling child function through a parent function

UPDATE: I have figured out a muuuuch simpler workaround by sing in the typescript file so the JS parent is no longer needed. ~facepalm~ Thanks for all your suggestions!
I am trying to get a button to trigger the function affTimer() inside the child function component but I keep getting the error "this is undefined" in relation to the function call. Here is the two code files:
affType.js
import React, {Component} from 'react';
import ReactPlayer from 'react-player'
import { Link } from 'react-router-dom';
import affirmationService from '../Services/requestService'
import affTrack from '../audio/inner.wav';
import warn from '../audio/warning.wav';
import Player from '../Player/Player';
import videoBG from '../videos/InnerStrength.mp4';
import Type from '../Type/Type.tsx';
import Button from "../customButton";
import {tXP} from '../Type/Type.tsx';
class affType extends Component {
constructor(props) {
super(props);
this.state = {character: undefined};
this.child = React.forwardRef();
this.startGame = this.startGame.bind(this);
}
async componentDidMount() {
const { match: { params } } = this.props;
//let affirmation_id = params.affirmation_id;
//let response = await affirmationService.getById(affirmation_id);
//this.setState({character: response.data});
setTimeout(() => {
document.getElementById('overlay_blk_fast').style.opacity = 0;
setTimeout(() => {
document.getElementById('overlay_blk_fast').style.display = 'none';
}, 1000);
}, 10);
}
spawnDialog() {
document.getElementById('overlay_1').style.display = 'block';
setTimeout(() => {
document.getElementById('overlay_1').style.opacity = 1;
}, 10);
}
destroyDialog() {
document.getElementById('overlay_1').style.opacity = 0;
setTimeout(() => {
document.getElementById('overlay_1').style.display = 'none';
}, 1000);
}
repeat() {
document.getElementById('overlay_2').style.opacity = 0;
document.querySelector('video').play();
setTimeout(() => {
document.getElementById('overlay_2').style.display = 'none';
}, 1000);
}
test_ended() {
document.getElementById('overlay_2').style.display = 'block';
setTimeout(() => {
document.getElementById('audio_end').play();
document.getElementById('overlay_2').style.opacity = 1;
}, 10);
}
startGame() {
var track = document.getElementById('aff');
track.play();
this.child.current.affTimer();
}
render() {
return (
<div>
<div className="contentplayer">
<audio id='aff'><source src={affTrack} /></audio>
<video autoPlay muted loop id="myVideo">
<source src={videoBG} type="video/mp4" />
</video>
<audio id="audio_end" src="/Audio/Inner Strength completed quest - play with completed quest prompt.wav"/>
</div>
<p>{tXP}</p>
<Button
border="none"
color="pink"
height = "200px"
onClick={this.startGame}
radius = "50%"
width = "200px"
children = "Start!"
/>
<Type ref={this.child}>
</Type>
<div className="aligntopright" onClick={() => {this.spawnDialog()}}>
<div className="backbtn-white"></div>
</div>
<div className="overlay_blk_fast" id="overlay_blk_fast"></div>
<div className="overlay" id="overlay_1">
<div className="dialog">
<div className="dialogcontainer">
<img className="dialogbg"/>
<h3 className="dialogtext">Are you sure you would like to go back to the selection page?</h3>
<h2 className="no" onClick={() => {this.destroyDialog()}}>No</h2>
<Link to="/affirmation"><h2 className="yes">Yes</h2></Link>
</div>
</div>
</div>
<div className="overlay" id="overlay_2">
<div className="dialog">
<div className="dialogcontainer">
<img className="dialogbg"/>
<h3 className="dialogtext">Would you like to repeat this quest?</h3>
<Link to="/affirmation"><h2 className="no">Go back</h2></Link>
<h2 className="yes" onClick={() => {this.repeat()}}>Repeat</h2>
</div>
</div>
</div>
</div>
)
}
}
export default affType;
type.tsx
import React, {Component} from 'react';
import useTypingGame from "react-typing-game-hook";
import { textSpanContainsTextSpan } from 'typescript';
var xpM = 0;
var i = 0;
var err = 0;
var xp = 5;
var tXP = 0;
var addXP = 1;
var bonus = 0;
var bonusCounter = 0;
//var warnP = new Audio({warn});
//var affTrackP = new Audio('../audio/inner.wav');
function TypeF() {
let text_array = [
"There is strength and solidity within me",
"Courage is flooding through my veins",
"I possess strength within my heart",
"I am leading the charge with courage, and a vigorous resolution",
"There is a force inside me that is unbelievably powerful",
"There is a brave, radiant spirit inside me",
"I am a tall tree, with thick and strong roots",
"I was born for this",
"There is a divinity within",
"I am a force of nature",
"I possess the mental fortitude of those who climb the highest peaks",
"I was born with a determined spirit",
"There is an intensity in my eyes"
];
let text = text_array[i];
const {
states: {
charsState,
length,
currIndex,
currChar,
correctChar,
errorChar,
phase,
startTime,
endTime
},
actions: { insertTyping, resetTyping, deleteTyping }
} = useTypingGame(text);
const handleKey = (key: any) => {
if (key === "Escape") {
resetTyping();
} else if (key === "Backspace") {
deleteTyping(false);
} else if (key.length === 1) {
insertTyping(key);
}
};
if (currIndex + 1 === length) {
xpM = xpM + 1;
bonusCounter = bonusCounter + 1;
err = err + errorChar;
addXP = ((xp * correctChar) - (err * 2)) * xpM;
if (err > correctChar) {
addXP = correctChar * 3;
}
tXP = tXP + addXP;
if (bonusCounter >= 5) {
bonus = bonus + 1;
bonusCounter = 0;
}
resetTyping();
}
var tmr;
var cd = 18;
function affTimer() {
tmr = setInterval(tock, 1000);
if (i >= text_array.length) {
clearInterval(tmr);
}
}
function tock() {
if (cd > 0) {
cd = cd - 1;
console.log(cd);
}
else if (cd <= 0) {
if (i < text_array.length) {
i = i + 1;
cd = 18;
resetTyping();
}
else {
i = text_array.length;
}
}
}
return (
<div className='container'>
<div
className="typing-test"
id="start"
onKeyDown={(e) => {
handleKey(e.key);
e.preventDefault();
}
}
tabIndex={0}
>
{text.split("").map((char: string, index: number) => {
let state = charsState[index];
let color = state === 0 ? "white" : state === 1 ? "green" : "red";
return (
<span
key={char + index}
style={{ color }}
className={currIndex + 1 === index ? "curr-letter" : ""}
>
{char}
</span>
);
})}
</div>
<h2 className='debug'> TIMER: {cd}, I: {i}, ERRORS: {err}, MULTIPLIER: {xpM}, Type XP: {correctChar * xp}, CurrXP: {correctChar * xp * xpM} XPTotal: {tXP} bonusCounter: {bonusCounter}, BONUS: {bonus}</h2>
</div>
);
}
export {tXP};
export default TypeF;
Any help would be amazing, I have been stuck on this for 2 days and it is the last bit I need to complete so I can move to the next phase.
Your child component is a function component. You can't get a ref to an instance of a function component, because there is no instance. If you really need to use a function component and also expose some custom object as a ref, then you can use the useImperativeHandle hook plus forwardRef to define what the parent component should receive on its ref. For example:
const Type = React.forwardRef((props, ref) => {
// ...
useImperativeHandle(ref, () => {
// The following object is what will get assigned to the
// parent component's this.child.current
return {
afftimer: function () {
tmr = setInterval(tock, 1000);
if (i >= text_array.length) {
clearInterval(tmr);
}
}
}
});
// ...
})
But useImperativeHandle is not a common thing to use. There is likely a more standard way to solve your problem. The normal way for a parent component to tell a child component what to do is with props, not with refs.

React Animations - change only one logo at a time from a list of 6 initial logos displayed?

I am working on a feature where I need to change only one logo at a time out of a list of 6 logos displayed initally.
The example is shown here:: https://www.loom.com/share/6a282423368a46418248a789ce4fc139
And its there in this website also in the bottom:: https://www.wonderlandams.com/about?fbclid=IwAR0wfFrqYVwor1UJfZGcWK2MaU0kBWNiaacg8kGb_IC--VaziorY6BDt7lA.
import React, { useState, useEffect } from 'react'
import tw from 'twin.macro'
import Image from './image'
const LogoGrid = ({ logos, style }) => {
const groupDisplay = logos.slice(0, 6);
const [group, setGroup] = useState(groupDisplay);
const [groupLength, setGroupLength] = useState(0);
let shuffledLogos, i;
const shuffle = (array) => array.sort(() => Math.random() - 0.5);
useEffect(() => {
console.log("UEFFECT RUNNING");
const timer = setInterval(() => {
i = Math.floor(Math.random() * 6);
shuffledLogos = shuffle(logos);
let gl = groupLength + 1 ;
if(groupDisplay[i] == logos[i]) {
let k = i + 1;
groupDisplay[i] = shuffledLogos[k];
}
else {
groupDisplay[i] = shuffledLogos[i];
}
setGroupLength(gl)
setGroup(groupDisplay)
}, 2000)
return () => clearInterval(timer)
}, [group, groupLength])
return (
<div css={[tw`relative`, style]}>
<div
css={[
tw`opacity-0 grid-cols-3 grid-rows-2 gap-12
lg:(gap-x-16 gap-y-12 mt-26) xl:gap-x-32`,
tw`grid transition transition-opacity duration-300 ease-in-out opacity-100`,
]}
>
{(group || []).map((logo, index) => (
<div key={index} css={tw`h-12`}>
<Image image={logo} />
</div>
))}
</div>
</div>
)
}
export default LogoGrid
I have made one component using tailwind css but it does not work as expected. Can anyone kindly point me in the right direction or any changes in the logic that can give me similar effects ?

How can I solve this circular dependency? (JavaScript)

I have the next problem: I had been creating a webpage for search GIFs using GIPHY API, everything was going OK until I had to implement a functionality to add GIFs to favorites, the favorites are stored in local storage and they are shown in a separate HTML file using CSS grid. I can add and delete favorites without problems outside /favorites.html, and I can also add favorites from trending GIFs inside this url, the problem is that when I need to remove a favorite and I am on /favorites.html I want the grid to be created from zero and therefore the user will see the gif disappear.
For that function I've created a GIFCard class which allow me to create all the HTML elements for a card and add all the classes they need to look fine using css styles, also this class handles the events so it automatically adds event listeners to all the buttons. I have a file called favoritesPage.js which defines a function called showAllUntilOffset() this function is used whenever the user press the favorite button for a GIF Card and is seeing favorites its work is to create again the GIF grid so it will be updated but I also need to import the GIFCard class in favoritesPage.js to create the Card object, add the html classes and add the event listener.
Here is an image of my webpage working correctly, it just stops working when I import "showAllUntilOffset" from favoritesPage.js because of the circular dependency (I guess).
Next is the code for my favoritesPage.js file and the GIFCard class, constructor and other methods that are not relevant to the problem are not shown here.
favoritesPage.js:
import { GIFCard as Card } from "../entities/GIFCard.js";
import { favoriteGIFs } from "../favorites.js";
import {
favoritesGrid,
favoritesEmpty,
favoritesShow,
seeMoreFavorites,
} from "../dom/favoritesSelectors.js";
import { displayComponent, hideComponent } from "../utils/utils.js";
function createAndAddFavGIFCards(start, end) {
for (let i = start; i < end; i += 1) {
const newCard = new Card(
favoriteGIFs[i].id,
favoriteGIFs[i].title,
favoriteGIFs[i].username,
favoriteGIFs[i].url,
favoriteGIFs[i].fixedHeightURL,
i,
"Favorite GIF"
);
if (window.innerWidth >= 1280) {
favoritesGrid.append(newCard.createCardDesktop());
} else {
favoritesGrid.append(newCard.createCardMobile());
}
}
}
function loadAndShowFavorites() {
favoritesGrid.innerHTML = "";
if (favoriteGIFs.length === 0) {
displayComponent(favoritesEmpty);
} else {
displayComponent(favoritesShow);
let limit = favoriteGIFs.length >= 12 ? 12 : favoriteGIFs.length;
if (favoriteGIFs.length > 12) {
displayComponent(seeMoreFavorites);
}
createAndAddFavGIFCards(0, limit);
}
}
export function showAllUntilOffset() {
favoritesGrid.innerHTML = "";
if (favoriteGIFs.length === 0) {
displayComponent(favoritesEmpty);
hideComponent(favoritesShow);
}
if (favoriteGIFs.length <= 12) {
hideComponent(seeMoreFavorites);
offset = favoriteGIFs.length;
} else if (favoriteGIFs.length < offset) {
offset -= 1;
}
createAndAddFavGIFCards(0, offset);
}
function getOffsetAndUpdateBtn() {
start = offset;
if (offset + 12 <= favoriteGIFs.length) {
offset += 12;
} else {
const excess = favoriteGIFs.length - offset;
offset += excess;
}
if (offset === favoriteGIFs.length) {
hideComponent(seeMoreFavorites);
}
}
let start;
let offset = 12;
loadAndShowFavorites();
seeMoreFavorites.addEventListener("click", () => {
getOffsetAndUpdateBtn();
createAndAddFavGIFCards(start, offset);
});
GIFCard Class:
import { showAllUntilOffset } from "../favorites/favoritesPage.js";
export class GIFCard {
createCardDesktop() {
const card = document.createElement("div");
card.classList.add("card");
const overlay = document.createElement("div");
overlay.classList.add("overlay");
const text = this.createCardInfo();
const favoriteBtn = this.createCardButton("favorite", "card-btn");
this.favoriteBtn = favoriteBtn;
const downloadBtn = this.createCardButton("download", "card-btn");
this.downloadBtn = downloadBtn;
const expandBtn = this.createCardButton("expand", "card-btn");
this.expandBtn = expandBtn;
const gifImg = this.createCardImg();
gifImg.classList.add("card-img");
card.append(overlay, text, favoriteBtn, downloadBtn, expandBtn, gifImg);
this.htmlDesktopCard = card;
this.checkAndUpdateFavBtn();
this.expandGIFDesktopEventListener();
this.addFavoriteEventListener();
return card;
}
favEventAction() {
if (!seeingFavorites()) {
if (!this.isInFavorites()) {
this.pushToFavorites();
} else {
this.removeFromFavorites();
}
} else {
if (!this.isInFavorites()) {
this.pushToFavorites();
if (this.type === "Trending GIF") {
hideComponent(favoritesEmpty);
displayComponent(favoritesShow);
if (window.innerWidth >= 1280) {
favoritesGrid.append(this.createCardDesktop());
} else {
favoritesGrid.append(this.createCardMobile());
}
}
} else {
this.removeFromFavorites();
if (this.type === "Favorite GIF") {
showAllUntilOffset();
}
}
}
}
addFavoriteEventListener() {
this.favoriteBtn.addEventListener("click", () => {
this.favEventAction();
});
}
}
Hope you can help me, I've tried everything and I just can't figure out a solution. This is my first JavaScript big project so if you can recommend me good practices I'd really appreciate it. Thank you so much.

Adding a simple left/right swipe gesture

I need to add a simple left/right swipe gesture so that the 'selected' image cycles when swiped on mobile, similar to clicking the buttons in the hero component, also similar to pressing the left/right arrow keys on a keyboard
I don't have the most experience with JavaScript so if anyone could tell me what exactly to write and where so that I can completely wrap up this project.
Here is a demo: http://nufaith.ca/justinatkins/
Code:
Vue.component('hero-bg', {
template: `
<div class="hero-bg">
<div class="hero">
<img id="pushed" :src="selected"/>
</div>
</div>
`,
props: ['selected']
});
Vue.component('hero-bg-empty', {
template: `
<div class="hero-bg">
<div class="hero">
<span style="display:block;height:100px;"></span>
</div>
</div>
`
});
Vue.component('hero', {
template: `
<div>
<topbar v-if="!gridEnabled"></topbar>
<topbar2 v-if="gridEnabled"></topbar2>
<hero-bg :selected="selectedItem.img" v-if="!gridEnabled"></hero-bg>
<hero-bg-empty v-if="gridEnabled"></hero-bg-empty>
<div class="hero-container" v-if="!gridEnabled">
<div class="hero">
<img :src="selectedItem.img" v-if="thing" alt=""/>
</div>
<div class="hero-desc">
<button class="control left" #click="previous">
<i class="zmdi zmdi-chevron-left"></i>
</button>
<span class="hero-desc-title" v-html="title"></span>
<button class="control right" #click="next">
<i class="zmdi zmdi-chevron-right"></i>
</button>
<br/>
<button class="view-all-button" #click="enableGrid">OVERVIEW</button>
</div>
</div>
</div>
`,
data() {
return {
gridEnabled: false,
selected: 0,
thing: true
};
},
computed: {
selectedItem() {
return info[this.selected];
},
title() {
const comma = this.selectedItem.title.indexOf(',');
const len = this.selectedItem.title.length;
const strBeginning = this.selectedItem.title.substring(comma, 0);
const strEnd = this.selectedItem.title.substring(comma, len);
if (this.selectedItem.title.includes(',')) {
return `<span>${strBeginning}<span class="font-regular font-muted">${strEnd}</span></span>`;
}
return this.selectedItem.title;
},
maxImages() {
return info.length - 1;
}
},
created() {
window.addEventListener('keydown', e => {
if (e.keyCode === 37) {
this.previous();
return;
}
if (e.keyCode === 39) {
this.next();
return;
}
});
Event.$on('updateImg', index => {
this.selected = index;
this.gridEnabled = !this.gridEnabled;
});
},
methods: {
next() {
this.selected === this.maxImages ? (this.selected = 0) : (this.selected += 1);
},
previous() {
this.selected === 0 ? (this.selected = this.maxImages) : (this.selected -= 1);
},
enableGrid() {
this.gridEnabled = !this.gridEnabled;
window.scroll(0, 0);
Event.$emit('enableGrid');
}
}
});
This is how I implemented a simple swipe gesture in one of my projects. You may check this out.
Code:
touchableElement.addEventListener('touchstart', function (event) {
touchstartX = event.changedTouches[0].screenX;
touchstartY = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', function (event) {
touchendX = event.changedTouches[0].screenX;
touchendY = event.changedTouches[0].screenY;
handleGesture();
}, false);
function handleGesture() {
if (touchendX < touchstartX) {
console.log('Swiped Left');
}
if (touchendX > touchstartX) {
console.log('Swiped Right');
}
if (touchendY < touchstartY) {
console.log('Swiped Up');
}
if (touchendY > touchstartY) {
console.log('Swiped Down');
}
if (touchendY === touchstartY) {
console.log('Tap');
}
}
Basically, touchableElement mentioned here, refers to the DOM Element that will receive the touch event. If you want to activate swipe options on your entire screen, then you may use your body tag as the touchable element. Or you may configure any specific div element as the touchable element, in case you just want the swipe gesture on that specific div.
On that touchableElement, we are adding 2 event-listeners here:
touchstart:
this is when user starts swiping. We take that initial coordinates (x,y) and
store them into touchstartX, touchstartY respectively.
touchend: this is when user stops swiping. We take that final coordinates (x, y) and store them into touchendX, touchendY respectively.
Keep in mind that, the origin of these coordinates is the top left corner of the screen. x-coordinate increases as you go from left to right and y-coordinate increases as you go from top to bottom.
Then, in handleGesture(), we just compare those 2 pair of coordinates (touchstartX, touchstartY) and (touchendX, touchendY), to detect different types of swipe gesture (up, down, left, right):
touchendX < touchstartX: says that, user started swiping at a higher X value & stopped swiping at a lower X value. That means, swiped from right to left (Swiped Left).
touchendX > touchstartX: says that, user started swiping at a lower X value & stopped swiping at a higher X value. That means, swiped from left to right (Swiped Right).
touchendY < touchstartY: says that, user started swiping at a higher Y value & stopped swiping at a lower Y value. That means, swiped from bottom to top (Swiped Up).
touchendY > touchstartY: says that, user started swiping at a lower Y value & stopped swiping at a higher Y value. That means, swiped from top to bottom (Swiped Down).
You may add the code for these 4 different events (Swipe Up/Down/Left/Right), on the corresponding if blocks, as shown on the code.
I took smmehrab’s answer, added some thresholds to avoid accidental swipes, and turned it into a little library. Might come in handy, so here it is:
export default class TouchEvent
{
static SWPIE_THRESHOLD = 50 // Minumum difference in pixels at which a swipe gesture is detected
static SWIPE_LEFT = 1
static SWIPE_RIGHT = 2
static SWIPE_UP = 3
static SWIPE_DOWN = 4
constructor(startEvent, endEvent)
{
this.startEvent = startEvent
this.endEvent = endEvent || null
}
isSwipeLeft()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_LEFT
}
isSwipeRight()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_RIGHT
}
isSwipeUp()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_UP
}
isSwipeDown()
{
return this.getSwipeDirection() == TouchEvent.SWIPE_DOWN
}
getSwipeDirection()
{
let start = this.startEvent.changedTouches[0]
let end = this.endEvent.changedTouches[0]
if (!start || !end) {
return null
}
let horizontalDifference = start.screenX - end.screenX
let verticalDifference = start.screenY - end.screenY
// Horizontal difference dominates
if (Math.abs(horizontalDifference) > Math.abs(verticalDifference)) {
if (horizontalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_LEFT
} else if (horizontalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_RIGHT
}
// Verical or no difference dominates
} else {
if (verticalDifference >= TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_UP
} else if (verticalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
return TouchEvent.SWIPE_DOWN
}
}
return null
}
setEndEvent(endEvent)
{
this.endEvent = endEvent
}
}
How to use
Simply feed it the events from touchstart and touchend:
import TouchEvent from '#/TouchEvent'
let touchEvent = null;
document.addEventListener('touchstart', (event) => {
touchEvent = new TouchEvent(event);
});
document.addEventListener('touchend', handleSwipe);
function handleSwipe(event)
{
if (!touchEvent) {
return;
}
touchEvent.setEndEvent(event);
if (touchEvent.isSwipeRight()) {
// Do something
} else if (touchEvent.isSwipeLeft()) {
// Do something different
}
// Reset event for next touch
touchEvent = null;
}
This sounds like a job for Hammer.JS, unless you're trying to avoid dependencies. They have good documentation and examples for getting started
My Vue knowledge is next to nothing, so I'm wary of this becoming a blind leading the blind scenario, but the first thing you'll have to do is add the dependency using either npm or yarn - then add it to the top of your file using
import Hammer from 'hammerjs'
Try adding the below code right above this line: Event.$on('updateImg', index => {
const swipeableEl = document.getElementsByClassName('.hero')[0];
this.hammer = Hammer(swipeableEl)
this.hammer.on('swipeleft', () => this.next())
this.hammer.on('swiperight', () => this.previous())
If it doesn't work you'll have to check your developer tools / console log to see if it's logged any useful errors.
This codepen might be a useful resource too:
Good luck.
Using #smmehrab answer, I created a Vue 3 composable that also works for SSR builds.
import { onMounted, Ref } from 'vue'
export type SwipeCallback = (event: TouchEvent) => void;
export type SwipeOptions = {
directinoal_threshold?: number; // Pixels offset to trigger swipe
};
export const useSwipe = (touchableElement: HTMLElement = null, options: Ref<SwipeOptions> = ref({
directinoal_threshold: 10
})) => {
const touchStartX = ref(0);
const touchEndX = ref(0);
const touchStartY = ref(0);
const touchEndY = ref(0);
onMounted(() => {
if (!touchableElement)
touchableElement = document.body;
touchableElement.addEventListener('touchstart', (event) => {
touchStartX.value = event.changedTouches[0].screenX;
touchStartY.value = event.changedTouches[0].screenY;
}, false);
touchableElement.addEventListener('touchend', (event) => {
touchEndX.value = event.changedTouches[0].screenX;
touchEndY.value = event.changedTouches[0].screenY;
handleGesture(event);
}, false);
});
const onSwipeLeft: Array<SwipeCallback> = [];
const onSwipeRight: Array<SwipeCallback> = [];
const onSwipeUp: Array<SwipeCallback> = [];
const onSwipeDown: Array<SwipeCallback> = [];
const onTap: Array<SwipeCallback> = [];
const addEventListener = (arr: Array<SwipeCallback>, callback: SwipeCallback) => {
arr.push(callback);
};
const handleGesture = (event: TouchEvent) => {
if (touchEndX.value < touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeLeft.forEach(callback => callback(event));
}
if (touchEndX.value > touchStartX.value && (Math.max(touchStartY.value, touchEndY.value) - Math.min(touchStartY.value, touchEndY.value)) < options.value.directinoal_threshold) {
onSwipeRight.forEach(callback => callback(event));
}
if (touchEndY.value < touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeUp.forEach(callback => callback(event));
}
if (touchEndY.value > touchStartY.value && (Math.max(touchStartX.value, touchEndX.value) - Math.min(touchStartX.value, touchEndX.value)) < options.value.directinoal_threshold) {
onSwipeDown.forEach(callback => callback(event));
}
if (touchEndY.value === touchStartY.value) {
onTap.forEach(callback => callback(event));
}
}
return {
onSwipeLeft: (callback: SwipeCallback) => addEventListener(onSwipeLeft, callback),
onSwipeRight: (callback: SwipeCallback) => addEventListener(onSwipeRight, callback),
onSwipeUp: (callback: SwipeCallback) => addEventListener(onSwipeUp, callback),
onSwipeDown: (callback: SwipeCallback) => addEventListener(onSwipeDown, callback),
onTap: (callback: SwipeCallback) => addEventListener(onTap, callback)
}
}
Example usage:
const { onSwipeLeft, onSwipeRight } = useSwipe(document.body);
onSwipeLeft((e:TouchEvent) => {
//logic
});

Logic for rotating a square

The following code is using React.
I have a square positioned at 45deg, that has four elements. Upon click on each element, I want the square to rotate the relevant degrees so that the clicked element is on top. (In the image, 3 is currently selected).
Please see the notes within the code. I hope it explains what my logic is.
Here is the component code:
class CentreCtrls extends React.Component {
constructor(props) {
super(props);
this.state = {
aboutUs: {
activeCtrl: 0
}
}
}
// animate the square to rotate the relevant degrees
// so that the clicked element is at the top.
// this logic is flawed and not working correctly
componentDidUpdate() {
const activeHeading = this.state.aboutUs.activeCtrl;
const { centreCtrl } = this.refs;
const style = centreCtrl.style;
const currentDeg = parseInt(style.transform.slice(7, -4), 10);
const position = this.getPosition(activeHeading, 0);
const degRotation = position * 90;
console.log('pos:', position, 'deg:', currentDeg, '-', degRotation);
anime({
targets: this.refs.centreCtrl,
rotate: `-${currentDeg + degRotation}deg`,
duration: 150,
easing: 'linear',
});
}
onClickHandler = e =>
const ele = e.target;
const i = ele.parentNode.getAttribute('data-i');
this.setState({
aboutUs: {
activeCtrl: parseInt(i, 10),
},
});
};
// the purpose of this function is to find the current value
// to be assigned to the 'data-i' property on element, this 'data-i'
// is read in the above 'componentDidUpdate' function to animate
// the square the relevant degrees so that the clicked square is at
// top (where the purple 3 is in the image).
getPosition = (i, activeHeading) => {
const total = 3; // start: 0, ttl: 4
let correctSeq = i;
if (i === activeHeading) {
correctSeq = 0;
// if i == 2, then for our rotation purpose, its at position 3 in DOM
// because of the way, DOM renders elements from left to right within
// square
} else if (i === 2) {
correctSeq = 3;
} else if (i === 3) {
correctSeq = 2;
}
let pos = total - activeHeading + (correctSeq + 0);
if (pos > 3) {
pos = pos - 3;
}
return pos;
};
render() {
const { data } = this.props;
const activeHeading = this.state.aboutUs.activeCtrl;
return (
// using the react v15 style ref
<div
className="centreCtrl"
ref="centreCtrl"
style={{ transform: 'rotate(45deg)' }}
>
{data.map((entry, i) => {
const j = this.getPosition(i, activeHeading);
return (
<div
className="ctrl"
key={i}
data-i={j}
id={`${entry.narrative.heading}`}
onClick={this.onClickHandler}
>
<div
className="textContainer"
id={entry.narrative.heading}
ref={`textRef${i}`}
>
{j}
</div>
</div>
);
})}
</div>
);
}
}
Edit: How can I get the rotation logic to work correctly. More to the point, I am looking for input into what is the best logic for rotation calculation of this square. It looks far easier than I estimated.
Assuming you want to rotate clockwise, something like this might help:
import React from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import { Container, Square, Block, Top, Bottom } from './components';
const rotateRight = items => {
const last = items.pop();
items.unshift(last);
};
class App extends React.Component {
state = {
items: ["a", "b", "c", "d"],
rotation: 0,
};
handleClick = i => {
let start = i;
let count = 0;
let items = [...this.state.items];
const end = items.length - 1;
while (start <= end) {
rotateRight(items);
start += 1;
count += 1;
}
this.setState(state => ({
rotation: ((count * 90) % 360),
}));
};
render() {
const { items, rotation } = this.state;
return (
<Container>
<Square rotation={rotation}>
<Top>
<Block onClick={() => this.handleClick(0)}>
{items[0]}
</Block>
<Block onClick={() => this.handleClick(1)}>
{items[1]}
</Block>
</Top>
<Bottom>
<Block onClick={() => this.handleClick(2)}>{
items[2]}
</Block>
<Block onClick={() => this.handleClick(3)}>
{items[3]}
</Block>
</Bottom>
</Square>
</Container>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
StackBlitz link here.

Categories