diff --git a/package.json b/package.json index a9d8f31..ca00e3a 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@types/lodash.get": "^4.4.6", "@types/lodash.set": "^4.3.6", "@types/node": "^15.6.0", + "@types/normalize-wheel": "^1.0.0", "@types/react": "^17.0.6", "@types/react-dom": "^17.0.5", "@types/react-modal": "^3.12.0", diff --git a/src/components/map/MapEditor.tsx b/src/components/map/MapEditor.tsx index de4efdf..3de53ba 100644 --- a/src/components/map/MapEditor.tsx +++ b/src/components/map/MapEditor.tsx @@ -23,6 +23,8 @@ import MapGrid from "./MapGrid"; import MapGridEditor from "./MapGridEditor"; import { Map } from "../../types/Map"; import { GridInset } from "../../types/Grid"; +import { Stage as StageType } from "konva/types/Stage"; +import { Layer as LayerType } from "konva/types/Layer"; type MapSettingsChangeEventHandler = (change: Partial) => void; @@ -41,8 +43,8 @@ function MapEditor({ map, onSettingsChange }: MapEditorProps) { const defaultInset = getGridDefaultInset(map.grid, map.width, map.height); const stageTranslateRef = useRef({ x: 0, y: 0 }); - const mapStageRef = useRef(); - const mapLayerRef = useRef(); + const mapStageRef = useRef(null); + const mapLayerRef = useRef(null); const [preventMapInteraction, setPreventMapInteraction] = useState(false); function handleResize(width?: number, height?: number): void { @@ -55,7 +57,7 @@ function MapEditor({ map, onSettingsChange }: MapEditorProps) { } } - const containerRef = useRef(null); + const containerRef = useRef(null); usePreventOverscroll(containerRef); const [mapWidth, mapHeight] = useImageCenter( diff --git a/src/contexts/AssetsContext.tsx b/src/contexts/AssetsContext.tsx index 8f2266c..1b4fea4 100644 --- a/src/contexts/AssetsContext.tsx +++ b/src/contexts/AssetsContext.tsx @@ -309,7 +309,7 @@ type DefaultData = { export function useDataURL( data: FileData | DefaultData, defaultSources: Record, - unknownSource: string | undefined, + unknownSource: string | undefined = undefined, thumbnail = false ) { const [assetId, setAssetId] = useState(); diff --git a/src/contexts/GroupContext.tsx b/src/contexts/GroupContext.tsx index 1916268..8a38923 100644 --- a/src/contexts/GroupContext.tsx +++ b/src/contexts/GroupContext.tsx @@ -7,7 +7,7 @@ import { useKeyboard, useBlur } from "./KeyboardContext"; import { getGroupItems, groupsFromIds } from "../helpers/group"; import shortcuts from "../shortcuts"; -import { Group, GroupContainer, GroupItem } from "../types/Group"; +import { Group, GroupItem } from "../types/Group"; export type GroupSelectMode = "single" | "multiple" | "range"; export type GroupSelectModeChangeEventHandler = ( diff --git a/src/contexts/MapStageContext.tsx b/src/contexts/MapStageContext.tsx index 684b297..81893dd 100644 --- a/src/contexts/MapStageContext.tsx +++ b/src/contexts/MapStageContext.tsx @@ -1,7 +1,7 @@ import React, { useContext } from "react"; import { Stage } from "konva/types/Stage"; -type MapStage = React.MutableRefObject; +export type MapStage = React.MutableRefObject; const MapStageContext = React.createContext(undefined); export const MapStageProvider = MapStageContext.Provider; diff --git a/src/hooks/useDebounce.tsx b/src/hooks/useDebounce.ts similarity index 100% rename from src/hooks/useDebounce.tsx rename to src/hooks/useDebounce.ts diff --git a/src/hooks/useGridSnapping.tsx b/src/hooks/useGridSnapping.ts similarity index 100% rename from src/hooks/useGridSnapping.tsx rename to src/hooks/useGridSnapping.ts diff --git a/src/hooks/useImageCenter.tsx b/src/hooks/useImageCenter.ts similarity index 68% rename from src/hooks/useImageCenter.tsx rename to src/hooks/useImageCenter.ts index e8a86f6..9830ca6 100644 --- a/src/hooks/useImageCenter.tsx +++ b/src/hooks/useImageCenter.ts @@ -1,26 +1,24 @@ +import { Layer } from "konva/types/Layer"; import { useEffect, useRef } from "react"; -type useImageCenterProps = { - data: - stageRef: - stageWidth: number; - stageHeight: number; - stageTranslateRef: - setStageScale: - imageLayerRef: - containerRef: - responsive?: boolean -} +import { MapStage } from "../contexts/MapStageContext"; +import Vector2 from "../helpers/Vector2"; + +type ImageData = { + id: string; + width: number; + height: number; +}; function useImageCenter( - data, - stageRef, - stageWidth, - stageHeight, - stageTranslateRef, - setStageScale, - imageLayerRef, - containerRef, + data: ImageData, + stageRef: MapStage, + stageWidth: number, + stageHeight: number, + stageTranslateRef: React.MutableRefObject, + setStageScale: React.Dispatch>, + imageLayerRef: React.RefObject, + containerRef: React.RefObject, responsive = false ) { const stageRatio = stageWidth / stageHeight; @@ -37,7 +35,7 @@ function useImageCenter( } // Reset image translate and stage scale - const previousDataIdRef = useRef(); + const previousDataIdRef = useRef(); const previousStageRatioRef = useRef(stageRatio); useEffect(() => { if (!data) { @@ -45,7 +43,12 @@ function useImageCenter( } const layer = imageLayerRef.current; - const containerRect = containerRef.current.getBoundingClientRect(); + const container = containerRef.current; + const stage = stageRef.current; + if (!container || !stage) { + return; + } + const containerRect = container.getBoundingClientRect(); const previousDataId = previousDataIdRef.current; const previousStageRatio = previousStageRatioRef.current; @@ -68,7 +71,7 @@ function useImageCenter( }; } layer.position(newTranslate); - stageRef.current.position({ x: 0, y: 0 }); + stage.position({ x: 0, y: 0 }); stageTranslateRef.current = { x: 0, y: 0 }; setStageScale(1); diff --git a/src/hooks/useImageDrop.tsx b/src/hooks/useImageDrop.ts similarity index 100% rename from src/hooks/useImageDrop.tsx rename to src/hooks/useImageDrop.ts diff --git a/src/hooks/useMapImage.tsx b/src/hooks/useMapImage.ts similarity index 65% rename from src/hooks/useMapImage.tsx rename to src/hooks/useMapImage.ts index 02cf5b6..89b7720 100644 --- a/src/hooks/useMapImage.tsx +++ b/src/hooks/useMapImage.ts @@ -5,14 +5,16 @@ import { useDataURL } from "../contexts/AssetsContext"; import { mapSources as defaultMapSources } from "../maps"; -function useMapImage(map) { +import { Map } from "../types/Map"; + +function useMapImage(map: Map) { const mapURL = useDataURL(map, defaultMapSources); - const [mapImage, mapImageStatus] = useImage(mapURL); + const [mapImage, mapImageStatus] = useImage(mapURL || ""); // Create a map source that only updates when the image is fully loaded - const [loadedMapImage, setLoadedMapImage] = useState(); + const [loadedMapImage, setLoadedMapImage] = useState(); useEffect(() => { - if (mapImageStatus === "loaded") { + if (mapImageStatus === "loaded" && mapImage) { setLoadedMapImage(mapImage); } }, [mapImage, mapImageStatus]); diff --git a/src/hooks/useNetworkedState.tsx b/src/hooks/useNetworkedState.ts similarity index 100% rename from src/hooks/useNetworkedState.tsx rename to src/hooks/useNetworkedState.ts diff --git a/src/hooks/usePortal.js b/src/hooks/usePortal.ts similarity index 79% rename from src/hooks/usePortal.js rename to src/hooks/usePortal.ts index cb8c2f9..d9c3499 100644 --- a/src/hooks/usePortal.js +++ b/src/hooks/usePortal.ts @@ -6,7 +6,7 @@ import { useRef, useEffect } from "react"; * Creates DOM element to be used as React root. * @returns {HTMLElement} */ -function createRootElement(id) { +function createRootElement(id: string) { const rootContainer = document.createElement("div"); rootContainer.setAttribute("id", id); return rootContainer; @@ -16,11 +16,13 @@ function createRootElement(id) { * Appends element as last child of body. * @param {HTMLElement} rootElem */ -function addRootElement(rootElem) { - document.body.insertBefore( - rootElem, - document.body.lastElementChild.nextElementSibling - ); +function addRootElement(rootElem: HTMLElement) { + if (document.body.lastElementChild) { + document.body.insertBefore( + rootElem, + document.body.lastElementChild?.nextElementSibling + ); + } } /** @@ -34,13 +36,15 @@ function addRootElement(rootElem) { * @param {String} id The id of the target container, e.g 'modal' or 'spotlight' * @returns {HTMLElement} The DOM node to use as the Portal target. */ -function usePortal(id) { - const rootElemRef = useRef(null); +function usePortal(id: string): HTMLElement { + const rootElemRef = useRef(null); useEffect( function setupElement() { // Look for existing target dom element to append to - const existingParent = document.querySelector(`#${id}`); + const existingParent: HTMLElement | null = document.querySelector( + `#${id}` + ); // Parent is either a new root or the existing dom element const parentElem = existingParent || createRootElement(id); @@ -50,10 +54,10 @@ function usePortal(id) { } // Add the detached element to the parent - parentElem.appendChild(rootElemRef.current); + rootElemRef.current && parentElem.appendChild(rootElemRef.current); return function removeElement() { - rootElemRef.current.remove(); + rootElemRef.current && rootElemRef.current.remove(); if (parentElem.childNodes.length === -1) { parentElem.remove(); } diff --git a/src/hooks/usePreventOverscroll.js b/src/hooks/usePreventOverscroll.ts similarity index 74% rename from src/hooks/usePreventOverscroll.js rename to src/hooks/usePreventOverscroll.ts index 73cd602..b0c74c6 100644 --- a/src/hooks/usePreventOverscroll.js +++ b/src/hooks/usePreventOverscroll.ts @@ -1,10 +1,10 @@ -import { useEffect } from "react"; +import React, { useEffect } from "react"; -function usePreventOverscroll(elementRef) { +function usePreventOverscroll(elementRef: React.RefObject) { useEffect(() => { // Stop overscroll on chrome and safari // also stop pinch to zoom on chrome - function preventOverscroll(event) { + function preventOverscroll(event: WheelEvent) { event.preventDefault(); } const element = elementRef.current; diff --git a/src/hooks/usePreventSelect.js b/src/hooks/usePreventSelect.ts similarity index 67% rename from src/hooks/usePreventSelect.js rename to src/hooks/usePreventSelect.ts index 82d36bd..7737fa9 100644 --- a/src/hooks/usePreventSelect.js +++ b/src/hooks/usePreventSelect.ts @@ -1,11 +1,8 @@ function usePreventSelect() { function clearSelection() { - if (window.getSelection) { - window.getSelection().removeAllRanges(); - } - if (document.selection) { - document.selection.empty(); - } + window?.getSelection()?.removeAllRanges(); + // @ts-ignore + document?.selection?.empty(); } function preventSelect() { clearSelection(); diff --git a/src/hooks/usePreventTouch.js b/src/hooks/usePreventTouch.ts similarity index 78% rename from src/hooks/usePreventTouch.js rename to src/hooks/usePreventTouch.ts index b101e4b..137b80f 100644 --- a/src/hooks/usePreventTouch.js +++ b/src/hooks/usePreventTouch.ts @@ -1,9 +1,9 @@ import { useEffect } from "react"; -function usePreventTouch(elementRef) { +function usePreventTouch(elementRef: React.RefObject) { useEffect(() => { // Stop 3d touch - function prevent3DTouch(event) { + function prevent3DTouch(event: TouchEvent) { event.preventDefault(); } const element = elementRef.current; diff --git a/src/hooks/usePrevious.js b/src/hooks/usePrevious.ts similarity index 68% rename from src/hooks/usePrevious.js rename to src/hooks/usePrevious.ts index 8b8ac74..2c621c2 100644 --- a/src/hooks/usePrevious.js +++ b/src/hooks/usePrevious.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; -function usePrevious(value) { - const ref = useRef(); +function usePrevious(value: T): T { + const ref = useRef(value); useEffect(() => { ref.current = value; }, [value]); diff --git a/src/hooks/useResponsiveLayout.js b/src/hooks/useResponsiveLayout.ts similarity index 100% rename from src/hooks/useResponsiveLayout.js rename to src/hooks/useResponsiveLayout.ts diff --git a/src/hooks/useStageInteraction.js b/src/hooks/useStageInteraction.ts similarity index 85% rename from src/hooks/useStageInteraction.js rename to src/hooks/useStageInteraction.ts index a35d69c..bba35a4 100644 --- a/src/hooks/useStageInteraction.js +++ b/src/hooks/useStageInteraction.ts @@ -1,35 +1,42 @@ import { useRef, useEffect, useState } from "react"; import { useGesture } from "react-use-gesture"; +import { Handlers } from "react-use-gesture/dist/types"; import normalizeWheel from "normalize-wheel"; +import { Stage } from "konva/types/Stage"; +import { Layer } from "konva/types/Layer"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import shortcuts from "../shortcuts"; +import Vector2 from "../helpers/Vector2"; + const wheelZoomSpeed = -1; const touchZoomSpeed = 0.005; const minZoom = 0.1; +type StageScaleChangeEventHandler = (newScale: number) => void; + function useStageInteraction( - stage, - stageScale, - onStageScaleChange, - stageTranslateRef, - layer, + stage: Stage, + stageScale: number, + onStageScaleChange: StageScaleChangeEventHandler, + stageTranslateRef: React.MutableRefObject, + layer: Layer, maxZoom = 10, tool = "move", preventInteraction = false, - gesture = {} + gesture: Handlers = {} ) { const isInteractingWithCanvas = useRef(false); - const pinchPreviousDistanceRef = useRef(); - const pinchPreviousOriginRef = useRef(); + const pinchPreviousDistanceRef = useRef(0); + const pinchPreviousOriginRef = useRef({ x: 0, y: 0 }); const [zoomSpeed, setZoomSpeed] = useState(1); // Prevent accessibility pinch to zoom on Mac useEffect(() => { - function handleGesture(e) { + function handleGesture(e: Event) { e.preventDefault(); } window.addEventListener("gesturestart", handleGesture); @@ -69,16 +76,18 @@ function useStageInteraction( // Center on pointer const pointer = stage.getPointerPosition(); - const newTranslate = { - x: pointer.x - ((pointer.x - stage.x()) / stageScale) * newScale, - y: pointer.y - ((pointer.y - stage.y()) / stageScale) * newScale, - }; + if (pointer) { + const newTranslate = { + x: pointer.x - ((pointer.x - stage.x()) / stageScale) * newScale, + y: pointer.y - ((pointer.y - stage.y()) / stageScale) * newScale, + }; - stage.position(newTranslate); + stage.position(newTranslate); - stageTranslateRef.current = newTranslate; + stageTranslateRef.current = newTranslate; - onStageScaleChange(newScale); + onStageScaleChange(newScale); + } } gesture.onWheel && gesture.onWheel(props); @@ -186,7 +195,7 @@ function useStageInteraction( } ); - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { // TODO: Find better way to detect whether keyboard event should fire. // This one fires on all open stages if (preventInteraction) { @@ -222,7 +231,7 @@ function useStageInteraction( } } - function handleKeyUp(event) { + function handleKeyUp(event: KeyboardEvent) { if (shortcuts.stagePrecisionZoom(event)) { setZoomSpeed(1); } diff --git a/yarn.lock b/yarn.lock index ecef399..98d0666 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3038,6 +3038,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/normalize-wheel@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/normalize-wheel/-/normalize-wheel-1.0.0.tgz#d973b53557dc59c6136b5b737ae930e9218cb452" + integrity sha512-SzWYVzP7Q8w4/976Gi3a6+J/8/VNTq6AW7wDafXorr1MYTxyZqJTbUvwQt1hiG3PXyFUMIKr+s6q3+MLz2c/TQ== + "@types/offscreencanvas@~2019.3.0": version "2019.3.0" resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz#3336428ec7e9180cf4566dfea5da04eb586a6553"