Skip to content

Commit

Permalink
feat: 🎸 Added mini map limiter
Browse files Browse the repository at this point in the history
  • Loading branch information
prc5 committed Jun 19, 2024
1 parent 4d9d9d5 commit 7401345
Show file tree
Hide file tree
Showing 14 changed files with 522 additions and 17 deletions.
10 changes: 10 additions & 0 deletions src/.eslintrc.json → .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@
"rules": {
"testing-library/render-result-naming-convention": "off"
}
},
{
"files": ["*.mdx"],
"rules": {
"react/jsx-filename-extension": "off"
}
},
{
"files": ["*.md", "*.mdx"],
"extends": "plugin:mdx/recommended"
}
]
}
14 changes: 13 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { StorybookConfig } from "@storybook/react-vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { mergeConfig } from "vite";
import svgr from "vite-plugin-svgr";

const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
Expand All @@ -20,7 +21,18 @@ const config: StorybookConfig = {
async viteFinal(config, { configType }) {
// return the customized config
return mergeConfig(config, {
plugins: [tsconfigPaths()],
plugins: [
tsconfigPaths(),
svgr({
svgrOptions: {
exportType: "named",
ref: true,
svgo: false,
titleProp: true,
},
include: "**/*.svg",
}),
],
});
},
};
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"ts-node": "~10.9.2",
"typescript": "^4.2.3",
"vite": "^5.0.11",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.3.1"
},
"engines": {
Expand Down
94 changes: 90 additions & 4 deletions src/components/mini-map/mini-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
} from "hooks";
import { useResize } from "./use-resize.hook";
import { ReactZoomPanPinchRef } from "models";
import { boundLimiter } from "core/bounds/bounds.utils";

export type MiniMapProps = {
children: React.ReactNode;
width?: number;
height?: number;
borderColor?: string;
panning?: boolean;
} & React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
Expand All @@ -34,23 +36,32 @@ const previewStyles = {
border: "3px solid red",
transformOrigin: "0% 0%",
boxShadow: "rgba(0,0,0,0.2) 0 0 0 10000000px",
pointerEvents: "none",
} as const;

