Merge branch 'master' into typescript
This commit is contained in:
@@ -17,12 +17,16 @@ import {
|
||||
/**
|
||||
* Returns a function that when called will snap a node to the current grid
|
||||
* @param {number=} snappingSensitivity 1 = Always snap, 0 = never snap if undefined the default user setting will be used
|
||||
* @param {boolean=} useCorners Snap to grid cell corners
|
||||
*/
|
||||
function useGridSnapping(snappingSensitivity) {
|
||||
function useGridSnapping(snappingSensitivity, useCorners = true) {
|
||||
const [defaultSnappingSensitivity] = useSetting(
|
||||
"map.gridSnappingSensitivity"
|
||||
);
|
||||
snappingSensitivity = snappingSensitivity || defaultSnappingSensitivity;
|
||||
snappingSensitivity =
|
||||
snappingSensitivity === undefined
|
||||
? defaultSnappingSensitivity
|
||||
: snappingSensitivity;
|
||||
|
||||
const grid = useGrid();
|
||||
const gridOffset = useGridOffset();
|
||||
@@ -57,7 +61,10 @@ function useGridSnapping(snappingSensitivity) {
|
||||
gridCellPixelSize
|
||||
);
|
||||
|
||||
const snapPoints = [cellPosition, ...cellCorners];
|
||||
const snapPoints = [cellPosition];
|
||||
if (useCorners) {
|
||||
snapPoints.push(...cellCorners);
|
||||
}
|
||||
|
||||
for (let snapPoint of snapPoints) {
|
||||
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
|
||||
|
||||
85
src/hooks/useImageDrop.js
Normal file
85
src/hooks/useImageDrop.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useState } from "react";
|
||||
import { useToasts } from "react-toast-notifications";
|
||||
|
||||
import Vector2 from "../helpers/Vector2";
|
||||
|
||||
function useImageDrop(
|
||||
onImageDrop,
|
||||
supportFileTypes = ["image/jpeg", "image/gif", "image/png", "image/webp"]
|
||||
) {
|
||||
const { addToast } = useToasts();
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
function onDragEnter(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setDragging(true);
|
||||
}
|
||||
|
||||
function onDragLeave(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
function onDragOver(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
|
||||
async function onDrop(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
let imageFiles = [];
|
||||
|
||||
// Check if the dropped image is from a URL
|
||||
const html = event.dataTransfer.getData("text/html");
|
||||
if (html) {
|
||||
try {
|
||||
const urlMatch = html.match(/src="?([^"\s]+)"?\s*/);
|
||||
const url = urlMatch[1].replace("&", "&"); // Reverse html encoding of url parameters
|
||||
let name = "";
|
||||
const altMatch = html.match(/alt="?([^"]+)"?\s*/);
|
||||
if (altMatch && altMatch.length > 1) {
|
||||
name = altMatch[1];
|
||||
}
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
const file = await response.blob();
|
||||
file.name = name;
|
||||
if (supportFileTypes.includes(file.type)) {
|
||||
imageFiles.push(file);
|
||||
} else {
|
||||
addToast(`Unsupported file type for ${file.name}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message === "Failed to fetch") {
|
||||
addToast("Unable to import image: failed to fetch");
|
||||
} else {
|
||||
addToast("Unable to import image");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const files = event.dataTransfer.files;
|
||||
for (let file of files) {
|
||||
if (supportFileTypes.includes(file.type)) {
|
||||
imageFiles.push(file);
|
||||
} else {
|
||||
addToast(`Unsupported file type for ${file.name}`);
|
||||
}
|
||||
}
|
||||
const dropPosition = new Vector2(event.clientX, event.clientY);
|
||||
onImageDrop(imageFiles, dropPosition);
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
const containerListeners = { onDragEnter };
|
||||
const overlayListeners = { onDragLeave, onDragOver, onDrop };
|
||||
|
||||
return { dragging, containerListeners, overlayListeners };
|
||||
}
|
||||
|
||||
export default useImageDrop;
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import useImage from "use-image";
|
||||
|
||||
import { useImageSource } from "../contexts/ImageSourceContext";
|
||||
import { useDataURL } from "../contexts/AssetsContext";
|
||||
|
||||
import { mapSources as defaultMapSources } from "../maps";
|
||||
|
||||
function useMapImage(map) {
|
||||
const mapSource = useImageSource(map, defaultMapSources);
|
||||
const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource);
|
||||
const mapURL = useDataURL(map, defaultMapSources);
|
||||
const [mapImage, mapImageStatus] = useImage(mapURL);
|
||||
|
||||
// Create a map source that only updates when the image is fully loaded
|
||||
const [loadedMapSourceImage, setLoadedMapSourceImage] = useState();
|
||||
const [loadedMapImage, setLoadedMapImage] = useState();
|
||||
useEffect(() => {
|
||||
if (mapSourceImageStatus === "loaded") {
|
||||
setLoadedMapSourceImage(mapSourceImage);
|
||||
if (mapImageStatus === "loaded") {
|
||||
setLoadedMapImage(mapImage);
|
||||
}
|
||||
}, [mapSourceImage, mapSourceImageStatus]);
|
||||
}, [mapImage, mapImageStatus]);
|
||||
|
||||
return [loadedMapSourceImage, mapSourceImageStatus];
|
||||
return [loadedMapImage, mapImageStatus];
|
||||
}
|
||||
|
||||
export default useMapImage;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
|
||||
import useDebounce from "./useDebounce";
|
||||
import { diff, applyChanges } from "../helpers/diff";
|
||||
@@ -76,7 +77,7 @@ function useNetworkedState(
|
||||
}
|
||||
dirtyRef.current = false;
|
||||
forceUpdateRef.current = false;
|
||||
lastSyncedStateRef.current = debouncedState;
|
||||
lastSyncedStateRef.current = cloneDeep(debouncedState);
|
||||
}
|
||||
}, [
|
||||
session.socket,
|
||||
|
||||
22
src/hooks/usePreventSelect.js
Normal file
22
src/hooks/usePreventSelect.js
Normal file
@@ -0,0 +1,22 @@
|
||||
function usePreventSelect() {
|
||||
function clearSelection() {
|
||||
if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
if (document.selection) {
|
||||
document.selection.empty();
|
||||
}
|
||||
}
|
||||
function preventSelect() {
|
||||
clearSelection();
|
||||
document.body.classList.add("no-select");
|
||||
}
|
||||
|
||||
function resumeSelect() {
|
||||
document.body.classList.remove("no-select");
|
||||
}
|
||||
|
||||
return [preventSelect, resumeSelect];
|
||||
}
|
||||
|
||||
export default usePreventSelect;
|
||||
@@ -21,7 +21,20 @@ function useResponsiveLayout() {
|
||||
? "medium"
|
||||
: "large";
|
||||
|
||||
return { screenSize, modalSize, tileSize };
|
||||
const tileGridColumns = isLargeScreen ? 4 : isMediumScreen ? 3 : 2;
|
||||
|
||||
const groupGridColumns = isLargeScreen ? 3 : 2;
|
||||
|
||||
const tileContainerHeight = isLargeScreen ? "600px" : "400px";
|
||||
|
||||
return {
|
||||
screenSize,
|
||||
modalSize,
|
||||
tileSize,
|
||||
tileGridColumns,
|
||||
tileContainerHeight,
|
||||
groupGridColumns,
|
||||
};
|
||||
}
|
||||
|
||||
export default useResponsiveLayout;
|
||||
|
||||
Reference in New Issue
Block a user