Skip to content

Commit

Permalink
mouse and touch actions refactor
Browse files Browse the repository at this point in the history
sadanandpai committed Mar 13, 2024
1 parent c4d9dc7 commit 153de41
Showing 5 changed files with 241 additions and 139 deletions.
167 changes: 31 additions & 136 deletions src/apps/path-finder/components/grid/grid.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
import { setCell } from '../../store/path-finder.slice';
import { useAppDispatch, useAppSelector } from '@/host/store/hooks';
import { useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';

import { CellType, Status } from '../../models/interfaces';
import { CellElement, CellType, Status } from '../../models/interfaces';
import { cellSize } from '../../config';
import classes from './grid.module.scss';
import useMouseAction from '../../hooks/useMouseAction.hook';

const getCellDetails = (element: HTMLElement | null) => {
if (!element) {
return { isValidCell: false };
}

if (element.tagName !== 'BUTTON') {
return { isValidCell: false };
}

const row = +(element.dataset.row ?? -1);
const col = +(element.dataset.col ?? -1);
const cellType = +(element.dataset.cellType ?? 0);

if (row === -1 || col === -1 || cellType === -1) {
return { isValidCell: false };
}

return {
isValidCell: true,
row,
col,
cellType,
};
};

function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
import { useTouch } from '../../hooks/use-touch.hook';
import { useMouse } from '../../hooks/use-mouse.hook';
import { isTouchDevice } from '../../helpers/action.helper';

function Grid() {
const dispatch = useAppDispatch();
@@ -43,117 +16,39 @@ function Grid() {
const exit = useAppSelector((state) => state.pathFinder.exit);
const status = useAppSelector((state) => state.pathFinder.status);
const ref = useRef<HTMLDivElement>(null);
const cellTypeRef = useRef<CellType | null>(null);
const cellMoveRef = useRef<CellType | null>(null);

const { element, isMouseDown } = useMouseAction(ref);
const { isValidCell, row, col, cellType } = getCellDetails(element);
const touchCell = useTouch({ isMobile: isTouchDevice(), ref });
const moveCell = useMouse({ isMobile: isTouchDevice(), ref });

useEffect(() => {
if (!isTouchDevice()) {
if (
isMouseDown &&
isValidCell &&
[CellType.entry, CellType.exit].includes(cellType!)
) {
cellTypeRef.current = cellType!;
}

if (!isMouseDown) {
cellTypeRef.current = null;
}
}
}, [isMouseDown, isValidCell, cellType]);

useEffect(() => {
if (isTouchDevice() && isValidCell && isMouseDown) {
if (
[CellType.entry, CellType.exit].includes(cellType!) &&
!cellMoveRef.current
) {
cellMoveRef.current = cellType!;
}
}
}, [isMouseDown, isValidCell, cellType]);

useEffect(() => {
if (!isValidCell || isTouchDevice()) {
return;
}

if (cellTypeRef.current) {
const cell = cellTypeRef.current === CellType.entry ? entry : exit;

if (
cell &&
(cell.row !== row || cell.col !== col) &&
cellType !== CellType.wall
) {
dispatch(
setCell({
row: cell?.row,
col: cell?.col,
cellType: CellType.clear,
})
);

dispatch(
setCell({ row: row!, col: col!, cellType: cellTypeRef.current })
const setupCell = useCallback(
function setupCell(cell: CellElement | null) {
if (cell) {
const type = cell.cellType === CellType.entry ? entry : exit;
const isEntryOrExit = [CellType.entry, CellType.exit].includes(
cell.cellType
);
if (
isEntryOrExit &&
!(cell.row === type.row && cell.col === type.col)
) {
dispatch(setCell({ ...type, cellType: CellType.clear }));
}

if (!(type.row === cell.row && type.col === cell.col)) {
dispatch(setCell(cell));
}
}
} else {
dispatch(
setCell({
row: row!,
col: col!,
cellType: cellType === CellType.wall ? CellType.clear : CellType.wall,
})
);
}
}, [cellType, col, dispatch, element, isValidCell, row, entry, exit]);
},
[dispatch, entry, exit]
);

useEffect(() => {
if (!isValidCell || !isTouchDevice()) {
return;
}
setupCell(touchCell);
}, [touchCell, setupCell]);

if (cellMoveRef.current) {
const cell = cellMoveRef.current === CellType.entry ? entry : exit;

if (
cell &&
(cell.row !== row || cell.col !== col) &&
cellType !== CellType.wall
) {
dispatch(
setCell({ row: row!, col: col!, cellType: cellMoveRef.current })
);

dispatch(
setCell({
row: cell?.row,
col: cell?.col,
cellType: CellType.clear,
})
);

cellMoveRef.current = null;
}
} else if (
!(
(row === entry.row && col === entry.col) ||
(row === exit.row && col === exit.col)
)
) {
dispatch(
setCell({
row: row!,
col: col!,
cellType: cellType === CellType.wall ? CellType.clear : CellType.wall,
})
);
}
}, [cellType, col, dispatch, element, isValidCell, row, entry, exit]);
useEffect(() => {
setupCell(moveCell);
}, [moveCell, setupCell]);

const gridStyle: React.CSSProperties = {
gridTemplateRows: `repeat(${grid.length}, ${cellSize}px)`,
38 changes: 38 additions & 0 deletions src/apps/path-finder/helpers/action.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { CellElement } from '../models/interfaces';

type CellDetails =
| {
isValidCell: false;
selectedCell?: null;
}
| {
isValidCell: true;
selectedCell: CellElement;
};

export function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}

export function getCellDetails(element: HTMLElement | null): CellDetails {
if (!element) {
return { isValidCell: false };
}

if (element.tagName !== 'BUTTON') {
return { isValidCell: false };
}

const row = +(element.dataset.row ?? -1);
const col = +(element.dataset.col ?? -1);
const cellType = +(element.dataset.cellType ?? -1);

if (row === -1 || col === -1 || cellType === -1) {
return { isValidCell: false };
}

return {
isValidCell: true,
selectedCell: { row, col, cellType },
};
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { useEffect, useRef, useState } from 'react';

function useMouseAction(ref: React.RefObject<HTMLDivElement>) {
function useMouseActions({
isMobile,
ref,
}: {
isMobile: boolean;
ref: React.RefObject<HTMLDivElement>;
}) {
const [element, setElement] = useState<HTMLElement | null>(null);
const isMouseDown = useRef(false);

useEffect(() => {
if (isMobile) {
return;
}

const referenceEl = ref.current;
if (!referenceEl) {
return;
@@ -21,7 +31,7 @@ function useMouseAction(ref: React.RefObject<HTMLDivElement>) {
referenceEl.removeEventListener('mouseup', onMouseUp);
referenceEl.removeEventListener('mouseleave', onMouseUp);
};
}, [ref]);
}, [isMobile, ref]);

const onMouseDown = (e: MouseEvent | TouchEvent) => {
if (e.target) {
@@ -44,4 +54,4 @@ function useMouseAction(ref: React.RefObject<HTMLDivElement>) {
return { element, isMouseDown: isMouseDown.current };
}

export default useMouseAction;
export default useMouseActions;
80 changes: 80 additions & 0 deletions src/apps/path-finder/hooks/use-mouse.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useRef, useState } from 'react';
import { CellElement, CellType } from '../models/interfaces';
import useMouseActions from './use-mouse-actions.hook';
import { getCellDetails } from '../helpers/action.helper';

export function useMouse({
isMobile,
ref,
}: {
isMobile: boolean;
ref: React.RefObject<HTMLDivElement>;
}) {
const [cell, setCell] = useState<CellElement | null>(null);
const { element, isMouseDown } = useMouseActions({ isMobile, ref });
const { isValidCell, selectedCell } = getCellDetails(element);
const previousCellRef = useRef<CellElement | null>(null);
const isEntryOrExitRef = useRef(false);

useEffect(() => {
if (!isValidCell) {
return;
}

const isNew = !(
previousCellRef.current?.col === selectedCell.col &&
previousCellRef.current?.row === selectedCell.row
);

if (!isNew) {
return;
}

if (isEntryOrExitRef.current && previousCellRef.current) {
if (
![CellType.entry, CellType.exit, CellType.wall].includes(
selectedCell.cellType
)
) {
setCell({
...selectedCell,
cellType: previousCellRef.current.cellType,
});
previousCellRef.current = {
...selectedCell,
cellType: previousCellRef.current.cellType,
};
}

return;
}

if (![CellType.entry, CellType.exit].includes(selectedCell.cellType)) {
setCell({
row: selectedCell.row,
col: selectedCell.col,
cellType:
selectedCell.cellType === CellType.wall
? CellType.clear
: CellType.wall,
});
previousCellRef.current = selectedCell;
}
}, [isValidCell, selectedCell]);

useEffect(() => {
if (isMouseDown && selectedCell && !previousCellRef.current) {
previousCellRef.current = selectedCell;
isEntryOrExitRef.current = [CellType.entry, CellType.exit].includes(
selectedCell.cellType
);
}

if (!isMouseDown) {
previousCellRef.current = null;
isEntryOrExitRef.current = false;
}
}, [isMouseDown, selectedCell]);

return cell;
}
79 changes: 79 additions & 0 deletions src/apps/path-finder/hooks/use-touch.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { CellElement, CellType } from '../models/interfaces';
import { getCellDetails } from '../helpers/action.helper';

export function useTouch({
isMobile,
ref,
}: {
isMobile: boolean;
ref: React.RefObject<HTMLDivElement>;
}) {
const [cell, setCell] = useState<CellElement | null>(null);
const previousCellRef = useRef<CellElement | null>(null);

const onClick = useCallback(
(e: MouseEvent | TouchEvent) => {
if (!isMobile) {
return;
}

const target = e.target as HTMLElement;
if (!target) {
return;
}

const { isValidCell, selectedCell } = getCellDetails(target);
if (!isValidCell) {
return;
}

const cellType = selectedCell.cellType;
const isEntryOrExit = [CellType.entry, CellType.exit].includes(cellType);
if (isEntryOrExit) {
previousCellRef.current = selectedCell;
return;
}

if (
![CellType.entry, CellType.exit, CellType.wall].includes(
selectedCell.cellType
) &&
previousCellRef.current
) {
setCell({
...selectedCell,
cellType: previousCellRef.current.cellType,
});
previousCellRef.current = null;
return;
}

setCell({
...selectedCell,
cellType: cellType === CellType.wall ? CellType.clear : CellType.wall,
});
previousCellRef.current = null;
},
[isMobile]
);

useEffect(() => {
if (!isMobile) {
return;
}

const referenceEl = ref.current;
if (!referenceEl) {
return;
}

referenceEl.addEventListener('click', onClick);

return () => {
referenceEl.removeEventListener('mousedown', onClick);
};
}, [isMobile, onClick, ref]);

return cell;
}

0 comments on commit 153de41

Please sign in to comment.