export const MiniMap: React.FC<MiniMapProps> = ({
width = 200,
height = 200,
borderColor = "red",
children,
panning = true,
...rest
}) => {
const [initialized, setInitialized] = useState(false);
const instance = useTransformContext();
const [isDown, setIsDown] = useState(false);
const miniMapInstance = useRef<ReactZoomPanPinchRef>(null);

const mainRef = useRef<HTMLDivElement | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const previewRef = useRef<HTMLDivElement | null>(null);

const computationCache = useRef({
scale: 1,
width: 0,
height: 0,
});

const getContentSize = useCallback(() => {
if (instance.contentComponent) {
const rect = instance.contentComponent.getBoundingClientRect();
Expand Down Expand Up @@ -93,7 +104,7 @@ export const MiniMap: React.FC<MiniMapProps> = ({
position: "absolute",
boxSizing: "border-box",
zIndex: 1,
overflow: "hidden",
// overflow: "hidden",
} as const;

Object.keys(style).forEach((key) => {
Expand Down Expand Up @@ -160,11 +171,81 @@ export const MiniMap: React.FC<MiniMapProps> = ({
});
}, [computeMiniMapScale, instance, miniMapInstance]);

useEffect(() => {
const move = (e: MouseEvent) => {
if (panning && isDown && instance.contentComponent) {
const scale = computeMiniMapScale();
const previewRect = previewRef.current?.getBoundingClientRect()!;
const mainRect = mainRef.current?.getBoundingClientRect()!;

const relativeX = (e.clientX - mainRect.left) / scale;
const relativeY = (e.clientY - mainRect.top) / scale;

const x = relativeX - previewRect.width / 2;
const y = relativeY - previewRect.height / 2;

const instanceWidth =
(instance.wrapperComponent?.offsetWidth || 0) *
instance.transformState.scale;
const instanceHeight =
(instance.wrapperComponent?.offsetHeight || 0) *
instance.transformState.scale;

const limitWidth =
instanceWidth - previewRect.width * 2 * instance.transformState.scale;
const limitHeight =
instanceHeight -
previewRect.height * 2 * instance.transformState.scale;

const boundedX = boundLimiter(
x * instance.transformState.scale,
0,
limitWidth,
true,
);

const boundedY = boundLimiter(
y * instance.transformState.scale,
0,
limitHeight,
true,
);

instance.setTransformState(
instance.transformState.scale,
-boundedX,
-boundedY,
);
}
};

const setDown = (e: MouseEvent) => {
if (
mainRef.current?.contains(e.target as Node) ||
e.target === mainRef.current
) {
move(e);
setIsDown(true);
}
};
const setUp = () => {
setIsDown(false);
};
document.addEventListener("mousedown", setDown);
document.addEventListener("mouseup", setUp);
document.addEventListener("mousemove", move);
return () => {
document.removeEventListener("mousemove", move);
document.removeEventListener("mouseup", setUp);
};
});

const wrapperStyle = useMemo(() => {
return {
position: "relative",
zIndex: 2,
overflow: "hidden",
// overflow: "hidden",
userSelect: "none",
} as const;
}, []);

Expand All @@ -175,11 +256,16 @@ export const MiniMap: React.FC<MiniMapProps> = ({
style={wrapperStyle}
className={`rzpp-mini-map ${rest.className || ""}`}
>
<div {...rest} ref={wrapperRef} className="rzpp-wrapper">
<div
{...rest}
style={{ pointerEvents: "none" }}
ref={wrapperRef}
className="rzpp-minimap-wrapper"
>
{children}
</div>
<div
className="rzpp-preview"
className="rzpp-minimap-preview"
ref={previewRef}
style={{ ...previewStyles, borderColor }}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/constants/state.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ export const initialSetup: LibrarySetup = {
centerZoomedOut: false,
centerOnInit: false,
disablePadding: false,
smooth: true,
detached: false,
wheel: {
step: 0.03,
smooth: true,
disabled: false,
wheelDisabled: false,
touchPadDisabled: false,
Expand Down
42 changes: 41 additions & 1 deletion src/core/instance.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ export class ZoomPanPinch {
public onChangeCallbacks: Set<(ctx: ReactZoomPanPinchRef) => void> =
new Set();
public onInitCallbacks: Set<(ctx: ReactZoomPanPinchRef) => void> = new Set();
public onTransformCallbacks: Set<
(data: {
scale: number;
positionX: number;
positionY: number;
previousScale: number;
ref: ReactZoomPanPinchRef;
}) => void
> = new Set();

// Components
public wrapperComponent: HTMLDivElement | null = null;
Expand Down Expand Up @@ -452,7 +461,21 @@ export class ZoomPanPinch {
if (!this.mounted || !this.contentComponent) return;
const { scale, positionX, positionY } = this.transformState;
const transform = this.handleTransformStyles(positionX, positionY, scale);
this.contentComponent.style.transform = transform;

// Detached mode do not apply transformation directly to content component
if (!this.props.detached) {
this.contentComponent.style.transform = transform;
}

this.onTransformCallbacks.forEach((callback) =>
callback({
scale,
positionX,
positionY,
previousScale: this.transformState.previousScale,
ref: getContext(this),
}),
);
};

getContext = () => {
Expand All @@ -463,6 +486,23 @@ export class ZoomPanPinch {
* Hooks
*/

onTransform = (
callback: (data: {
scale: number;
positionX: number;
positionY: number;
previousScale: number;
ref: ReactZoomPanPinchRef;
}) => void,
) => {
if (!this.onTransformCallbacks.has(callback)) {
this.onTransformCallbacks.add(callback);
}
return () => {
this.onTransformCallbacks.delete(callback);
};
};

onChange = (callback: (ref: ReactZoomPanPinchRef) => void) => {
if (!this.onChangeCallbacks.has(callback)) {
this.onChangeCallbacks.add(callback);
Expand Down
3 changes: 2 additions & 1 deletion src/core/wheel/wheel.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export const handleWheelZoom = (
zoomAnimation,
wheel,
disablePadding,
smooth,
} = setup;
const { size, disabled } = zoomAnimation;
const { step, smooth } = wheel;
const { step } = wheel;

if (!contentComponent) {
throw new Error("Component not mounted");
Expand Down
45 changes: 45 additions & 0 deletions src/hooks/use-zoom-pan-pinch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { initialSetup } from "constants/state.constants";
import { ZoomPanPinch } from "core/instance.core";
import { ReactZoomPanPinchProps, ReactZoomPanPinchRef } from "models";
import { useLayoutEffect, useRef } from "react";

export const useZoomPanPinch = (props?: ReactZoomPanPinchProps) => {
const contentRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const instance = useRef(new ZoomPanPinch({ ...initialSetup, ...props }));

const useTransformCallback = useRef<
(data: {
positionX: number;
positionY: number;
scale: number;
previousScale: number;
ref: ReactZoomPanPinchRef;
}) => void
>(() => {});

useLayoutEffect(() => {
if (contentRef.current && wrapperRef.current) {
instance.current.init(wrapperRef.current, contentRef.current);
}
const unmount = instance.current.onTransform((data) => {
useTransformCallback.current(data);
});

return () => {
instance.current.cleanupWindowEvents();
unmount();
};
}, [contentRef.current, wrapperRef.current]);

const useTransform = (callback: typeof useTransformCallback.current) => {
useTransformCallback.current = callback;
};

return {
contentRef,
wrapperRef,
instance,
useTransform,
};
};
3 changes: 2 additions & 1 deletion src/models/context.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type ReactZoomPanPinchProps = {
| React.ReactNode
| ((ref: ReactZoomPanPinchContentRef) => React.ReactNode);
ref?: React.Ref<ReactZoomPanPinchRef>;
detached: boolean;
initialScale?: number;
initialPositionX?: number;
initialPositionY?: number;
Expand All @@ -66,9 +67,9 @@ export type ReactZoomPanPinchProps = {
centerOnInit?: boolean;
disablePadding?: boolean;
customTransform?: (x: number, y: number, scale: number) => string;
smooth?: boolean;
wheel?: {
step?: number;
smooth?: boolean;
disabled?: boolean;
wheelDisabled?: boolean;
touchPadDisabled?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meta, Story, ArgsTable, Canvas } from "@storybook/addon-docs/blocks";
import { action } from "@storybook/addon-actions";

import { TransformComponent, TransformWrapper } from "components";
import { TransformComponent, TransformWrapper } from "../../../components/";
import { argsTypes } from "../../types/args.types";
import { Example } from "./example";

Expand Down
5 changes: 4 additions & 1 deletion src/stories/examples/mini-map/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ export const Template = (args: any) => {
zIndex: 5,
top: "50px",
right: "50px",
border: "1px solid blue",
}}
>
<MiniMap width={200}>{element}</MiniMap>
<MiniMap width={400} height={800}>
{element}
</MiniMap>
</div>
<Controls {...utils} />
<TransformComponent
Expand Down
Loading

0 comments on commit 7401345

Please sign in to comment.