@omer-x/svg-viewport 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,7 +26,7 @@ __export(index_exports, {
26
26
  module.exports = __toCommonJS(index_exports);
27
27
 
28
28
  // src/components/SvgViewport.tsx
29
- var import_react2 = require("react");
29
+ var import_react = require("react");
30
30
 
31
31
  // src/core/matrix.ts
32
32
  function transform(matrix) {
@@ -58,16 +58,6 @@ function getFocusedMatrix(focusPoint, width, height) {
58
58
  }
59
59
  }
60
60
 
61
- // src/hooks/polyfill-state.ts
62
- var import_react = require("react");
63
- function usePolyfillState(state, dispatch) {
64
- const [polyfill, setPolyfill] = (0, import_react.useState)(state);
65
- if (dispatch) {
66
- return [state, dispatch];
67
- }
68
- return [polyfill, setPolyfill];
69
- }
70
-
71
61
  // src/components/SvgViewport.tsx
72
62
  var import_jsx_runtime = require("react/jsx-runtime");
73
63
  var SvgViewport = ({
@@ -77,55 +67,64 @@ var SvgViewport = ({
77
67
  zoomable = false,
78
68
  minZoom = 0.5,
79
69
  maxZoom = 2,
80
- panning = false,
81
- setPanning,
82
- transformation = null,
83
- setTransformation,
70
+ transformation: externalTransformation,
71
+ onTransformationChange,
84
72
  initialFocusPoint = "center",
85
73
  style,
86
74
  children,
87
75
  ...otherProps
88
76
  }) => {
89
- const pointer = (0, import_react2.useRef)({ x: 0, y: 0 });
90
- const [grabbing, setGrabbing] = (0, import_react2.useState)(false);
91
- const [activeTransformation, activeSetTransformation] = usePolyfillState(transformation, setTransformation);
92
- const [activePanning, setActivePanning] = usePolyfillState(panning, setPanning);
77
+ const isControlled = externalTransformation !== void 0;
78
+ const [internalTransformation, setInternalTransformation] = (0, import_react.useState)(null);
79
+ const pointer = (0, import_react.useRef)({ x: 0, y: 0 });
80
+ const [grabbing, setGrabbing] = (0, import_react.useState)(false);
81
+ const [isPanning, setIsPanning] = (0, import_react.useState)(false);
82
+ const transformation = isControlled ? externalTransformation : internalTransformation;
83
+ const transformationRef = (0, import_react.useRef)(transformation);
84
+ (0, import_react.useEffect)(() => {
85
+ transformationRef.current = transformation;
86
+ }, [transformation]);
87
+ (0, import_react.useEffect)(() => {
88
+ if (!isControlled && !internalTransformation) {
89
+ setInternalTransformation({
90
+ zoom: 1,
91
+ matrix: getFocusedMatrix(initialFocusPoint, width, height)
92
+ });
93
+ }
94
+ }, []);
95
+ const setTransformation = (0, import_react.useCallback)((value) => {
96
+ if (!isControlled) {
97
+ setInternalTransformation(value);
98
+ }
99
+ onTransformationChange?.(value);
100
+ }, [isControlled, onTransformationChange]);
93
101
  const stopGrabbing = () => {
94
102
  setGrabbing(false);
95
103
  };
96
- (0, import_react2.useEffect)(() => {
97
- if (setTransformation) return;
98
- activeSetTransformation({
99
- zoom: 1,
100
- matrix: getFocusedMatrix(initialFocusPoint, width, height)
101
- });
102
- }, [setTransformation]);
103
104
  const down = (e) => {
104
- if (e.button === 0) {
105
- pointer.current = {
106
- x: e.clientX,
107
- y: e.clientY
108
- };
109
- setActivePanning(true);
110
- }
105
+ if (e.button !== 0) return;
106
+ pointer.current = { x: e.clientX, y: e.clientY };
107
+ setIsPanning(true);
111
108
  setGrabbing(true);
112
109
  };
113
- const move = (0, import_react2.useCallback)((e) => {
114
- if (activePanning && activeTransformation) {
115
- const x = (e.clientX - pointer.current.x) / activeTransformation.zoom;
116
- const y = (e.clientY - pointer.current.y) / activeTransformation.zoom;
117
- pointer.current = {
118
- x: e.clientX,
119
- y: e.clientY
120
- };
121
- activeSetTransformation((t) => t ? { ...t, matrix: t.matrix.translate(x, y) } : t);
110
+ const move = (0, import_react.useCallback)((e) => {
111
+ const currentTrans = transformationRef.current;
112
+ if (currentTrans) {
113
+ const x = (e.clientX - pointer.current.x) / currentTrans.zoom;
114
+ const y = (e.clientY - pointer.current.y) / currentTrans.zoom;
115
+ pointer.current = { x: e.clientX, y: e.clientY };
116
+ setTransformation({
117
+ ...currentTrans,
118
+ matrix: currentTrans.matrix.translate(x, y)
119
+ });
122
120
  }
123
- }, [activePanning, activeTransformation]);
124
- const up = (0, import_react2.useCallback)(() => {
125
- setActivePanning(false);
121
+ }, [setTransformation]);
122
+ const up = (0, import_react.useCallback)(() => {
123
+ setIsPanning(false);
124
+ setGrabbing(false);
126
125
  }, []);
127
- (0, import_react2.useEffect)(() => {
128
- if (activePanning) {
126
+ (0, import_react.useEffect)(() => {
127
+ if (isPanning) {
129
128
  document.addEventListener("mousemove", move);
130
129
  document.addEventListener("mouseup", up);
131
130
  }
@@ -133,24 +132,21 @@ var SvgViewport = ({
133
132
  document.removeEventListener("mousemove", move);
134
133
  document.removeEventListener("mouseup", up);
135
134
  };
136
- }, [activePanning]);
135
+ }, [isPanning, move, up]);
137
136
  const adjustZoom = (e) => {
137
+ if (!zoomable) return;
138
+ e.preventDefault();
138
139
  const scale = e.deltaY < 0 ? 1.25 : 0.8;
139
140
  const eventTarget = e.currentTarget;
140
- const eventClientX = e.clientX;
141
- const eventClientY = e.clientY;
142
- activeSetTransformation((t) => {
143
- if (t && t.zoom * scale > minZoom && t.zoom * scale < maxZoom) {
144
- return {
145
- ...t,
146
- zoom: t.zoom * scale,
147
- matrix: adjustWithZoom(t.matrix, scale, eventTarget, eventClientX, eventClientY)
148
- };
149
- }
150
- return t;
151
- });
141
+ const { clientX, clientY } = e;
142
+ if (transformation && transformation.zoom * scale > minZoom && transformation.zoom * scale < maxZoom) {
143
+ setTransformation({
144
+ zoom: transformation.zoom * scale,
145
+ matrix: adjustWithZoom(transformation.matrix, scale, eventTarget, clientX, clientY)
146
+ });
147
+ }
152
148
  };
153
- const cursor = pannable ? grabbing || panning ? "grabbing" : "grab" : "auto";
149
+ const cursor = pannable ? grabbing ? "grabbing" : "grab" : "auto";
154
150
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
155
151
  "svg",
156
152
  {
@@ -163,7 +159,7 @@ var SvgViewport = ({
163
159
  onContextMenu: (e) => e.preventDefault(),
164
160
  style: { ...style, cursor },
165
161
  ...otherProps,
166
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { transform: transform(activeTransformation?.matrix), children: activeTransformation && children })
162
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { transform: transform(transformation?.matrix), children: transformation && children })
167
163
  }
168
164
  );
169
165
  };
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentProps, Dispatch, SetStateAction } from 'react';
2
+ import { ComponentProps } from 'react';
3
3
 
4
4
  type FocusPoint = "center" | "top-left";
5
5
 
6
- type ViewportTransform = {
6
+ type ViewportTransformation = {
7
7
  zoom: number;
8
8
  matrix: DOMMatrix;
9
9
  };
@@ -37,21 +37,13 @@ type SvgViewportProps = ComponentProps<"svg"> & {
37
37
  */
38
38
  maxZoom?: number;
39
39
  /**
40
- * Indicates if panning is currently active.
40
+ * Current transformation state of the viewport.
41
41
  */
42
- panning?: boolean;
42
+ transformation?: ViewportTransformation | null;
43
43
  /**
44
- * Setter for the panning state.
44
+ * Callback to update the transformation state.
45
45
  */
46
- setPanning?: Dispatch<SetStateAction<boolean>>;
47
- /**
48
- * Current transformation state.
49
- */
50
- transformation?: ViewportTransform | null;
51
- /**
52
- * Setter for the transformation state.
53
- */
54
- setTransformation?: Dispatch<SetStateAction<ViewportTransform | null>>;
46
+ onTransformationChange?: (tranformation: ViewportTransformation) => void;
55
47
  /**
56
48
  * Initial focus point of the viewport.
57
49
  */
@@ -60,6 +52,6 @@ type SvgViewportProps = ComponentProps<"svg"> & {
60
52
  /**
61
53
  * SVG Viewport component that supports panning and zooming.
62
54
  */
63
- declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, initialFocusPoint, style, children, ...otherProps }: SvgViewportProps) => react_jsx_runtime.JSX.Element;
55
+ declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, transformation: externalTransformation, onTransformationChange, initialFocusPoint, style, children, ...otherProps }: SvgViewportProps) => react_jsx_runtime.JSX.Element;
64
56
 
65
57
  export { SvgViewport as default };
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentProps, Dispatch, SetStateAction } from 'react';
2
+ import { ComponentProps } from 'react';
3
3
 
4
4
  type FocusPoint = "center" | "top-left";
5
5
 
6
- type ViewportTransform = {
6
+ type ViewportTransformation = {
7
7
  zoom: number;
8
8
  matrix: DOMMatrix;
9
9
  };
@@ -37,21 +37,13 @@ type SvgViewportProps = ComponentProps<"svg"> & {
37
37
  */
38
38
  maxZoom?: number;
39
39
  /**
40
- * Indicates if panning is currently active.
40
+ * Current transformation state of the viewport.
41
41
  */
42
- panning?: boolean;
42
+ transformation?: ViewportTransformation | null;
43
43
  /**
44
- * Setter for the panning state.
44
+ * Callback to update the transformation state.
45
45
  */
46
- setPanning?: Dispatch<SetStateAction<boolean>>;
47
- /**
48
- * Current transformation state.
49
- */
50
- transformation?: ViewportTransform | null;
51
- /**
52
- * Setter for the transformation state.
53
- */
54
- setTransformation?: Dispatch<SetStateAction<ViewportTransform | null>>;
46
+ onTransformationChange?: (tranformation: ViewportTransformation) => void;
55
47
  /**
56
48
  * Initial focus point of the viewport.
57
49
  */
@@ -60,6 +52,6 @@ type SvgViewportProps = ComponentProps<"svg"> & {
60
52
  /**
61
53
  * SVG Viewport component that supports panning and zooming.
62
54
  */
63
- declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, panning, setPanning, transformation, setTransformation, initialFocusPoint, style, children, ...otherProps }: SvgViewportProps) => react_jsx_runtime.JSX.Element;
55
+ declare const SvgViewport: ({ width, height, pannable, zoomable, minZoom, maxZoom, transformation: externalTransformation, onTransformationChange, initialFocusPoint, style, children, ...otherProps }: SvgViewportProps) => react_jsx_runtime.JSX.Element;
64
56
 
65
57
  export { SvgViewport as default };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  // src/components/SvgViewport.tsx
4
- import { useCallback, useEffect, useRef, useState as useState2 } from "react";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
5
 
6
6
  // src/core/matrix.ts
7
7
  function transform(matrix) {
@@ -33,16 +33,6 @@ function getFocusedMatrix(focusPoint, width, height) {
33
33
  }
34
34
  }
35
35
 
36
- // src/hooks/polyfill-state.ts
37
- import { useState } from "react";
38
- function usePolyfillState(state, dispatch) {
39
- const [polyfill, setPolyfill] = useState(state);
40
- if (dispatch) {
41
- return [state, dispatch];
42
- }
43
- return [polyfill, setPolyfill];
44
- }
45
-
46
36
  // src/components/SvgViewport.tsx
47
37
  import { jsx } from "react/jsx-runtime";
48
38
  var SvgViewport = ({
@@ -52,55 +42,64 @@ var SvgViewport = ({
52
42
  zoomable = false,
53
43
  minZoom = 0.5,
54
44
  maxZoom = 2,
55
- panning = false,
56
- setPanning,
57
- transformation = null,
58
- setTransformation,
45
+ transformation: externalTransformation,
46
+ onTransformationChange,
59
47
  initialFocusPoint = "center",
60
48
  style,
61
49
  children,
62
50
  ...otherProps
63
51
  }) => {
52
+ const isControlled = externalTransformation !== void 0;
53
+ const [internalTransformation, setInternalTransformation] = useState(null);
64
54
  const pointer = useRef({ x: 0, y: 0 });
65
- const [grabbing, setGrabbing] = useState2(false);
66
- const [activeTransformation, activeSetTransformation] = usePolyfillState(transformation, setTransformation);
67
- const [activePanning, setActivePanning] = usePolyfillState(panning, setPanning);
55
+ const [grabbing, setGrabbing] = useState(false);
56
+ const [isPanning, setIsPanning] = useState(false);
57
+ const transformation = isControlled ? externalTransformation : internalTransformation;
58
+ const transformationRef = useRef(transformation);
59
+ useEffect(() => {
60
+ transformationRef.current = transformation;
61
+ }, [transformation]);
62
+ useEffect(() => {
63
+ if (!isControlled && !internalTransformation) {
64
+ setInternalTransformation({
65
+ zoom: 1,
66
+ matrix: getFocusedMatrix(initialFocusPoint, width, height)
67
+ });
68
+ }
69
+ }, []);
70
+ const setTransformation = useCallback((value) => {
71
+ if (!isControlled) {
72
+ setInternalTransformation(value);
73
+ }
74
+ onTransformationChange?.(value);
75
+ }, [isControlled, onTransformationChange]);
68
76
  const stopGrabbing = () => {
69
77
  setGrabbing(false);
70
78
  };
71
- useEffect(() => {
72
- if (setTransformation) return;
73
- activeSetTransformation({
74
- zoom: 1,
75
- matrix: getFocusedMatrix(initialFocusPoint, width, height)
76
- });
77
- }, [setTransformation]);
78
79
  const down = (e) => {
79
- if (e.button === 0) {
80
- pointer.current = {
81
- x: e.clientX,
82
- y: e.clientY
83
- };
84
- setActivePanning(true);
85
- }
80
+ if (e.button !== 0) return;
81
+ pointer.current = { x: e.clientX, y: e.clientY };
82
+ setIsPanning(true);
86
83
  setGrabbing(true);
87
84
  };
88
85
  const move = useCallback((e) => {
89
- if (activePanning && activeTransformation) {
90
- const x = (e.clientX - pointer.current.x) / activeTransformation.zoom;
91
- const y = (e.clientY - pointer.current.y) / activeTransformation.zoom;
92
- pointer.current = {
93
- x: e.clientX,
94
- y: e.clientY
95
- };
96
- activeSetTransformation((t) => t ? { ...t, matrix: t.matrix.translate(x, y) } : t);
86
+ const currentTrans = transformationRef.current;
87
+ if (currentTrans) {
88
+ const x = (e.clientX - pointer.current.x) / currentTrans.zoom;
89
+ const y = (e.clientY - pointer.current.y) / currentTrans.zoom;
90
+ pointer.current = { x: e.clientX, y: e.clientY };
91
+ setTransformation({
92
+ ...currentTrans,
93
+ matrix: currentTrans.matrix.translate(x, y)
94
+ });
97
95
  }
98
- }, [activePanning, activeTransformation]);
96
+ }, [setTransformation]);
99
97
  const up = useCallback(() => {
100
- setActivePanning(false);
98
+ setIsPanning(false);
99
+ setGrabbing(false);
101
100
  }, []);
102
101
  useEffect(() => {
103
- if (activePanning) {
102
+ if (isPanning) {
104
103
  document.addEventListener("mousemove", move);
105
104
  document.addEventListener("mouseup", up);
106
105
  }
@@ -108,24 +107,21 @@ var SvgViewport = ({
108
107
  document.removeEventListener("mousemove", move);
109
108
  document.removeEventListener("mouseup", up);
110
109
  };
111
- }, [activePanning]);
110
+ }, [isPanning, move, up]);
112
111
  const adjustZoom = (e) => {
112
+ if (!zoomable) return;
113
+ e.preventDefault();
113
114
  const scale = e.deltaY < 0 ? 1.25 : 0.8;
114
115
  const eventTarget = e.currentTarget;
115
- const eventClientX = e.clientX;
116
- const eventClientY = e.clientY;
117
- activeSetTransformation((t) => {
118
- if (t && t.zoom * scale > minZoom && t.zoom * scale < maxZoom) {
119
- return {
120
- ...t,
121
- zoom: t.zoom * scale,
122
- matrix: adjustWithZoom(t.matrix, scale, eventTarget, eventClientX, eventClientY)
123
- };
124
- }
125
- return t;
126
- });
116
+ const { clientX, clientY } = e;
117
+ if (transformation && transformation.zoom * scale > minZoom && transformation.zoom * scale < maxZoom) {
118
+ setTransformation({
119
+ zoom: transformation.zoom * scale,
120
+ matrix: adjustWithZoom(transformation.matrix, scale, eventTarget, clientX, clientY)
121
+ });
122
+ }
127
123
  };
128
- const cursor = pannable ? grabbing || panning ? "grabbing" : "grab" : "auto";
124
+ const cursor = pannable ? grabbing ? "grabbing" : "grab" : "auto";
129
125
  return /* @__PURE__ */ jsx(
130
126
  "svg",
131
127
  {
@@ -138,7 +134,7 @@ var SvgViewport = ({
138
134
  onContextMenu: (e) => e.preventDefault(),
139
135
  style: { ...style, cursor },
140
136
  ...otherProps,
141
- children: /* @__PURE__ */ jsx("g", { transform: transform(activeTransformation?.matrix), children: activeTransformation && children })
137
+ children: /* @__PURE__ */ jsx("g", { transform: transform(transformation?.matrix), children: transformation && children })
142
138
  }
143
139
  );
144
140
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omer-x/svg-viewport",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Provides a simple React component for displaying SVG content with zooming and panning capabilities",
5
5
  "keywords": [
6
6
  "react",