sanity-plugin-hotspot-array 1.0.1 → 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.mjs ADDED
@@ -0,0 +1,314 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { getImageDimensions } from "@sanity/asset-utils";
3
+ import imageUrlBuilder from "@sanity/image-url";
4
+ import { Card, Text, Tooltip, Box, Stack, Flex } from "@sanity/ui";
5
+ import { randomKey } from "@sanity/util/content";
6
+ import { get } from "lodash-es";
7
+ import { useState, useEffect, useCallback, createElement, useRef, useMemo } from "react";
8
+ import { useClient, useFormValue, PatchEvent, setIfMissing, insert, set, definePlugin } from "sanity";
9
+ import { useMotionValue, motion } from "framer-motion";
10
+ function Feedback({ children }) {
11
+ return /* @__PURE__ */ jsx(Card, { padding: 4, radius: 2, shadow: 1, tone: "caution", children: /* @__PURE__ */ jsx(Text, { children }) });
12
+ }
13
+ const dragStyle = {
14
+ width: "1.4rem",
15
+ height: "1.4rem",
16
+ position: "absolute",
17
+ boxSizing: "border-box",
18
+ top: 0,
19
+ left: 0,
20
+ margin: "-0.7rem 0 0 -0.7rem",
21
+ cursor: "pointer",
22
+ display: "flex",
23
+ justifyContent: "center",
24
+ alignItems: "center",
25
+ borderRadius: "50%",
26
+ background: "#000",
27
+ color: "white"
28
+ }, dragStyleWhileDrag = {
29
+ background: "rgba(0, 0, 0, 0.1)",
30
+ border: "1px solid #fff",
31
+ cursor: "none"
32
+ }, dragStyleWhileHover = {
33
+ background: "rgba(0, 0, 0, 0.1)",
34
+ border: "1px solid #fff"
35
+ }, dotStyle = {
36
+ position: "absolute",
37
+ left: "50%",
38
+ top: "50%",
39
+ height: "0.2rem",
40
+ width: "0.2rem",
41
+ margin: "-0.1rem 0 0 -0.1rem",
42
+ background: "#fff",
43
+ visibility: "hidden",
44
+ borderRadius: "50%",
45
+ // make sure pointer events only run on the parent
46
+ pointerEvents: "none"
47
+ }, dotStyleWhileActive = {
48
+ visibility: "visible"
49
+ }, labelStyle = {
50
+ color: "white",
51
+ fontSize: "0.7rem",
52
+ fontWeight: 600,
53
+ lineHeight: "1"
54
+ }, labelStyleWhileActive = {
55
+ visibility: "hidden"
56
+ }, round = (num) => Math.round(num * 100) / 100;
57
+ function Spot({
58
+ value,
59
+ bounds,
60
+ update,
61
+ hotspotDescriptionPath,
62
+ tooltip,
63
+ index,
64
+ schemaType,
65
+ renderPreview
66
+ }) {
67
+ const [isDragging, setIsDragging] = useState(!1), [isHovering, setIsHovering] = useState(!1), x = useMotionValue(round(bounds.width * (value.x / 100))), y = useMotionValue(round(bounds.height * (value.y / 100)));
68
+ useEffect(() => {
69
+ x.set(round(bounds.width * (value.x / 100))), y.set(round(bounds.height * (value.y / 100)));
70
+ }, [x, y, value, bounds]);
71
+ const handleDragEnd = useCallback(() => {
72
+ setIsDragging(!1);
73
+ const currentX = x.get(), currentY = y.get(), newX = round(currentX * 100 / bounds.width), newY = round(currentY * 100 / bounds.height), safeX = Math.max(0, Math.min(100, newX)), safeY = Math.max(0, Math.min(100, newY));
74
+ update(value._key, safeX, safeY);
75
+ }, [x, y, value, update, bounds]), handleDragStart = useCallback(() => setIsDragging(!0), []), handleHoverStart = useCallback(() => setIsHovering(!0), []), handleHoverEnd = useCallback(() => setIsHovering(!1), []);
76
+ return !x || !y ? null : /* @__PURE__ */ jsx(
77
+ Tooltip,
78
+ {
79
+ disabled: isDragging,
80
+ portal: !0,
81
+ content: tooltip && typeof tooltip == "function" ? createElement(tooltip, { value, renderPreview, schemaType }) : /* @__PURE__ */ jsx(Box, { padding: 2, style: { maxWidth: 200, pointerEvents: "none" }, children: /* @__PURE__ */ jsx(Text, { textOverflow: "ellipsis", children: hotspotDescriptionPath ? get(value, hotspotDescriptionPath) : `${value.x}% x ${value.y}%` }) }),
82
+ children: /* @__PURE__ */ jsxs(
83
+ motion.div,
84
+ {
85
+ drag: !0,
86
+ dragConstraints: bounds,
87
+ dragElastic: 0,
88
+ dragMomentum: !1,
89
+ onDragEnd: handleDragEnd,
90
+ onDragStart: handleDragStart,
91
+ onHoverStart: handleHoverStart,
92
+ onHoverEnd: handleHoverEnd,
93
+ style: {
94
+ ...dragStyle,
95
+ x,
96
+ y,
97
+ ...isDragging && { ...dragStyleWhileDrag },
98
+ ...isHovering && { ...dragStyleWhileHover }
99
+ },
100
+ children: [
101
+ /* @__PURE__ */ jsx(
102
+ Box,
103
+ {
104
+ style: {
105
+ ...dotStyle,
106
+ ...(isDragging || isHovering) && { ...dotStyleWhileActive }
107
+ }
108
+ }
109
+ ),
110
+ /* @__PURE__ */ jsx(
111
+ "div",
112
+ {
113
+ style: {
114
+ ...labelStyle,
115
+ ...(isDragging || isHovering) && { ...labelStyleWhileActive }
116
+ },
117
+ children: index + 1
118
+ }
119
+ )
120
+ ]
121
+ }
122
+ )
123
+ },
124
+ value._key
125
+ );
126
+ }
127
+ function useUnmountEffect(effect) {
128
+ const effectRef = useRef(effect);
129
+ effectRef.current = effect, useEffect(() => () => effectRef.current(), []);
130
+ }
131
+ function useDebouncedCallback(callback, deps, delay, maxWait = 0) {
132
+ const timeout = useRef(), waitTimeout = useRef(), cb = useRef(callback), lastCall = useRef(), clear = () => {
133
+ timeout.current && (clearTimeout(timeout.current), timeout.current = void 0), waitTimeout.current && (clearTimeout(waitTimeout.current), waitTimeout.current = void 0);
134
+ };
135
+ return useUnmountEffect(clear), useEffect(() => {
136
+ cb.current = callback;
137
+ }, deps), useMemo(() => {
138
+ const execute = () => {
139
+ if (clear(), !lastCall.current)
140
+ return;
141
+ const context = lastCall.current;
142
+ lastCall.current = void 0, cb.current.apply(context.this, context.args);
143
+ }, wrapped = function(...args) {
144
+ timeout.current && clearTimeout(timeout.current), lastCall.current = { args, this: this }, timeout.current = setTimeout(execute, delay), maxWait > 0 && !waitTimeout.current && (waitTimeout.current = setTimeout(execute, maxWait));
145
+ };
146
+ return Object.defineProperties(wrapped, {
147
+ length: { value: callback.length },
148
+ name: { value: `${callback.name || "anonymous"}__debounced__${delay}` }
149
+ }), wrapped;
150
+ }, [delay, maxWait, ...deps]);
151
+ }
152
+ const isBrowser = typeof window < "u" && typeof navigator < "u" && typeof document < "u";
153
+ let observerSingleton;
154
+ function getResizeObserver() {
155
+ if (!isBrowser)
156
+ return;
157
+ if (observerSingleton)
158
+ return observerSingleton;
159
+ const callbacks = /* @__PURE__ */ new Map(), observer = new ResizeObserver((entries) => {
160
+ var _a;
161
+ for (const entry of entries)
162
+ (_a = callbacks.get(entry.target)) == null || _a.forEach(
163
+ (cb) => setTimeout(() => {
164
+ cb(entry);
165
+ }, 0)
166
+ );
167
+ });
168
+ return observerSingleton = {
169
+ observer,
170
+ subscribe(target, callback) {
171
+ let cbs = callbacks.get(target);
172
+ cbs || (cbs = /* @__PURE__ */ new Set(), callbacks.set(target, cbs), observer.observe(target)), cbs.add(callback);
173
+ },
174
+ unsubscribe(target, callback) {
175
+ const cbs = callbacks.get(target);
176
+ cbs && (cbs.delete(callback), cbs.size === 0 && (callbacks.delete(target), observer.unobserve(target)));
177
+ }
178
+ }, observerSingleton;
179
+ }
180
+ function useResizeObserver(target, callback, enabled = !0) {
181
+ const ro = enabled && getResizeObserver(), cb = useRef(callback);
182
+ cb.current = callback;
183
+ const tgt = target && "current" in target ? target.current : target;
184
+ useEffect(() => {
185
+ const _tgt = target && "current" in target ? target.current : target;
186
+ if (!ro || !_tgt)
187
+ return;
188
+ let subscribed = !0;
189
+ const handler = (...args) => {
190
+ subscribed && cb.current(...args);
191
+ };
192
+ return ro.subscribe(_tgt, handler), () => {
193
+ subscribed = !1, ro.unsubscribe(_tgt, handler);
194
+ };
195
+ }, [tgt, ro]);
196
+ }
197
+ const imageStyle = { width: "100%", height: "auto" }, VALID_ROOT_PATHS = ["document", "parent"];
198
+ function ImageHotspotArray(props) {
199
+ var _a;
200
+ const { value, onChange, imageHotspotOptions, schemaType, renderPreview } = props, sanityClient = useClient({ apiVersion: "2022-01-01" }), imageHotspotPathRoot = useMemo(() => {
201
+ var _a2;
202
+ return (VALID_ROOT_PATHS.includes((_a2 = imageHotspotOptions.pathRoot) != null ? _a2 : "") ? imageHotspotOptions.pathRoot : "document") === "document" ? [] : props.path.slice(0, -1);
203
+ }, [imageHotspotOptions.pathRoot, props.path]), rootObject = useFormValue(imageHotspotPathRoot), hotspotImage = useMemo(() => get(rootObject, imageHotspotOptions.imagePath), [rootObject, imageHotspotOptions.imagePath]), displayImage = useMemo(() => {
204
+ var _a2, _b;
205
+ const builder = imageUrlBuilder(sanityClient).dataset((_a2 = sanityClient.config().dataset) != null ? _a2 : ""), urlFor = (source) => builder.image(source);
206
+ if ((_b = hotspotImage == null ? void 0 : hotspotImage.asset) != null && _b._ref) {
207
+ const { aspectRatio } = getImageDimensions(hotspotImage.asset._ref), width = 1200, height = Math.round(width / aspectRatio), url = urlFor(hotspotImage).width(width).url();
208
+ return { width, height, url };
209
+ }
210
+ return null;
211
+ }, [hotspotImage, sanityClient]), handleHotspotImageClick = useCallback(
212
+ (event) => {
213
+ const { nativeEvent, currentTarget } = event, x = Number((nativeEvent.offsetX * 100 / currentTarget.width).toFixed(2)), y = Number((nativeEvent.offsetY * 100 / currentTarget.height).toFixed(2)), description = `New Hotspot at ${x}% x ${y}%`, newRow = {
214
+ _key: randomKey(12),
215
+ _type: schemaType.of[0].name,
216
+ x,
217
+ y
218
+ };
219
+ imageHotspotOptions != null && imageHotspotOptions.descriptionPath && (newRow[imageHotspotOptions.descriptionPath] = description), onChange(PatchEvent.from([setIfMissing([]), insert([newRow], "after", [-1])]));
220
+ },
221
+ [imageHotspotOptions, onChange, schemaType]
222
+ ), handleHotspotMove = useCallback(
223
+ (key, x, y) => {
224
+ onChange(
225
+ PatchEvent.from([
226
+ // Set the `x` value of this array key item
227
+ set(x, [{ _key: key }, "x"]),
228
+ // Set the `y` value of this array key item
229
+ set(y, [{ _key: key }, "y"])
230
+ ])
231
+ );
232
+ },
233
+ [onChange]
234
+ ), hotspotImageRef = useRef(null), [imageRect, setImageRect] = useState();
235
+ return useResizeObserver(
236
+ hotspotImageRef,
237
+ useDebouncedCallback(
238
+ (e) => setImageRect(e.contentRect),
239
+ [setImageRect],
240
+ 200
241
+ )
242
+ ), /* @__PURE__ */ jsxs(Stack, { space: [2, 2, 3], children: [
243
+ displayImage != null && displayImage.url ? /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
244
+ imageRect && ((_a = value == null ? void 0 : value.length) != null ? _a : 0) > 0 && (value == null ? void 0 : value.map((spot, index) => /* @__PURE__ */ jsx(
245
+ Spot,
246
+ {
247
+ index,
248
+ value: spot,
249
+ bounds: imageRect,
250
+ update: handleHotspotMove,
251
+ hotspotDescriptionPath: imageHotspotOptions == null ? void 0 : imageHotspotOptions.descriptionPath,
252
+ tooltip: imageHotspotOptions == null ? void 0 : imageHotspotOptions.tooltip,
253
+ renderPreview,
254
+ schemaType: schemaType.of[0]
255
+ },
256
+ spot._key
257
+ ))),
258
+ /* @__PURE__ */ jsx(Card, { __unstable_checkered: !0, shadow: 1, children: /* @__PURE__ */ jsx(Flex, { align: "center", justify: "center", children: /* @__PURE__ */ jsx(
259
+ "img",
260
+ {
261
+ ref: hotspotImageRef,
262
+ src: displayImage.url,
263
+ width: displayImage.width,
264
+ height: displayImage.height,
265
+ alt: "",
266
+ style: imageStyle,
267
+ onClick: handleHotspotImageClick
268
+ }
269
+ ) }) })
270
+ ] }) : /* @__PURE__ */ jsx(Feedback, { children: imageHotspotOptions.imagePath ? /* @__PURE__ */ jsxs(Fragment, { children: [
271
+ "No Hotspot image found at path ",
272
+ /* @__PURE__ */ jsx("code", { children: imageHotspotOptions.imagePath })
273
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
274
+ "Define a path in this field using to the image field in this document at",
275
+ " ",
276
+ /* @__PURE__ */ jsx("code", { children: "options.hotspotImagePath" })
277
+ ] }) }),
278
+ imageHotspotOptions.pathRoot && !VALID_ROOT_PATHS.includes(imageHotspotOptions.pathRoot) && /* @__PURE__ */ jsxs(Feedback, { children: [
279
+ 'The supplied imageHotspotPathRoot "',
280
+ imageHotspotOptions.pathRoot,
281
+ '" is not valid, falling back to "document". Available values are "',
282
+ VALID_ROOT_PATHS.join(", "),
283
+ '".'
284
+ ] }),
285
+ props.renderDefault(props)
286
+ ] });
287
+ }
288
+ const imageHotspotArrayPlugin = definePlugin({
289
+ name: "image-hotspot-array",
290
+ form: {
291
+ components: {
292
+ input: (props) => {
293
+ var _a, _b;
294
+ if (props.schemaType.jsonType === "array" && (_a = props.schemaType.options) != null && _a.imageHotspot) {
295
+ const imageHotspotOptions = (_b = props.schemaType.options) == null ? void 0 : _b.imageHotspot;
296
+ if (imageHotspotOptions)
297
+ return /* @__PURE__ */ jsx(
298
+ ImageHotspotArray,
299
+ {
300
+ ...props,
301
+ imageHotspotOptions
302
+ }
303
+ );
304
+ }
305
+ return props.renderDefault(props);
306
+ }
307
+ }
308
+ }
309
+ });
310
+ export {
311
+ ImageHotspotArray,
312
+ imageHotspotArrayPlugin
313
+ };
314
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/Feedback.tsx","../src/Spot.tsx","../src/useDebouncedCallback.ts","../src/useResizeObserver.ts","../src/ImageHotspotArray.tsx","../src/plugin.tsx"],"sourcesContent":["import {Card, Text} from '@sanity/ui'\nimport {type ReactNode} from 'react'\n\nexport default function Feedback({children}: {children: ReactNode}): ReactNode {\n return (\n <Card padding={4} radius={2} shadow={1} tone=\"caution\">\n <Text>{children}</Text>\n </Card>\n )\n}\n","import {Box, Text, Tooltip} from '@sanity/ui'\nimport {motion, useMotionValue} from 'framer-motion'\nimport {get} from 'lodash-es'\nimport {\n ComponentType,\n createElement,\n CSSProperties,\n ReactNode,\n useCallback,\n useEffect,\n useState,\n} from 'react'\nimport {type ObjectSchemaType, type RenderPreviewCallback} from 'sanity'\n\nimport {FnHotspotMove, HotspotItem} from './ImageHotspotArray'\n\nconst dragStyle: CSSProperties = {\n width: '1.4rem',\n height: '1.4rem',\n position: 'absolute',\n boxSizing: 'border-box',\n top: 0,\n left: 0,\n margin: '-0.7rem 0 0 -0.7rem',\n cursor: 'pointer',\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n borderRadius: '50%',\n background: '#000',\n color: 'white',\n}\n\nconst dragStyleWhileDrag: CSSProperties = {\n background: 'rgba(0, 0, 0, 0.1)',\n border: '1px solid #fff',\n cursor: 'none',\n}\n\nconst dragStyleWhileHover: CSSProperties = {\n background: 'rgba(0, 0, 0, 0.1)',\n border: '1px solid #fff',\n}\n\nconst dotStyle: CSSProperties = {\n position: 'absolute',\n left: '50%',\n top: '50%',\n height: '0.2rem',\n width: '0.2rem',\n margin: '-0.1rem 0 0 -0.1rem',\n background: '#fff',\n visibility: 'hidden',\n borderRadius: '50%',\n // make sure pointer events only run on the parent\n pointerEvents: 'none',\n}\n\nconst dotStyleWhileActive: CSSProperties = {\n visibility: 'visible',\n}\n\nconst labelStyle: CSSProperties = {\n color: 'white',\n fontSize: '0.7rem',\n fontWeight: 600,\n lineHeight: '1',\n}\n\nconst labelStyleWhileActive: CSSProperties = {\n visibility: 'hidden',\n}\n\nconst round = (num: number) => Math.round(num * 100) / 100\n\nexport interface HotspotTooltipProps<HotspotFields = {[key: string]: unknown}> {\n value: HotspotItem<HotspotFields>\n schemaType: ObjectSchemaType\n renderPreview: RenderPreviewCallback\n}\n\ninterface HotspotProps<HotspotFields = {[key: string]: unknown}> {\n value: HotspotItem\n bounds: DOMRectReadOnly\n update: FnHotspotMove\n hotspotDescriptionPath?: string\n tooltip?: ComponentType<HotspotTooltipProps<HotspotFields>>\n index: number\n schemaType: ObjectSchemaType\n renderPreview: RenderPreviewCallback\n}\n\nexport default function Spot({\n value,\n bounds,\n update,\n hotspotDescriptionPath,\n tooltip,\n index,\n schemaType,\n renderPreview,\n}: HotspotProps): ReactNode {\n const [isDragging, setIsDragging] = useState(false)\n const [isHovering, setIsHovering] = useState(false)\n\n // x/y are stored as % but need to be converted to px\n const x = useMotionValue(round(bounds.width * (value.x / 100)))\n const y = useMotionValue(round(bounds.height * (value.y / 100)))\n\n /**\n * update x/y if the bounds change when resizing the window\n */\n useEffect(() => {\n x.set(round(bounds.width * (value.x / 100)))\n y.set(round(bounds.height * (value.y / 100)))\n }, [x, y, value, bounds])\n\n const handleDragEnd = useCallback(() => {\n setIsDragging(false)\n\n // get current values for x/y in px\n const currentX = x.get()\n const currentY = y.get()\n\n // Which we need to convert back to `%` to patch the document\n const newX = round((currentX * 100) / bounds.width)\n const newY = round((currentY * 100) / bounds.height)\n\n // Don't go below 0 or above 100\n const safeX = Math.max(0, Math.min(100, newX))\n const safeY = Math.max(0, Math.min(100, newY))\n\n update(value._key, safeX, safeY)\n }, [x, y, value, update, bounds])\n const handleDragStart = useCallback(() => setIsDragging(true), [])\n\n const handleHoverStart = useCallback(() => setIsHovering(true), [])\n const handleHoverEnd = useCallback(() => setIsHovering(false), [])\n\n if (!x || !y) {\n return null\n }\n\n return (\n <Tooltip\n key={value._key}\n disabled={isDragging}\n portal\n content={\n tooltip && typeof tooltip === 'function' ? (\n createElement(tooltip, {value, renderPreview, schemaType})\n ) : (\n <Box padding={2} style={{maxWidth: 200, pointerEvents: `none`}}>\n <Text textOverflow=\"ellipsis\">\n {hotspotDescriptionPath\n ? (get(value, hotspotDescriptionPath) as string)\n : `${value.x}% x ${value.y}%`}\n </Text>\n </Box>\n )\n }\n >\n <motion.div\n drag\n dragConstraints={bounds}\n dragElastic={0}\n dragMomentum={false}\n onDragEnd={handleDragEnd}\n onDragStart={handleDragStart}\n onHoverStart={handleHoverStart}\n onHoverEnd={handleHoverEnd}\n style={{\n ...dragStyle,\n x,\n y,\n ...(isDragging && {...dragStyleWhileDrag}),\n ...(isHovering && {...dragStyleWhileHover}),\n }}\n >\n {/* Dot */}\n <Box\n style={{\n ...dotStyle,\n ...((isDragging || isHovering) && {...dotStyleWhileActive}),\n }}\n />\n {/* Label */}\n <div\n style={{\n ...labelStyle,\n ...((isDragging || isHovering) && {...labelStyleWhileActive}),\n }}\n >\n {index + 1}\n </div>\n </motion.div>\n </Tooltip>\n )\n}\n","// copied from react-hookz/web\n// https://github.com/react-hookz/web/blob/579a445fcc9f4f4bb5b9d5e670b2e57448b4ee50/src/useDebouncedCallback/index.ts\nimport {type DependencyList, useEffect, useMemo, useRef} from 'react'\n\n/**\n * Run effect only when component is unmounted.\n *\n * @param effect Effector to run on unmount\n */\nexport function useUnmountEffect(effect: CallableFunction): void {\n const effectRef = useRef(effect)\n effectRef.current = effect\n useEffect(() => () => effectRef.current(), [])\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type DebouncedFunction<Fn extends (...args: any[]) => any> = (\n this: ThisParameterType<Fn>,\n ...args: Parameters<Fn>\n) => void\n\n/**\n * Makes passed function debounced, otherwise acts like `useCallback`.\n *\n * @param callback Function that will be debounced.\n * @param deps Dependencies list when to update callback. It also replaces invoked\n * \tcallback for scheduled debounced invocations.\n * @param delay Debounce delay.\n * @param maxWait The maximum time `callback` is allowed to be delayed before\n * it's invoked. 0 means no max wait.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useDebouncedCallback<Fn extends (...args: any[]) => any>(\n callback: Fn,\n deps: DependencyList,\n delay: number,\n maxWait = 0,\n): DebouncedFunction<Fn> {\n const timeout = useRef<ReturnType<typeof setTimeout>>()\n const waitTimeout = useRef<ReturnType<typeof setTimeout>>()\n const cb = useRef(callback)\n const lastCall = useRef<{args: Parameters<Fn>; this: ThisParameterType<Fn>}>()\n\n const clear = () => {\n if (timeout.current) {\n clearTimeout(timeout.current)\n timeout.current = undefined\n }\n\n if (waitTimeout.current) {\n clearTimeout(waitTimeout.current)\n waitTimeout.current = undefined\n }\n }\n\n // Cancel scheduled execution on unmount\n useUnmountEffect(clear)\n\n useEffect(() => {\n cb.current = callback\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, deps)\n\n return useMemo(() => {\n const execute = () => {\n clear()\n\n // Barely possible to test this line\n /* istanbul ignore next */\n if (!lastCall.current) return\n\n const context = lastCall.current\n lastCall.current = undefined\n\n cb.current.apply(context.this, context.args)\n }\n\n const wrapped = function (this, ...args) {\n if (timeout.current) {\n clearTimeout(timeout.current)\n }\n\n lastCall.current = {args, this: this}\n\n // Plan regular execution\n timeout.current = setTimeout(execute, delay)\n\n // Plan maxWait execution if required\n if (maxWait > 0 && !waitTimeout.current) {\n waitTimeout.current = setTimeout(execute, maxWait)\n }\n } as DebouncedFunction<Fn>\n\n Object.defineProperties(wrapped, {\n length: {value: callback.length},\n name: {value: `${callback.name || 'anonymous'}__debounced__${delay}`},\n })\n\n return wrapped\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [delay, maxWait, ...deps])\n}\n","// copied from react-hookz/web\n// https://github.com/react-hookz/web/blob/579a445fcc9f4f4bb5b9d5e670b2e57448b4ee50/src/useResizeObserver/index.ts\nimport {type RefObject, useEffect, useRef} from 'react'\n\nconst isBrowser =\n typeof window !== 'undefined' &&\n typeof navigator !== 'undefined' &&\n typeof document !== 'undefined'\n\nexport type UseResizeObserverCallback = (entry: ResizeObserverEntry) => void\n\ntype ResizeObserverSingleton = {\n observer: ResizeObserver\n subscribe: (target: Element, callback: UseResizeObserverCallback) => void\n unsubscribe: (target: Element, callback: UseResizeObserverCallback) => void\n}\n\nlet observerSingleton: ResizeObserverSingleton\n\nfunction getResizeObserver(): ResizeObserverSingleton | undefined {\n if (!isBrowser) return undefined\n\n if (observerSingleton) return observerSingleton\n\n const callbacks = new Map<Element, Set<UseResizeObserverCallback>>()\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries)\n callbacks.get(entry.target)?.forEach((cb) =>\n setTimeout(() => {\n cb(entry)\n }, 0),\n )\n })\n\n observerSingleton = {\n observer,\n subscribe(target, callback) {\n let cbs = callbacks.get(target)\n\n if (!cbs) {\n // If target has no observers yet - register it\n cbs = new Set<UseResizeObserverCallback>()\n callbacks.set(target, cbs)\n observer.observe(target)\n }\n\n // As Set is duplicate-safe - simply add callback on each call\n cbs.add(callback)\n },\n unsubscribe(target, callback) {\n const cbs = callbacks.get(target)\n\n // Else branch should never occur in case of normal execution\n // because callbacks map is hidden in closure - it is impossible to\n // simulate situation with non-existent `cbs` Set\n /* istanbul ignore else */\n if (cbs) {\n // Remove current observer\n cbs.delete(callback)\n\n if (cbs.size === 0) {\n // If no observers left unregister target completely\n callbacks.delete(target)\n observer.unobserve(target)\n }\n }\n },\n }\n\n return observerSingleton\n}\n\n/**\n * Invokes a callback whenever ResizeObserver detects a change to target's size.\n *\n * @param target React reference or Element to track.\n * @param callback Callback that will be invoked on resize.\n * @param enabled Whether resize observer is enabled or not.\n */\nexport function useResizeObserver<T extends Element>(\n target: RefObject<T> | T | null,\n callback: UseResizeObserverCallback,\n enabled = true,\n): void {\n const ro = enabled && getResizeObserver()\n const cb = useRef(callback)\n cb.current = callback\n\n const tgt = target && 'current' in target ? target.current : target\n\n useEffect(() => {\n // This secondary target resolve required for case when we receive ref object, which, most\n // likely, contains null during render stage, but already populated with element during\n // effect stage.\n\n const _tgt = target && 'current' in target ? target.current : target\n\n if (!ro || !_tgt) return undefined\n\n // As unsubscription in internals of our ResizeObserver abstraction can\n // happen a bit later than effect cleanup invocation - we need a marker,\n // that this handler should not be invoked anymore\n let subscribed = true\n\n const handler: UseResizeObserverCallback = (...args) => {\n // It is reinsurance for the highly asynchronous invocations, almost\n // impossible to achieve in tests, thus excluding from LOC\n /* istanbul ignore else */\n if (subscribed) {\n cb.current(...args)\n }\n }\n\n ro.subscribe(_tgt, handler)\n\n return () => {\n subscribed = false\n ro.unsubscribe(_tgt, handler)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [tgt, ro])\n}\n","import {getImageDimensions} from '@sanity/asset-utils'\nimport imageUrlBuilder from '@sanity/image-url'\nimport {Card, Flex, Stack} from '@sanity/ui'\nimport {randomKey} from '@sanity/util/content'\nimport {get} from 'lodash-es'\nimport {type MouseEvent, type ReactNode, useCallback, useMemo, useRef, useState} from 'react'\nimport {\n ArrayOfObjectsInputProps,\n ImageValue,\n insert,\n ObjectSchemaType,\n PatchEvent,\n set,\n setIfMissing,\n useClient,\n useFormValue,\n} from 'sanity'\n\nimport Feedback from './Feedback'\nimport {ImageHotspotOptions} from './plugin'\nimport Spot from './Spot'\nimport {useDebouncedCallback} from './useDebouncedCallback'\nimport {useResizeObserver} from './useResizeObserver'\n\nconst imageStyle = {width: `100%`, height: `auto`}\n\nconst VALID_ROOT_PATHS = ['document', 'parent']\n\nexport type FnHotspotMove = (key: string, x: number, y: number) => void\n\nexport type HotspotItem<HotspotFields = {[key: string]: unknown}> = {\n _key: string\n _type: string\n x: number\n y: number\n} & HotspotFields\n\nexport function ImageHotspotArray(\n props: ArrayOfObjectsInputProps<HotspotItem> & {imageHotspotOptions: ImageHotspotOptions},\n): ReactNode {\n const {value, onChange, imageHotspotOptions, schemaType, renderPreview} = props\n\n const sanityClient = useClient({apiVersion: '2022-01-01'})\n\n const imageHotspotPathRoot = useMemo(() => {\n const pathRoot = VALID_ROOT_PATHS.includes(imageHotspotOptions.pathRoot ?? '')\n ? imageHotspotOptions.pathRoot\n : 'document'\n return pathRoot === 'document' ? [] : props.path.slice(0, -1)\n }, [imageHotspotOptions.pathRoot, props.path])\n\n const rootObject = useFormValue(imageHotspotPathRoot)\n\n /**\n * Finding the image from the imageHotspotPathRoot (defaults to document),\n * using the path from the hotspot's `options` field\n *\n * when changes in imageHotspotPathRoot (e.g. document) occur,\n * check if there are any changes to the hotspotImage and update the reference\n */\n const hotspotImage = useMemo(() => {\n return get(rootObject, imageHotspotOptions.imagePath) as ImageValue | undefined\n }, [rootObject, imageHotspotOptions.imagePath])\n\n const displayImage = useMemo(() => {\n const builder = imageUrlBuilder(sanityClient).dataset(sanityClient.config().dataset ?? '')\n const urlFor = (source: ImageValue) => builder.image(source)\n\n if (hotspotImage?.asset?._ref) {\n const {aspectRatio} = getImageDimensions(hotspotImage.asset._ref)\n const width = 1200\n const height = Math.round(width / aspectRatio)\n const url = urlFor(hotspotImage).width(width).url()\n\n return {width, height, url}\n }\n\n return null\n }, [hotspotImage, sanityClient])\n\n const handleHotspotImageClick = useCallback(\n (event: MouseEvent<HTMLImageElement>) => {\n const {nativeEvent, currentTarget} = event\n\n // Calculate the x/y percentage of the click position\n const x = Number(((nativeEvent.offsetX * 100) / currentTarget.width).toFixed(2))\n const y = Number(((nativeEvent.offsetY * 100) / currentTarget.height).toFixed(2))\n const description = `New Hotspot at ${x}% x ${y}%`\n\n const newRow: HotspotItem = {\n _key: randomKey(12),\n _type: schemaType.of[0].name,\n x,\n y,\n }\n\n if (imageHotspotOptions?.descriptionPath) {\n newRow[imageHotspotOptions.descriptionPath] = description\n }\n\n onChange(PatchEvent.from([setIfMissing([]), insert([newRow], 'after', [-1])]))\n },\n [imageHotspotOptions, onChange, schemaType],\n )\n\n const handleHotspotMove: FnHotspotMove = useCallback(\n (key, x, y) => {\n onChange(\n PatchEvent.from([\n // Set the `x` value of this array key item\n set(x, [{_key: key}, 'x']),\n // Set the `y` value of this array key item\n set(y, [{_key: key}, 'y']),\n ]),\n )\n },\n [onChange],\n )\n\n const hotspotImageRef = useRef<HTMLImageElement | null>(null)\n\n const [imageRect, setImageRect] = useState<DOMRectReadOnly>()\n\n useResizeObserver(\n hotspotImageRef,\n useDebouncedCallback(\n (e: ResizeObserverEntry) => setImageRect(e.contentRect),\n [setImageRect],\n 200,\n ),\n )\n\n return (\n <Stack space={[2, 2, 3]}>\n {displayImage?.url ? (\n <div style={{position: `relative`}}>\n {imageRect &&\n (value?.length ?? 0) > 0 &&\n value?.map((spot, index) => (\n <Spot\n index={index}\n key={spot._key}\n value={spot}\n bounds={imageRect}\n update={handleHotspotMove}\n hotspotDescriptionPath={imageHotspotOptions?.descriptionPath}\n tooltip={imageHotspotOptions?.tooltip}\n renderPreview={renderPreview}\n schemaType={schemaType.of[0] as ObjectSchemaType}\n />\n ))}\n\n <Card __unstable_checkered shadow={1}>\n <Flex align=\"center\" justify=\"center\">\n <img\n ref={hotspotImageRef}\n src={displayImage.url}\n width={displayImage.width}\n height={displayImage.height}\n alt=\"\"\n style={imageStyle}\n onClick={handleHotspotImageClick}\n />\n </Flex>\n </Card>\n </div>\n ) : (\n <Feedback>\n {imageHotspotOptions.imagePath ? (\n <>\n No Hotspot image found at path <code>{imageHotspotOptions.imagePath}</code>\n </>\n ) : (\n <>\n Define a path in this field using to the image field in this document at{' '}\n <code>options.hotspotImagePath</code>\n </>\n )}\n </Feedback>\n )}\n {imageHotspotOptions.pathRoot && !VALID_ROOT_PATHS.includes(imageHotspotOptions.pathRoot) && (\n <Feedback>\n The supplied imageHotspotPathRoot &quot;{imageHotspotOptions.pathRoot}&quot; is not valid,\n falling back to &quot;document&quot;. Available values are &quot;\n {VALID_ROOT_PATHS.join(', ')}&quot;.\n </Feedback>\n )}\n {props.renderDefault(props as unknown as ArrayOfObjectsInputProps)}\n </Stack>\n )\n}\n","import {ComponentType} from 'react'\nimport {type ArrayOfObjectsInputProps, definePlugin} from 'sanity'\n\nimport {type HotspotItem, ImageHotspotArray} from './ImageHotspotArray'\nimport {type HotspotTooltipProps} from './Spot'\n\nexport interface ImageHotspotOptions<HotspotFields = {[key: string]: unknown}> {\n pathRoot?: 'document' | 'parent'\n imagePath: string\n descriptionPath?: string\n tooltip?: ComponentType<HotspotTooltipProps<HotspotFields>>\n}\n\ndeclare module '@sanity/types' {\n export interface ArrayOptions {\n imageHotspot?: ImageHotspotOptions\n }\n}\n\nexport const imageHotspotArrayPlugin = definePlugin({\n name: 'image-hotspot-array',\n form: {\n components: {\n input: (props) => {\n if (props.schemaType.jsonType === 'array' && props.schemaType.options?.imageHotspot) {\n const imageHotspotOptions = props.schemaType.options?.imageHotspot\n if (imageHotspotOptions) {\n return (\n <ImageHotspotArray\n {...(props as unknown as ArrayOfObjectsInputProps<HotspotItem>)}\n imageHotspotOptions={imageHotspotOptions}\n />\n )\n }\n }\n return props.renderDefault(props)\n },\n },\n },\n})\n"],"names":["_a"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "sanity-plugin-hotspot-array",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
+ "workspaces": [
5
+ "./examples/*"
6
+ ],
4
7
  "description": "A configurable Custom Input for Arrays that will add and update items by clicking on an Image",
5
8
  "keywords": [
6
9
  "sanity",
@@ -16,76 +19,76 @@
16
19
  },
17
20
  "license": "MIT",
18
21
  "author": "Sanity.io <hello@sanity.io>",
22
+ "sideEffects": false,
23
+ "type": "commonjs",
19
24
  "exports": {
20
25
  ".": {
21
- "types": "./lib/src/index.d.ts",
22
26
  "source": "./src/index.ts",
23
- "import": "./lib/index.esm.js",
24
- "require": "./lib/index.js",
25
- "default": "./lib/index.esm.js"
27
+ "import": "./dist/index.mjs",
28
+ "default": "./dist/index.js"
26
29
  },
27
30
  "./package.json": "./package.json"
28
31
  },
29
- "main": "./lib/index.js",
30
- "module": "./lib/index.esm.js",
31
- "source": "./src/index.ts",
32
- "types": "./lib/src/index.d.ts",
32
+ "main": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
33
34
  "files": [
34
35
  "src",
35
- "lib",
36
+ "dist",
36
37
  "v2-incompatible.js",
37
38
  "sanity.json"
38
39
  ],
39
40
  "scripts": {
40
41
  "prebuild": "npm run clean && plugin-kit verify-package --silent && pkg-utils",
41
- "build": "pkg-utils build --strict",
42
- "clean": "rimraf lib",
42
+ "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
43
+ "clean": "rimraf dist",
43
44
  "link-watch": "plugin-kit link-watch",
44
45
  "lint": "eslint .",
45
46
  "lint:fix": "eslint . --fix",
46
47
  "prepare": "husky install",
47
48
  "prepublishOnly": "npm run build",
48
- "watch": "pkg-utils watch"
49
+ "watch": "pkg-utils watch --strict"
49
50
  },
51
+ "browserslist": "extends @sanity/browserslist-config",
50
52
  "dependencies": {
51
- "@react-hookz/web": "^14.2.2",
52
- "@sanity/asset-utils": "^1.2.3",
53
- "@sanity/image-url": "^1.0.1",
53
+ "@sanity/asset-utils": "^1.3.0",
54
+ "@sanity/image-url": "^1.0.2",
54
55
  "@sanity/incompatible-plugin": "^1.0.4",
55
- "@sanity/ui": "^1.0.0",
56
56
  "@sanity/util": "^3.0.0",
57
- "framer-motion": "^6.3.11",
58
- "lodash": "^4.17.21"
57
+ "@types/lodash-es": "^4.17.12",
58
+ "framer-motion": "^11.0.0",
59
+ "lodash-es": "^4.17.21"
59
60
  },
60
61
  "devDependencies": {
61
- "@commitlint/cli": "^17.2.0",
62
- "@commitlint/config-conventional": "^17.2.0",
63
- "@sanity/pkg-utils": "^1.17.2",
64
- "@sanity/plugin-kit": "^2.1.5",
65
- "@sanity/semantic-release-preset": "^2.0.2",
66
- "@typescript-eslint/eslint-plugin": "^5.42.0",
67
- "@typescript-eslint/parser": "^5.42.0",
68
- "eslint": "^8.26.0",
69
- "eslint-config-prettier": "^8.5.0",
70
- "eslint-config-sanity": "^6.0.0",
71
- "eslint-plugin-prettier": "^4.2.1",
72
- "eslint-plugin-react": "^7.31.10",
62
+ "@commitlint/cli": "^19.2.2",
63
+ "@commitlint/config-conventional": "^19.2.2",
64
+ "@sanity/browserslist-config": "^1.0.3",
65
+ "@sanity/pkg-utils": "^6.7.0",
66
+ "@sanity/plugin-kit": "^4.0.4",
67
+ "@sanity/semantic-release-preset": "^4.1.7",
68
+ "@typescript-eslint/eslint-plugin": "^7.7.0",
69
+ "@typescript-eslint/parser": "^7.7.0",
70
+ "eslint": "^8.57.0",
71
+ "eslint-config-prettier": "^9.1.0",
72
+ "eslint-config-sanity": "^7.1.2",
73
+ "eslint-plugin-prettier": "^5.1.3",
74
+ "eslint-plugin-react": "^7.34.1",
73
75
  "eslint-plugin-react-hooks": "^4.6.0",
74
76
  "husky": "^8.0.1",
75
77
  "lint-staged": "^13.0.3",
76
- "pinst": "^2.1.6",
77
- "prettier": "^2.7.1",
78
- "prettier-plugin-packagejson": "^2.3.0",
78
+ "prettier": "^3.2.5",
79
+ "prettier-plugin-packagejson": "^2.5.0",
79
80
  "react": "^18",
80
- "rimraf": "^3.0.2",
81
+ "rimraf": "^5.0.5",
81
82
  "sanity": "^3.0.0",
82
- "typescript": "^4.8.4"
83
+ "typescript": "^5.4.5"
83
84
  },
84
85
  "peerDependencies": {
86
+ "@sanity/ui": "^2.0.0",
85
87
  "react": "^18",
86
- "sanity": "^3.0.0"
88
+ "sanity": "^3.0.0",
89
+ "styled-components": "^6.1"
87
90
  },
88
91
  "engines": {
89
- "node": ">=14"
92
+ "node": ">=18"
90
93
  }
91
94
  }
package/src/Feedback.tsx CHANGED
@@ -1,7 +1,7 @@
1
- import React, {ReactNode} from 'react'
2
1
  import {Card, Text} from '@sanity/ui'
2
+ import {type ReactNode} from 'react'
3
3
 
4
- export default function Feedback({children}: {children: ReactNode}) {
4
+ export default function Feedback({children}: {children: ReactNode}): ReactNode {
5
5
  return (
6
6
  <Card padding={4} radius={2} shadow={1} tone="caution">
7
7
  <Text>{children}</Text>
@@ -1,6 +1,9 @@
1
- /* eslint-disable react/display-name */
2
-
3
1
  import {getImageDimensions} from '@sanity/asset-utils'
2
+ import imageUrlBuilder from '@sanity/image-url'
3
+ import {Card, Flex, Stack} from '@sanity/ui'
4
+ import {randomKey} from '@sanity/util/content'
5
+ import {get} from 'lodash-es'
6
+ import {type MouseEvent, type ReactNode, useCallback, useMemo, useRef, useState} from 'react'
4
7
  import {
5
8
  ArrayOfObjectsInputProps,
6
9
  ImageValue,
@@ -12,16 +15,12 @@ import {
12
15
  useClient,
13
16
  useFormValue,
14
17
  } from 'sanity'
15
- import imageUrlBuilder from '@sanity/image-url'
16
- import {Card, Flex, Stack} from '@sanity/ui'
17
- import {randomKey} from '@sanity/util/content'
18
- import get from 'lodash/get'
19
- import React, {useCallback, useMemo, useRef, useState} from 'react'
20
18
 
21
- import {IUseResizeObserverCallback, useDebouncedCallback, useResizeObserver} from '@react-hookz/web'
22
19
  import Feedback from './Feedback'
23
- import Spot from './Spot'
24
20
  import {ImageHotspotOptions} from './plugin'
21
+ import Spot from './Spot'
22
+ import {useDebouncedCallback} from './useDebouncedCallback'
23
+ import {useResizeObserver} from './useResizeObserver'
25
24
 
26
25
  const imageStyle = {width: `100%`, height: `auto`}
27
26
 
@@ -37,8 +36,8 @@ export type HotspotItem<HotspotFields = {[key: string]: unknown}> = {
37
36
  } & HotspotFields
38
37
 
39
38
  export function ImageHotspotArray(
40
- props: ArrayOfObjectsInputProps<HotspotItem> & {imageHotspotOptions: ImageHotspotOptions}
41
- ) {
39
+ props: ArrayOfObjectsInputProps<HotspotItem> & {imageHotspotOptions: ImageHotspotOptions},
40
+ ): ReactNode {
42
41
  const {value, onChange, imageHotspotOptions, schemaType, renderPreview} = props
43
42
 
44
43
  const sanityClient = useClient({apiVersion: '2022-01-01'})
@@ -80,12 +79,12 @@ export function ImageHotspotArray(
80
79
  }, [hotspotImage, sanityClient])
81
80
 
82
81
  const handleHotspotImageClick = useCallback(
83
- (event: any) => {
84
- const {nativeEvent} = event
82
+ (event: MouseEvent<HTMLImageElement>) => {
83
+ const {nativeEvent, currentTarget} = event
85
84
 
86
85
  // Calculate the x/y percentage of the click position
87
- const x = Number(((nativeEvent.offsetX * 100) / nativeEvent.srcElement.width).toFixed(2))
88
- const y = Number(((nativeEvent.offsetY * 100) / nativeEvent.srcElement.height).toFixed(2))
86
+ const x = Number(((nativeEvent.offsetX * 100) / currentTarget.width).toFixed(2))
87
+ const y = Number(((nativeEvent.offsetY * 100) / currentTarget.height).toFixed(2))
89
88
  const description = `New Hotspot at ${x}% x ${y}%`
90
89
 
91
90
  const newRow: HotspotItem = {
@@ -101,7 +100,7 @@ export function ImageHotspotArray(
101
100
 
102
101
  onChange(PatchEvent.from([setIfMissing([]), insert([newRow], 'after', [-1])]))
103
102
  },
104
- [imageHotspotOptions, onChange, schemaType]
103
+ [imageHotspotOptions, onChange, schemaType],
105
104
  )
106
105
 
107
106
  const handleHotspotMove: FnHotspotMove = useCallback(
@@ -112,21 +111,24 @@ export function ImageHotspotArray(
112
111
  set(x, [{_key: key}, 'x']),
113
112
  // Set the `y` value of this array key item
114
113
  set(y, [{_key: key}, 'y']),
115
- ])
114
+ ]),
116
115
  )
117
116
  },
118
- [onChange]
117
+ [onChange],
119
118
  )
120
119
 
121
120
  const hotspotImageRef = useRef<HTMLImageElement | null>(null)
122
121
 
123
122
  const [imageRect, setImageRect] = useState<DOMRectReadOnly>()
124
- const updateImageRectCallback = useDebouncedCallback(
125
- ((e) => setImageRect(e.contentRect)) as IUseResizeObserverCallback,
126
- [setImageRect],
127
- 200
123
+
124
+ useResizeObserver(
125
+ hotspotImageRef,
126
+ useDebouncedCallback(
127
+ (e: ResizeObserverEntry) => setImageRect(e.contentRect),
128
+ [setImageRect],
129
+ 200,
130
+ ),
128
131
  )
129
- useResizeObserver(hotspotImageRef, updateImageRectCallback)
130
132
 
131
133
  return (
132
134
  <Stack space={[2, 2, 3]}>
@@ -178,8 +180,9 @@ export function ImageHotspotArray(
178
180
  )}
179
181
  {imageHotspotOptions.pathRoot && !VALID_ROOT_PATHS.includes(imageHotspotOptions.pathRoot) && (
180
182
  <Feedback>
181
- The supplied imageHotspotPathRoot "{imageHotspotOptions.pathRoot}" is not valid, falling
182
- back to "document". Available values are "{VALID_ROOT_PATHS.join(', ')}".
183
+ The supplied imageHotspotPathRoot &quot;{imageHotspotOptions.pathRoot}&quot; is not valid,
184
+ falling back to &quot;document&quot;. Available values are &quot;
185
+ {VALID_ROOT_PATHS.join(', ')}&quot;.
183
186
  </Feedback>
184
187
  )}
185
188
  {props.renderDefault(props as unknown as ArrayOfObjectsInputProps)}