sanity-plugin-hotspot-array 4.0.0 → 5.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/src/Spot.tsx DELETED
@@ -1,201 +0,0 @@
1
- import {Box, Text, Tooltip} from '@sanity/ui'
2
- import {motion, useMotionValue} from 'framer-motion'
3
- import {get} from 'lodash-es'
4
- import {
5
- ComponentType,
6
- createElement,
7
- CSSProperties,
8
- ReactNode,
9
- useCallback,
10
- useEffect,
11
- useState,
12
- } from 'react'
13
- import {type ObjectSchemaType, type RenderPreviewCallback} from 'sanity'
14
-
15
- import {FnHotspotMove, HotspotItem} from './ImageHotspotArray'
16
-
17
- const dragStyle: CSSProperties = {
18
- width: '1.4rem',
19
- height: '1.4rem',
20
- position: 'absolute',
21
- boxSizing: 'border-box',
22
- top: 0,
23
- left: 0,
24
- margin: '-0.7rem 0 0 -0.7rem',
25
- cursor: 'pointer',
26
- display: 'flex',
27
- justifyContent: 'center',
28
- alignItems: 'center',
29
- borderRadius: '50%',
30
- background: '#000',
31
- color: 'white',
32
- }
33
-
34
- const dragStyleWhileDrag: CSSProperties = {
35
- background: 'rgba(0, 0, 0, 0.1)',
36
- border: '1px solid #fff',
37
- cursor: 'none',
38
- }
39
-
40
- const dragStyleWhileHover: CSSProperties = {
41
- background: 'rgba(0, 0, 0, 0.1)',
42
- border: '1px solid #fff',
43
- }
44
-
45
- const dotStyle: CSSProperties = {
46
- position: 'absolute',
47
- left: '50%',
48
- top: '50%',
49
- height: '0.2rem',
50
- width: '0.2rem',
51
- margin: '-0.1rem 0 0 -0.1rem',
52
- background: '#fff',
53
- visibility: 'hidden',
54
- borderRadius: '50%',
55
- // make sure pointer events only run on the parent
56
- pointerEvents: 'none',
57
- }
58
-
59
- const dotStyleWhileActive: CSSProperties = {
60
- visibility: 'visible',
61
- }
62
-
63
- const labelStyle: CSSProperties = {
64
- color: 'white',
65
- fontSize: '0.7rem',
66
- fontWeight: 600,
67
- lineHeight: '1',
68
- }
69
-
70
- const labelStyleWhileActive: CSSProperties = {
71
- visibility: 'hidden',
72
- }
73
-
74
- const round = (num: number) => Math.round(num * 100) / 100
75
-
76
- export interface HotspotTooltipProps<HotspotFields = {[key: string]: unknown}> {
77
- value: HotspotItem<HotspotFields>
78
- schemaType: ObjectSchemaType
79
- renderPreview: RenderPreviewCallback
80
- }
81
-
82
- interface HotspotProps<HotspotFields = {[key: string]: unknown}> {
83
- value: HotspotItem
84
- bounds: DOMRectReadOnly
85
- update: FnHotspotMove
86
- hotspotDescriptionPath?: string
87
- tooltip?: ComponentType<HotspotTooltipProps<HotspotFields>>
88
- index: number
89
- schemaType: ObjectSchemaType
90
- renderPreview: RenderPreviewCallback
91
- }
92
-
93
- export default function Spot({
94
- value,
95
- bounds,
96
- update,
97
- hotspotDescriptionPath,
98
- tooltip,
99
- index,
100
- schemaType,
101
- renderPreview,
102
- }: HotspotProps): ReactNode {
103
- const [isDragging, setIsDragging] = useState(false)
104
- const [isHovering, setIsHovering] = useState(false)
105
-
106
- // x/y are stored as % but need to be converted to px
107
- const x = useMotionValue(round(bounds.width * (value.x / 100)))
108
- const y = useMotionValue(round(bounds.height * (value.y / 100)))
109
-
110
- /**
111
- * update x/y if the bounds change when resizing the window
112
- */
113
- useEffect(() => {
114
- x.set(round(bounds.width * (value.x / 100)))
115
- y.set(round(bounds.height * (value.y / 100)))
116
- }, [x, y, value, bounds])
117
-
118
- const handleDragEnd = useCallback(() => {
119
- setIsDragging(false)
120
-
121
- // get current values for x/y in px
122
- const currentX = x.get()
123
- const currentY = y.get()
124
-
125
- // Which we need to convert back to `%` to patch the document
126
- const newX = round((currentX * 100) / bounds.width)
127
- const newY = round((currentY * 100) / bounds.height)
128
-
129
- // Don't go below 0 or above 100
130
- const safeX = Math.max(0, Math.min(100, newX))
131
- const safeY = Math.max(0, Math.min(100, newY))
132
-
133
- update(value._key, safeX, safeY)
134
- }, [x, y, value, update, bounds])
135
- const handleDragStart = useCallback(() => setIsDragging(true), [])
136
-
137
- const handleHoverStart = useCallback(() => setIsHovering(true), [])
138
- const handleHoverEnd = useCallback(() => setIsHovering(false), [])
139
-
140
- if (!x || !y) {
141
- return null
142
- }
143
-
144
- return (
145
- <motion.div
146
- drag
147
- dragConstraints={bounds}
148
- dragElastic={0}
149
- dragMomentum={false}
150
- onDragEnd={handleDragEnd}
151
- onDragStart={handleDragStart}
152
- onHoverStart={handleHoverStart}
153
- onHoverEnd={handleHoverEnd}
154
- style={{
155
- ...dragStyle,
156
- x,
157
- y,
158
- ...(isDragging && {...dragStyleWhileDrag}),
159
- ...(isHovering && {...dragStyleWhileHover}),
160
- }}
161
- >
162
- <Tooltip
163
- key={value._key}
164
- disabled={isDragging}
165
- portal
166
- content={
167
- tooltip && typeof tooltip === 'function' ? (
168
- createElement(tooltip, {value, renderPreview, schemaType})
169
- ) : (
170
- <Box padding={2} style={{maxWidth: 200, pointerEvents: `none`}}>
171
- <Text textOverflow="ellipsis">
172
- {hotspotDescriptionPath
173
- ? (get(value, hotspotDescriptionPath) as string)
174
- : `${value.x}% x ${value.y}%`}
175
- </Text>
176
- </Box>
177
- )
178
- }
179
- >
180
- <div>
181
- {/* Dot */}
182
- <Box
183
- style={{
184
- ...dotStyle,
185
- ...((isDragging || isHovering) && {...dotStyleWhileActive}),
186
- }}
187
- />
188
- {/* Label */}
189
- <div
190
- style={{
191
- ...labelStyle,
192
- ...((isDragging || isHovering) && {...labelStyleWhileActive}),
193
- }}
194
- >
195
- {index + 1}
196
- </div>
197
- </div>
198
- </Tooltip>
199
- </motion.div>
200
- )
201
- }
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export {type HotspotItem, ImageHotspotArray} from './ImageHotspotArray'
2
- export {imageHotspotArrayPlugin, type ImageHotspotOptions} from './plugin'
3
- export {type HotspotTooltipProps} from './Spot'
package/src/plugin.tsx DELETED
@@ -1,40 +0,0 @@
1
- import {ComponentType} from 'react'
2
- import {type ArrayOfObjectsInputProps, definePlugin} from 'sanity'
3
-
4
- import {type HotspotItem, ImageHotspotArray} from './ImageHotspotArray'
5
- import {type HotspotTooltipProps} from './Spot'
6
-
7
- export interface ImageHotspotOptions<HotspotFields = {[key: string]: unknown}> {
8
- pathRoot?: 'document' | 'parent'
9
- imagePath: string
10
- descriptionPath?: string
11
- tooltip?: ComponentType<HotspotTooltipProps<HotspotFields>>
12
- }
13
-
14
- declare module 'sanity' {
15
- export interface ArrayOptions {
16
- imageHotspot?: ImageHotspotOptions
17
- }
18
- }
19
-
20
- export const imageHotspotArrayPlugin = definePlugin({
21
- name: 'image-hotspot-array',
22
- form: {
23
- components: {
24
- input: (props) => {
25
- if (props.schemaType.jsonType === 'array' && props.schemaType.options?.imageHotspot) {
26
- const imageHotspotOptions = props.schemaType.options?.imageHotspot
27
- if (imageHotspotOptions) {
28
- return (
29
- <ImageHotspotArray
30
- {...(props as unknown as ArrayOfObjectsInputProps<HotspotItem>)}
31
- imageHotspotOptions={imageHotspotOptions}
32
- />
33
- )
34
- }
35
- }
36
- return props.renderDefault(props)
37
- },
38
- },
39
- },
40
- })
@@ -1,102 +0,0 @@
1
- // copied from react-hookz/web
2
- // https://github.com/react-hookz/web/blob/579a445fcc9f4f4bb5b9d5e670b2e57448b4ee50/src/useDebouncedCallback/index.ts
3
- import {type DependencyList, useEffect, useMemo, useRef} from 'react'
4
-
5
- /**
6
- * Run effect only when component is unmounted.
7
- *
8
- * @param effect Effector to run on unmount
9
- */
10
- export function useUnmountEffect(effect: CallableFunction): void {
11
- const effectRef = useRef(effect)
12
- effectRef.current = effect
13
- useEffect(() => () => effectRef.current(), [])
14
- }
15
-
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- export type DebouncedFunction<Fn extends (...args: any[]) => any> = (
18
- this: ThisParameterType<Fn>,
19
- ...args: Parameters<Fn>
20
- ) => void
21
-
22
- /**
23
- * Makes passed function debounced, otherwise acts like `useCallback`.
24
- *
25
- * @param callback Function that will be debounced.
26
- * @param deps Dependencies list when to update callback. It also replaces invoked
27
- * callback for scheduled debounced invocations.
28
- * @param delay Debounce delay.
29
- * @param maxWait The maximum time `callback` is allowed to be delayed before
30
- * it's invoked. 0 means no max wait.
31
- */
32
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
- export function useDebouncedCallback<Fn extends (...args: any[]) => any>(
34
- callback: Fn,
35
- deps: DependencyList,
36
- delay: number,
37
- maxWait = 0,
38
- ): DebouncedFunction<Fn> {
39
- const timeout = useRef<ReturnType<typeof setTimeout>>(undefined)
40
- const waitTimeout = useRef<ReturnType<typeof setTimeout>>(undefined)
41
- const cb = useRef(callback)
42
- const lastCall = useRef<{args: Parameters<Fn>; this: ThisParameterType<Fn>}>(undefined)
43
-
44
- const clear = () => {
45
- if (timeout.current) {
46
- clearTimeout(timeout.current)
47
- timeout.current = undefined
48
- }
49
-
50
- if (waitTimeout.current) {
51
- clearTimeout(waitTimeout.current)
52
- waitTimeout.current = undefined
53
- }
54
- }
55
-
56
- // Cancel scheduled execution on unmount
57
- useUnmountEffect(clear)
58
-
59
- useEffect(() => {
60
- cb.current = callback
61
- // eslint-disable-next-line react-hooks/exhaustive-deps
62
- }, deps)
63
-
64
- return useMemo(() => {
65
- const execute = () => {
66
- clear()
67
-
68
- // Barely possible to test this line
69
- /* istanbul ignore next */
70
- if (!lastCall.current) return
71
-
72
- const context = lastCall.current
73
- lastCall.current = undefined
74
-
75
- cb.current.apply(context.this, context.args)
76
- }
77
-
78
- const wrapped = function (this, ...args) {
79
- if (timeout.current) {
80
- clearTimeout(timeout.current)
81
- }
82
-
83
- lastCall.current = {args, this: this}
84
-
85
- // Plan regular execution
86
- timeout.current = setTimeout(execute, delay)
87
-
88
- // Plan maxWait execution if required
89
- if (maxWait > 0 && !waitTimeout.current) {
90
- waitTimeout.current = setTimeout(execute, maxWait)
91
- }
92
- } as DebouncedFunction<Fn>
93
-
94
- Object.defineProperties(wrapped, {
95
- length: {value: callback.length},
96
- name: {value: `${callback.name || 'anonymous'}__debounced__${delay}`},
97
- })
98
-
99
- return wrapped
100
- // eslint-disable-next-line react-hooks/exhaustive-deps
101
- }, [delay, maxWait, ...deps])
102
- }
@@ -1,123 +0,0 @@
1
- // copied from react-hookz/web
2
- // https://github.com/react-hookz/web/blob/579a445fcc9f4f4bb5b9d5e670b2e57448b4ee50/src/useResizeObserver/index.ts
3
- import {type RefObject, useEffect, useRef} from 'react'
4
-
5
- const isBrowser =
6
- typeof window !== 'undefined' &&
7
- typeof navigator !== 'undefined' &&
8
- typeof document !== 'undefined'
9
-
10
- export type UseResizeObserverCallback = (entry: ResizeObserverEntry) => void
11
-
12
- type ResizeObserverSingleton = {
13
- observer: ResizeObserver
14
- subscribe: (target: Element, callback: UseResizeObserverCallback) => void
15
- unsubscribe: (target: Element, callback: UseResizeObserverCallback) => void
16
- }
17
-
18
- let observerSingleton: ResizeObserverSingleton
19
-
20
- function getResizeObserver(): ResizeObserverSingleton | undefined {
21
- if (!isBrowser) return undefined
22
-
23
- if (observerSingleton) return observerSingleton
24
-
25
- const callbacks = new Map<Element, Set<UseResizeObserverCallback>>()
26
-
27
- const observer = new ResizeObserver((entries) => {
28
- for (const entry of entries)
29
- callbacks.get(entry.target)?.forEach((cb) =>
30
- setTimeout(() => {
31
- cb(entry)
32
- }, 0),
33
- )
34
- })
35
-
36
- observerSingleton = {
37
- observer,
38
- subscribe(target, callback) {
39
- let cbs = callbacks.get(target)
40
-
41
- if (!cbs) {
42
- // If target has no observers yet - register it
43
- cbs = new Set<UseResizeObserverCallback>()
44
- callbacks.set(target, cbs)
45
- observer.observe(target)
46
- }
47
-
48
- // As Set is duplicate-safe - simply add callback on each call
49
- cbs.add(callback)
50
- },
51
- unsubscribe(target, callback) {
52
- const cbs = callbacks.get(target)
53
-
54
- // Else branch should never occur in case of normal execution
55
- // because callbacks map is hidden in closure - it is impossible to
56
- // simulate situation with non-existent `cbs` Set
57
- /* istanbul ignore else */
58
- if (cbs) {
59
- // Remove current observer
60
- cbs.delete(callback)
61
-
62
- if (cbs.size === 0) {
63
- // If no observers left unregister target completely
64
- callbacks.delete(target)
65
- observer.unobserve(target)
66
- }
67
- }
68
- },
69
- }
70
-
71
- return observerSingleton
72
- }
73
-
74
- /**
75
- * Invokes a callback whenever ResizeObserver detects a change to target's size.
76
- *
77
- * @param target React reference or Element to track.
78
- * @param callback Callback that will be invoked on resize.
79
- * @param enabled Whether resize observer is enabled or not.
80
- */
81
- export function useResizeObserver<T extends Element>(
82
- target: RefObject<T | null>,
83
- callback: UseResizeObserverCallback,
84
- enabled = true,
85
- ): void {
86
- const ro = enabled && getResizeObserver()
87
- const cb = useRef(callback)
88
- cb.current = callback
89
-
90
- const tgt = target && 'current' in target ? target.current : target
91
-
92
- useEffect(() => {
93
- // This secondary target resolve required for case when we receive ref object, which, most
94
- // likely, contains null during render stage, but already populated with element during
95
- // effect stage.
96
-
97
- const _tgt = target && 'current' in target ? target.current : target
98
-
99
- if (!ro || !_tgt) return undefined
100
-
101
- // As unsubscription in internals of our ResizeObserver abstraction can
102
- // happen a bit later than effect cleanup invocation - we need a marker,
103
- // that this handler should not be invoked anymore
104
- let subscribed = true
105
-
106
- const handler: UseResizeObserverCallback = (...args) => {
107
- // It is reinsurance for the highly asynchronous invocations, almost
108
- // impossible to achieve in tests, thus excluding from LOC
109
- /* istanbul ignore else */
110
- if (subscribed) {
111
- cb.current(...args)
112
- }
113
- }
114
-
115
- ro.subscribe(_tgt, handler)
116
-
117
- return () => {
118
- subscribed = false
119
- ro.unsubscribe(_tgt, handler)
120
- }
121
- // eslint-disable-next-line react-hooks/exhaustive-deps
122
- }, [tgt, ro])
123
- }