@omer-x/svg-viewport 1.0.0 → 2.0.0

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,62 @@ 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)(() => {
79
+ if (isControlled) return null;
80
+ return {
81
+ zoom: 1,
82
+ matrix: getFocusedMatrix(initialFocusPoint, width, height)
83
+ };
84
+ });
85
+ const transformation = isControlled ? externalTransformation : internalTransformation;
86
+ const transformationRef = (0, import_react.useRef)(transformation);
87
+ (0, import_react.useEffect)(() => {
88
+ transformationRef.current = transformation;
89
+ }, [transformation]);
90
+ const pointer = (0, import_react.useRef)({ x: 0, y: 0 });
91
+ const [grabbing, setGrabbing] = (0, import_react.useState)(false);
92
+ const [isPanning, setIsPanning] = (0, import_react.useState)(false);
93
+ const setTransformation = (0, import_react.useCallback)((value) => {
94
+ if (!isControlled) {
95
+ setInternalTransformation(value);
96
+ }
97
+ onTransformationChange?.(value);
98
+ }, [isControlled, onTransformationChange]);
93
99
  const stopGrabbing = () => {
94
100
  setGrabbing(false);
95
101
  };
96
- (0, import_react2.useEffect)(() => {
97
- if (setTransformation) return;
98
- activeSetTransformation({
99
- zoom: 1,
100
- matrix: getFocusedMatrix(initialFocusPoint, width, height)
101
- });
102
- }, [setTransformation]);
103
102
  const down = (e) => {
104
- if (e.button === 0) {
105
- pointer.current = {
106
- x: e.clientX,
107
- y: e.clientY
108
- };
109
- setActivePanning(true);
110
- }
103
+ if (e.button !== 0) return;
104
+ pointer.current = { x: e.clientX, y: e.clientY };
105
+ setIsPanning(true);
111
106
  setGrabbing(true);
112
107
  };
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);
108
+ const move = (0, import_react.useCallback)((e) => {
109
+ const currentTrans = transformationRef.current;
110
+ if (currentTrans) {
111
+ const x = (e.clientX - pointer.current.x) / currentTrans.zoom;
112
+ const y = (e.clientY - pointer.current.y) / currentTrans.zoom;
113
+ pointer.current = { x: e.clientX, y: e.clientY };
114
+ setTransformation({
115
+ ...currentTrans,
116
+ matrix: currentTrans.matrix.translate(x, y)
117
+ });
122
118
  }
123
- }, [activePanning, activeTransformation]);
124
- const up = (0, import_react2.useCallback)(() => {
125
- setActivePanning(false);
119
+ }, [setTransformation]);
120
+ const up = (0, import_react.useCallback)(() => {
121
+ setIsPanning(false);
122
+ setGrabbing(false);
126
123
  }, []);
127
- (0, import_react2.useEffect)(() => {
128
- if (activePanning) {
124
+ (0, import_react.useEffect)(() => {
125
+ if (isPanning) {
129
126
  document.addEventListener("mousemove", move);
130
127
  document.addEventListener("mouseup", up);
131
128
  }
@@ -133,24 +130,21 @@ var SvgViewport = ({
133
130
  document.removeEventListener("mousemove", move);
134
131
  document.removeEventListener("mouseup", up);
135
132
  };
136
- }, [activePanning]);
133
+ }, [isPanning, move, up]);
137
134
  const adjustZoom = (e) => {
135
+ if (!zoomable) return;
136
+ e.preventDefault();
138
137
  const scale = e.deltaY < 0 ? 1.25 : 0.8;
139
138
  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
- });
139
+ const { clientX, clientY } = e;
140
+ if (transformation && transformation.zoom * scale > minZoom && transformation.zoom * scale < maxZoom) {
141
+ setTransformation({
142
+ zoom: transformation.zoom * scale,
143
+ matrix: adjustWithZoom(transformation.matrix, scale, eventTarget, clientX, clientY)
144
+ });
145
+ }
152
146
  };
153
- const cursor = pannable ? grabbing || panning ? "grabbing" : "grab" : "auto";
147
+ const cursor = pannable ? grabbing ? "grabbing" : "grab" : "auto";
154
148
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
155
149
  "svg",
156
150
  {
@@ -163,7 +157,7 @@ var SvgViewport = ({
163
157
  onContextMenu: (e) => e.preventDefault(),
164
158
  style: { ...style, cursor },
165
159
  ...otherProps,
166
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { transform: transform(activeTransformation?.matrix), children: activeTransformation && children })
160
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("g", { transform: transform(transformation?.matrix), children: transformation && children })
167
161
  }
168
162
  );
