UNPKG

react-special-cursor

Version:
145 lines (144 loc) 6.93 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import "./Cursor.scss"; import useFollowCursor from "../hooks/useFollowCursor"; import PropTypes from "prop-types"; import IsDevice from "../helpers/isDevice"; /** * * @author AmirHossein Salighedar (https://github.com/amirho1) * @component Customable cursor * * @param props * @param props.children elements that you want to get the cursor shape usually at top level * @param props.hoverClasses an array of objects that accept 2 property on the name of class that you want while hovering having an action an another the style class name that you want to set on dotElement * @param props.borderClassName this class name will pass to cursor-border * @param props.dotClassName this class name will pass to cursor-dot * @param props.turnOffOnPhone boolean that makes it off on phone by default true * @returns JSX.Element * * @example <Cursor>{restOfYourSite}</Cursor> */ function Cursor({ children, borderClassName, dotClassName, hoverClasses = [], turnOffOnPhone = true, }) { const [dotChild, setDotChild] = useState(null); // remove default cursor useEffect(() => { if (!turnOffOnPhone || !(IsDevice === null || IsDevice === void 0 ? void 0 : IsDevice.any())) { document.body.classList.add("cursor-none"); } else { document.body.classList.add("initial-body"); } }, [turnOffOnPhone]); const [classes, setClasses] = useState([]); // get The cursor wrapper also cursorDotElement const cursorWrapperElement = useRef(null); const cursorDotElement = useRef(null); const cursorBorderElement = useRef(null); useEffect(() => { if (hoverClasses.length) { hoverClasses.forEach(hoverClass => { const elements = document.querySelectorAll(`.${hoverClass.classNameOfTargetElement}`); setClasses(current => { const cl = { elements, className: hoverClass.classNameOfStyle, cursorChildren: hoverClass.cursorChildren, }; return [...current, cl]; }); }); } // redo on changing hoverClasses }, [hoverClasses]); // get mouse x and y coordinate const { mouseX, mouseY } = useFollowCursor(); // styles const styles = useMemo(() => ({ cursorBorder: { top: mouseY, left: mouseX, }, innerDot: { top: mouseY, left: mouseX, }, }), [mouseX, mouseY]); // mousedown handler const mouseDownHandler = useCallback(() => { if (cursorBorderElement.current && cursorBorderElement.current.classList) cursorBorderElement.current.classList.add("smaller-cursor-border"); }, []); //mouseup handler const mouseUpHandler = useCallback(() => { if (cursorBorderElement.current && cursorBorderElement.current.classList) cursorBorderElement.current.classList.remove("smaller-cursor-border"); }, []); // mouseup handler const mouseOverHandler = useCallback(() => { if (classes.length) { classes.forEach(className => { for (let i = 0; i < className.elements.length; i++) { className.elements[i].addEventListener("mouseover", () => { var _a, _b; (_a = cursorWrapperElement.current) === null || _a === void 0 ? void 0 : _a.classList.add(className.className); if (className === null || className === void 0 ? void 0 : className.cursorChildren) { (_b = cursorDotElement.current) === null || _b === void 0 ? void 0 : _b.classList.add("transition-none"); setDotChild(className === null || className === void 0 ? void 0 : className.cursorChildren); } }); } }); } }, [classes]); // mouse out handler const mouseOutHandler = useCallback(() => { if (classes.length) classes.forEach(className => { for (let i = 0; i < className.elements.length; i++) { className.elements[i].addEventListener("mouseout", () => { var _a, _b; (_a = cursorWrapperElement.current) === null || _a === void 0 ? void 0 : _a.classList.remove(className.className); if (className.cursorChildren) { (_b = cursorDotElement.current) === null || _b === void 0 ? void 0 : _b.classList.remove("transition-none"); setDotChild(null); } }); } }); }, [classes]); // add event listeners useEffect(() => { window.addEventListener("mousedown", mouseDownHandler); window.addEventListener("mouseup", mouseUpHandler); window.addEventListener("mouseover", mouseOverHandler); window.addEventListener("mouseout", mouseOutHandler); return () => { window.removeEventListener("mousedown", mouseDownHandler); window.removeEventListener("mouseup", mouseUpHandler); window.removeEventListener("mouseover", mouseOverHandler); window.removeEventListener("mouseout", mouseOutHandler); }; // function again only when hoverClasses has changed }, [mouseDownHandler, mouseOutHandler, mouseOverHandler, mouseUpHandler]); // if device is phone and turnOffOnPhone is true return only children if ((IsDevice === null || IsDevice === void 0 ? void 0 : IsDevice.any()) && turnOffOnPhone) return _jsx(React.Fragment, { children: children }, void 0); return (_jsxs("div", Object.assign({ ref: cursorWrapperElement, className: "cursor-wrapper", "data-testid": "cursor" }, { children: [_jsx("div", { className: `cursor-border ${borderClassName}`, style: styles.cursorBorder, ref: cursorBorderElement }, void 0), _jsx("div", Object.assign({ style: styles.innerDot, ref: cursorDotElement, className: `cursor-dot ${dotClassName}` }, { children: dotChild }), void 0), children] }), void 0)); } Cursor.propTypes = { children: PropTypes.element.isRequired, borderClassName: PropTypes.string, dotClassName: PropTypes.string, hoverClasses: PropTypes.arrayOf(PropTypes.shape({ classNameOfTargetElement: PropTypes.string.isRequired, classNameOfStyle: PropTypes.string.isRequired, cursorChildren: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.element, ]), })), turnOffOnPhone: PropTypes.bool, }; export default Cursor;