169
163
  };
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,62 @@ 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(() => {
54
+ if (isControlled) return null;
55
+ return {
56
+ zoom: 1,
57
+ matrix: getFocusedMatrix(initialFocusPoint, width, height)
58
+ };
59
+ });
60
+ const transformation = isControlled ? externalTransformation : internalTransformation;
61
+ const transformationRef = useRef(transformation);
62
+ useEffect(() => {
63
+ transformationRef.current = transformation;
64
+ }, [transformation]);
64
65
  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);
66
+ const [grabbing, setGrabbing] = useState(false);
67
+ const [isPanning, setIsPanning] = useState(false);
68
+ const setTransformation = useCallback((value) => {
69
+ if (!isControlled) {
70
+ setInternalTransformation(value);
71
+ }
72
+ onTransformationChange?.(value);
73
+ }, [isControlled, onTransformationChange]);
68
74
  const stopGrabbing = () => {
69
75
  setGrabbing(false);
70
76
  };
71
- useEffect(() => {
72
- if (setTransformation) return;
73
- activeSetTransformation({
74
- zoom: 1,
75
- matrix: getFocusedMatrix(initialFocusPoint, width, height)
76
- });
77
- }, [setTransformation]);
78
77
  const down = (e) => {
79
- if (e.button === 0) {
80
- pointer.current = {
81
- x: e.clientX,
82
- y: e.clientY
83
- };
84
- setActivePanning(true);
85
- }
78
+ if (e.button !== 0) return;
79
+ pointer.current = { x: e.clientX, y: e.clientY };
80
+ setIsPanning(true);
86
81
  setGrabbing(true);
87
82
  };
88
83
  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);
84
+ const currentTrans = transformationRef.current;
85
+ if (currentTrans) {
86
+ const x = (e.clientX - pointer.current.x) / currentTrans.zoom;
87
+ const y = (e.clientY - pointer.current.y) / currentTrans.zoom;
88
+ pointer.current = { x: e.clientX, y: e.clientY };
89
+ setTransformation({
90
+ ...currentTrans,
91
+ matrix: currentTrans.matrix.translate(x, y)
92
+ });
97
93
  }
98
- }, [activePanning, activeTransformation]);
94
+ }, [setTransformation]);
99
95
  const up = useCallback(() => {
100
- setActivePanning(false);
96
+ setIsPanning(false);
97
+ setGrabbing(false);
101
98
  }, []);
102
99
  useEffect(() => {
103
- if (activePanning) {
100
+ if (isPanning) {
104
101
  document.addEventListener("mousemove", move);
105
102
  document.addEventListener("mouseup", up);
106
103
  }
@@ -108,24 +105,21 @@ var SvgViewport = ({
108
105
  document.removeEventListener("mousemove", move);
109
106
  document.removeEventListener("mouseup", up);
110
107
  };
111
- }, [activePanning]);
108
+ }, [isPanning, move, up]);
112
109
  const adjustZoom = (e) => {
110
+ if (!zoomable) return;
111
+ e.preventDefault();
113
112
  const scale = e.deltaY < 0 ? 1.25 : 0.8;
114
113
  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
- });
114
+ const { clientX, clientY } = e;
115
+ if (transformation && transformation.zoom * scale > minZoom && transformation.zoom * scale < maxZoom) {
116
+ setTransformation({
117
+ zoom: transformation.zoom * scale,
118
+ matrix: adjustWithZoom(transformation.matrix, scale, eventTarget, clientX, clientY)
119
+ });
120
+ }
127
121
  };
128
- const cursor = pannable ? grabbing || panning ? "grabbing" : "grab" : "auto";
122
+ const cursor = pannable ? grabbing ? "grabbing" : "grab" : "auto";
129
123
  return /* @__PURE__ */ jsx(
130
124
  "svg",
131
125
  {
@@ -138,7 +132,7 @@ var SvgViewport = ({
138
132
  onContextMenu: (e) => e.preventDefault(),
139
133
  style: { ...style, cursor },
140
134
  ...otherProps,
141
- children: /* @__PURE__ */ jsx("g", { transform: transform(activeTransformation?.matrix), children: activeTransformation && children })
135
+ children: /* @__PURE__ */ jsx("g", { transform: transform(transformation?.matrix), children: transformation && children })
142
136
  }
143
137
  );
144
138
  };
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.0",
4
4
  "description": "Provides a simple React component for displaying SVG content with zooming and panning capabilities",
5
5
  "keywords": [
6
6
  "react",